diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 2ab02e2..b5e5e17 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -1,6 +1,6 @@ name: SlimeVR OpenVR Driver -on: [ push, pull_request ] +on: [ push, pull_request, workflow_dispatch ] jobs: build: @@ -15,11 +15,11 @@ jobs: - os: windows-latest triplet: x64-windows-static-md target: ALL_BUILD - release_dir: Release/driver # dir of driver binaries within env.CMAKE_BUILD_DIR, VS multi-config uses / subfolder + release_dir: Release # dir of driver binaries within env.CMAKE_BUILD_DIR, VS multi-config uses / subfolder - os: ubuntu-latest triplet: x64-linux target: all - release_dir: driver # makefile single config won't have subfolder + release_dir: "" # makefile single config won't have subfolder env: # Indicates the CMake build directory where project files and binaries are being produced. CMAKE_BUILD_DIR: ${{ github.workspace }}/build @@ -30,15 +30,12 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - + - uses: lukka/get-cmake@latest - - - name: Clone vcpkg - uses: actions/checkout@v2 - with: - repository: microsoft/vcpkg - path: ${{ env.VCPKG_ROOT }} - submodules: true + + - name: Get submodule commit hashes + id: submodule_hashes + run: git submodule foreach --recursive git rev-parse HEAD > submodule_hashes.txt - name: Restore vcpkg and its artifacts uses: actions/cache@v2 @@ -52,12 +49,11 @@ jobs: !${{ env.VCPKG_ROOT }}/buildtrees !${{ env.VCPKG_ROOT }}/packages !${{ env.VCPKG_ROOT }}/downloads - # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg's Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. + # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg/submodule Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. # The key includes: hash of the vcpkg.json file, the hash of the vcpkg Git commit id, and the used vcpkg's triplet. The vcpkg's commit id would suffice, but computing an hash out it does not harm. # Note: given a key, the cache content is immutable. If a cache entry has been created improperly, in order the recreate the right content the key must be changed as well, and it must be brand new (i.e. not existing already). - key: | - ${{ hashFiles( 'vcpkg_manifest/vcpkg.json' ) }}-${{ hashFiles( '.git/modules/vcpkg/HEAD' )}}-${{ hashFiles( '${{ env.VCPKG_ROOT }}/.git/HEAD' )}}-${{ matrix.triplet }}-invalidate - + key: ${{ matrix.triplet }}-${{ hashFiles( '**/vcpkg.json', '**/CMakeLists.txt' ) }}-${{ hashFiles( 'submodule_hashes.txt' )}} + - if: matrix.os == 'windows-latest' name: Set up vcpkg for Windows run: ${{ env.VCPKG_ROOT }}/bootstrap-vcpkg.bat @@ -80,16 +76,46 @@ jobs: # A file, directory or wildcard pattern that describes what to upload # Using wildcards so that only the driver directory gets included (if you specify it, then it won't be included) path: | - ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/* + ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/driver/* + ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/tests* - name: Zip if: startsWith(github.ref, 'refs/tags/') - working-directory: ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }} - run: cmake -E tar "cf" "${{env.CMAKE_BUILD_DIR}}/slimevr-openvr-driver-${{ matrix.triplet }}.zip" --format=zip -- ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/slimevr + working-directory: ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/driver + run: cmake -E tar "cf" "${{env.CMAKE_BUILD_DIR}}/slimevr-openvr-driver-${{ matrix.triplet }}.zip" --format=zip -- ${{env.CMAKE_BUILD_DIR}}/${{ matrix.release_dir }}/driver/slimevr - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: "${{env.CMAKE_BUILD_DIR}}/slimevr-openvr-driver-${{ matrix.triplet }}.zip" - + + test: + name: Run tests + needs: build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + include: + - os: windows-latest + triplet: x64-windows-static-md + target: RUN_TESTS + - os: ubuntu-latest + triplet: x64-linux + target: test + steps: + - name: Download build artifact + uses: actions/download-artifact@v2 + with: + name: slimevr-openvr-driver-${{ matrix.triplet }} + path: ${{ github.workspace }} + - if: matrix.os == 'windows-latest' + name: Run tests on Windows + working-directory: ${{ github.workspace }}/ + run: .\tests.exe + - if: matrix.os != 'windows-latest' + name: Run tests on Unix + working-directory: ${{ github.workspace }}/ + run: chmod +x ./tests && ./tests diff --git a/.gitmodules b/.gitmodules index bf5392b..df7de3c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "libraries/linalg"] path = libraries/linalg url = https://github.com/sgorsten/linalg.git +[submodule "vcpkg"] + path = vcpkg + url = https://github.com/Microsoft/vcpkg.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 0873831..6d724a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,17 +69,56 @@ SET_SOURCE_FILES_PROPERTIES(${PROTO_SRC} ${PROTO_INCL} PROPERTIES GENERATED TRUE find_package(simdjson CONFIG REQUIRED) +# libuv +find_package(uvw CONFIG REQUIRED) + +# Catch2 +find_package(Catch2 3 REQUIRED) + # Project -file(GLOB_RECURSE HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp") +set(DEPS_INCLUDES + "${OPENVR_INCLUDE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/libraries/linalg" + "${CMAKE_CURRENT_SOURCE_DIR}/src/" +) +set(DEPS_LIBS + "${OPENVR_LIB}" + protobuf::libprotoc + protobuf::libprotobuf + protobuf::libprotobuf-lite + simdjson::simdjson + uvw::uvw +) + +# compile into a static lib file(GLOB_RECURSE SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") -add_library("${PROJECT_NAME}" SHARED "${HEADERS}" "${SOURCES}" ${PROTO_HEADER} ${PROTO_SRC}) -target_include_directories("${PROJECT_NAME}" PUBLIC "${OPENVR_INCLUDE_DIR}") -target_include_directories("${PROJECT_NAME}" PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/libraries/linalg") -target_include_directories("${PROJECT_NAME}" PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src/") -target_link_libraries("${PROJECT_NAME}" PUBLIC "${OPENVR_LIB}" protobuf::libprotoc protobuf::libprotobuf protobuf::libprotobuf-lite simdjson::simdjson) +file(GLOB_RECURSE HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp") +add_library("${PROJECT_NAME}_static" STATIC ${SOURCES} ${PROTO_HEADER} ${PROTO_SRC}) +target_link_libraries("${PROJECT_NAME}_static" PUBLIC ${DEPS_LIBS}) +set_property(TARGET "${PROJECT_NAME}_static" PROPERTY CXX_STANDARD 17) +include_directories("${PROJECT_NAME}_static" PUBLIC ${DEPS_INCLUDES} ${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) +if(UNIX) + target_compile_options("${PROJECT_NAME}_static" PRIVATE "-fPIC") + target_link_libraries("${PROJECT_NAME}_static" PUBLIC atomic) +endif() + +# compile driver +file(GLOB_RECURSE DRIVER_MAIN "${CMAKE_CURRENT_SOURCE_DIR}/src/DriverFactory.cpp") +add_library("${PROJECT_NAME}" SHARED ${DRIVER_MAIN} ${HEADERS} ${PROTO_HEADER}) +target_link_libraries("${PROJECT_NAME}" PUBLIC "${PROJECT_NAME}_static") set_property(TARGET "${PROJECT_NAME}" PROPERTY CXX_STANDARD 17) -include_directories(${Protobuf_INCLUDE_DIRS}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +# compile tests +function(build_tests target_name test_dir) + file(GLOB TESTS "${CMAKE_CURRENT_SOURCE_DIR}/${test_dir}/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/${test_dir}/*.hpp") + file(GLOB TESTS_COMMON "${CMAKE_CURRENT_SOURCE_DIR}/test/common/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test/common/*.hpp") + add_executable(${target_name} ${TESTS} ${TESTS_COMMON} ${HEADERS} ${PROTO_HEADER}) + target_link_libraries(${target_name} PUBLIC "${PROJECT_NAME}_static" Catch2::Catch2WithMain) + set_property(TARGET ${target_name} PROPERTY CXX_STANDARD 17) +endfunction() +build_tests(tests "test") +build_tests(tests_integration "test/integration") +add_test(NAME tests COMMAND "tests") # IDE Config source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX "Header Files" FILES ${HEADERS}) diff --git a/README.md b/README.md index 66420aa..09d36b5 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,16 @@ okay with this and that you are authorized to provide the above licenses. ### Building -To build the project with VSCode you need to install two things: [vcpkg](https://vcpkg.io/en/getting-started.html) and [VS Build Tools](https://visualstudio.microsoft.com/downloads/). +Clone the repo with `git clone --recurse-submodules https://github.com/SlimeVR/SlimeVR-OpenVR-Driver` to clone with all libraries and [vcpkg](https://vcpkg.io/en/getting-started.html) registry. + +To build the project with VSCode on Windows you need to install [VS Build Tools](https://visualstudio.microsoft.com/downloads/). + +Run the bootstrap script to build vcpkg binary `.\vcpkg\bootstrap-vcpkg.bat` or `./vcpkg/bootstrap-vcpkg.sh`. After installing vcpkg if you're on Windows, you need to run `vcpkg integrate install` command from the vcpkg folder to integrate it for VSCode. For other systems and IDEs instructions are not available as of now, contributions are welcome. + +### Updating vcpkg packages + +To update vcpkg packages set the vcpkg registry submodule to a newer commit. \ No newline at end of file diff --git a/src/DriverFactory.cpp b/src/DriverFactory.cpp index 8dc0e52..cbd05cf 100644 --- a/src/DriverFactory.cpp +++ b/src/DriverFactory.cpp @@ -6,21 +6,22 @@ static std::shared_ptr driver; void* HmdDriverFactory(const char* interface_name, int* return_code) { - if (std::strcmp(interface_name, vr::IServerTrackedDeviceProvider_Version) == 0) { - if (!driver) { - // Instantiate concrete impl - driver = std::make_shared(); - } - // We always have at least 1 ref to the shared ptr in "driver" so passing out raw pointer is ok - return driver.get(); - } + if (std::strcmp(interface_name, vr::IServerTrackedDeviceProvider_Version) == 0) { + if (!driver) { + // Instantiate concrete impl + driver = std::make_shared(); + } + // We always have at least 1 ref to the shared ptr in "driver" so passing out raw pointer is ok + return driver.get(); + } - if (return_code) - *return_code = vr::VRInitError_Init_InterfaceNotFound; + if (return_code) { + *return_code = vr::VRInitError_Init_InterfaceNotFound; + } - return nullptr; + return nullptr; } std::shared_ptr SlimeVRDriver::GetDriver() { - return driver; + return driver; } diff --git a/src/IVRDevice.hpp b/src/IVRDevice.hpp index 3038f23..b2681ab 100644 --- a/src/IVRDevice.hpp +++ b/src/IVRDevice.hpp @@ -6,38 +6,41 @@ #include "ProtobufMessages.pb.h" namespace SlimeVRDriver { - class IVRDevice : public vr::ITrackedDeviceServerDriver { public: - /// - /// Returns the serial string for this device - /// - /// Device serial + /** + * Returns the serial string for this device. + * + * @return Device serial. + */ virtual std::string GetSerial() = 0; - /// - /// Runs any update logic for this device. - /// Called once per frame - /// + /** + * Runs any update logic for this device. + * Called once per frame. + */ virtual void Update() = 0; - /// - /// Returns the OpenVR device index - /// This should be 0 for HMDs - /// - /// OpenVR device index + /** + * Returns the OpenVR device index. + * This should be 0 for HMDs. + * + * @returns OpenVR device index. + */ virtual vr::TrackedDeviceIndex_t GetDeviceIndex() = 0; - /// - /// Returns which type of device this device is - /// - /// The type of device + /** + * Returns which type of device this device is. + * + * @returns The type of device. + */ virtual DeviceType GetDeviceType() = 0; - /// - /// Makes a default device pose - /// - /// Default initialised pose + /** + * Makes a default device pose. + * + * @returns Default initialised pose. + */ static inline vr::DriverPose_t MakeDefaultPose(bool connected = true, bool tracking = true) { vr::DriverPose_t out_pose = { 0 }; @@ -51,6 +54,31 @@ namespace SlimeVRDriver { return out_pose; } + /** + * Returns the device id. + */ + virtual int GetDeviceId() = 0; + + /** + * Sets the device id. + */ + virtual void SetDeviceId(int device_id) = 0; + + /** + * Updates device position from a received message. + */ + virtual void PositionMessage(messages::Position& position) = 0; + + /** + * Updates device status from a received message. + */ + virtual void StatusMessage(messages::TrackerStatus& status) = 0; + + /** + * Updates battery indicator from a received message. + */ + virtual void BatteryMessage(messages::Battery& battery) = 0; + // Inherited via ITrackedDeviceServerDriver virtual vr::EVRInitError Activate(uint32_t unObjectId) = 0; virtual void Deactivate() = 0; @@ -59,12 +87,6 @@ namespace SlimeVRDriver { virtual void DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) = 0; virtual vr::DriverPose_t GetPose() = 0; - virtual int getDeviceId() = 0; - virtual void PositionMessage(messages::Position& position) = 0; - virtual void StatusMessage(messages::TrackerStatus& status) = 0; - - virtual void BatteryMessage(messages::Battery& battery) = 0; - ~IVRDevice() = default; }; }; \ No newline at end of file diff --git a/src/IVRDriver.hpp b/src/IVRDriver.hpp index d47e6b6..819823f 100644 --- a/src/IVRDriver.hpp +++ b/src/IVRDriver.hpp @@ -22,66 +22,74 @@ namespace SlimeVRDriver { class IVRDriver : protected vr::IServerTrackedDeviceProvider { public: - - /// - /// Returns all devices being managed by this driver - /// - /// All managed devices + /** + * Returns all devices being managed by this driver. + * + * @return A vector of shared pointers to all managed devices. + */ virtual std::vector> GetDevices() = 0; - /// - /// Returns all OpenVR events that happened on the current frame - /// - /// Current frame's OpenVR events + /** + * Returns all OpenVR events that happened on the current frame. + * + * @return A vector of current frame's OpenVR events. + */ virtual std::vector GetOpenVREvents() = 0; - /// - /// Returns the milliseconds between last frame and this frame - /// - /// MS between last frame and this frame + /** + * Returns the milliseconds between last frame and this frame. + * + * @return Milliseconds between last frame and this frame. + */ virtual std::chrono::milliseconds GetLastFrameTime() = 0; - /// - /// Adds a device to the driver - /// - /// Device instance - /// True on success, false on failure + /** + * Adds a device to the driver. + * + * @param device A shared pointer to the device to be added. + * @return True on success, false on failure. + */ virtual bool AddDevice(std::shared_ptr device) = 0; - /// - /// Returns the value of a settings key - /// - /// The settings key - /// Value of the key, std::monostate if the value is malformed or missing + /** + * Returns the value of a settings key. + * + * @param key The settings key + * @return Value of the key, std::monostate if the value is malformed or missing. + */ virtual SettingsValue GetSettingsValue(std::string key) = 0; - /// - /// Gets the OpenVR VRDriverInput pointer - /// - /// OpenVR VRDriverInput pointer + /** + * Gets the OpenVR VRDriverInput pointer. + * + * @return OpenVR VRDriverInput pointer. + */ virtual vr::IVRDriverInput* GetInput() = 0; - /// - /// Gets the OpenVR VRDriverProperties pointer - /// - /// OpenVR VRDriverProperties pointer + /** + * Gets the OpenVR VRDriverProperties pointer. + * + * @return OpenVR VRDriverProperties pointer. + */ virtual vr::CVRPropertyHelpers* GetProperties() = 0; - /// - /// Gets the OpenVR VRServerDriverHost pointer - /// - /// OpenVR VRServerDriverHost pointer + /** + * Gets the OpenVR VRServerDriverHost pointer. + * + * @return OpenVR VRServerDriverHost pointer. + */ virtual vr::IVRServerDriverHost* GetDriverHost() = 0; - /// - /// Gets the current UniverseTranslation - /// + /** + * Gets the current UniverseTranslation. + */ virtual std::optional GetCurrentUniverse() = 0; - /// - /// Writes a log message - /// - /// Message to log + /** + * Writes a log message. + * + * @param message Message string to log. + */ virtual void Log(std::string message) = 0; virtual inline const char* const* GetInterfaceVersions() override { @@ -89,6 +97,5 @@ namespace SlimeVRDriver { }; virtual ~IVRDriver() {} - }; } \ No newline at end of file diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000..cd5efb7 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,33 @@ +#include "Logger.hpp" +#include + +void Logger::Log(const char* format, ...) { + auto prefixed_format = std::string(format); + if (!name_.empty()) { + std::ostringstream ss; + ss << name_ << ": " << format; + prefixed_format = ss.str(); + } + + va_list args; + va_start(args, format); + va_list args2; + va_copy(args2, args); + size_t len = std::vsnprintf(nullptr, 0, prefixed_format.data(), args2); + va_end(args2); + + std::vector buf(len + 1); + std::vsnprintf(buf.data(), buf.size(), prefixed_format.data(), args); + va_end(args); + + std::lock_guard lock(mutex_); + LogImpl(buf.data()); +} + +void ConsoleLogger::LogImpl(const char* message) { + std::cout << message << '\n' << std::flush; +} + +void VRLogger::LogImpl(const char* message) { + vr::VRDriverLog()->Log(message); +} \ No newline at end of file diff --git a/src/Logger.hpp b/src/Logger.hpp new file mode 100644 index 0000000..f1d3508 --- /dev/null +++ b/src/Logger.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class Logger { +public: + Logger() : name_("") { } + Logger(const std::string& name) : name_(name) { } + void Log(const char* format, ...); +protected: + virtual void LogImpl(const char* string) = 0; + std::string name_; + std::mutex mutex_; +}; + +class NullLogger: public Logger { + using Logger::Logger; +protected: + void LogImpl(const char* message) override {}; +}; + +class ConsoleLogger: public Logger { + using Logger::Logger; +protected: + void LogImpl(const char* message) override; +}; + +class VRLogger: public Logger { + using Logger::Logger; +protected: + void LogImpl(const char* message) override; +}; \ No newline at end of file diff --git a/src/TrackerDevice.cpp b/src/TrackerDevice.cpp index fbd1242..1818ae5 100644 --- a/src/TrackerDevice.cpp +++ b/src/TrackerDevice.cpp @@ -1,54 +1,50 @@ #include "TrackerDevice.hpp" -SlimeVRDriver::TrackerDevice::TrackerDevice(std::string serial, int deviceId, TrackerRole trackerRole_): - serial_(serial), trackerRole(trackerRole_), deviceId_(deviceId) -{ - this->last_pose_ = MakeDefaultPose(); - this->isSetup = false; +SlimeVRDriver::TrackerDevice::TrackerDevice(std::string serial, int device_id, TrackerRole tracker_role): + serial_(serial), + tracker_role_(tracker_role), + device_id_(device_id), + last_pose_(MakeDefaultPose()), + is_setup_(false) +{ } + +std::string SlimeVRDriver::TrackerDevice::GetSerial() { + return serial_; } -std::string SlimeVRDriver::TrackerDevice::GetSerial() -{ - return this->serial_; -} - -void SlimeVRDriver::TrackerDevice::Update() -{ - if (this->device_index_ == vr::k_unTrackedDeviceIndexInvalid) - return; +void SlimeVRDriver::TrackerDevice::Update() { + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; // Check if this device was asked to be identified auto events = GetDriver()->GetOpenVREvents(); for (auto event : events) { - // Note here, event.trackedDeviceIndex does not necessarily equal this->device_index_, not sure why, but the component handle will match so we can just use that instead - //if (event.trackedDeviceIndex == this->device_index_) { + // Note here, event.trackedDeviceIndex does not necessarily equal device_index_, not sure why, but the component handle will match so we can just use that instead + //if (event.trackedDeviceIndex == device_index_) { if (event.eventType == vr::EVREventType::VREvent_Input_HapticVibration) { - if (event.data.hapticVibration.componentHandle == this->haptic_component_) { - this->did_vibrate_ = true; + if (event.data.hapticVibration.componentHandle == haptic_component_) { + did_vibrate_ = true; } } //} } // Check if we need to keep vibrating - if (this->did_vibrate_) { - this->vibrate_anim_state_ += (GetDriver()->GetLastFrameTime().count()/1000.f); - if (this->vibrate_anim_state_ > 1.0f) { - this->did_vibrate_ = false; - this->vibrate_anim_state_ = 0.0f; + if (did_vibrate_) { + vibrate_anim_state_ += GetDriver()->GetLastFrameTime().count() / 1000.f; + if (vibrate_anim_state_ > 1.0f) { + did_vibrate_ = false; + vibrate_anim_state_ = 0.0f; } } } -void SlimeVRDriver::TrackerDevice::PositionMessage(messages::Position &position) -{ - if (this->device_index_ == vr::k_unTrackedDeviceIndexInvalid) - return; +void SlimeVRDriver::TrackerDevice::PositionMessage(messages::Position &position) { + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; // Setup pose for this frame - auto pose = this->last_pose_; + auto pose = MakeDefaultPose(); //send the new position and rotation from the pipe to the tracker object - if(position.has_x()) { + if (position.has_x()) { pose.vecPosition[0] = position.x(); pose.vecPosition[1] = position.y(); pose.vecPosition[2] = position.z(); @@ -74,13 +70,12 @@ void SlimeVRDriver::TrackerDevice::PositionMessage(messages::Position &position) pose.qWorldFromDriverRotation.z = 0; } - // Post pose - GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(this->device_index_, pose, sizeof(vr::DriverPose_t)); - this->last_pose_ = pose; + // Notify SteamVR that pose was updated + last_pose_ = pose; + GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(device_index_, pose, sizeof(vr::DriverPose_t)); } -void SlimeVRDriver::TrackerDevice::BatteryMessage(messages::Battery &battery) -{ +void SlimeVRDriver::TrackerDevice::BatteryMessage(messages::Battery &battery) { if (this->device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; @@ -105,52 +100,48 @@ void SlimeVRDriver::TrackerDevice::BatteryMessage(messages::Battery &battery) vr::VRProperties()->SetFloatProperty(props, vr::Prop_DeviceBatteryPercentage_Float, battery.battery_level()); } -void SlimeVRDriver::TrackerDevice::StatusMessage(messages::TrackerStatus &status) -{ - auto pose = this->last_pose_; - switch (status.status()) - { - case messages::TrackerStatus_Status_OK: - pose.deviceIsConnected = true; - pose.poseIsValid = true; - break; - case messages::TrackerStatus_Status_DISCONNECTED: - pose.deviceIsConnected = false; - pose.poseIsValid = false; - break; - default: - case messages::TrackerStatus_Status_ERROR: - case messages::TrackerStatus_Status_BUSY: - pose.deviceIsConnected = true; - pose.poseIsValid = false; - break; +void SlimeVRDriver::TrackerDevice::StatusMessage(messages::TrackerStatus &status) { + if (device_index_ == vr::k_unTrackedDeviceIndexInvalid) return; + + vr::DriverPose_t pose = last_pose_; + switch (status.status()) { + case messages::TrackerStatus_Status_OK: + pose.deviceIsConnected = true; + pose.poseIsValid = true; + break; + case messages::TrackerStatus_Status_DISCONNECTED: + pose.deviceIsConnected = false; + pose.poseIsValid = false; + break; + case messages::TrackerStatus_Status_ERROR: + case messages::TrackerStatus_Status_BUSY: + default: + pose.deviceIsConnected = true; + pose.poseIsValid = false; + break; } // TODO: send position/rotation of 0 instead of last pose? - - GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(this->device_index_, pose, sizeof(vr::DriverPose_t)); - - // TODO: update this->last_pose_? + + last_pose_ = pose; + GetDriver()->GetDriverHost()->TrackedDevicePoseUpdated(device_index_, pose, sizeof(vr::DriverPose_t)); } -DeviceType SlimeVRDriver::TrackerDevice::GetDeviceType() -{ +DeviceType SlimeVRDriver::TrackerDevice::GetDeviceType() { return DeviceType::TRACKER; } -vr::TrackedDeviceIndex_t SlimeVRDriver::TrackerDevice::GetDeviceIndex() -{ - return this->device_index_; +vr::TrackedDeviceIndex_t SlimeVRDriver::TrackerDevice::GetDeviceIndex() { + return device_index_; } -vr::EVRInitError SlimeVRDriver::TrackerDevice::Activate(uint32_t unObjectId) -{ - this->device_index_ = unObjectId; +vr::EVRInitError SlimeVRDriver::TrackerDevice::Activate(uint32_t unObjectId) { + device_index_ = unObjectId; - GetDriver()->Log("Activating tracker " + this->serial_); + GetDriver()->Log("Activating tracker " + serial_); // Get the properties handle - auto props = GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer(this->device_index_); + auto props = GetDriver()->GetProperties()->TrackedDeviceToPropertyContainer(device_index_); // Set some universe ID (Must be 2 or higher) GetDriver()->GetProperties()->SetUint64Property(props, vr::Prop_CurrentUniverseId_Uint64, 4); @@ -159,7 +150,7 @@ vr::EVRInitError SlimeVRDriver::TrackerDevice::Activate(uint32_t unObjectId) GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_ModelNumber_String, "SlimeVR Virtual Tracker"); // Opt out of hand selection - GetDriver()->GetProperties()->SetInt32Property(props, vr::Prop_ControllerRoleHint_Int32, vr::ETrackedControllerRole::TrackedControllerRole_OptOut); + GetDriver()->GetProperties()->SetInt32Property(props, vr::Prop_ControllerRoleHint_Int32, vr::ETrackedControllerRole::TrackedControllerRole_OptOut); vr::VRProperties()->SetInt32Property(props, vr::Prop_DeviceClass_Int32, vr::TrackedDeviceClass_GenericTracker); vr::VRProperties()->SetInt32Property(props, vr::Prop_ControllerHandSelectionPriority_Int32, -1); @@ -178,43 +169,44 @@ vr::EVRInitError SlimeVRDriver::TrackerDevice::Activate(uint32_t unObjectId) GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_NamedIconPathDeviceAlertLow_String, "{slimevr}/icons/tracker_status_ready_low.png"); // Automatically select vive tracker roles and set hints for games that need it (Beat Saber avatar mod, for example) - auto roleHint = getViveRoleHint(trackerRole); - if(roleHint != "") - GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_ControllerType_String, roleHint.c_str()); + auto role_hint = GetViveRoleHint(tracker_role_); + if (role_hint != "") { + GetDriver()->GetProperties()->SetStringProperty(props, vr::Prop_ControllerType_String, role_hint.c_str()); + } - auto role = getViveRole(trackerRole); - if(role != "") - vr::VRSettings()->SetString(vr::k_pch_Trackers_Section, ("/devices/slimevr/" + this->serial_).c_str(), role.c_str()); + auto role = GetViveRole(tracker_role_); + if (role != "") { + vr::VRSettings()->SetString(vr::k_pch_Trackers_Section, ("/devices/slimevr/" + serial_).c_str(), role.c_str()); + } return vr::EVRInitError::VRInitError_None; } -void SlimeVRDriver::TrackerDevice::Deactivate() -{ - this->device_index_ = vr::k_unTrackedDeviceIndexInvalid; +void SlimeVRDriver::TrackerDevice::Deactivate() { + device_index_ = vr::k_unTrackedDeviceIndexInvalid; } -void SlimeVRDriver::TrackerDevice::EnterStandby() -{ +void SlimeVRDriver::TrackerDevice::EnterStandby() { } -void* SlimeVRDriver::TrackerDevice::GetComponent(const char* pchComponentNameAndVersion) -{ +void* SlimeVRDriver::TrackerDevice::GetComponent(const char* pchComponentNameAndVersion) { return nullptr; } -void SlimeVRDriver::TrackerDevice::DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) -{ - if (unResponseBufferSize >= 1) +void SlimeVRDriver::TrackerDevice::DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) { + if (unResponseBufferSize >= 1) { pchResponseBuffer[0] = 0; + } } -vr::DriverPose_t SlimeVRDriver::TrackerDevice::GetPose() -{ +vr::DriverPose_t SlimeVRDriver::TrackerDevice::GetPose() { return last_pose_; } -int SlimeVRDriver::TrackerDevice::getDeviceId() -{ - return deviceId_; +int SlimeVRDriver::TrackerDevice::GetDeviceId() { + return device_id_; +} + +void SlimeVRDriver::TrackerDevice::SetDeviceId(int device_id) { + device_id_ = device_id; } diff --git a/src/TrackerDevice.hpp b/src/TrackerDevice.hpp index 3a72ec4..93217b1 100644 --- a/src/TrackerDevice.hpp +++ b/src/TrackerDevice.hpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -12,53 +13,48 @@ #include #include #include -#include "bridge/bridge.hpp" #include "TrackerRole.hpp" namespace SlimeVRDriver { class TrackerDevice : public IVRDevice { - public: + public: + TrackerDevice(std::string serial, int device_id, TrackerRole tracker_role); + ~TrackerDevice() = default; + + // Inherited via IVRDevice + virtual std::string GetSerial() override; + virtual void Update() override; + virtual vr::TrackedDeviceIndex_t GetDeviceIndex() override; + virtual DeviceType GetDeviceType() override; + virtual int GetDeviceId() override; + virtual void SetDeviceId(int device_id) override; + virtual void PositionMessage(messages::Position &position) override; + virtual void StatusMessage(messages::TrackerStatus &status) override; + virtual void BatteryMessage(messages::Battery &battery) override; + + // Inherited via ITrackedDeviceServerDriver + virtual vr::EVRInitError Activate(uint32_t unObjectId) override; + virtual void Deactivate() override; + virtual void EnterStandby() override; + virtual void* GetComponent(const char* pchComponentNameAndVersion) override; + virtual void DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) override; + virtual vr::DriverPose_t GetPose() override; - TrackerDevice(std::string serial, int deviceId, TrackerRole trackerRole); - ~TrackerDevice() = default; - - virtual void BatteryMessage(messages::Battery &battery); - - // Inherited via IVRDevice - virtual std::string GetSerial() override; - virtual void Update() override; - virtual vr::TrackedDeviceIndex_t GetDeviceIndex() override; - virtual DeviceType GetDeviceType() override; - - virtual vr::EVRInitError Activate(uint32_t unObjectId) override; - virtual void Deactivate() override; - virtual void EnterStandby() override; - virtual void* GetComponent(const char* pchComponentNameAndVersion) override; - virtual void DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) override; - virtual vr::DriverPose_t GetPose() override; - virtual int getDeviceId() override; - virtual void PositionMessage(messages::Position &position) override; - virtual void StatusMessage(messages::TrackerStatus &status) override; private: - vr::TrackedDeviceIndex_t device_index_ = vr::k_unTrackedDeviceIndexInvalid; + std::atomic device_index_ = vr::k_unTrackedDeviceIndexInvalid; std::string serial_; - bool isSetup; + bool is_setup_; - char buffer[1024]; - uint32_t dwWritten; - uint32_t dwRead; - int deviceId_; - TrackerRole trackerRole; + int device_id_; + TrackerRole tracker_role_; - vr::DriverPose_t last_pose_ = IVRDevice::MakeDefaultPose(); + std::atomic last_pose_ = IVRDevice::MakeDefaultPose(); bool did_vibrate_ = false; float vibrate_anim_state_ = 0.f; vr::VRInputComponentHandle_t haptic_component_ = 0; - vr::VRInputComponentHandle_t system_click_component_ = 0; vr::VRInputComponentHandle_t system_touch_component_ = 0; - }; }; \ No newline at end of file diff --git a/src/TrackerRole.cpp b/src/TrackerRole.cpp index 2cb4b4a..865a5e6 100644 --- a/src/TrackerRole.cpp +++ b/src/TrackerRole.cpp @@ -22,9 +22,8 @@ */ #include "TrackerRole.hpp" - -std::string getViveRoleHint(TrackerRole role) { - switch(role) { +std::string GetViveRoleHint(TrackerRole role) { + switch (role) { case LEFT_CONTROLLER: case RIGHT_CONTROLLER: case GENERIC_CONTROLLER: @@ -55,13 +54,12 @@ std::string getViveRoleHint(TrackerRole role) { return "vive_tracker_camera"; case KEYBOARD: return "vive_tracker_keyboard"; - } return ""; } -std::string getViveRole(TrackerRole role) { - switch(role) { +std::string GetViveRole(TrackerRole role) { + switch (role) { case LEFT_CONTROLLER: case RIGHT_CONTROLLER: case GENERIC_CONTROLLER: @@ -92,13 +90,12 @@ std::string getViveRole(TrackerRole role) { return "TrackerRole_Camera"; case KEYBOARD: return "TrackerRole_Keyboard"; - } return ""; } -DeviceType getDeviceType(TrackerRole role) { - switch(role) { +DeviceType GetDeviceType(TrackerRole role) { + switch (role) { case LEFT_CONTROLLER: case RIGHT_CONTROLLER: case GENERIC_CONTROLLER: diff --git a/src/TrackerRole.hpp b/src/TrackerRole.hpp index c0854cf..d5f9e18 100644 --- a/src/TrackerRole.hpp +++ b/src/TrackerRole.hpp @@ -50,8 +50,8 @@ enum TrackerRole { GENERIC_CONTROLLER = 21, }; -std::string getViveRoleHint(TrackerRole role); +std::string GetViveRoleHint(TrackerRole role); -std::string getViveRole(TrackerRole role); +std::string GetViveRole(TrackerRole role); -DeviceType getDeviceType(TrackerRole role); +DeviceType GetDeviceType(TrackerRole role); diff --git a/src/VRDriver.cpp b/src/VRDriver.cpp index df6550c..c588744 100644 --- a/src/VRDriver.cpp +++ b/src/VRDriver.cpp @@ -1,136 +1,112 @@ #include "VRDriver.hpp" #include -#include "bridge/bridge.hpp" #include "TrackerRole.hpp" #include #include #include "VRPaths_openvr.hpp" - -vr::EVRInitError SlimeVRDriver::VRDriver::Init(vr::IVRDriverContext* pDriverContext) -{ +vr::EVRInitError SlimeVRDriver::VRDriver::Init(vr::IVRDriverContext* pDriverContext) { // Perform driver context initialisation if (vr::EVRInitError init_error = vr::InitServerDriverContext(pDriverContext); init_error != vr::EVRInitError::VRInitError_None) { return init_error; } - Log("Activating SlimeVR Driver..."); + logger_->Log("Activating SlimeVR Driver..."); try { auto json = simdjson::padded_string::load(GetVRPathRegistryFilename()); // load VR Path Registry - simdjson::ondemand::document doc = json_parser.iterate(json); + simdjson::ondemand::document doc = json_parser_.iterate(json); auto path = std::string { doc.get_object()["config"].at(0).get_string().value() }; - // Log(path); default_chap_path_ = GetDefaultChaperoneFromConfigPath(path); } catch (simdjson::simdjson_error& e) { - std::stringstream ss; - ss << "Error getting VR Config path, continuing: " << e.error(); - Log(ss.str()); + logger_->Log("Error getting VR Config path, continuing: %s", e.error()); } - Log("SlimeVR Driver Loaded Successfully"); + logger_->Log("SlimeVR Driver Loaded Successfully"); + + bridge_ = std::make_shared( + std::static_pointer_cast(std::make_shared("Bridge")), + std::bind(&SlimeVRDriver::VRDriver::OnBridgeMessage, this, std::placeholders::_1) + ); + bridge_->Start(); + + exiting_pose_request_thread_ = false; + pose_request_thread_ = + std::make_unique(&SlimeVRDriver::VRDriver::RunPoseRequestThread, this); return vr::VRInitError_None; } -void SlimeVRDriver::VRDriver::Cleanup() -{ +void SlimeVRDriver::VRDriver::Cleanup() { + exiting_pose_request_thread_ = true; + pose_request_thread_->join(); + pose_request_thread_.reset(); + bridge_->Stop(); } -void SlimeVRDriver::VRDriver::RunFrame() -{ - google::protobuf::Arena arena; - - // Collect events - vr::VREvent_t event; - std::vector events; - while (vr::VRServerDriverHost()->PollNextEvent(&event, sizeof(event))) - { - events.push_back(event); - } - this->openvr_events_ = std::move(events); - - // Update frame timing - std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - this->frame_timing_ = std::chrono::duration_cast(now - this->last_frame_time_); - this->last_frame_time_ = now; - - // Update devices - for(auto& device : this->devices_) - device->Update(); - - BridgeStatus status = runBridgeFrame(*this); - if(status == BRIDGE_CONNECTED) { - messages::ProtobufMessage* message = google::protobuf::Arena::CreateMessage(&arena); - // Read all messages from the bridge - while(getNextBridgeMessage(*message, *this)) { - if(message->has_tracker_added()) { - messages::TrackerAdded ta = message->tracker_added(); - switch(getDeviceType(static_cast(ta.tracker_role()))) { - case DeviceType::TRACKER: - this->AddDevice(std::make_shared(ta.tracker_serial(), ta.tracker_id(), static_cast(ta.tracker_role()))); - Log("New tracker device added " + ta.tracker_serial() + " (id " + std::to_string(ta.tracker_id()) + ")"); - break; - } - } else if(message->has_position()) { - messages::Position pos = message->position(); - auto device = this->devices_by_id.find(pos.tracker_id()); - if(device != this->devices_by_id.end()) { - device->second->PositionMessage(pos); - } - } else if(message->has_tracker_status()) { - messages::TrackerStatus status = message->tracker_status(); - auto device = this->devices_by_id.find(status.tracker_id()); - if (device != this->devices_by_id.end()) { - device->second->StatusMessage(status); - } - } else if (message->has_battery()) { - messages::Battery bat = message->battery(); - auto device = this->devices_by_id.find(bat.tracker_id()); - if (device != this->devices_by_id.end()) { - device->second->BatteryMessage(bat); - } - } +void SlimeVRDriver::VRDriver::RunPoseRequestThread() { + logger_->Log("Pose request thread started"); + while (!exiting_pose_request_thread_) { + if (!bridge_->IsConnected()) { + // If bridge not connected, assume we need to resend hmd tracker add message + send_hmd_add_message_ = false; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; } - if(!sentHmdAddMessage) { + messages::ProtobufMessage* message = google::protobuf::Arena::CreateMessage(&arena_); + + if (!send_hmd_add_message_) { // Send add message for HMD - messages::TrackerAdded* trackerAdded = google::protobuf::Arena::CreateMessage(&arena); - message->set_allocated_tracker_added(trackerAdded); - trackerAdded->set_tracker_id(0); - trackerAdded->set_tracker_role(TrackerRole::HMD); - trackerAdded->set_tracker_serial("HMD"); - trackerAdded->set_tracker_name("HMD"); - sendBridgeMessage(*message, *this); - - messages::TrackerStatus* trackerStatus = google::protobuf::Arena::CreateMessage(&arena); - message->set_allocated_tracker_status(trackerStatus); - trackerStatus->set_tracker_id(0); - trackerStatus->set_status(messages::TrackerStatus_Status::TrackerStatus_Status_OK); - sendBridgeMessage(*message, *this); - - sentHmdAddMessage = true; - Log("Sent HMD hello message"); + messages::TrackerAdded* tracker_added = google::protobuf::Arena::CreateMessage(&arena_); + message->set_allocated_tracker_added(tracker_added); + tracker_added->set_tracker_id(0); + tracker_added->set_tracker_role(TrackerRole::HMD); + tracker_added->set_tracker_serial("HMD"); + tracker_added->set_tracker_name("HMD"); + bridge_->SendBridgeMessage(*message); + + messages::TrackerStatus* tracker_status = google::protobuf::Arena::CreateMessage(&arena_); + message->set_allocated_tracker_status(tracker_status); + tracker_status->set_tracker_id(0); + tracker_status->set_status(messages::TrackerStatus_Status::TrackerStatus_Status_OK); + bridge_->SendBridgeMessage(*message); + + send_hmd_add_message_ = true; + logger_->Log("Sent HMD hello message"); } - uint64_t universe = vr::VRProperties()->GetUint64Property(vr::VRProperties()->TrackedDeviceToPropertyContainer(0), vr::Prop_CurrentUniverseId_Uint64); - if (!current_universe.has_value() || current_universe.value().first != universe) { - auto res = search_universes(universe); - if (res.has_value()) { - current_universe.emplace(universe, res.value()); - } else { - Log("Failed to find current universe!"); + vr::PropertyContainerHandle_t hmd_prop_container = + vr::VRProperties()->TrackedDeviceToPropertyContainer(vr::k_unTrackedDeviceIndex_Hmd); + + vr::ETrackedPropertyError universe_error; + uint64_t universe = vr::VRProperties()->GetUint64Property(hmd_prop_container, vr::Prop_CurrentUniverseId_Uint64, &universe_error); + if (universe_error == vr::ETrackedPropertyError::TrackedProp_Success) { + if (!current_universe_.has_value() || current_universe_.value().first != universe) { + auto result = SearchUniverses(universe); + if (result.has_value()) { + current_universe_.emplace(universe, result.value()); + logger_->Log("Found current universe"); + } else { + logger_->Log("Failed to find current universe!"); + } } + } else if (universe_error != last_universe_error_) { + logger_->Log("Failed to find current universe: Prop_CurrentUniverseId_Uint64 error = %s", + vr::VRPropertiesRaw()->GetPropErrorNameFromEnum(universe_error) + ); } + last_universe_error_ = universe_error; - vr::TrackedDevicePose_t hmd_pose[10]; - vr::VRServerDriverHost()->GetRawTrackedDevicePoses(0, hmd_pose, 10); + vr::TrackedDevicePose_t hmd_pose; + vr::VRServerDriverHost()->GetRawTrackedDevicePoses( + vr::k_unTrackedDeviceIndex_Hmd, &hmd_pose, 1); - vr::HmdQuaternion_t q = GetRotation(hmd_pose[0].mDeviceToAbsoluteTracking); - vr::HmdVector3_t pos = GetPosition(hmd_pose[0].mDeviceToAbsoluteTracking); + vr::HmdQuaternion_t q = GetRotation(hmd_pose.mDeviceToAbsoluteTracking); + vr::HmdVector3_t pos = GetPosition(hmd_pose.mDeviceToAbsoluteTracking); - if (current_universe.has_value()) { - auto trans = current_universe.value().second; + if (current_universe_.has_value()) { + auto trans = current_universe_.value().second; pos.v[0] += trans.translation.v[0]; pos.v[1] += trans.translation.v[1]; pos.v[2] += trans.translation.v[2]; @@ -159,67 +135,128 @@ void SlimeVRDriver::VRDriver::RunFrame() pos.v[2] = pos_z; } - messages::Position* hmdPosition = google::protobuf::Arena::CreateMessage(&arena); - message->set_allocated_position(hmdPosition); - - hmdPosition->set_tracker_id(0); - hmdPosition->set_data_source(messages::Position_DataSource_FULL); - hmdPosition->set_x(pos.v[0]); - hmdPosition->set_y(pos.v[1]); - hmdPosition->set_z(pos.v[2]); - hmdPosition->set_qx((float) q.x); - hmdPosition->set_qy((float) q.y); - hmdPosition->set_qz((float) q.z); - hmdPosition->set_qw((float) q.w); - - sendBridgeMessage(*message, *this); - - vr::ETrackedPropertyError err; - if (vr::VRProperties()->GetBoolProperty(vr::VRProperties()->TrackedDeviceToPropertyContainer(0), vr::Prop_DeviceProvidesBatteryStatus_Bool, &err) == true) { - messages::Battery* hmdBattery = google::protobuf::Arena::CreateMessage(&arena); - message->set_allocated_battery(hmdBattery); - hmdBattery->set_tracker_id(0); - hmdBattery->set_battery_level(vr::VRProperties()->GetFloatProperty(vr::VRProperties()->TrackedDeviceToPropertyContainer(0), vr::Prop_DeviceBatteryPercentage_Float, &err) * 100); - hmdBattery->set_is_charging(vr::VRProperties()->GetBoolProperty(vr::VRProperties()->TrackedDeviceToPropertyContainer(0), vr::Prop_DeviceIsCharging_Bool, &err)); - sendBridgeMessage(*message, *this); + messages::Position* hmd_position = google::protobuf::Arena::CreateMessage(&arena_); + message->set_allocated_position(hmd_position); + hmd_position->set_tracker_id(0); + hmd_position->set_data_source(messages::Position_DataSource_FULL); + hmd_position->set_x(pos.v[0]); + hmd_position->set_y(pos.v[1]); + hmd_position->set_z(pos.v[2]); + hmd_position->set_qx((float) q.x); + hmd_position->set_qy((float) q.y); + hmd_position->set_qz((float) q.z); + hmd_position->set_qw((float) q.w); + bridge_->SendBridgeMessage(*message); + + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - battery_sent_at_).count() > 100) { + vr::ETrackedPropertyError err; + if (vr::VRProperties()->GetBoolProperty(hmd_prop_container, vr::Prop_DeviceProvidesBatteryStatus_Bool, &err)) { + messages::Battery* hmdBattery = google::protobuf::Arena::CreateMessage(&arena_); + message->set_allocated_battery(hmdBattery); + hmdBattery->set_tracker_id(0); + hmdBattery->set_battery_level(vr::VRProperties()->GetFloatProperty(hmd_prop_container, vr::Prop_DeviceBatteryPercentage_Float, &err) * 100); + hmdBattery->set_is_charging(vr::VRProperties()->GetBoolProperty(hmd_prop_container, vr::Prop_DeviceIsCharging_Bool, &err)); + bridge_->SendBridgeMessage(*message); + } + battery_sent_at_ = now; } - } else { - // If bridge not connected, assume we need to resend hmd tracker add message - sentHmdAddMessage = false; + arena_.Reset(); + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + logger_->Log("Pose request thread exited"); +} + +void SlimeVRDriver::VRDriver::RunFrame() { + // Collect events + vr::VREvent_t event; + std::vector events; + while (vr::VRServerDriverHost()->PollNextEvent(&event, sizeof(event))) { + events.push_back(event); + } + openvr_events_ = std::move(events); + + // Update frame timing + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + frame_timing_ = std::chrono::duration_cast(now - last_frame_time_); + last_frame_time_ = now; + + // Update devices + { + std::lock_guard lock(devices_mutex_); + for (auto& device : devices_) { + device->Update(); + } + } +} + +void SlimeVRDriver::VRDriver::OnBridgeMessage(const messages::ProtobufMessage& message) { + std::lock_guard lock(devices_mutex_); + if (message.has_tracker_added()) { + messages::TrackerAdded ta = message.tracker_added(); + switch(GetDeviceType(static_cast(ta.tracker_role()))) { + case DeviceType::TRACKER: + AddDevice(std::make_shared(ta.tracker_serial(), ta.tracker_id(), static_cast(ta.tracker_role()))); + break; + } + } else if (message.has_position()) { + messages::Position pos = message.position(); + auto device = devices_by_id_.find(pos.tracker_id()); + if (device != devices_by_id_.end()) { + device->second->PositionMessage(pos); + } + } else if (message.has_tracker_status()) { + messages::TrackerStatus status = message.tracker_status(); + auto device = devices_by_id_.find(status.tracker_id()); + if (device != devices_by_id_.end()) { + device->second->StatusMessage(status); + static const std::unordered_map status_map = { + { messages::TrackerStatus_Status_OK, "OK" }, + { messages::TrackerStatus_Status_DISCONNECTED, "DISCONNECTED" }, + { messages::TrackerStatus_Status_ERROR, "ERROR" }, + { messages::TrackerStatus_Status_BUSY, "BUSY" }, + }; + if (status_map.count(status.status())) { + logger_->Log("Tracker status id %i status %s", status.tracker_id(), status_map.at(status.status()).c_str()); + } + } + } else if (message.has_battery()) { + messages::Battery bat = message.battery(); + auto device = this->devices_by_id_.find(bat.tracker_id()); + if (device != this->devices_by_id_.end()) { + device->second->BatteryMessage(bat); + } } } -bool SlimeVRDriver::VRDriver::ShouldBlockStandbyMode() -{ +bool SlimeVRDriver::VRDriver::ShouldBlockStandbyMode() { return false; } -void SlimeVRDriver::VRDriver::EnterStandby() -{ +void SlimeVRDriver::VRDriver::EnterStandby() { } -void SlimeVRDriver::VRDriver::LeaveStandby() -{ +void SlimeVRDriver::VRDriver::LeaveStandby() { } -std::vector> SlimeVRDriver::VRDriver::GetDevices() -{ - return this->devices_; +std::vector> SlimeVRDriver::VRDriver::GetDevices() { + std::lock_guard lock(devices_mutex_); + std::vector> devices; + devices.assign(devices.begin(), devices.end()); + return devices; } -std::vector SlimeVRDriver::VRDriver::GetOpenVREvents() -{ - return this->openvr_events_; +std::vector SlimeVRDriver::VRDriver::GetOpenVREvents() { + return openvr_events_; } -std::chrono::milliseconds SlimeVRDriver::VRDriver::GetLastFrameTime() -{ - return this->frame_timing_; +std::chrono::milliseconds SlimeVRDriver::VRDriver::GetLastFrameTime() { + return frame_timing_; } -bool SlimeVRDriver::VRDriver::AddDevice(std::shared_ptr device) -{ +bool SlimeVRDriver::VRDriver::AddDevice(std::shared_ptr device) { vr::ETrackedDeviceClass openvr_device_class; // Remember to update this switch when new device types are added switch (device->GetDeviceType()) { @@ -238,25 +275,31 @@ bool SlimeVRDriver::VRDriver::AddDevice(std::shared_ptr device) default: return false; } - bool result = vr::VRServerDriverHost()->TrackedDeviceAdded(device->GetSerial().c_str(), openvr_device_class, device.get()); - if(result) { - this->devices_.push_back(device); - this->devices_by_id[device->getDeviceId()] = device; - this->devices_by_serial[device->GetSerial()] = device; + if (!devices_by_serial_.count(device->GetSerial())) { + bool result = vr::VRServerDriverHost()->TrackedDeviceAdded(device->GetSerial().c_str(), openvr_device_class, device.get()); + if (result) { + devices_.push_back(device); + devices_by_id_[device->GetDeviceId()] = device; + devices_by_serial_[device->GetSerial()] = device; + logger_->Log("New tracker device added %s (id %i)", device->GetSerial().c_str(), device->GetDeviceId()); + } else { + logger_->Log("Failed to add tracker device %s (id %i)", device->GetSerial().c_str(), device->GetDeviceId()); + return false; + } } else { - std::shared_ptr oldDevice = this->devices_by_serial[device->GetSerial()]; - if(oldDevice->getDeviceId() != device->getDeviceId()) { - this->devices_by_id[device->getDeviceId()] = oldDevice; - Log("Device overridden from id " + std::to_string(oldDevice->getDeviceId()) + " to " + std::to_string(device->getDeviceId()) + " for serial " + device->GetSerial()); + std::shared_ptr oldDevice = devices_by_serial_[device->GetSerial()]; + if (oldDevice->GetDeviceId() != device->GetDeviceId()) { + devices_by_id_[device->GetDeviceId()] = oldDevice; + oldDevice->SetDeviceId(device->GetDeviceId()); + logger_->Log("Device overridden from id %i to %i for serial %s", oldDevice->GetDeviceId(), device->GetDeviceId(), device->GetSerial()); } else { - Log("Device readded id " + std::to_string(device->getDeviceId()) + ", serial " + device->GetSerial()); + logger_->Log("Device readded id %i, serial %s", device->GetDeviceId(), device->GetSerial().c_str()); } } - return result; + return true; } -SlimeVRDriver::SettingsValue SlimeVRDriver::VRDriver::GetSettingsValue(std::string key) -{ +SlimeVRDriver::SettingsValue SlimeVRDriver::VRDriver::GetSettingsValue(std::string key) { vr::EVRSettingsError err = vr::EVRSettingsError::VRSettingsError_None; int int_value = vr::VRSettings()->GetInt32(settings_key_.c_str(), key.c_str(), &err); if (err == vr::EVRSettingsError::VRSettingsError_None) { @@ -283,24 +326,15 @@ SlimeVRDriver::SettingsValue SlimeVRDriver::VRDriver::GetSettingsValue(std::stri return SettingsValue(); } -void SlimeVRDriver::VRDriver::Log(std::string message) -{ - std::string message_endl = message + "\n"; - vr::VRDriverLog()->Log(message_endl.c_str()); -} - -vr::IVRDriverInput* SlimeVRDriver::VRDriver::GetInput() -{ +vr::IVRDriverInput* SlimeVRDriver::VRDriver::GetInput() { return vr::VRDriverInput(); } -vr::CVRPropertyHelpers* SlimeVRDriver::VRDriver::GetProperties() -{ +vr::CVRPropertyHelpers* SlimeVRDriver::VRDriver::GetProperties() { return vr::VRProperties(); } -vr::IVRServerDriverHost* SlimeVRDriver::VRDriver::GetDriverHost() -{ +vr::IVRServerDriverHost* SlimeVRDriver::VRDriver::GetDriverHost() { return vr::VRServerDriverHost(); } @@ -343,18 +377,18 @@ SlimeVRDriver::UniverseTranslation SlimeVRDriver::UniverseTranslation::parse(sim if (iii > 2) { break; // TODO: 4 components in a translation vector? should this be an error? } - res.translation.v[iii] = component.get_double(); + res.translation.v[iii] = static_cast(component.get_double()); iii += 1; } - res.yaw = obj["yaw"].get_double(); + res.yaw = static_cast(obj["yaw"].get_double()); return res; } -std::optional SlimeVRDriver::VRDriver::search_universe(std::string path, uint64_t target) { +std::optional SlimeVRDriver::VRDriver::SearchUniverse(std::string path, uint64_t target) { try { auto json = simdjson::padded_string::load(path); // load VR Path Registry - simdjson::ondemand::document doc = json_parser.iterate(json); + simdjson::ondemand::document doc = json_parser_.iterate(json); for (simdjson::ondemand::object uni: doc["universes"]) { // TODO: universeID comes after the translation, would it be faster to unconditionally parse the translation? @@ -374,35 +408,33 @@ std::optional SlimeVRDriver::VRDriver::searc } } } catch (simdjson::simdjson_error& e) { - std::stringstream ss; - ss << "Error getting universes from \"" << path << "\": " << e.error(); - Log(ss.str()); + logger_->Log("Error getting universes from %s: %s", path.c_str(), e.what()); return std::nullopt; } return std::nullopt; } -std::optional SlimeVRDriver::VRDriver::search_universes(uint64_t target) { +std::optional SlimeVRDriver::VRDriver::SearchUniverses(uint64_t target) { auto driver_chap_path = vr::VRProperties()->GetStringProperty(vr::VRProperties()->TrackedDeviceToPropertyContainer(0), vr::Prop_DriverProvidedChaperonePath_String); if (driver_chap_path != "") { - auto driver_res = search_universe(driver_chap_path, target); + auto driver_res = SearchUniverse(driver_chap_path, target); if (driver_res.has_value()) { return driver_res.value(); } } if (default_chap_path_.has_value()) { - return search_universe(default_chap_path_.value(), target); + return SearchUniverse(default_chap_path_.value(), target); } return std::nullopt; } std::optional SlimeVRDriver::VRDriver::GetCurrentUniverse() { - if (current_universe.has_value()) { - return current_universe.value().second; - } else { - return std::nullopt; + if (current_universe_.has_value()) { + return current_universe_.value().second; } + + return std::nullopt; } \ No newline at end of file diff --git a/src/VRDriver.hpp b/src/VRDriver.hpp index b8b5a47..fced819 100644 --- a/src/VRDriver.hpp +++ b/src/VRDriver.hpp @@ -12,17 +12,21 @@ #include +#include "bridge/BridgeClient.hpp" +#include "Logger.hpp" + namespace SlimeVRDriver { class VRDriver : public IVRDriver { public: - // Inherited via IVRDriver virtual std::vector> GetDevices() override; virtual std::vector GetOpenVREvents() override; virtual std::chrono::milliseconds GetLastFrameTime() override; virtual bool AddDevice(std::shared_ptr device) override; virtual SettingsValue GetSettingsValue(std::string key) override; - virtual void Log(std::string message) override; + virtual void Log(std::string message) override { + logger_->Log("%s", message.c_str()); + }; virtual vr::IVRDriverInput* GetInput() override; virtual vr::CVRPropertyHelpers* GetProperties() override; @@ -39,27 +43,38 @@ namespace SlimeVRDriver { virtual std::optional GetCurrentUniverse() override; + void OnBridgeMessage(const messages::ProtobufMessage& message); + void RunPoseRequestThread(); + private: + std::unique_ptr pose_request_thread_ = nullptr; + std::atomic exiting_pose_request_thread_ = false; + + std::shared_ptr bridge_ = nullptr; + google::protobuf::Arena arena_; + std::shared_ptr logger_ = std::make_shared(); + std::mutex devices_mutex_; std::vector> devices_; std::vector openvr_events_; - std::map> devices_by_id; - std::map> devices_by_serial; + std::map> devices_by_id_; + std::map> devices_by_serial_; std::chrono::milliseconds frame_timing_ = std::chrono::milliseconds(16); - std::chrono::system_clock::time_point last_frame_time_ = std::chrono::system_clock::now(); + std::chrono::steady_clock::time_point last_frame_time_ = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point battery_sent_at_ = std::chrono::steady_clock::now(); std::string settings_key_ = "driver_slimevr"; vr::HmdQuaternion_t GetRotation(vr::HmdMatrix34_t &matrix); vr::HmdVector3_t GetPosition(vr::HmdMatrix34_t &matrix); - bool sentHmdAddMessage = false; + bool send_hmd_add_message_ = false; - simdjson::ondemand::parser json_parser; + simdjson::ondemand::parser json_parser_; std::optional default_chap_path_ = std::nullopt; //std::map universes; - std::optional> current_universe = std::nullopt; - - std::optional search_universe(std::string path, uint64_t target); - std::optional search_universes(uint64_t target); + vr::ETrackedPropertyError last_universe_error_; + std::optional> current_universe_ = std::nullopt; + std::optional SearchUniverse(std::string path, uint64_t target); + std::optional SearchUniverses(uint64_t target); }; }; \ No newline at end of file diff --git a/src/bridge/BridgeClient.cpp b/src/bridge/BridgeClient.cpp new file mode 100644 index 0000000..097d81d --- /dev/null +++ b/src/bridge/BridgeClient.cpp @@ -0,0 +1,80 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#include "BridgeClient.hpp" + +using namespace std::literals::chrono_literals; + +void BridgeClient::CreateConnection() { + ResetBuffers(); + + if (!last_error_.has_value()) { + logger_->Log("connecting"); + } + + std::string path = GetBridgePath(); + + /* ipc = false -> pipe will be used for handle passing between processes? no */ + connection_handle_ = GetLoop()->resource(false); + connection_handle_->on([this](const uvw::connect_event&, uvw::pipe_handle&) { + connection_handle_->read(); + logger_->Log("connected"); + connected_ = true; + last_error_ = std::nullopt; + }); + connection_handle_->on([this](const uvw::end_event&, uvw::pipe_handle&) { + logger_->Log("disconnected"); + Reconnect(); + }); + connection_handle_->on([this](const uvw::data_event& event, uvw::pipe_handle&) { + OnRecv(event); + }); + connection_handle_->on([this, path](const uvw::error_event& event, uvw::pipe_handle&) { + if (!last_error_.has_value() || last_error_ != event.what()) { + logger_->Log("[%s] pipe error: %s", path.c_str(), event.what()); + last_error_ = event.what(); + } + Reconnect(); + }); + + connection_handle_->connect(path); +} + +void BridgeClient::ResetConnection() { + Reconnect(); +} + +void BridgeClient::Reconnect() { + CloseConnectionHandles(); + reconnect_timeout_ = GetLoop()->resource(); + reconnect_timeout_->start(1000ms, 0ms); + reconnect_timeout_->on([this](const uvw::timer_event&, uvw::timer_handle& handle) { + CreateConnection(); + handle.close(); + }); +} + +void BridgeClient::CloseConnectionHandles() { + if (connection_handle_) connection_handle_->close(); + if (reconnect_timeout_) reconnect_timeout_->close(); + connected_ = false; +} diff --git a/src/bridge/BridgeClient.hpp b/src/bridge/BridgeClient.hpp new file mode 100644 index 0000000..ee75fa1 --- /dev/null +++ b/src/bridge/BridgeClient.hpp @@ -0,0 +1,55 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#pragma once + +#include +#include +#include + +#include "BridgeTransport.hpp" + +/** + * @brief Client implementation for communication with SlimeVR Server using pipes or unix sockets. + * + * This class provides a set of methods to start, stop an IO thread, send messages over a named pipe or unix socket + * and is abstracted through `libuv`. + * + * When a message is received and parsed from the pipe, the messageCallback function passed in the constructor is called + * from the event loop thread with the message as a parameter. + * + * @param logger A shared pointer to an Logger object to log messages from the transport. + * @param on_message_received A function to be called from event loop thread when a message is received and parsed from the pipe. + */ +class BridgeClient: public BridgeTransport { +public: + using BridgeTransport::BridgeTransport; + +private: + void CreateConnection() override; + void ResetConnection() override; + void CloseConnectionHandles() override; + void Reconnect(); + + std::optional last_error_; + std::shared_ptr reconnect_timeout_; +}; \ No newline at end of file diff --git a/src/bridge/BridgeTransport.cpp b/src/bridge/BridgeTransport.cpp new file mode 100644 index 0000000..2f9a94b --- /dev/null +++ b/src/bridge/BridgeTransport.cpp @@ -0,0 +1,138 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#include "BridgeTransport.hpp" + +void BridgeTransport::Start() { + thread_ = std::make_unique(&BridgeTransport::RunThread, this); +} + +void BridgeTransport::Stop() { + if (!thread_ || !thread_->joinable()) return; + StopAsync(); + logger_->Log("stopping"); + thread_->join(); + thread_.reset(); +} + +void BridgeTransport::StopAsync() { + if (!stop_signal_handle_ || stop_signal_handle_->closing()) return; + stop_signal_handle_->send(); +} + +void BridgeTransport::RunThread() { + logger_->Log("thread started"); + loop_ = uvw::loop::create(); + stop_signal_handle_ = GetLoop()->resource(); + write_signal_handle_ = GetLoop()->resource(); + + stop_signal_handle_->on([this](const uvw::async_event&, uvw::async_handle& handle) { + logger_->Log("closing handles"); + CloseConnectionHandles(); + write_signal_handle_->close(); + stop_signal_handle_->close(); + }); + + write_signal_handle_->on([this](const uvw::async_event&, uvw::async_handle& handle) { + SendWrites(); + }); + + CreateConnection(); + GetLoop()->run(); + GetLoop()->close(); + logger_->Log("thread exited"); +} + +void BridgeTransport::ResetBuffers() { + recv_buf_.Clear(); + send_buf_.Clear(); +} + +void BridgeTransport::OnRecv(const uvw::data_event& event) { + if (!recv_buf_.Push(event.data.get(), event.length)) { + logger_->Log("recv_buf_.Push(%i) failed", event.length); + ResetConnection(); + return; + } + + size_t available; + while (available = recv_buf_.BytesAvailable()) { + if (available < 4) return; + + char len_buf[4]; + recv_buf_.Peek(len_buf, 4); + uint32_t size = LE32_TO_NATIVE(*reinterpret_cast(len_buf)); + + if (size > VRBRIDGE_MAX_MESSAGE_SIZE) { + logger_->Log("message size overflow"); + ResetConnection(); + return; + } + + auto unwrapped_size = size - 4; + if (available < unwrapped_size) return; + + auto message_buf = std::make_unique(size); + if (!recv_buf_.Skip(4) || !recv_buf_.Pop(message_buf.get(), unwrapped_size)) { + logger_->Log("recv_buf_.Pop(%i) failed", size); + ResetConnection(); + return; + } + + messages::ProtobufMessage message; + if (message.ParseFromArray(message_buf.get(), unwrapped_size)) { + message_callback_(message); + } else { + logger_->Log("receivedMessage.ParseFromArray failed"); + ResetConnection(); + return; + } + } +} + +void BridgeTransport::SendBridgeMessage(const messages::ProtobufMessage& message) { + if (!IsConnected()) return; + + uint32_t size = static_cast(message.ByteSizeLong()); + uint32_t wrapped_size = size + 4; + + auto message_buf = std::make_unique(wrapped_size); + *reinterpret_cast(message_buf.get()) = NATIVE_TO_LE32(wrapped_size); + message.SerializeToArray(message_buf.get() + 4, size); + if (!send_buf_.Push(message_buf.get(), wrapped_size)) { + ResetConnection(); + return; + } + + write_signal_handle_->send(); +} + +void BridgeTransport::SendWrites() { + if (!IsConnected()) return; + + auto available = send_buf_.BytesAvailable(); + if (!available) return; + + auto write_buf = std::make_unique(available); + send_buf_.Pop(write_buf.get(), available); + connection_handle_->write(write_buf.get(), static_cast(available)); +} \ No newline at end of file diff --git a/src/bridge/BridgeTransport.hpp b/src/bridge/BridgeTransport.hpp new file mode 100644 index 0000000..def23d9 --- /dev/null +++ b/src/bridge/BridgeTransport.hpp @@ -0,0 +1,178 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#pragma once + +#include +#include +#include +#include + +#include "Logger.hpp" +#include "CircularBuffer.hpp" +#include "ProtobufMessages.pb.h" + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + #define LE32_TO_NATIVE(x) (x) +#else + #define LE32_TO_NATIVE(x) ( \ + ((uint32_t)(x) << 24) | \ + (((uint32_t)(x) << 8) & 0x00FF0000) | \ + (((uint32_t)(x) >> 8) & 0x0000FF00) | \ + ((uint32_t)(x) >> 24) \ + ) +#endif +#define NATIVE_TO_LE32 LE32_TO_NATIVE + +#define VRBRIDGE_MAX_MESSAGE_SIZE 1024 +#define VRBRIDGE_BUFFERS_SIZE 8192 + +namespace fs = std::filesystem; + +#define WINDOWS_PIPE_NAME "\\\\.\\pipe\\SlimeVRDriver" +#define UNIX_XDG_DATA_DIR_DEFAULT ".local/share/" +#define UNIX_SLIMEVR_DIR "slimevr" +#define UNIX_TMP_DIR "/tmp" +#define UNIX_SOCKET_NAME "SlimeVRDriver" + +/** + * @brief Passes messages between SlimeVR Server and SteamVR Driver using pipes or unix sockets. + * + * Client or Server connection handling is implemented by extending this class. + * + * This class provides a set of methods to start, stop an IO thread, send messages over a named pipe or unix socket + * and is abstracted through `libuv`. + * + * When a message is received and parsed from the pipe, the messageCallback function passed in the constructor is called + * from the libuv event loop thread with the message as a parameter. + * + * @param logger A shared pointer to an Logger object to log messages from the transport. + * @param on_message_received A function to be called from event loop thread when a message is received and parsed from the pipe. + */ +class BridgeTransport { +public: + BridgeTransport(std::shared_ptr logger, std::function on_message_received) : + logger_(logger), + message_callback_(on_message_received), + send_buf_(VRBRIDGE_BUFFERS_SIZE), + recv_buf_(VRBRIDGE_BUFFERS_SIZE) + { } + + ~BridgeTransport() { + Stop(); + } + + /** + * @brief Starts the channel by creating a thread with an libuv event loop. + * + * Connects and automatic reconnects with a timeout are implemented internally. + */ + void Start(); + + /** + * @brief Stops the channel by stopping the libuv event loop and closing the connection handles. + * + * Blocks until the event loop is stopped and the connection handles are closed. + */ + void Stop(); + + /** + * @brief Stops the channel asynchronously by sending a signal to the libuv event loop to stop and returning immediately. + * + * The `Stop()` function calls this method. + */ + void StopAsync(); + + /** + * @brief Sends a message over the channel. + * + * Queues the message to the send buffer to be sent over the pipe. + * + * @param message The message to send. + */ + void SendBridgeMessage(const messages::ProtobufMessage& message); + + /** + * @brief Checks if the channel is connected. + * + * @return true if the channel is connected, false otherwise. + */ + bool IsConnected() { + return connected_; + }; + +protected: + virtual void CreateConnection() = 0; + virtual void ResetConnection() = 0; + virtual void CloseConnectionHandles() = 0; + void ResetBuffers(); + void OnRecv(const uvw::data_event& event); + auto GetLoop() { + return loop_; + } + + static std::string GetBridgePath() { +#ifdef __linux__ + std::vector paths = { }; + if (const char* ptr = std::getenv("XDG_RUNTIME_DIR")) { + const fs::path xdg_runtime = ptr; + paths.push_back((xdg_runtime / UNIX_SOCKET_NAME).string()); + } + + if (const char* ptr = std::getenv("XDG_DATA_DIR")) { + const fs::path xdg_data = ptr; + paths.push_back((xdg_data / UNIX_SLIMEVR_DIR / UNIX_SOCKET_NAME).string()); + } + + if (const char* ptr = std::getenv("HOME")) { + const fs::path home = ptr; + paths.push_back((home / UNIX_XDG_DATA_DIR_DEFAULT / UNIX_SLIMEVR_DIR / UNIX_SOCKET_NAME).string()); + } + + for (auto path : paths) { + if (fs::exists(path)) { + return path; + } + } + + return (fs::path(UNIX_TMP_DIR) / UNIX_SOCKET_NAME).string(); +#else + return WINDOWS_PIPE_NAME; +#endif + } + + std::shared_ptr logger_; + std::atomic connected_ = false; + std::shared_ptr connection_handle_ = nullptr; + +private: + void RunThread(); + void SendWrites(); + + CircularBuffer send_buf_; + CircularBuffer recv_buf_; + std::shared_ptr stop_signal_handle_ = nullptr; + std::shared_ptr write_signal_handle_ = nullptr; + std::unique_ptr thread_ = nullptr; + std::shared_ptr loop_ = nullptr; + const std::function message_callback_; +}; \ No newline at end of file diff --git a/src/bridge/CircularBuffer.hpp b/src/bridge/CircularBuffer.hpp new file mode 100644 index 0000000..4994887 --- /dev/null +++ b/src/bridge/CircularBuffer.hpp @@ -0,0 +1,148 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#pragma once + +#include +#include +#include +#include +#include + +/** + * A fixed-size queue using contiguous memory ONLY for a single producer and a single consumer (SPSC). + * + * @param size Size of the queue in bytes. + */ +class CircularBuffer { +public: + /** + * Constructs a fixed-size queue using contiguous memory. + * + * @param size Size of the queue in bytes. + */ + CircularBuffer(size_t size) : + size_(size), + buffer_(std::make_unique(size)) + { } + ~CircularBuffer() = default; + + /** + * Pushes data into the queue. + * + * @param data A pointer to the data to be pushed. + * @param size Number of bytes to push. + * @return True if the data was successfully pushed, false if the queue is full. + */ + bool Push(const char* data, size_t size) { + if (size > BytesFree()) return false; + size_t size1 = std::min(size, size_ - (head_ % size_)); + size_t size2 = size - size1; + std::memcpy(buffer_.get() + (head_ % size_), data, size1); + std::memcpy(buffer_.get(), data + size1, size2); + head_ += size; + count_ += size; + return true; + } + + /** + * Pops data from the queue. + * + * @param data A pointer to the location where the data should be stored. + * @param size Number of bytes to pop. + * @return True if the data was successfully popped, false if there is not enough data. + */ + bool Pop(char* data, size_t size) { + if (size > BytesAvailable()) return false; + size_t size1 = std::min(size, size_ - (tail_ % size_)); + size_t size2 = size - size1; + std::memcpy(data, buffer_.get() + (tail_ % size_ ), size1); + std::memcpy(data + size1, buffer_.get(), size2); + tail_ += size; + count_ -= size; + return true; + } + + /** + * Copies data from the queue into the given data pointer, without removing it. + * + * @param data A pointer to the location where the data should be copied to. + * @param size Number of bytes to peek. + * @return Number of bytes actually copied, 0 if there is not enough data. + */ + size_t Peek(char* data, size_t size) { + size_t available = BytesAvailable(); + if (size > available) return 0; + size_t size1 = std::min(size, size_ - (tail_ % size_)); + size_t size2 = std::min(size - size1, available - size1); + std::memcpy(data, buffer_.get() + (tail_ % size_), size1); + std::memcpy(data + size1, buffer_.get(), size2); + return size1 + size2; + } + + /** + * Skips n bytes in the queue. + * + * @param n Number of bytes to skip. + * @return True if n bytes were successfully skipped, false if n is greater than the number of bytes available in the queue. + */ + bool Skip(size_t n) { + if (n > BytesAvailable()) return false; + tail_ += n; + count_ -= n; + return true; + } + + /** + * Clears the queue. + */ + void Clear() { + head_ = 0; + tail_ = 0; + count_ = 0; + } + + /** + * Returns the number of bytes available in the queue. + * + * @return Number of bytes available in the queue. + */ + size_t BytesAvailable() const { + return count_; + } + + /** + * Returns the number of free bytes in the queue. + * + * @return Number of free bytes in the queue. + */ + size_t BytesFree() const { + return size_ - BytesAvailable(); + } + +private: + const size_t size_; + std::unique_ptr buffer_ = nullptr; + std::atomic head_ = 0; + std::atomic tail_ = 0; + std::atomic count_ = 0; +}; \ No newline at end of file diff --git a/src/bridge/bridge-unix-sockets.cpp b/src/bridge/bridge-unix-sockets.cpp deleted file mode 100644 index 8a78bc3..0000000 --- a/src/bridge/bridge-unix-sockets.cpp +++ /dev/null @@ -1,198 +0,0 @@ -/* - SlimeVR Code is placed under the MIT license - Copyright (c) 2021 Eiren Rain - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ -/** - * Linux specific IPC between SteamVR driver/app and SlimeVR server based - * on unix sockets - */ -#include "bridge.hpp" -#ifdef __linux__ -#include "unix-sockets.hpp" -#include -#include -#include -#include - -#define TMP_DIR "/tmp" -#define XDG_DATA_DIR_DEFAULT ".local/share" -#define SLIMEVR_DATA_DIR "slimevr" -#define SOCKET_NAME "SlimeVRDriver" - -namespace fs = std::filesystem; -namespace { - -inline constexpr int HEADER_SIZE = 4; -/// @return iterator after header -template -std::optional WriteHeader(TBufIt bufBegin, int bufSize, int msgSize) { - const int totalSize = msgSize + HEADER_SIZE; // include header bytes in total size - if (bufSize < totalSize) return std::nullopt; // header won't fit - - const auto size = static_cast(totalSize); - TBufIt it = bufBegin; - *(it++) = static_cast(size); - *(it++) = static_cast(size >> 8U); - *(it++) = static_cast(size >> 16U); - *(it++) = static_cast(size >> 24U); - return it; -} - -/// @return iterator after header -template -std::optional ReadHeader(TBufIt bufBegin, int numBytesRecv, int& outMsgSize) { - if (numBytesRecv < HEADER_SIZE) return std::nullopt; // header won't fit - - uint32_t size = 0; - TBufIt it = bufBegin; - size = static_cast(*(it++)); - size |= static_cast(*(it++)) << 8U; - size |= static_cast(*(it++)) << 16U; - size |= static_cast(*(it++)) << 24U; - - const auto totalSize = static_cast(size); - if (totalSize < HEADER_SIZE) return std::nullopt; - outMsgSize = totalSize - HEADER_SIZE; - return it; -} - -BasicLocalClient client{}; - -inline constexpr int BUFFER_SIZE = 1024; -using ByteBuffer = std::array; -ByteBuffer byteBuffer; - -} - -bool getNextBridgeMessage(messages::ProtobufMessage& message, SlimeVRDriver::VRDriver& driver) { - if (!client.IsOpen()) return false; - - int bytesRecv = 0; - try { - bytesRecv = client.RecvOnce(byteBuffer.begin(), HEADER_SIZE); - } catch (const std::exception& e) { - client.Close(); - driver.Log("bridge send error: " + std::string(e.what())); - return false; - } - if (bytesRecv == 0) return false; // no message waiting - - int msgSize = 0; - const std::optional msgBeginIt = ReadHeader(byteBuffer.begin(), bytesRecv, msgSize); - if (!msgBeginIt) { - driver.Log("bridge recv error: invalid message header or size"); - return false; - } - if (msgSize <= 0) { - driver.Log("bridge recv error: empty message"); - return false; - } - try { - if (!client.RecvAll(*msgBeginIt, msgSize)) { - driver.Log("bridge recv error: client closed"); - return false; - } - } catch (const std::exception& e) { - client.Close(); - driver.Log("bridge send error: " + std::string(e.what())); - return false; - } - if (!message.ParseFromArray(&(**msgBeginIt), msgSize)) { - driver.Log("bridge recv error: failed to parse"); - return false; - } - - return true; -} - -bool sendBridgeMessage(messages::ProtobufMessage& message, SlimeVRDriver::VRDriver& driver) { - if (!client.IsOpen()) return false; - const auto bufBegin = byteBuffer.begin(); - const auto bufferSize = static_cast(std::distance(bufBegin, byteBuffer.end())); - const auto msgSize = static_cast(message.ByteSizeLong()); - const std::optional msgBeginIt = WriteHeader(bufBegin, bufferSize, msgSize); - if (!msgBeginIt) { - driver.Log("bridge send error: failed to write header"); - return false; - } - if (!message.SerializeToArray(&(**msgBeginIt), msgSize)) { - driver.Log("bridge send error: failed to serialize"); - return false; - } - int bytesToSend = static_cast(std::distance(bufBegin, *msgBeginIt + msgSize)); - if (bytesToSend <= 0) { - driver.Log("bridge send error: empty message"); - return false; - } - if (bytesToSend > bufferSize) { - driver.Log("bridge send error: message too big"); - return false; - } - try { - return client.Send(bufBegin, bytesToSend); - } catch (const std::exception& e) { - client.Close(); - driver.Log("bridge send error: " + std::string(e.what())); - return false; - } -} - -BridgeStatus runBridgeFrame(SlimeVRDriver::VRDriver& driver) { - try { - if (!client.IsOpen()) { - fs::path socket; - // TODO: do this once in the constructor or something - if(const char* ptr = std::getenv("XDG_RUNTIME_DIR")) { - const fs::path xdg_runtime = ptr; - socket = (xdg_runtime / SOCKET_NAME); - } - if(!fs::exists(socket)) { - socket = (fs::path(TMP_DIR) / SOCKET_NAME); - } - // try using home dir if the vrserver is run in a chroot like - if(!fs::exists(socket)) { - if (const char* ptr = std::getenv("XDG_DATA_DIR")) { - const fs::path data_dir = ptr; - socket = (data_dir / SLIMEVR_DATA_DIR / SOCKET_NAME); - } else if (const char* ptr = std::getenv("HOME")) { - const fs::path home = ptr; - socket = (home / XDG_DATA_DIR_DEFAULT / SLIMEVR_DATA_DIR / SOCKET_NAME); - } - } - if(fs::exists(socket)) { - driver.Log("bridge socket: " + std::string(socket)); - client.Open(socket.native()); - } - } - client.UpdateOnce(); - - if (!client.IsOpen()) { - return BRIDGE_DISCONNECTED; - } - return BRIDGE_CONNECTED; - } catch (const std::exception& e) { - client.Close(); - driver.Log("bridge error: " + std::string(e.what())); - return BRIDGE_ERROR; - } -} - -#endif // linux diff --git a/src/bridge/bridge-windows-pipes.cpp b/src/bridge/bridge-windows-pipes.cpp deleted file mode 100644 index ef73846..0000000 --- a/src/bridge/bridge-windows-pipes.cpp +++ /dev/null @@ -1,137 +0,0 @@ -/* - SlimeVR Code is placed under the MIT license - Copyright (c) 2021 Eiren Rain - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ -/** - * Windows specific IPC between SteamVR driver/app and SlimeVR server based - * on named pipes - */ -#include "bridge.hpp" -#if defined(WIN32) && defined(BRIDGE_USE_PIPES) -#include - -#define PIPE_NAME "\\\\.\\pipe\\SlimeVRDriver" - -unsigned long lastReconnectFrame = 0; - -HANDLE pipe = INVALID_HANDLE_VALUE; -BridgeStatus currentBridgeStatus = BRIDGE_DISCONNECTED; -char buffer[1024]; - -void updatePipe(SlimeVRDriver::VRDriver &driver); -void resetPipe(SlimeVRDriver::VRDriver &driver); -void attemptPipeConnect(SlimeVRDriver::VRDriver &driver); - -BridgeStatus runBridgeFrame(SlimeVRDriver::VRDriver &driver) { - switch(currentBridgeStatus) { - case BRIDGE_DISCONNECTED: - attemptPipeConnect(driver); - break; - case BRIDGE_ERROR: - resetPipe(driver); - break; - case BRIDGE_CONNECTED: - updatePipe(driver); - break; - } - - return currentBridgeStatus; -} - -bool getNextBridgeMessage(messages::ProtobufMessage &message, SlimeVRDriver::VRDriver &driver) { - DWORD dwRead; - DWORD dwAvailable; - if(currentBridgeStatus == BRIDGE_CONNECTED) { - if(PeekNamedPipe(pipe, buffer, 4, &dwRead, &dwAvailable, NULL)) { - if(dwRead == 4) { - uint32_t messageLength = (buffer[3] << 24) | (buffer[2] << 16) | (buffer[1] << 8) | buffer[0]; - if(messageLength > 1024) { - // TODO Buffer overflow - } - if(dwAvailable >= messageLength) { - if(ReadFile(pipe, buffer, messageLength, &dwRead, NULL)) { - if(message.ParseFromArray(buffer + 4, messageLength - 4)) - return true; - } else { - currentBridgeStatus = BRIDGE_ERROR; - driver.Log("Bridge error: " + std::to_string(GetLastError())); - } - } - } - } else { - currentBridgeStatus = BRIDGE_ERROR; - driver.Log("Bridge error: " + std::to_string(GetLastError())); - } - } - return false; -} - -bool sendBridgeMessage(messages::ProtobufMessage &message, SlimeVRDriver::VRDriver &driver) { - if(currentBridgeStatus == BRIDGE_CONNECTED) { - uint32_t size = (uint32_t) message.ByteSizeLong(); - if(size > 1020) { - driver.Log("Message too big"); - return false; - } - message.SerializeToArray(buffer + 4, size); - size += 4; - buffer[0] = size & 0xFF; - buffer[1] = (size >> 8) & 0xFF; - buffer[2] = (size >> 16) & 0xFF; - buffer[3] = (size >> 24) & 0xFF; - if(WriteFile(pipe, buffer, size, NULL, NULL)) { - return true; - } - currentBridgeStatus = BRIDGE_ERROR; - driver.Log("Bridge error: " + std::to_string(GetLastError())); - } - return false; -} - -void updatePipe(SlimeVRDriver::VRDriver &driver) { -} - -void resetPipe(SlimeVRDriver::VRDriver &driver) { - if(pipe != INVALID_HANDLE_VALUE) { - CloseHandle(pipe); - pipe = INVALID_HANDLE_VALUE; - currentBridgeStatus = BRIDGE_DISCONNECTED; - driver.Log("Pipe was reset"); - } -} - -void attemptPipeConnect(SlimeVRDriver::VRDriver &driver) { - pipe = CreateFileA(PIPE_NAME, - GENERIC_READ | GENERIC_WRITE, - 0, - NULL, - OPEN_EXISTING, - 0, // TODO : Overlapped - NULL); - if(pipe != INVALID_HANDLE_VALUE) { - currentBridgeStatus = BRIDGE_CONNECTED; - driver.Log("Pipe was connected"); - return; - } -} - - -#endif // PLATFORM_WINDOWS && BRIDGE_USE_PIPES \ No newline at end of file diff --git a/src/bridge/unix-sockets.hpp b/src/bridge/unix-sockets.hpp deleted file mode 100644 index 3dd6249..0000000 --- a/src/bridge/unix-sockets.hpp +++ /dev/null @@ -1,446 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -/// AF_UNIX / local socket specific address -using sockaddr_un_t = struct sockaddr_un; -/// generic address, usually pointer argument -using sockaddr_t = struct sockaddr; -/// used as list for poll() -using pollfd_t = struct pollfd; -/// file descriptor -using Descriptor = int; - -/// std::errc or int -/// Unwrap will either return the int, or throw the errc as a system_error -class SysReturn { - static constexpr std::errc sNotAnError = std::errc(); -public: - constexpr explicit SysReturn(int value) noexcept : mCode(sNotAnError), mValue(value) {} - constexpr explicit SysReturn(std::errc code) noexcept : mCode(code), mValue() {} - constexpr bool IsError() const { return mCode != sNotAnError; } - [[noreturn]] void ThrowCode() const { throw std::system_error(std::make_error_code(mCode)); } - constexpr int Unwrap() const { if (IsError()) ThrowCode(); return mValue; } - constexpr std::errc GetCode() const { return mCode; } -private: - std::errc mCode; - int mValue; -}; - -/// call a system function and wrap the errno or int result in a SysReturn -template -[[nodiscard]] inline SysReturn SysCall(Fn&& func, Args&&... args) noexcept { - const int result = static_cast(func(std::forward(args)...)); - if (result != -1) return SysReturn(result); - return SysReturn(std::errc(errno)); -} - -/// wrap a blocking syscall and return nullopt if it would block -template -[[nodiscard]] inline std::optional SysCallBlocking(Fn&& func, Args&&... args) noexcept { - const int result = static_cast(func(std::forward(args)...)); - if (result != -1) return std::optional(result); - const auto code = static_cast(errno); - if (code == std::errc::operation_would_block || code == std::errc::resource_unavailable_try_again) { - return std::nullopt; - } - return std::optional(code); -} - -namespace event { - -enum class SockMode { - Acceptor, - Connector -}; - -/// bitmask for which events to return -using Mask = short; -inline constexpr Mask Readable = POLLIN; /// enable Readable events -inline constexpr Mask Priority = POLLPRI; /// enable Priority events -inline constexpr Mask Writable = POLLOUT; /// enable Writable events - -class Result { -public: - explicit Result(short events) : v(events) {} - bool IsReadable() const { return (v & POLLIN) != 0; } /// without blocking, connector can call read or acceptor can call accept - bool IsPriority() const { return (v & POLLPRI) != 0; } /// some exceptional condition, for tcp this is OOB data - bool IsWritable() const { return (v & POLLOUT) != 0; } /// can call write without blocking - bool IsErrored() const { return (v & POLLERR) != 0; } /// error to be checked with Socket::GetError(), or write pipe's target read pipe was closed - bool IsClosed() const { return (v & POLLHUP) != 0; } /// socket closed, however for connector, subsequent reads must be called until returns 0 - bool IsInvalid() const { return (v & POLLNVAL) != 0; } /// not an open descriptor and shouldn't be polled -private: - short v; -}; -/// poll an acceptor and its connections -class Poller { - static constexpr Mask mConnectorMask = Readable | Writable; - static constexpr Mask mAcceptorMask = Readable; -public: - void Poll(int timeoutMs) { - SysCall(::poll, mPollList.data(), mPollList.size(), timeoutMs).Unwrap(); - } - /// @tparam TPred (Descriptor, event::Result, event::SockMode) -> void - template - void Poll(int timeoutMs, TPred&& pred) { - Poll(timeoutMs); - for (const pollfd_t& elem : mPollList) { - SockMode mode = (elem.events == mAcceptorMask) ? SockMode::Acceptor : SockMode::Connector; - pred(elem.fd, Result(elem.revents), mode); - } - } - void AddConnector(Descriptor descriptor) { - mPollList.push_back({descriptor, mConnectorMask, 0}); - } - void AddAcceptor(Descriptor descriptor) { - mPollList.push_back({descriptor, mAcceptorMask, 0}); - } - Result At(int idx) const { - return Result(mPollList.at(idx).revents); - } - bool Remove(Descriptor descriptor) { - auto it = std::find_if(mPollList.begin(), mPollList.end(), - [&](const pollfd_t& elem){ return elem.fd == descriptor; }); - if (it == mPollList.end()) return false; - mPollList.erase(it); - return true; - } - void Clear() { mPollList.clear(); } - int GetSize() const { return static_cast(mPollList.size()); } -private: - std::vector mPollList{}; -}; - -} - -/// owned socket file descriptor -class Socket { - static constexpr Descriptor sInvalidSocket = -1; -public: - /// open a new socket - Socket(int domain, int type, int protocol) : mDescriptor(SysCall(::socket, domain, type, protocol).Unwrap()) { - SetNonBlocking(); - } - /// using file descriptor returned from system call - explicit Socket(Descriptor descriptor) : mDescriptor(descriptor) { - if (descriptor == sInvalidSocket) throw std::invalid_argument("invalid socket descriptor"); - SetNonBlocking(); // accepted from non-blocking socket still needs to be set - } - ~Socket() { - // owns resource and must close it, descriptor will be set invalid if moved from - // discard any errors thrown by close, most mean it never owned it or didn't exist - if (mDescriptor != sInvalidSocket) (void)SysCall(::close, mDescriptor); - } - // manage descriptor like never null unique_ptr - Socket(Socket&& other) noexcept : - mDescriptor(other.mDescriptor), - mIsReadable(other.mIsReadable), - mIsWritable(other.mIsWritable), - mIsNonBlocking(other.mIsNonBlocking) { - other.mDescriptor = sInvalidSocket; - } - Socket& operator=(Socket&& rhs) noexcept { - std::swap(mDescriptor, rhs.mDescriptor); - mIsReadable = rhs.mIsReadable; - mIsWritable = rhs.mIsWritable; - mIsNonBlocking = rhs.mIsNonBlocking; - return *this; - } - Socket(const Socket&) = delete; - Socket& operator=(const Socket&) = delete; - /// get underlying file descriptor - Descriptor GetDescriptor() const { return mDescriptor; } - /// get an error on the socket, indicated by errored poll event - std::errc GetError() const { - return static_cast(GetSockOpt(SOL_SOCKET, SO_ERROR).first); - } - void SetBlocking() { mIsNonBlocking = false; SetStatusFlags(GetStatusFlags() & ~(O_NONBLOCK)); } - void SetNonBlocking() { mIsNonBlocking = true; SetStatusFlags(GetStatusFlags() | O_NONBLOCK); } - // only applies to non blocking, and set from Update (poll), always return true if blocking - bool GetAndResetIsReadable() { const bool temp = mIsReadable; mIsReadable = false; return temp || !mIsNonBlocking; } - bool GetAndResetIsWritable() { const bool temp = mIsWritable; mIsWritable = false; return temp || !mIsNonBlocking; } - /// @return false if socket should close - bool Update(event::Result res) { - if (res.IsErrored()) { - throw std::system_error(std::make_error_code(GetError())); - } - if (res.IsInvalid() || res.IsClosed()) { - // TODO: technically could still have bytes waiting to be read on the close event - return false; - } - if (res.IsReadable()) { - mIsReadable = true; - } - if (res.IsWritable()) { - mIsWritable = true; - } - return true; - } - -private: - int GetStatusFlags() const { return SysCall(::fcntl, mDescriptor, F_GETFL, 0).Unwrap(); } - void SetStatusFlags(int flags) { SysCall(::fcntl, mDescriptor, F_SETFL, flags).Unwrap(); } - - /// get or set socket option, most are ints, non default length is only for strings - template - std::pair GetSockOpt(int level, int optname, T inputValue = T(), socklen_t inputSize = sizeof(T)) const { - T outValue = inputValue; - socklen_t outSize = inputSize; - SysCall(::getsockopt, mDescriptor, level, optname, &outValue, &outSize).Unwrap(); - return std::make_pair(outValue, outSize); - } - template - void SetSockOpt(int level, int optname, const T& inputValue, socklen_t inputSize = sizeof(T)) { - SysCall(::setsockopt, level, optname, &inputValue, inputSize).Unwrap(); - } - - Descriptor mDescriptor; - bool mIsReadable = false; - bool mIsWritable = false; - bool mIsNonBlocking = false; -}; - -/// address for unix sockets -class LocalAddress { - static constexpr sa_family_t sFamily = AF_UNIX; // always AF_UNIX - /// max returned by Size() - static constexpr socklen_t sMaxSize = sizeof(sockaddr_un_t); - /// offset of sun_path within the address object, sun_path is a char array - static constexpr socklen_t sPathOffset = sMaxSize - sizeof(sockaddr_un_t::sun_path); -public: - /// empty address - LocalAddress() noexcept : mSize(sMaxSize) {} - /// almost always bind before use - explicit LocalAddress(std::string_view path) - : mSize(sPathOffset + path.size() + 1) { // beginning of object + length of path + null terminator - if (path.empty()) throw std::invalid_argument("path empty"); - if (mSize > sMaxSize) throw std::length_error("path too long"); - // copy and null terminate path - std::strncpy(&mAddress.sun_path[0], path.data(), path.size()); - mAddress.sun_path[path.size()] = '\0'; - mAddress.sun_family = sFamily; - } - - /// deletes the path on the filesystem, usually called before bind - void Unlink() const { - // TODO: keep important errors (permissions, logic, ...) - (void)SysCall(::unlink, GetPath()); - } - - /// system calls with address - const sockaddr_t* GetPtr() const { return reinterpret_cast(&mAddress); } - /// system calls with address out - sockaddr_t* GetPtr() { return reinterpret_cast(&mAddress); } - /// system calls with addrLen - socklen_t GetSize() const { return mSize; } - /// system calls with addrLen out - socklen_t* GetSizePtr() { - mSize = sMaxSize; - return &mSize; - } - const char* GetPath() const { return &mAddress.sun_path[0]; } - bool IsValid() const { - return mAddress.sun_family == sFamily; // not used with wrong socket type - } -private: - socklen_t mSize; - sockaddr_un_t mAddress{}; -}; - -class LocalSocket : public Socket { - static constexpr int sDomain = AF_UNIX, // unix domain socket - sType = SOCK_STREAM, // connection oriented, no message boundaries - sProtocol = 0; // auto selected -public: - explicit LocalSocket(std::string_view path) : Socket(sDomain, sType, sProtocol), mAddress(path) {} - LocalSocket(Descriptor descriptor, LocalAddress address) : Socket(descriptor), mAddress(address) { - if (!mAddress.IsValid()) throw std::invalid_argument("invalid local socket address"); - } - -protected: - void UnlinkAddress() const { mAddress.Unlink(); } - void Bind() const { SysCall(::bind, GetDescriptor(), mAddress.GetPtr(), mAddress.GetSize()).Unwrap(); } - void Listen(int backlog) const { SysCall(::listen, GetDescriptor(), backlog).Unwrap(); } - void Connect() const { SysCall(::connect, GetDescriptor(), mAddress.GetPtr(), mAddress.GetSize()).Unwrap(); } - -private: - LocalAddress mAddress; -}; - -/// connector manages a connection, can send/recv with -class LocalConnectorSocket : public LocalSocket { -public: - /// open as outbound connector to path - explicit LocalConnectorSocket(std::string_view path) : LocalSocket(path) { - Connect(); - } - /// open as inbound connector from accept - LocalConnectorSocket(Descriptor descriptor, LocalAddress address) : LocalSocket(descriptor, address) {} - /// send a byte buffer - /// @tparam TBufIt iterator to contiguous memory - /// @return number of bytes sent or nullopt if blocking - template - std::optional TrySend(TBufIt bufBegin, int bytesToSend) { - if (!GetAndResetIsWritable()) return std::nullopt; - constexpr int flags = 0; - if (auto bytesSent = SysCallBlocking(::send, GetDescriptor(), &(*bufBegin), bytesToSend, flags)) { - return (*bytesSent).Unwrap(); - } - return std::nullopt; - } - /// receive a byte buffer - /// @tparam TBufIt iterator to contiguous memory - /// @return number of bytes written to buffer or nullopt if blocking - template - std::optional TryRecv(TBufIt bufBegin, int bufSize) { - if (!GetAndResetIsReadable()) return std::nullopt; - constexpr int flags = 0; - if (auto bytesRecv = SysCallBlocking(::recv, GetDescriptor(), &(*bufBegin), bufSize, flags)) { - return (*bytesRecv).Unwrap(); - } - return std::nullopt; - } -}; - -/// aka listener/passive socket, accepts connectors -class LocalAcceptorSocket : public LocalSocket { -public: - /// open as acceptor on path, backlog is accept queue size - LocalAcceptorSocket(std::string_view path, int backlog) : LocalSocket(path) { - UnlinkAddress(); - Bind(); - Listen(backlog); - } - /// accept an inbound connector or nullopt if blocking - std::optional Accept() { - if (!GetAndResetIsReadable()) return std::nullopt; - LocalAddress address; - if (auto desc = SysCallBlocking(::accept, GetDescriptor(), address.GetPtr(), address.GetSizePtr())) { - return LocalConnectorSocket((*desc).Unwrap(), address); - } - return std::nullopt; - } -}; - -/// manage a single outbound connector -class BasicLocalClient { -public: - void Open(std::string_view path) { - if (IsOpen()) throw std::runtime_error("connection already open"); - mConnector = LocalConnectorSocket(path); - mPoller.AddConnector(mConnector->GetDescriptor()); - assert(mPoller.GetSize() == 1); - } - void Close() { - mConnector.reset(); - mPoller.Clear(); - } - - /// default timeout returns immediately - void UpdateOnce(int timeoutMs = 0) { - if (!IsOpen()) throw std::runtime_error("connection not open"); - mPoller.Poll(timeoutMs); - - if (!mConnector->Update(mPoller.At(0))) { - Close(); - } - } - - /// send a byte buffer, continuously updates until entire message is sent - /// @tparam TBufIt iterator to contiguous memory - /// @return false if the send fails (connection closed) - template - bool Send(TBufIt bufBegin, int bufSize) { - TBufIt msgIt = bufBegin; - int bytesToSend = bufSize; - int maxIter = 100; - while (--maxIter && bytesToSend > 0) { - if (!IsOpen()) return false; - std::optional bytesSent = mConnector->TrySend(msgIt, bytesToSend); - if (!bytesSent) { - // blocking, poll and try again - UpdateOnce(); - } else if (*bytesSent <= 0) { - // returning 0 is very unlikely given the small amount of data, something about filling up the internal buffer? - // handle it the same as a would block error, and hope eventually it'll resolve itself - UpdateOnce(20); // 20ms timeout to give the buffer time to be emptied - } else if (*bytesSent > bytesToSend) { - // probably guaranteed to not happen, but just in case - throw std::runtime_error("bytes sent > bytes to send"); - } else { - // SOCK_STREAM allows partial sends, but almost guaranteed to not happen on local sockets - bytesToSend -= *bytesSent; - msgIt += *bytesSent; - } - } - if (maxIter == 0) { - throw std::runtime_error("send stuck in infinite loop"); - } - return true; - } - - /// receive into byte buffer - /// @tparam TBufIt iterator to contiguous memory - /// @return number of bytes written to buffer, 0 indicating there is no message waiting - template - int RecvOnce(TBufIt bufBegin, int bytesToRead) { - std::optional bytesRecv = mConnector->TryRecv(bufBegin, bytesToRead); - // if the user is doing while(messageReceived) { } to empty the message queue - // then need to poll once before the next iteration, but only if there were bytes received - if (bytesRecv && *bytesRecv > 0) UpdateOnce(); - return bytesRecv.value_or(0); - } - - /// receive into byte buffer, continously updates until all bytes are read - /// @tparam TBufIt iterator to contiguous memory - /// @return true if bytesToRead bytes were written to buffer - template - bool RecvAll(const TBufIt bufBegin, int bytesToRead) { - int maxIter = 100; - auto bufIt = bufBegin; - while (--maxIter && bytesToRead > 0) { - if (!IsOpen()) return false; - std::optional bytesRecv = mConnector->TryRecv(bufIt, bytesToRead); - if (!bytesRecv || *bytesRecv == 0) { - // try again - } else if (*bytesRecv < 0 || *bytesRecv > bytesToRead) { - // should not be possible - throw std::length_error("bytesRecv"); - } else { - // read some or all of the message - bytesToRead -= *bytesRecv; - bufIt += *bytesRecv; - } - // set readable for next bytes, or a future call to Recv - UpdateOnce(); - } - if (maxIter == 0) { - throw std::runtime_error("recv stuck in infinite loop"); - } - return true; - } - - bool IsOpen() const { return mConnector.has_value(); } - -private: - std::optional mConnector{}; - event::Poller mPoller{}; // index 0 is connector if open -}; diff --git a/test/BridgeServerMock.cpp b/test/BridgeServerMock.cpp new file mode 100644 index 0000000..0caca26 --- /dev/null +++ b/test/BridgeServerMock.cpp @@ -0,0 +1,74 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2022 SlimeVR Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#include "BridgeServerMock.hpp" + +using namespace std::literals::chrono_literals; + +void BridgeServerMock::CreateConnection() { + logger_->Log("listening"); + + std::string path = GetBridgePath(); + + server_handle_ = GetLoop()->resource(false); + server_handle_->on([this](const uvw::listen_event &event, uvw::pipe_handle &) { + logger_->Log("new client"); + ResetBuffers(); + + /* ipc = false -> pipe will be used for handle passing between processes? no */ + connection_handle_ = GetLoop()->resource(false); + + connection_handle_->on([this](const uvw::end_event &, uvw::pipe_handle &) { + logger_->Log("disconnected"); + StopAsync(); + }); + connection_handle_->on([this](const uvw::data_event &event, uvw::pipe_handle &) { + OnRecv(event); + }); + connection_handle_->on([this](const uvw::error_event &event, uvw::pipe_handle &) { + logger_->Log("pipe error: %s", event.what()); + StopAsync(); + }); + + server_handle_->accept(*connection_handle_); + connection_handle_->read(); + logger_->Log("connected"); + connected_ = true; + }); + server_handle_->on([this, path](const uvw::error_event &event, uvw::pipe_handle &) { + logger_->Log("[%s] bind error: %s", path.c_str(), event.what()); + StopAsync(); + }); + + server_handle_->bind(path); + server_handle_->listen(); +} + +void BridgeServerMock::ResetConnection() { + CloseConnectionHandles(); +} + +void BridgeServerMock::CloseConnectionHandles() { + if (server_handle_) server_handle_->close(); + if (connection_handle_) connection_handle_->close(); + connected_ = false; +} diff --git a/src/bridge/bridge.hpp b/test/BridgeServerMock.hpp similarity index 65% rename from src/bridge/bridge.hpp rename to test/BridgeServerMock.hpp index fb1e1bf..f5f52ba 100644 --- a/src/bridge/bridge.hpp +++ b/test/BridgeServerMock.hpp @@ -1,6 +1,6 @@ /* SlimeVR Code is placed under the MIT license - Copyright (c) 2021 Eiren Rain + Copyright (c) 2022 SlimeVR Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,26 +20,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/** - * Header file for cross-platform handling of IPC between SteamVR driver/app - * and SlimeVR server - */ #pragma once -#define BRIDGE_USE_PIPES 1 -#include "ProtobufMessages.pb.h" -#include -#include -#include "../VRDriver.hpp" +#include +#include -enum BridgeStatus { - BRIDGE_DISCONNECTED = 0, - BRIDGE_CONNECTED = 1, - BRIDGE_ERROR = 2 -}; +#include "bridge/BridgeTransport.hpp" -BridgeStatus runBridgeFrame(SlimeVRDriver::VRDriver &driver); +class BridgeServerMock: public BridgeTransport { +public: + using BridgeTransport::BridgeTransport; -bool getNextBridgeMessage(messages::ProtobufMessage &message, SlimeVRDriver::VRDriver &driver); +private: + void CreateConnection() override; + void ResetConnection() override; + void CloseConnectionHandles() override; -bool sendBridgeMessage(messages::ProtobufMessage &message, SlimeVRDriver::VRDriver &driver); \ No newline at end of file + std::shared_ptr server_handle_ = nullptr; +}; \ No newline at end of file diff --git a/test/TestBridgeClientMock.cpp b/test/TestBridgeClientMock.cpp new file mode 100644 index 0000000..b3f1f8e --- /dev/null +++ b/test/TestBridgeClientMock.cpp @@ -0,0 +1,93 @@ +#include + +#include "common/TestBridgeClient.hpp" +#include "BridgeServerMock.hpp" + +TEST_CASE("IO with a mock server", "[Bridge]") { + using namespace std::chrono; + + std::unordered_map> serials = { + { 3, { TrackerRole::WAIST, "human://WAIST" } }, + { 4, { TrackerRole::LEFT_FOOT, "human://LEFT_FOOT" } }, + { 5, { TrackerRole::RIGHT_FOOT, "human://RIGHT_FOOT" } }, + { 6, { TrackerRole::LEFT_KNEE, "human://LEFT_KNEE" } }, + { 7, { TrackerRole::RIGHT_KNEE, "human://RIGHT_KNEE" } }, + }; + + int positions = 0; + int invalid_messages = 0; + + bool last_logged_position = false; + bool trackers_sent = false; + + google::protobuf::Arena arena; + + auto logger = std::static_pointer_cast(std::make_shared("ServerMock")); + + std::shared_ptr server_mock; + server_mock = std::make_shared( + logger, + [&](const messages::ProtobufMessage& message) { + if (message.has_tracker_added()) { + TestLogTrackerAdded(logger, message); + } else if (message.has_tracker_status()) { + TestLogTrackerStatus(logger, message); + } else if (message.has_position()) { + messages::Position pos = message.position(); + if (!last_logged_position) logger->Log("... tracker positions response"); + last_logged_position = true; + positions++; + + messages::ProtobufMessage* server_message = google::protobuf::Arena::CreateMessage(&arena); + + if (!trackers_sent) { + for (int32_t id = 3; id <= 7; id++) { + messages::TrackerAdded* tracker_added = google::protobuf::Arena::CreateMessage(&arena); + server_message->set_allocated_tracker_added(tracker_added); + tracker_added->set_tracker_id(id); + tracker_added->set_tracker_role(serials[id].first); + tracker_added->set_tracker_serial(serials[id].second); + tracker_added->set_tracker_name(serials[id].second); + server_mock->SendBridgeMessage(*server_message); + + messages::TrackerStatus* tracker_status = google::protobuf::Arena::CreateMessage(&arena); + server_message->set_allocated_tracker_status(tracker_status); + tracker_status->set_tracker_id(id); + tracker_status->set_status(messages::TrackerStatus_Status::TrackerStatus_Status_OK); + server_mock->SendBridgeMessage(*server_message); + } + + trackers_sent = true; + } + + for (int32_t id = 3; id <= 7; id++) { + messages::Position* tracker_position = google::protobuf::Arena::CreateMessage(&arena); + server_message->set_allocated_position(tracker_position); + tracker_position->set_tracker_id(id); + tracker_position->set_data_source(messages::Position_DataSource_FULL); + tracker_position->set_x(0); + tracker_position->set_y(0); + tracker_position->set_z(0); + tracker_position->set_qx(0); + tracker_position->set_qy(0); + tracker_position->set_qz(0); + tracker_position->set_qw(0); + server_mock->SendBridgeMessage(*server_message); + } + } else { + invalid_messages++; + } + + if (!message.has_position()) { + last_logged_position = false; + } + } + ); + + server_mock->Start(); + std::this_thread::sleep_for(10ms); + TestBridgeClient(); + server_mock->Stop(); + + if (invalid_messages) FAIL("Invalid messages received"); +} \ No newline at end of file diff --git a/test/TestCircularBuffer.cpp b/test/TestCircularBuffer.cpp new file mode 100644 index 0000000..66dbe43 --- /dev/null +++ b/test/TestCircularBuffer.cpp @@ -0,0 +1,46 @@ +#include + +#include "bridge/CircularBuffer.hpp" + +TEST_CASE("Push/Pop", "[CircularBuffer]") { + CircularBuffer buffer(4); + char data[4]; + + REQUIRE(buffer.Push("1234", 4)); // [1234] + REQUIRE(buffer.BytesAvailable() == 4); + REQUIRE(buffer.Pop(data, 2)); // [34] + REQUIRE(buffer.BytesAvailable() == 2); + REQUIRE(std::string(data, 2) == "12"); + + // test wraparound + REQUIRE(buffer.Push("56", 2)); // [3456] + REQUIRE(buffer.BytesAvailable() == 4); + REQUIRE_FALSE(buffer.Push("78", 2)); // [3456] buffer full + REQUIRE(buffer.BytesAvailable() == 4); + REQUIRE(buffer.Pop(data, 4)); // [] + REQUIRE(buffer.BytesAvailable() == 0); + REQUIRE(std::string(data, 4) == "3456"); + REQUIRE_FALSE(buffer.Pop(data, 4)); // [] buffer empty + REQUIRE(buffer.BytesAvailable() == 0); +} + +TEST_CASE("Peek/Skip", "[CircularBuffer]") { + CircularBuffer buffer(4); + char data[4]; + + REQUIRE_FALSE(buffer.Peek(data, 2)); // [] nothing to peek + REQUIRE(buffer.BytesAvailable() == 0); + REQUIRE_FALSE(buffer.Skip(2)); // [] nothing to skip + REQUIRE(buffer.BytesAvailable() == 0); + + REQUIRE(buffer.Push("1234", 4)); // [1234] + REQUIRE(buffer.BytesAvailable() == 4); + REQUIRE(buffer.Peek(data, 2) == 2); // [1234] + REQUIRE(buffer.BytesAvailable() == 4); + REQUIRE(std::string(data, 2) == "12"); + REQUIRE(buffer.Skip(2)); // [34] + REQUIRE(buffer.BytesAvailable() == 2); + REQUIRE(buffer.Peek(data, 1) == 1); // [34] + REQUIRE(buffer.BytesAvailable() == 2); + REQUIRE(std::string(data, 1) == "3"); +} \ No newline at end of file diff --git a/test/TestSleepTimes.cpp b/test/TestSleepTimes.cpp new file mode 100644 index 0000000..e6e734e --- /dev/null +++ b/test/TestSleepTimes.cpp @@ -0,0 +1,37 @@ +#include + +#include +#include +#include +#include +#include +#include + +TEST_CASE("Sleep times") { + const int sleep_duration_ms = 2; + const int benchmark_duration_sec = 1; + const int num_iterations = 1000000; + std::vector sleep_times; + sleep_times.reserve(num_iterations); + + printf("Benching std::this_thread::sleep_for(std::chrono::milliseconds(%i));\n", sleep_duration_ms); + auto start_time = std::chrono::high_resolution_clock::now(); + while (std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count() < benchmark_duration_sec) { + auto iteration_start_time = std::chrono::high_resolution_clock::now(); + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_duration_ms)); + auto iteration_end_time = std::chrono::high_resolution_clock::now(); + + sleep_times.push_back(std::chrono::duration_cast(iteration_end_time - iteration_start_time).count()); + } + std::sort(sleep_times.begin(), sleep_times.end()); + + const size_t num_samples = sleep_times.size(); + const size_t p1_index = num_samples * 1 / 100; + const size_t p99_index = num_samples * 99 / 100; + const double avg_time_ms = static_cast(std::accumulate(sleep_times.begin(), sleep_times.end(), 0LL)) / num_samples / 1000; + const double p1_time_ms = static_cast(sleep_times[p1_index]) / 1000; + const double p99_time_ms = static_cast(sleep_times[p99_index]) / 1000; + printf("p1: %.3lf ms %.3lf tps\n", p1_time_ms, 1e3 / p1_time_ms); + printf("avg: %.3lf ms %.3lf tps\n", avg_time_ms, 1e3 / avg_time_ms); + printf("p99: %.3lf ms %.3lf tps\n", p99_time_ms, 1e3 / p99_time_ms); +} \ No newline at end of file diff --git a/test/common/TestBridgeClient.cpp b/test/common/TestBridgeClient.cpp new file mode 100644 index 0000000..fecbc48 --- /dev/null +++ b/test/common/TestBridgeClient.cpp @@ -0,0 +1,134 @@ +#include "TestBridgeClient.hpp" + +void TestLogTrackerAdded(std::shared_ptr logger, const messages::ProtobufMessage& message) { + if (!message.has_tracker_added()) return; + messages::TrackerAdded tracker_added = message.tracker_added(); + logger->Log("tracker added id %i name %s role %i serial %s", + tracker_added.tracker_id(), + tracker_added.tracker_name().c_str(), + tracker_added.tracker_role(), + tracker_added.tracker_serial().c_str() + ); +} + +void TestLogTrackerStatus(std::shared_ptr logger, const messages::ProtobufMessage& message) { + if (!message.has_tracker_status()) return; + messages::TrackerStatus status = message.tracker_status(); + static const std::unordered_map status_map = { + { messages::TrackerStatus_Status_OK, "OK" }, + { messages::TrackerStatus_Status_DISCONNECTED, "DISCONNECTED" }, + { messages::TrackerStatus_Status_ERROR, "ERROR" }, + { messages::TrackerStatus_Status_BUSY, "BUSY" }, + }; + if (status_map.count(status.status())) { + logger->Log("tracker status id %i status %s", status.tracker_id(), status_map.at(status.status()).c_str()); + } +} + +void TestBridgeClient() { + using namespace std::chrono; + + std::atomic ready_to_bench = false; + std::atomic position_requested_at = steady_clock::now(); + std::map latency_nanos_sum; + std::map latency_nanos_count; + + int invalid_messages = 0; + int trackers = 0; + int positions = 0; + + bool last_logged_position = false; + + auto logger = std::static_pointer_cast(std::make_shared("Test")); + auto bridge = std::make_shared( + logger, + [&](const messages::ProtobufMessage& message) { + if (message.has_tracker_added()) { + trackers++; + TestLogTrackerAdded(logger, message); + } else if (message.has_tracker_status()) { + TestLogTrackerStatus(logger, message); + } else if (message.has_position()) { + messages::Position pos = message.position(); + if (!last_logged_position) logger->Log("... tracker positions"); + last_logged_position = true; + positions++; + + if (!ready_to_bench) return; + + auto id = pos.tracker_id(); + auto dt = duration_cast(steady_clock::now() - position_requested_at.load()); + latency_nanos_count[id]++; + latency_nanos_sum[id] += dt.count(); + } else { + invalid_messages++; + } + + if (!message.has_position()) { + last_logged_position = false; + } + } + ); + + bridge->Start(); + + for (int i = 0; i < 20; i++) { + if (bridge->IsConnected()) break; + std::this_thread::sleep_for(milliseconds(100)); + } + + if (!bridge->IsConnected()) { + FAIL("Connection attempt timed out"); + bridge->Stop(); + return; + } + + google::protobuf::Arena arena; + messages::ProtobufMessage* message = google::protobuf::Arena::CreateMessage(&arena); + + messages::TrackerAdded* tracker_added = google::protobuf::Arena::CreateMessage(&arena); + message->set_allocated_tracker_added(tracker_added); + tracker_added->set_tracker_id(0); + tracker_added->set_tracker_role(TrackerRole::HMD); + tracker_added->set_tracker_serial("HMD"); + tracker_added->set_tracker_name("HMD"); + bridge->SendBridgeMessage(*message); + + messages::TrackerStatus* tracker_status = google::protobuf::Arena::CreateMessage(&arena); + message->set_allocated_tracker_status(tracker_status); + tracker_status->set_tracker_id(0); + tracker_status->set_status(messages::TrackerStatus_Status::TrackerStatus_Status_OK); + bridge->SendBridgeMessage(*message); + + ready_to_bench = true; + + for (int i = 0; i < 50; i++) { + messages::Position* hmd_position = google::protobuf::Arena::CreateMessage(&arena); + message->set_allocated_position(hmd_position); + hmd_position->set_tracker_id(0); + hmd_position->set_data_source(messages::Position_DataSource_FULL); + hmd_position->set_x(0); + hmd_position->set_y(0); + hmd_position->set_z(0); + hmd_position->set_qx(0); + hmd_position->set_qy(0); + hmd_position->set_qz(0); + hmd_position->set_qw(0); + + position_requested_at = steady_clock::now(); + bridge->SendBridgeMessage(*message); + std::this_thread::sleep_for(milliseconds(10)); + } + + bridge->Stop(); + + for (const auto& [id, sum] : latency_nanos_sum) { + auto avg_latency_nanos = static_cast(latency_nanos_count[id] ? sum / latency_nanos_count[id] : -1); + auto avg_latency_ms = duration_cast>(nanoseconds(avg_latency_nanos)); + logger->Log("avg latency for tracker %i: %.3fms", id, avg_latency_ms.count()); + } + + if (invalid_messages) FAIL("Invalid messages received"); + if (!trackers) FAIL("No trackers received"); + if (!positions) FAIL("No tracker positions received"); +} diff --git a/test/common/TestBridgeClient.hpp b/test/common/TestBridgeClient.hpp new file mode 100644 index 0000000..65a5c28 --- /dev/null +++ b/test/common/TestBridgeClient.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "DriverFactory.hpp" +#include "bridge/BridgeClient.hpp" +#include "TrackerRole.hpp" + +void TestLogTrackerAdded(std::shared_ptr logger, const messages::ProtobufMessage& message); +void TestLogTrackerStatus(std::shared_ptr logger, const messages::ProtobufMessage& message); +void TestBridgeClient(); diff --git a/test/integration/TestBridgeClientReal.cpp b/test/integration/TestBridgeClientReal.cpp new file mode 100644 index 0000000..6149c55 --- /dev/null +++ b/test/integration/TestBridgeClientReal.cpp @@ -0,0 +1,9 @@ +#include + +#include "DriverFactory.hpp" +#include "TrackerRole.hpp" +#include "../common/TestBridgeClient.hpp" + +TEST_CASE("IO with a real server", "[Bridge]") { + TestBridgeClient(); +} \ No newline at end of file diff --git a/vcpkg b/vcpkg new file mode 160000 index 0000000..0e2a0e0 --- /dev/null +++ b/vcpkg @@ -0,0 +1 @@ +Subproject commit 0e2a0e0ad1a370f85a637452be52e2ddf2dcac0b diff --git a/vcpkg.json b/vcpkg.json index 8272723..2cc1da5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,10 @@ { - "name": "slimevr-openvr-driver", - "version": "0.2.0", - "dependencies": [ - "protobuf", - "simdjson" - ] -} \ No newline at end of file + "name": "slimevr-openvr-driver", + "version": "0.2.0", + "dependencies": [ + "protobuf", + "simdjson", + "uvw", + "catch2" + ] +}