diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64c75649dc..67c79f01f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,8 @@ Now you can open the codebase in [IDEA](https://www.jetbrains.com/idea/download/ ### Java (server) +Before contributing to the server, read [server/README.md](server/README.md) for an overview of its architecture and design guidelines. + The Java code is built with `gradle`, a CLI tool that manages java projects and their dependencies. - You can run the server by running `./gradlew run` in your IDE's terminal. diff --git a/README.md b/README.md index 409b21e1b3..fd5f0655ea 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Latest setup instructions are [in our docs](https://docs.slimevr.dev/server/inde ## Building & Contributing For information on building and contributing to the codebase, see [CONTRIBUTING.md](CONTRIBUTING.md). +For an overview of the server architecture and design guidelines, see [server/README.md](server/README.md). + ## Translating Translation is done via Pontoon at [i18n.slimevr.dev](https://i18n.slimevr.dev/). Please join our [Discord translation forum](https://discord.com/channels/817184208525983775/1050413434249949235) to coordinate. diff --git a/flake.nix b/flake.nix index 272217281c..55d3764279 100644 --- a/flake.nix +++ b/flake.nix @@ -12,8 +12,10 @@ perSystem = { pkgs, ... }: let + java = pkgs.javaPackages.compiler.temurin-bin.jdk-24; + runtimeLibs = pkgs: (with pkgs; [ - jdk17 + java alsa-lib at-spi2-atk at-spi2-core cairo cups dbus expat gdk-pixbuf glib gtk3 libdrm libgbm libglvnd libnotify @@ -33,8 +35,8 @@ name = "slimevr-env"; targetPkgs = runtimeLibs; profile = '' - export JAVA_HOME=${pkgs.jdk17} - export PATH="${pkgs.jdk17}/bin:$PATH" + export JAVA_HOME=${java} + export PATH="${java}/bin:$PATH" # Tell electron-builder to use system tools instead of downloading them export USE_SYSTEM_FPM=true diff --git a/gradle.properties b/gradle.properties index 5d83448471..9be6eddd32 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,4 @@ buildconfigVersion=6.0.7 # We should probably stop using grgit, see: # https://andrewoberstar.com/posts/2024-04-02-dont-commit-to-grgit/ grgitVersion=5.3.3 +wireVersion=5.3.1 diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000000..a962344b06 --- /dev/null +++ b/server/README.md @@ -0,0 +1,529 @@ +# SlimeVR Server Architecture + +This document explains how the server is structured, why it's structured that way, and how to extend it correctly. + +--- + +## Mental Model + +Every significant part of the server a tracker, a UDP connection, a SolarXR session, a config file is a **module**. Every module is a list of behaviours sharing one context: + +![Architecture diagram](architecture.svg) + +That's the entire system. Everything else is an application of this pattern. + +--- + +## Why This Pattern + +The server handles many concurrent connections, each with independent state that many other components need to observe. A shared mutable object works fine until two coroutines update it at the same time, or you need to know *when* something changed, or you want to add a feature without touching existing code. + +The reducer pattern solves all three: + +- **Thread safety**: `StateFlow.update` is atomic. Concurrent dispatches serialize without locks. +- **Observability**: `context.state` is a hot `StateFlow` it always holds the current value and any code can `collect` it and react to changes without being coupled to the producer. +- **Extensibility**: adding a feature means adding a `Behaviour` to a list. Nothing else changes. + +The cost is that you cannot mutate state directly. Everything goes through `dispatch`. That constraint is the point. + +--- + +## The Context + +`Context` (`context/context.kt`) holds the current state as a `StateFlow`, the coroutine scope that defines this module's lifetime, and the behaviour list. + +```kotlin +class Context( + val state: StateFlow, + val scope: CoroutineScope, + val behaviours: CopyOnWriteArrayList>, +) { + fun dispatch(action: A) + fun dispatchAll(actions: List) + fun observeAll(receiver: C) +} +``` + +`Context.create` takes an initial state and a list of behaviours. On each `dispatch`, it folds every behaviour's `reduce` over the current state in order and publishes the result. Behaviours that don't care about an action return the state unchanged. + +`dispatchAll(actions)` applies a list of actions in one atomic `StateFlow.update`. Use it when you need multiple state changes to be visible as one - intermediate states are never published, so no observer fires between them. + +`observeAll(receiver)` calls `observe(receiver)` on every behaviour in the list. This is how observation is wired after construction - see [Module Lifecycle](#module-lifecycle). + +The `behaviours` list is a `CopyOnWriteArrayList`. This allows adding behaviours after construction, which is needed for modules whose behaviour list depends on runtime context (e.g. `TrackingChecklist` adds step behaviours in `startObserving`). + +--- + +## Behaviours + +A behaviour is one feature of a module. It has two methods, both optional: + +```kotlin +interface Behaviour { + fun reduce(state: S, action: A): S = state // pure, no side effects + fun observe(receiver: C) {} // side effects go here +} +``` + +`reduce` is a pure function - it cannot launch coroutines, call external services, or read from a clock. It just maps `(state, action) → state`. + +`observe` is called once after construction, after the context exists. This is where you launch coroutines, subscribe to flows, register event listeners. All coroutines launched here should use `receiver.context.scope` so they die when the module dies - no manual cleanup, no leaks. + +**Why behaviours instead of methods on the module class?** + +The natural alternative is one class with methods for each feature - ping handling, handshake logic, sensor registration, timeout detection, all in the same file. That works until the class has 20 features and every change has global blast radius. Behaviours make the feature list explicit: you read `create()`, you see exactly what the module does. Adding or removing a feature is one line in that list, touching nothing else. + +**Why `observe` is separate from `reduce`:** + +`reduce` must stay pure so state transitions are testable and predictable. Side effects in `observe` are isolated - a behaviour that logs packets doesn't affect state, a behaviour that saves config doesn't affect packet handling. + +### Scoping Behaviours to Module Lifetime + +Every behaviour's coroutines run inside `receiver.context.scope`. When a UDP connection drops, its scope is cancelled, which cancels every coroutine every behaviour launched for that connection. No teardown code needed anywhere. + +This is why `observe` receives the module (or context) rather than taking a raw `CoroutineScope` parameter - it enforces that behaviours cannot outlive their module. + +### Stateless vs. Stateful Behaviours + +Behaviours with no constructor dependencies are `object`s - singletons, zero allocation: + +```kotlin +object PacketBehaviour : UDPConnectionBehaviour { ... } +``` + +Behaviours that need external services are `class`es, constructed at the call site: + +```kotlin +class TrackerConfigBehaviour( + private val settings: Settings, + private val hardwareId: String, +) : TrackerBehaviour { ... } +``` + +When a behaviour is conditional (depends on a nullable service), use `buildList`: + +```kotlin +val behaviours: List = buildList { + add(AlwaysPresentBehaviour) + optionalService?.let { add(OptionalServiceBehaviour(it)) } +} +``` + +For modules like `SolarXRBridge` where the behaviour list is large and reusable, extract it into a named function on the companion object: + +```kotlin +companion object { + fun buildBehaviours(appContext: AppContextProvider): List = buildList { + add(DataFeedInitBehaviour(appContext.server, appContext.skeleton)) + add(FirmwareBehaviour(appContext.server, appContext.firmwareManager)) + // ... + } +} +``` + +This lets tests call `buildBehaviours` with a stub context, or ignore it entirely and construct the module directly with only the behaviours they need. + +### The Receiver Type + +The type parameter `C` controls what `observe` receives. For simple modules where behaviours only need to dispatch or subscribe to state, `C` is the context itself. For modules where behaviours also need to call methods (send bytes, emit events), `C` is the module class: + +```kotlin +// behaviours only dispatch/observe state +typealias GlobalConfigBehaviour = Behaviour + +// behaviours also call receiver.send(), receiver.packetEvents.on(...) +typealias UDPConnectionBehaviour = Behaviour +``` + +### File Layout + +Behaviours live in a `behaviours.kt` file in the same package as the module, separate from the module class itself: + +``` +tracker/ +├── module.kt ← Tracker class, TrackerState, TrackerActions, typealiases +├── behaviours.kt ← TrackerBasicBehaviour, TrackerTPSBehaviour, ... +└── config.kt ← TrackerConfigBehaviour (its own file - distinct concern) +``` + +`module.kt` is the standard name for the file containing the module class, state, actions, and typealiases. `behaviours.kt` holds the implementations. For modules with many distinct feature areas, split behaviours by feature instead of one file - `solarxr/` uses this: `datafeed.kt`, `firmware.kt`, `provisioning.kt`, etc. each live alongside `module.kt`. + +--- + +## Module Lifecycle + +Every module has a two-step lifecycle: **construction** then **observation wiring**. + +```kotlin +// Step 1: create the module (pure construction, no side effects) +val manager = FirmwareManager.create(ctx = phase1, scope = scope) + +// Step 2: wire observation (launches coroutines, subscribes to flows) +manager.startObserving() +``` + +`create()` builds the context with its behaviour list and constructs the module. No coroutines are launched, no flows are subscribed. The module is inert. + +`startObserving()` calls `context.observeAll(this)`, which calls `observe(receiver)` on every behaviour. This is where all side effects begin. + +**Why separate construction from observation?** + +Some modules need to exist before all their dependencies are assembled. For example, `TrackingChecklist` is constructed before `AppContext` exists, but its step behaviours depend on `Skeleton` and `VRCConfigManager` which are part of `AppContext`. By deferring observation, the module can be constructed in any order and wired later: + +```kotlin +// Constructed early - no AppContext needed yet +val trackingChecklist = TrackingChecklist.create(scope = this) + +// AppContext assembled from all modules +val appContext = AppContext(server, config, serialServer, skeleton, ..., trackingChecklist) + +// Observation wired now - TrackingChecklist adds its step behaviours using AppContext +appContext.startObserving() +``` + +`AppContext.startObserving()` calls `startObserving()` on every bootstrap module in sequence, and `trackingChecklist.startObserving(appContext)` adds its behaviours to `context.behaviours` before calling `context.observeAll(this)`. + +**Runtime modules** (UDP connections, SolarXR bridges, HID receivers) are created and immediately observed - they have all their dependencies at creation time: + +```kotlin +val bridge = SolarXRBridge.create(id = id, appContext = appContext, scope = scope) +// startObserving() is called inside create() +``` + +--- + +## Dependency Injection + +The server uses a typed context hierarchy for dependency injection. Contexts are interfaces - modules declare the minimum phase they need in `create()`, which gives compile-time guarantees that their dependencies exist. + +``` +Phase1ContextProvider +└── AppContextProvider +``` + +**`Phase1ContextProvider`** - the bootstrap phase. Available before `AppContext` is assembled: + +```kotlin +interface Phase1ContextProvider { + val server: VRServer + val config: AppConfig + val serialServer: SerialServer +} +``` + +**`AppContextProvider`** - the full application context. Available after all bootstrap modules are constructed: + +```kotlin +interface AppContextProvider : Phase1ContextProvider { + val skeleton: Skeleton + val firmwareManager: FirmwareManager + val vrcConfigManager: VRCConfigManager? + val provisioningManager: ProvisioningManager + val heightCalibrationManager: HeightCalibrationManager + val trackingChecklist: TrackingChecklist + fun startObserving() +} +``` + +**The rule:** always use `AppContextProvider` unless the module genuinely cannot be constructed after `AppContext` is assembled. In practice, only bootstrap modules (`FirmwareManager`, `Skeleton`, `ProvisioningManager`, `HeightCalibrationManager`) use `Phase1ContextProvider` in `create()`. Runtime modules like `SolarXRBridge` always receive `AppContextProvider`. + +The concrete classes `Phase1Context` and `AppContext` implement these interfaces. Tests implement the interfaces directly with only the fields they need - see [Testing](#testing). + +--- + +## Actions + +Actions are `sealed interface`s. This matters because the compiler enforces exhaustive `when` in reducers - you cannot forget to handle a new action type. Named actions also mean you can grep for all places a specific transition can occur. + +```kotlin +sealed interface UDPConnectionActions { + data class Handshake(val deviceId: Int) : UDPConnectionActions + data class LastPacket(val packetNum: Long?, val time: Long) : UDPConnectionActions + data object Disconnected : UDPConnectionActions +} +``` + +The choice between a flexible `Update` action and a named action is about who else needs to react: + +- **`Update`** when only the caller cares about the change. It carries a lambda that transforms state directly. No other behaviour pattern-matches it, so there's no coupling. +- **Named action** when other behaviours need to react to this specific event. `Handshake` in `UDPConnectionActions` signals to multiple behaviours that a connection is now established; each one matches it independently. + +```kotlin +// Nothing else needs to react - use Update +tracker.context.dispatch(TrackerActions.Update { copy(tps = newTps) }) + +// Multiple behaviours react to this - use a named action +connection.context.dispatch(UDPConnectionActions.Handshake(deviceId = id)) +``` + +If you find yourself dispatching an `Update` that other behaviours start matching against, it's time to promote it to a named action. + +--- + +## EventDispatcher + +`EventDispatcher` routes events to typed listeners. Each behaviour registers its own listener in `observe`: + +```kotlin +receiver.packetEvents.on { packet -> ... } // only SensorInfo +receiver.packetEvents.onAny { packet -> ... } // every packet +``` + +**Why not a central `when` block?** + +A central switch means every new packet type requires editing the dispatch hub. With `EventDispatcher`, each behaviour owns its subscription. Adding a new packet type means adding a class to `packets.kt` and a listener in a behaviour - the routing hub never changes. + +It also means behaviours are self-contained: `SensorInfoBehaviour` is the only place that knows what to do with `SensorInfo`. No shared routing code. + +`EventDispatcher` dispatches by the runtime type of the event by default. When events are wrapped (e.g. `PacketEvent` where the actual packet is the inner value), pass a `keyOf` lambda to tell the dispatcher which type to route by - otherwise all events would be bucketed under `PacketEvent` regardless of the inner packet type. + +--- + +## Coroutine Scope and Lifetime + +Every module receives a `CoroutineScope` at creation. That scope defines when the module lives and dies: + +- Cancelling the scope cancels all coroutines - all behaviours launched inside it +- No `close()`, `stop()`, or `dispose()` methods needed anywhere +- A disconnected client, a dropped UDP connection, a closed serial port - all cleaned up by scope cancellation + +Blocking I/O goes on `Dispatchers.IO`. State updates and logic stay on the default dispatcher. Never use `runBlocking` inside an observer - it blocks the coroutine thread pool. + +--- + +## State vs. Plain Data + +Not everything needs to be in a `StateFlow`. The rule: **put data in state only if other code needs to react to it changing**. + +- `VRServer.handleCounter` is an `AtomicInt` - nothing reacts to it, so a dispatch round-trip would be waste. +- `UDPTrackerServer` has no `Context` at all. Its connection map is a `MutableMap` internal to the server loop. Nothing outside reads it. +- Tracker config *is* in state because the SolarXR layer and the config autosave both react to body part assignments changing. + +--- + +## Cross-Module Communication + +Modules don't call each other directly. A behaviour in one module subscribes to another module's `StateFlow` and dispatches into its own context (or another module's context) when something relevant changes. + +Two common patterns: + +**React to another module's state:** +```kotlin +// Inside TrackerConfigBehaviour.observe() +// When tracker state changes, persist the relevant fields to Settings +receiver.context.state + .distinctUntilChangedBy { stateToConfig(it) } + .drop(1) + .onEach { state -> + settings.context.dispatch(SettingsActions.UpdateTracker(hardwareId) { stateToConfig(state) }) + } + .launchIn(receiver.context.scope) +``` + +**Dispatch into VRServer from a connection:** +```kotlin +// Inside HandshakeBehaviour.observe() - a UDP connection registers a new device +receiver.packetEvents.on { event -> + val device = Device.create(...) + receiver.serverContext.context.dispatch(VRServerActions.NewDevice(deviceId, device)) +} +``` + +The key rule: behaviours own the subscription lifetime via `launchIn(receiver.context.scope)`. When the module dies, the subscription stops - no dangling listeners in foreign modules. + +--- + +## IPC and Transport + +The server exposes four connection points: + +| Transport | Client | Protocol | +|---|---|---| +| Unix socket / named pipe `SlimeVRDriver` | OpenVR driver | Protobuf (Wire) | +| Unix socket / named pipe `SlimeVRInput` | External feeder | Protobuf (Wire) | +| Unix socket / named pipe `SlimeVRRpc` | SolarXR (IPC path) | FlatBuffers | +| WebSocket port 21110 | GUI / third-party (SolarXR) | FlatBuffers | + +**Why separate transport from protocol?** + +Platform files (`linux.kt`, `windows.kt`) own reading frames off a socket and produce a `Flow` + a `send` function. Protocol handlers (`protocol.kt`, `ipc.kt`) are plain `suspend fun`s that take those two things and know nothing about Unix sockets or named pipes. + +The same `handleSolarXRBridge` function runs on Linux sockets, Windows pipes, and WebSocket. The WebSocket adapter in `ws-server.kt` converts Ktor frames to the same `Flow` + `send` abstraction. The handler never changes. + +**Why events for IPC message routing?** + +Same reason as UDP packets. The SolarXR protocol has dozens of RPC message types. Behaviours that handle firmware updates, skeleton config, data feeds, etc. each register their own listener on the `rpcDispatcher`. Adding a new RPC handler doesn't touch existing ones. + +### Wire Framing (named sockets) + +All three named sockets use the same framing: a **LE u32 length** prefix (including the 4-byte header itself) followed by the raw payload bytes. + +### UDP Packet Pipeline + +The UDP receive loop runs on `Dispatchers.IO`. It reads a datagram, parses the packet type and payload via `readPacket`, wraps the result in a `PacketEvent`, and pushes it into a `Channel>` per connection. + +A dedicated coroutine per connection drains the channel and calls `EventDispatcher.emit`. Pre-handshake packets are filtered at the channel drain step - a single guard in one place, not spread across every behaviour. + +--- + +## Config + +Config is not just file I/O - it is a live state machine. The server supports runtime profile switching, so config data lives in a `StateFlow` like everything else. Code that needs to react to config changes (e.g. tracker body part assignment saving to disk) subscribes to the settings state flow. + +The config system has three layers: + +- **`AppConfig`** - global state: which user profile and settings profile are active +- **`UserConfig`** - per-user data (body proportions, etc.) +- **`Settings`** - per-profile settings (tracker assignments, port, VRC warnings); persists to disk + +Each is a full module with its own `Context`, behaviours, and JSON autosave. Migrations live in the module's `parseAndMigrate` function - add a `version < N` branch there. + +--- + +## Testing + +The module pattern is designed to be testable at each layer independently. + +### Testing reducers + +Create a context directly with the behaviours you want to test. No module class, no scope tricks: + +```kotlin +val context = Context.create( + initialState = FirmwareManagerState(jobs = mapOf()), + scope = this, + behaviours = listOf(FirmwareManagerBaseBehaviour), +) +context.dispatch(FirmwareManagerActions.UpdateJob(...)) +assertEquals(FirmwareUpdateStatus.UPLOADING, context.state.value.jobs["COM1"]?.status) +``` + +### Testing observe behaviour + +Construct the module directly - bypassing `create()` - with only the behaviours relevant to the test. This avoids constructing unrelated dependencies: + +```kotlin +val serialServer = buildTestSerialServer(backgroundScope) +val context = Context.create( + initialState = ProvisioningManager.INITIAL_STATE, + scope = backgroundScope, + behaviours = listOf(ProvisioningManagerBaseBehaviour), +) +val manager = ProvisioningManager(context = context, serialServer = serialServer, scope = backgroundScope) +manager.startObserving() + +manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) +``` + +For modules that need an `AppContextProvider` (e.g. `SolarXRBridge`), extend `TestAppContext` and override only what the test uses. Every other field defaults to `error("not used in test")`: + +```kotlin +val server = buildTestVrServer(backgroundScope) +val skeleton = buildTestSkeleton(backgroundScope) +val appContext = object : TestAppContext() { + override val server = server + override val skeleton = skeleton +} +val context = Context.create( + initialState = SolarXRBridgeState(dataFeedConfigs = listOf(), datafeedTimers = listOf()), + scope = backgroundScope, + behaviours = listOf(DataFeedInitBehaviour(server, skeleton)), +) +val bridge = SolarXRBridge(id = 1, context = context, appContext = appContext, + dataFeedDispatcher = EventDispatcher(), rpcDispatcher = EventDispatcher()) +bridge.startObserving() +``` + +`TestAppContext` lives in `TestServer.kt`. When a new module is added to `AppContextProvider`, add a default stub there - all existing tests automatically get the default without changes. + +**The pattern:** only initialize what the test exercises. If a behaviour accesses a field that isn't overridden, it throws - that's intentional. It keeps tests focused and stubs minimal. + +### Test helpers (`TestServer.kt`) + +`buildTestVrServer`, `buildTestSerialServer`, `buildTestUserConfig`, and `buildTestSkeleton` are available for constructing real lightweight instances of common dependencies. + +--- + +## What Goes Where + +| Location | Purpose | +|---|---| +| `server/core` | Protocol-agnostic business logic | +| `server/desktop` | Platform entry point, IPC wiring, HID, serial flash | +| `context/context.kt` | `Context` and `Behaviour` primitives - no domain logic here | +| `udp/` | UDP wire protocol: connection state, packets, behaviours, server loop | +| `hid/` | HID tracker receiver: registration, rotation, battery, status | +| `solarxr/` | `SolarXRBridge` + per-feature behaviour files (one per RPC area) | +| `config/` | 3-tier config: `AppConfig`, `UserConfig`, `Settings` | +| `firmware/` | OTA and serial flash; independent of UDP tracker protocol | +| `trackingchecklist/` | Checklist steps as behaviours observing server/tracker/VRC state | +| `skeleton/` | Skeleton solving, proportions, bone geometry | +| `vrchat/` | VRChat config monitoring and recommended-value computation | + +--- + +## Extending the Server + +### Adding a New Module + +1. **Define state and actions** in `module.kt`: +```kotlin +data class MyState(val connected: Boolean) + +sealed interface MyActions { + data object Connected : MyActions + data object Disconnected : MyActions +} + +typealias MyContext = Context +typealias MyBehaviour = Behaviour +``` + +2. **Define the module class** with a `create()` factory and a `startObserving()` method: +```kotlin +class MyModule(val context: MyContext, val server: VRServer) { + fun startObserving() = context.observeAll(this) + + companion object { + fun create(ctx: Phase1ContextProvider, scope: CoroutineScope): MyModule { + val context = Context.create( + initialState = MyState(false), + scope = scope, + behaviours = listOf(MyCoreBehaviour), + ) + return MyModule(context, ctx.server) + } + } +} +``` + +3. **Register in `AppContext`** and call `startObserving()` inside `AppContext.startObserving()`. + +4. **Write behaviours** in `behaviours.kt` (or per-feature files for large modules). + +### Adding a Behaviour to an Existing Module + +Add it to the `behaviours` list in `create()`. The behaviour's `reduce` and `observe` are automatically wired when `startObserving()` is called. Nothing else changes. + +### Adding a New UDP Packet Type + +1. Add the packet class and its `read` function in `udp/packets.kt` +2. Add an entry to the `PacketType` enum and a branch in `readPacket()` +3. Register a listener in a behaviour's `observe`: +```kotlin +receiver.packetEvents.on { event -> + receiver.context.dispatch(...) +} +``` + +--- + +## Style Conventions + +- **Prefer plain functions over classes.** A single-method interface should almost always be a function type (`() -> Unit`, `suspend (ByteArray) -> Unit`). A class with no mutable state should be a plain function or `object`. When in doubt, write a function. +- **Prefer plain functions over extension functions.** Only use extensions when the receiver type is genuinely the primary subject. +- **State data classes are all `val`.** Mutable fields in a state class are a design mistake - use `var` in a plain class if you need local mutability, never in state. +- **Use `sealed interface` for actions**, not `sealed class` - no constructor overhead. +- **Module creation lives in `companion object { fun create(...) }`.** +- **Module observation wiring lives in `startObserving()`**, called via `context.observeAll(this)`. +- **Never expose `MutableStateFlow` directly.** Expose `StateFlow` via `context.state`. diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index e48cd0c3fb..39180108e9 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -22,12 +22,12 @@ plugins { kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } @@ -83,7 +83,7 @@ val deleteTempKeyStore = tasks.register("deleteTempKeyStore") { tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_22) freeCompilerArgs.set(listOf("-Xvalue-classes")) } } @@ -217,7 +217,7 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 } } diff --git a/server/architecture.dot b/server/architecture.dot new file mode 100644 index 0000000000..fa6c53d9ee --- /dev/null +++ b/server/architecture.dot @@ -0,0 +1,123 @@ +digraph module_architecture { + rankdir=TB + splines=false + nodesep=0.8 + ranksep=0.8 + forcelabels=true + fontname="monospace" + node [fontname="monospace", fontsize=11, style="filled,rounded", shape=box, margin="0.3,0.2"] + edge [fontname="monospace", fontsize=10] + + ext [ + label="external events\n(packets · messages)" + shape=ellipse fillcolor="#dae8fc" color="#6c8ebf" + ] + + subgraph cluster_A { + label="Module A" + style="rounded" bgcolor="#fafafa" color="#999999" penwidth=1.5 + + subgraph cluster_A_beh { + label="Behaviours" + style="dashed" color="#aaaaaa" bgcolor="#fffff8" + + bA1 [label="BehaviourA1\n────────────────────\nobserve(receiver)\n subscribes to flows\n calls dispatch()\n────────────────────\nreduce(state, action)\n pure: State -> State" + fillcolor="#fff9db" color="#d6ae00"] + + bA2 [label="BehaviourA2\n────────────────────\nobserve(receiver)\n ...\n────────────────────\nreduce(state, action)\n ..." + fillcolor="#fff9db" color="#d6ae00"] + + bAmore [label="···\n────────────────────\nobserve(receiver)\n ...\n────────────────────\nreduce(state, action)\n ..." + fillcolor="#fff9db" color="#d6ae00"] + } + + foldA [ + label="reduce fold\n──────────────────────────────────\nstate = BehaviourA1.reduce(state, a)\nstate = BehaviourA2.reduce(state, a)\nstate = ···" + fillcolor="#d5e8d4" color="#82b366" + ] + + sfA [ + label="StateFlow" + shape=cylinder fillcolor="#dae8fc" color="#6c8ebf" + ] + + } + + subgraph cluster_B { + label="Module B" + style="rounded" bgcolor="#fafafa" color="#999999" penwidth=1.5 + + subgraph cluster_B_beh { + label="Behaviours" + style="dashed" color="#aaaaaa" bgcolor="#fffff8" + + bBmore [label="···\n────────────────────\nobserve(receiver)\n ...\n────────────────────\nreduce(state, action)\n ..." + fillcolor="#fff9db" color="#d6ae00"] + } + + foldB [ + label="reduce fold" + fillcolor="#d5e8d4" color="#82b366" + ] + + sfB [ + label="StateFlow" + shape=cylinder fillcolor="#dae8fc" color="#6c8ebf" + ] + } + + subgraph cluster_C { + label="Module C" + style="rounded" bgcolor="#fafafa" color="#999999" penwidth=1.5 + + subgraph cluster_C_beh { + label="Behaviours" + style="dashed" color="#aaaaaa" bgcolor="#fffff8" + + bCmore [label="···\n────────────────────\nobserve(receiver)\n ...\n────────────────────\nreduce(state, action)\n ..." + fillcolor="#fff9db" color="#d6ae00"] + } + + foldC [ + label="reduce fold" + fillcolor="#d5e8d4" color="#82b366" + ] + + sfC [ + label="StateFlow" + shape=cylinder fillcolor="#dae8fc" color="#6c8ebf" + ] + + } + + // ext -> all modules + ext -> bA1 [color="#6c8ebf" fontcolor="#6c8ebf" weight=1] + ext -> bA2 [color="#6c8ebf" weight=1] + ext -> bAmore [color="#6c8ebf" weight=1] + ext -> bBmore [color="#6c8ebf" weight=1] + ext -> bCmore [color="#6c8ebf" weight=1] + + // Module A internal flow + bA1 -> foldA [xlabel="dispatch(action)" color="#d6ae00" fontcolor="#d6ae00"] + bA2 -> foldA [xlabel="dispatch(action)" color="#d6ae00" fontcolor="#d6ae00"] + bAmore -> foldA [xlabel="dispatch(action)" color="#d6ae00" fontcolor="#d6ae00"] + foldA -> sfA [xlabel="new state" color="#82b366" fontcolor="#82b366"] + + // Module B internal flow + bBmore -> foldB [xlabel="dispatch(action)" color="#d6ae00" fontcolor="#d6ae00"] + foldB -> sfB [color="#82b366"] + + // Module C internal flow + bCmore -> foldC [xlabel="dispatch(action)" color="#d6ae00" fontcolor="#d6ae00"] + foldC -> sfC [color="#82b366"] + // Within-module feedback (gray) + bA1 -> sfA [xlabel="collect()" dir=back style=dashed color="#888888" fontcolor="#888888" constraint=true] + bBmore -> sfB [xlabel="collect()" dir=back style=dashed color="#888888" fontcolor="#888888" constraint=true] + bCmore -> sfC [xlabel="collect()" dir=back style=dashed color="#888888" fontcolor="#888888" constraint=true] + + + // Cross-module: routed via waypoints at rank 4 — exits downward, travels below all modules + bBmore -> sfA [xlabel="collect()" dir=back style=dashed color="#FF0000" fontcolor="#FF0000" constraint=false] + bCmore -> sfA [xlabel="collect()" dir=back style=dashed color="#FF0000" fontcolor="#FF0000" constraint=false] + bCmore -> sfB [xlabel="collect()" dir=back style=dashed color="#FF0000" fontcolor="#FF0000" constraint=false] +} diff --git a/server/architecture.png b/server/architecture.png new file mode 100644 index 0000000000..c3368f27ff Binary files /dev/null and b/server/architecture.png differ diff --git a/server/architecture.svg b/server/architecture.svg new file mode 100644 index 0000000000..0b9a9ef08e --- /dev/null +++ b/server/architecture.svg @@ -0,0 +1,280 @@ + + + + + + +module_architecture + + +cluster_A + +Module A + + +cluster_A_beh + +Behaviours + + +cluster_B + +Module B + + +cluster_B_beh + +Behaviours + + +cluster_C + +Module C + + +cluster_C_beh + +Behaviours + + + +ext + +external events +(packets · messages) + + + +bA1 + +BehaviourA1 +──────────────────── +observe(receiver) +  subscribes to flows +  calls dispatch() +──────────────────── +reduce(state, action) +  pure: State -> State + + + +ext->bA1 + + + + + +bA2 + +BehaviourA2 +──────────────────── +observe(receiver) +  ... +──────────────────── +reduce(state, action) +  ... + + + +ext->bA2 + + + + + +bAmore + +··· +──────────────────── +observe(receiver) +  ... +──────────────────── +reduce(state, action) +  ... + + + +ext->bAmore + + + + + +bBmore + +··· +──────────────────── +observe(receiver) +  ... +──────────────────── +reduce(state, action) +  ... + + + +ext->bBmore + + + + + +bCmore + +··· +──────────────────── +observe(receiver) +  ... +──────────────────── +reduce(state, action) +  ... + + + +ext->bCmore + + + + + +foldA + +reduce fold +────────────────────────────────── +state = BehaviourA1.reduce(state, a) +state = BehaviourA2.reduce(state, a) +state = ··· + + + +bA1->foldA + + +dispatch(action) + + + +sfA + + +StateFlow<StateA> + + + +bA1->sfA + + +collect() + + + +bA2->foldA + + +dispatch(action) + + + +bAmore->foldA + + +dispatch(action) + + + +foldA->sfA + + +new state + + + +bBmore->sfA + + +collect() + + + +foldB + +reduce fold + + + +bBmore->foldB + + +dispatch(action) + + + +sfB + + +StateFlow<StateB> + + + +bBmore->sfB + + +collect() + + + +foldB->sfB + + + + + +bCmore->sfA + + +collect() + + + +bCmore->sfB + + +collect() + + + +foldC + +reduce fold + + + +bCmore->foldC + + +dispatch(action) + + + +sfC + + +StateFlow<StateC> + + + +bCmore->sfC + + +collect() + + + +foldC->sfC + + + + + diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 6e0662e73b..da4159f54a 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -42,6 +42,7 @@ configure { ",dev.slimevr.tracking.trackers.*,dev.slimevr.desktop.platform.ProtobufMessages.*" + ",solarxr_protocol.rpc.*,kotlinx.coroutines.*,com.illposed.osc.*,android.app.*", "ij_kotlin_allow_trailing_comma" to true, + "ktlint_standard_filename" to "disabled", ) val ktlintVersion = "1.8.0" kotlinGradle { diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index 0f34ebb915..c63b1b0ccb 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -14,20 +14,19 @@ plugins { `java-library` } -// FIXME: Please replace these to Java 11 as that's what they actually are kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_24) freeCompilerArgs.set(listOf("-Xvalue-classes")) } } @@ -60,24 +59,23 @@ allprojects { dependencies { implementation(project(":solarxr-protocol")) - // This dependency is used internally, - // and not exposed to consumers on their own compile classpath. implementation("com.google.flatbuffers:flatbuffers-java:22.10.26") - implementation("commons-cli:commons-cli:1.11.0") - implementation("com.fasterxml.jackson.core:jackson-databind:2.21.0") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.0") - - implementation("com.github.jonpeterson:jackson-module-model-versioning:1.2.2") - implementation("org.apache.commons:commons-math3:3.6.1") - implementation("org.apache.commons:commons-lang3:3.20.0") - implementation("org.apache.commons:commons-collections4:4.5.0") implementation("com.illposed.osc:javaosc-core:0.9") - implementation("org.java-websocket:Java-WebSocket:1.+") implementation("com.melloware:jintellitype:1.+") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("com.mayakapps.kache:kache:2.1.1") + implementation("io.klogging:klogging:0.11.7") + implementation("io.klogging:slf4j-klogging:0.11.7") + + val ktor_version = "3.4.1" + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("io.ktor:ktor-utils:$ktor_version") api("com.github.loucass003:EspflashKotlin:v0.11.0") @@ -92,6 +90,7 @@ dependencies { testImplementation(platform("org.junit:junit-bom:6.0.2")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.platform:junit-platform-launcher") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") } tasks.test { diff --git a/server/core/src/main/java/dev/slimevr/Keybinding.kt b/server/core/src/main/java/dev/slimevr/Keybinding.kt deleted file mode 100644 index 9f20f768a8..0000000000 --- a/server/core/src/main/java/dev/slimevr/Keybinding.kt +++ /dev/null @@ -1,96 +0,0 @@ -package dev.slimevr - -import com.melloware.jintellitype.HotkeyListener -import com.melloware.jintellitype.JIntellitype -import dev.slimevr.config.KeybindingsConfig -import dev.slimevr.tracking.trackers.TrackerUtils -import io.eiren.util.OperatingSystem -import io.eiren.util.OperatingSystem.Companion.currentPlatform -import io.eiren.util.ann.AWTThread -import io.eiren.util.logging.LogManager - -class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener { - val config: KeybindingsConfig = server.configManager.vrConfig.keybindings - - init { - if (currentPlatform != OperatingSystem.WINDOWS) { - LogManager - .info( - "[Keybinding] Currently only supported on Windows. Keybindings will be disabled.", - ) - } else { - try { - if (JIntellitype.getInstance() != null) { - JIntellitype.getInstance().addHotKeyListener(this) - - val fullResetBinding = config.fullResetBinding - JIntellitype.getInstance() - .registerHotKey(FULL_RESET, fullResetBinding) - LogManager.info("[Keybinding] Bound full reset to $fullResetBinding") - - val yawResetBinding = config.yawResetBinding - JIntellitype.getInstance() - .registerHotKey(YAW_RESET, yawResetBinding) - LogManager.info("[Keybinding] Bound yaw reset to $yawResetBinding") - - val mountingResetBinding = config.mountingResetBinding - JIntellitype.getInstance() - .registerHotKey(MOUNTING_RESET, mountingResetBinding) - LogManager.info("[Keybinding] Bound reset mounting to $mountingResetBinding") - - val feetMountingResetBinding = config.feetMountingResetBinding - JIntellitype.getInstance() - .registerHotKey(FEET_MOUNTING_RESET, feetMountingResetBinding) - LogManager.info("[Keybinding] Bound feet reset mounting to $feetMountingResetBinding") - - val pauseTrackingBinding = config.pauseTrackingBinding - JIntellitype.getInstance() - .registerHotKey(PAUSE_TRACKING, pauseTrackingBinding) - LogManager.info("[Keybinding] Bound pause tracking to $pauseTrackingBinding") - } - } catch (e: Throwable) { - LogManager - .warning( - "[Keybinding] JIntellitype initialization failed. Keybindings will be disabled. Try restarting your computer.", - ) - } - } - } - - @AWTThread - override fun onHotKey(identifier: Int) { - when (identifier) { - FULL_RESET -> server.scheduleResetTrackersFull(RESET_SOURCE_NAME, config.fullResetDelay) - - YAW_RESET -> server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, config.yawResetDelay) - - MOUNTING_RESET -> server.scheduleResetTrackersMounting( - RESET_SOURCE_NAME, - config.mountingResetDelay, - ) - - FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting( - RESET_SOURCE_NAME, - config.feetMountingResetDelay, - TrackerUtils.feetsBodyParts, - ) - - PAUSE_TRACKING -> - server - .scheduleTogglePauseTracking( - RESET_SOURCE_NAME, - config.pauseTrackingDelay, - ) - } - } - - companion object { - private const val RESET_SOURCE_NAME = "Keybinding" - - private const val FULL_RESET = 1 - private const val YAW_RESET = 2 - private const val MOUNTING_RESET = 3 - private const val FEET_MOUNTING_RESET = 4 - private const val PAUSE_TRACKING = 5 - } -} diff --git a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt deleted file mode 100644 index 4c6e77b0ab..0000000000 --- a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.slimevr - -data class NetworkInfo( - val name: String?, - val description: String?, - val category: NetworkCategory?, - val connectivity: Set?, - val connected: Boolean?, -) - -/** - * @see NLM_NETWORK_CATEGORY enumeration (netlistmgr.h) - */ -enum class NetworkCategory(val value: Int) { - PUBLIC(0), - PRIVATE(1), - DOMAIN_AUTHENTICATED(2), - ; - - companion object { - fun fromInt(value: Int) = values().find { it.value == value } - } -} - -/** - * @see NLM_CONNECTIVITY enumeration (netlistmgr.h) - */ -enum class ConnectivityFlags(val value: Int) { - DISCONNECTED(0), - IPV4_NOTRAFFIC(0x1), - IPV6_NOTRAFFIC(0x2), - IPV4_SUBNET(0x10), - IPV4_LOCALNETWORK(0x20), - IPV4_INTERNET(0x40), - IPV6_SUBNET(0x100), - IPV6_LOCALNETWORK(0x200), - IPV6_INTERNET(0x400), - ; - - companion object { - fun fromInt(value: Int): Set = if (value == 0) { - setOf(DISCONNECTED) - } else { - values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet() - } - } -} - -abstract class NetworkProfileChecker { - abstract val isSupported: Boolean - abstract val publicNetworks: List -} - -class NetworkProfileCheckerStub : NetworkProfileChecker() { - override val isSupported: Boolean - get() = false - override val publicNetworks: List - get() = listOf() -} diff --git a/server/core/src/main/java/dev/slimevr/NetworkProtocol.java b/server/core/src/main/java/dev/slimevr/NetworkProtocol.java deleted file mode 100644 index 07366b1c81..0000000000 --- a/server/core/src/main/java/dev/slimevr/NetworkProtocol.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.slimevr; - -public enum NetworkProtocol { - OWO_LEGACY, - SLIMEVR_RAW, - SLIMEVR_FLATBUFFER, - SLIMEVR_WEBSOCKET -} diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt deleted file mode 100644 index 4db758d06d..0000000000 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ /dev/null @@ -1,487 +0,0 @@ -package dev.slimevr - -import com.jme3.system.NanoTimer -import dev.slimevr.autobone.AutoBoneHandler -import dev.slimevr.bridge.Bridge -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.config.ConfigManager -import dev.slimevr.firmware.FirmwareUpdateHandler -import dev.slimevr.firmware.SerialFlashingHandler -import dev.slimevr.games.vrchat.VRCConfigHandler -import dev.slimevr.games.vrchat.VRCConfigHandlerStub -import dev.slimevr.games.vrchat.VRChatConfigManager -import dev.slimevr.guards.ServerGuards -import dev.slimevr.osc.OSCHandler -import dev.slimevr.osc.OSCRouter -import dev.slimevr.osc.VMCHandler -import dev.slimevr.osc.VRCOSCHandler -import dev.slimevr.posestreamer.BVHRecorder -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler -import dev.slimevr.reset.ResetHandler -import dev.slimevr.reset.ResetTimerManager -import dev.slimevr.reset.resetTimer -import dev.slimevr.serial.ProvisioningHandler -import dev.slimevr.serial.SerialHandler -import dev.slimevr.serial.SerialHandlerStub -import dev.slimevr.setup.HandshakeHandler -import dev.slimevr.setup.TapSetupHandler -import dev.slimevr.status.StatusSystem -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.tracking.trackers.* -import dev.slimevr.tracking.trackers.udp.TrackersUDPServer -import dev.slimevr.trackingchecklist.TrackingChecklistManager -import dev.slimevr.util.ann.VRServerThread -import dev.slimevr.websocketapi.WebSocketVRBridge -import io.eiren.util.ann.ThreadSafe -import io.eiren.util.ann.ThreadSecure -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import solarxr_protocol.datatypes.TrackerIdT -import solarxr_protocol.rpc.ResetType -import java.util.* -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.atomic.AtomicInteger -import java.util.function.Consumer -import kotlin.collections.ArrayList -import kotlin.concurrent.schedule - -typealias BridgeProvider = ( - server: VRServer, - computedTrackers: List, -) -> Sequence - -const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR" - -class VRServer @JvmOverloads constructor( - bridgeProvider: BridgeProvider = { _, _ -> sequence {} }, - serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() }, - flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null }, - vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() }, - networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> NetworkProfileCheckerStub() }, - acquireMulticastLock: () -> Any? = { null }, - @JvmField val configManager: ConfigManager, -) : Thread("VRServer") { - - @JvmField - val humanPoseManager: HumanPoseManager - private val trackers: MutableList = FastList() - val trackersServer: TrackersUDPServer - private val bridges: MutableList = FastList() - private val tasks: Queue = LinkedBlockingQueue() - private val newTrackersConsumers: MutableList> = FastList() - private val trackerStatusListeners: MutableList = FastList() - private val onTick: MutableList = FastList() - private val lock = acquireMulticastLock() - val oSCRouter: OSCRouter - - @JvmField - val vrcOSCHandler: VRCOSCHandler - val vMCHandler: VMCHandler - - @JvmField - val deviceManager: DeviceManager - - @JvmField - val bvhRecorder: BVHRecorder - - @JvmField - val serialHandler: SerialHandler - - var serialFlashingHandler: SerialFlashingHandler? - - val firmwareUpdateHandler: FirmwareUpdateHandler - - val vrcConfigManager: VRChatConfigManager - - @JvmField - val autoBoneHandler: AutoBoneHandler - - @JvmField - val tapSetupHandler: TapSetupHandler - - @JvmField - val protocolAPI: ProtocolAPI - private val timer = Timer() - private val resetTimerManager = ResetTimerManager() - val fpsTimer = NanoTimer() - - @JvmField - val provisioningHandler: ProvisioningHandler - - @JvmField - val resetHandler: ResetHandler - - @JvmField - val statusSystem = StatusSystem() - - @JvmField - val handshakeHandler = HandshakeHandler() - - val trackingChecklistManager: TrackingChecklistManager - - val networkProfileChecker: NetworkProfileChecker - - val serverGuards = ServerGuards() - - init { - // UwU - deviceManager = DeviceManager(this) - serialHandler = serialHandlerProvider(this) - serialFlashingHandler = flashingHandlerProvider(this) - provisioningHandler = ProvisioningHandler(this) - resetHandler = ResetHandler() - tapSetupHandler = TapSetupHandler() - humanPoseManager = HumanPoseManager(this) - // AutoBone requires HumanPoseManager first - autoBoneHandler = AutoBoneHandler(this) - firmwareUpdateHandler = FirmwareUpdateHandler(this) - vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this)) - networkProfileChecker = networkProfileProvider(this) - trackingChecklistManager = TrackingChecklistManager(this) - protocolAPI = ProtocolAPI(this) - val computedTrackers = humanPoseManager.computedTrackers - - // Start server for SlimeVR trackers - val trackerPort = configManager.vrConfig.server.trackerPort - LogManager.info("Starting the tracker server on port $trackerPort...") - trackersServer = TrackersUDPServer( - trackerPort, - "Sensors UDP server", - ) { tracker: Tracker -> registerTracker(tracker) } - - // Start bridges and WebSocket server - for (bridge in bridgeProvider(this, computedTrackers) + sequenceOf(WebSocketVRBridge(computedTrackers, this))) { - tasks.add(Runnable { bridge.startBridge() }) - bridges.add(bridge) - } - - // Initialize OSC handlers - vrcOSCHandler = VRCOSCHandler( - this, - configManager.vrConfig.vrcOSC, - computedTrackers, - ) - vMCHandler = VMCHandler( - this, - humanPoseManager, - configManager.vrConfig.vmc, - ) - - // Initialize OSC router - val oscHandlers = FastList() - oscHandlers.add(vrcOSCHandler) - oscHandlers.add(vMCHandler) - oSCRouter = OSCRouter(configManager.vrConfig.oscRouter, oscHandlers) - bvhRecorder = BVHRecorder(this) - for (tracker in computedTrackers) { - registerTracker(tracker) - } - - instance = this - } - - fun hasBridge(bridgeClass: Class): Boolean { - for (bridge in bridges) { - if (bridgeClass.isAssignableFrom(bridge.javaClass)) { - return true - } - } - return false - } - - // FIXME: Code using this function normally uses this to get the SteamVR driver but - // that's because we first save the SteamVR driver bridge and then the feeder in the array. - // Not really a great thing to have. - @ThreadSafe - fun getVRBridge(bridgeClass: Class): E? { - for (bridge in bridges) { - if (bridgeClass.isAssignableFrom(bridge.javaClass)) { - return bridgeClass.cast(bridge) - } - } - return null - } - - fun addOnTick(runnable: Runnable) { - onTick.add(runnable) - } - - @ThreadSafe - fun addNewTrackerConsumer(consumer: Consumer) { - queueTask { - newTrackersConsumers.add(consumer) - for (tracker in trackers) { - consumer.accept(tracker) - } - } - } - - @ThreadSafe - fun trackerUpdated(tracker: Tracker?) { - queueTask { - humanPoseManager.trackerUpdated(tracker) - updateSkeletonModel() - refreshTrackersDriftCompensationEnabled() - configManager.vrConfig.writeTrackerConfig(tracker) - configManager.saveConfig() - } - } - - @ThreadSafe - fun addSkeletonUpdatedCallback(consumer: Consumer) { - queueTask { humanPoseManager.addSkeletonUpdatedCallback(consumer) } - } - - @VRServerThread - override fun run() { - trackersServer.start() - while (true) { - // final long start = System.currentTimeMillis(); - fpsTimer.update() - do { - val task = tasks.poll() ?: break - task.run() - } while (true) - for (task in onTick) { - task.run() - } - for (bridge in bridges) { - bridge.dataRead() - } - for (tracker in trackers) { - tracker.tick(fpsTimer.timePerFrame) - } - humanPoseManager.update() - for (bridge in bridges) { - bridge.dataWrite() - } - vrcOSCHandler.update() - vMCHandler.update() - // final long time = System.currentTimeMillis() - start; - try { - sleep(1) // 1000Hz - } catch (error: InterruptedException) { - LogManager.info("VRServer thread interrupted") - break - } - } - } - - @ThreadSafe - fun queueTask(r: Runnable) { - tasks.add(r) - } - - @VRServerThread - private fun trackerAdded(tracker: Tracker) { - humanPoseManager.trackerAdded(tracker) - updateSkeletonModel() - if (tracker.isComputed) { - vMCHandler.addComputedTracker(tracker) - } - refreshTrackersDriftCompensationEnabled() - } - - @ThreadSecure - fun registerTracker(tracker: Tracker) { - configManager.vrConfig.readTrackerConfig(tracker) - queueTask { - trackers.add(tracker) - trackerAdded(tracker) - for (tc in newTrackersConsumers) { - tc.accept(tracker) - } - } - } - - @ThreadSafe - fun updateSkeletonModel() { - queueTask { - humanPoseManager.updateSkeletonModelFromServer() - vrcOSCHandler.setHeadTracker(TrackerUtils.getTrackerForSkeleton(trackers, TrackerPosition.HEAD)) - if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) { - RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler) - } - } - } - - fun resetTrackersFull(resetSourceName: String?, bodyParts: List = ArrayList()) { - queueTask { humanPoseManager.resetTrackersFull(resetSourceName, bodyParts) } - } - - fun resetTrackersYaw(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { - queueTask { humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) } - } - - fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) { - queueTask { humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) } - } - - fun clearTrackersMounting(resetSourceName: String?) { - queueTask { humanPoseManager.clearTrackersMounting(resetSourceName) } - } - - fun getPauseTracking(): Boolean = humanPoseManager.getPauseTracking() - - fun setPauseTracking(pauseTracking: Boolean, sourceName: String?) { - queueTask { - humanPoseManager.setPauseTracking(pauseTracking, sourceName) - // Toggle trackers as they don't toggle when tracking is paused - if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) { - RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler) - } - } - } - - fun togglePauseTracking(sourceName: String?) { - queueTask { - humanPoseManager.togglePauseTracking(sourceName) - // Toggle trackers as they don't toggle when tracking is paused - if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) { - RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler) - } - } - } - - fun scheduleResetTrackersFull(resetSourceName: String?, delay: Long, bodyParts: List = ArrayList()) { - resetTimer( - resetTimerManager, - delay, - onTick = { progress -> - resetHandler.sendStarted(ResetType.Full, bodyParts, progress, delay.toInt()) - }, - onComplete = { - queueTask { - humanPoseManager.resetTrackersFull(resetSourceName, bodyParts) - resetHandler.sendFinished(ResetType.Full, bodyParts, delay.toInt()) - } - }, - ) - } - - fun scheduleResetTrackersYaw(resetSourceName: String?, delay: Long, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { - resetTimer( - resetTimerManager, - delay, - onTick = { progress -> - resetHandler.sendStarted(ResetType.Yaw, bodyParts, progress, delay.toInt()) - }, - onComplete = { - queueTask { - humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) - resetHandler.sendFinished(ResetType.Yaw, bodyParts, delay.toInt()) - } - }, - ) - } - - fun scheduleResetTrackersMounting(resetSourceName: String?, delay: Long, bodyParts: List? = null) { - resetTimer( - resetTimerManager, - delay, - onTick = { progress -> - resetHandler.sendStarted(ResetType.Mounting, bodyParts, progress, delay.toInt()) - }, - onComplete = { - queueTask { - humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) - resetHandler.sendFinished(ResetType.Mounting, bodyParts, delay.toInt()) - } - }, - ) - } - - fun scheduleSetPauseTracking(pauseTracking: Boolean, sourceName: String?, delay: Long) { - timer.schedule(delay) { - queueTask { humanPoseManager.setPauseTracking(pauseTracking, sourceName) } - } - } - - fun scheduleTogglePauseTracking(sourceName: String?, delay: Long) { - timer.schedule(delay) { - queueTask { humanPoseManager.togglePauseTracking(sourceName) } - } - } - - fun setLegTweaksEnabled(value: Boolean) { - queueTask { humanPoseManager.setLegTweaksEnabled(value) } - } - - fun setSkatingReductionEnabled(value: Boolean) { - queueTask { humanPoseManager.setSkatingCorrectionEnabled(value) } - } - - fun setFloorClipEnabled(value: Boolean) { - queueTask { humanPoseManager.setFloorClipEnabled(value) } - } - - val trackersCount: Int - get() = trackers.size - val allTrackers: List - get() = FastList(trackers) - - fun getTrackerById(id: TrackerIdT): Tracker? { - for (tracker in trackers) { - if (tracker.trackerNum != id.trackerNum) { - continue - } - - // Handle synthetic devices - if (id.deviceId == null && tracker.device == null) { - return tracker - } - if (tracker.device != null && id.deviceId != null && id.deviceId.id == tracker.device.id) { - // This is a physical tracker, and both device id and the - // tracker num match - return tracker - } - } - return null - } - - fun clearTrackersDriftCompensation() { - for (t in allTrackers) { - if (t.isImu()) { - t.resetsHandler.clearDriftCompensation() - } - } - } - - fun refreshTrackersDriftCompensationEnabled() { - for (t in allTrackers) { - if (t.isImu()) { - t.resetsHandler.refreshDriftCompensationEnabled() - } - } - } - - fun trackerStatusChanged(tracker: Tracker, oldStatus: TrackerStatus, newStatus: TrackerStatus) { - trackerStatusListeners.forEach { it.onTrackerStatusChanged(tracker, oldStatus, newStatus) } - } - - fun addTrackerStatusListener(listener: TrackerStatusListener) { - trackerStatusListeners.add(listener) - } - - fun removeTrackerStatusListener(listener: TrackerStatusListener) { - trackerStatusListeners.removeIf { listener == it } - } - - companion object { - private val nextLocalTrackerId = AtomicInteger() - lateinit var instance: VRServer - private set - - val instanceInitialized: Boolean - get() = ::instance.isInitialized - - @JvmStatic - fun getNextLocalTrackerId(): Int = nextLocalTrackerId.incrementAndGet() - - @JvmStatic - val currentLocalTrackerId: Int - get() = nextLocalTrackerId.get() - } -} diff --git a/server/core/src/main/java/dev/slimevr/app-context.kt b/server/core/src/main/java/dev/slimevr/app-context.kt new file mode 100644 index 0000000000..03564300cd --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/app-context.kt @@ -0,0 +1,57 @@ +package dev.slimevr + +import dev.slimevr.config.AppConfig +import dev.slimevr.firmware.FirmwareManager +import dev.slimevr.heightcalibration.HeightCalibrationManager +import dev.slimevr.provisioning.ProvisioningManager +import dev.slimevr.serial.SerialServer +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.trackingchecklist.TrackingChecklist +import dev.slimevr.udp.UdpServer +import dev.slimevr.vrchat.VRCConfigManager + +interface Phase1ContextProvider { + val server: VRServer + val config: AppConfig + val serialServer: SerialServer +} + +data class Phase1Context( + override val server: VRServer, + override val config: AppConfig, + override val serialServer: SerialServer, +) : Phase1ContextProvider + +interface AppContextProvider : Phase1ContextProvider { + val skeleton: Skeleton + val firmwareManager: FirmwareManager + val vrcConfigManager: VRCConfigManager? + val provisioningManager: ProvisioningManager + val heightCalibrationManager: HeightCalibrationManager + val trackingChecklist: TrackingChecklist + val udpServer: UdpServer + fun startObserving() +} + +class AppContext( + override val server: VRServer, + override val config: AppConfig, + override val serialServer: SerialServer, + override val skeleton: Skeleton, + override val firmwareManager: FirmwareManager, + override val vrcConfigManager: VRCConfigManager?, + override val provisioningManager: ProvisioningManager, + override val heightCalibrationManager: HeightCalibrationManager, + override val trackingChecklist: TrackingChecklist, + override val udpServer: UdpServer, +) : AppContextProvider { + override fun startObserving() { + skeleton.startObserving() + firmwareManager.startObserving() + provisioningManager.startObserving() + heightCalibrationManager.startObserving() + vrcConfigManager?.startObserving() + trackingChecklist.startObserving(this) + udpServer.startReceiving(this, server.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt deleted file mode 100644 index 179602d615..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt +++ /dev/null @@ -1,698 +0,0 @@ -package dev.slimevr.autobone - -import dev.slimevr.SLIMEVR_IDENTIFIER -import dev.slimevr.VRServer -import dev.slimevr.autobone.errors.* -import dev.slimevr.config.AutoBoneConfig -import dev.slimevr.config.SkeletonConfig -import dev.slimevr.poseframeformat.PfrIO -import dev.slimevr.poseframeformat.PfsIO -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import dev.slimevr.tracking.trackers.TrackerRole -import io.eiren.util.OperatingSystem -import io.eiren.util.StringUtils -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Vector3 -import org.apache.commons.lang3.tuple.Pair -import java.io.File -import java.util.* -import java.util.function.Consumer -import java.util.function.Function -import kotlin.math.* - -class AutoBone(private val server: VRServer) { - // This is filled by loadConfigValues() - val offsets = EnumMap( - SkeletonConfigOffsets::class.java, - ) - val adjustOffsets = FastList( - arrayOf( - SkeletonConfigOffsets.HEAD, - SkeletonConfigOffsets.NECK, - SkeletonConfigOffsets.UPPER_CHEST, - SkeletonConfigOffsets.CHEST, - SkeletonConfigOffsets.WAIST, - SkeletonConfigOffsets.HIP, - // HIPS_WIDTH now works when using body proportion error! It's not the - // best still, but it is somewhat functional - SkeletonConfigOffsets.HIPS_WIDTH, - SkeletonConfigOffsets.UPPER_LEG, - SkeletonConfigOffsets.LOWER_LEG, - ), - ) - - var estimatedHeight: Float = 1f - - // The total height of the normalized adjusted offsets - var adjustedHeightNormalized: Float = 1f - - // #region Error functions - var slideError = SlideError() - var offsetSlideError = OffsetSlideError() - var footHeightOffsetError = FootHeightOffsetError() - var bodyProportionError = BodyProportionError() - var heightError = HeightError() - var positionError = PositionError() - var positionOffsetError = PositionOffsetError() - // #endregion - - val globalConfig: AutoBoneConfig = server.configManager.vrConfig.autoBone - val globalSkeletonConfig: SkeletonConfig = server.configManager.vrConfig.skeleton - - init { - loadConfigValues() - } - - private fun loadConfigValues() { - // Remove all previous values - offsets.clear() - - // Get current or default skeleton configs - val skeleton = server.humanPoseManager - // Still compensate for a null skeleton, as it may not be initialized yet - val getOffset: Function = - if (skeleton != null) { - Function { key: SkeletonConfigOffsets -> skeleton.getOffset(key) } - } else { - val defaultConfig = SkeletonConfigManager(false) - Function { config: SkeletonConfigOffsets -> - defaultConfig.getOffset(config) - } - } - for (bone in adjustOffsets) { - val offset = getOffset.apply(bone) - if (offset > 0f) { - offsets[bone] = offset - } - } - } - - fun applyConfig( - humanPoseManager: HumanPoseManager, - offsets: Map = this.offsets, - ) { - for ((offset, value) in offsets) { - humanPoseManager.setOffset(offset, value) - } - } - - @JvmOverloads - fun applyAndSaveConfig(humanPoseManager: HumanPoseManager? = this.server.humanPoseManager): Boolean { - if (humanPoseManager == null) return false - applyConfig(humanPoseManager) - humanPoseManager.saveConfig() - server.configManager.saveConfig() - LogManager.info("[AutoBone] Configured skeleton bone lengths") - return true - } - - fun calcTargetHmdHeight( - frames: PoseFrames, - config: AutoBoneConfig = globalConfig, - ): Float { - val targetHeight: Float - // Get the current skeleton from the server - val humanPoseManager = server.humanPoseManager - // Still compensate for a null skeleton, as it may not be initialized yet - @Suppress("SENSELESS_COMPARISON") - if (config.useSkeletonHeight && humanPoseManager != null) { - // If there is a skeleton available, calculate the target height - // from its configs - targetHeight = humanPoseManager.userHeightFromConfig - LogManager - .warning( - "[AutoBone] Target height loaded from skeleton (Make sure you reset before running!): $targetHeight", - ) - } else { - // Otherwise if there is no skeleton available, attempt to get the - // max HMD height from the recording - val hmdHeight = frames.maxHmdHeight - if (hmdHeight <= MIN_HEIGHT) { - LogManager - .warning( - "[AutoBone] Max headset height detected (Value seems too low, did you not stand up straight while measuring?): $hmdHeight", - ) - } else { - LogManager.info("[AutoBone] Max headset height detected: $hmdHeight") - } - - // Estimate target height from HMD height - targetHeight = hmdHeight - } - return targetHeight - } - - private fun updateRecordingScale(step: PoseFrameStep, scale: Float) { - step.framePlayer1.setScales(scale) - step.framePlayer2.setScales(scale) - step.skeleton1.update() - step.skeleton2.update() - } - - fun filterFrames(frames: PoseFrames, step: PoseFrameStep) { - // Calculate the initial frame errors and recording stats - val frameErrors = FloatArray(frames.maxFrameCount) - val frameStats = StatsCalculator() - val recordingStats = StatsCalculator() - for (i in 0 until frames.maxFrameCount) { - frameStats.reset() - for (j in 0 until frames.maxFrameCount) { - if (i == j) continue - - step.setCursors( - i, - j, - updatePlayerCursors = true, - ) - - frameStats.addValue(getErrorDeriv(step)) - } - frameErrors[i] = frameStats.mean - recordingStats.addValue(frameStats.mean) - // LogManager.info("[AutoBone] Frame: ${i + 1}, Mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})") - } - LogManager.info("[AutoBone] Full recording mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})") - - // Remove outlier frames - val sdMult = 1.4f - val mean = recordingStats.mean - val sd = recordingStats.standardDeviation * sdMult - for (i in frameErrors.size - 1 downTo 0) { - val err = frameErrors[i] - if (err < mean - sd || err > mean + sd) { - for (frameHolder in frames.frameHolders) { - frameHolder.frames.removeAt(i) - } - } - } - step.maxFrameCount = frames.maxFrameCount - - // Calculate and print the resulting recording stats - recordingStats.reset() - for (i in 0 until frames.maxFrameCount) { - frameStats.reset() - for (j in 0 until frames.maxFrameCount) { - if (i == j) continue - - step.setCursors( - i, - j, - updatePlayerCursors = true, - ) - - frameStats.addValue(getErrorDeriv(step)) - } - recordingStats.addValue(frameStats.mean) - } - LogManager.info("[AutoBone] Full recording after mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})") - } - - @Throws(AutoBoneException::class) - fun processFrames( - frames: PoseFrames, - config: AutoBoneConfig = globalConfig, - skeletonConfig: SkeletonConfig = globalSkeletonConfig, - epochCallback: Consumer? = null, - ): AutoBoneResults { - check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." } - check(frames.maxFrameCount > 0) { "Recording has no frames." } - - // Load current values for adjustable configs - loadConfigValues() - - // Set the target heights either from config or calculate them - val targetHmdHeight = if (skeletonConfig.userHeight > MIN_HEIGHT) { - skeletonConfig.userHeight - } else { - calcTargetHmdHeight(frames, config) - } - check(targetHmdHeight > MIN_HEIGHT) { "Configured height ($targetHmdHeight) is too small (<= $MIN_HEIGHT)." } - - // Set up the current state, making all required players and setting up the - // skeletons appropriately - val step = PoseFrameStep( - config = config, - serverConfig = server.configManager, - frames = frames, - preEpoch = { step -> - // Set the current adjust rate based on the current epoch - step.data.adjustRate = decayFunc(step.config.initialAdjustRate, step.config.adjustRateDecay, step.epoch) - }, - onStep = this::step, - postEpoch = { step -> epoch(step, epochCallback) }, - randomSeed = config.randSeed, - data = AutoBoneStep( - targetHmdHeight = targetHmdHeight, - adjustRate = 1f, - ), - ) - - // Normalize the skeletons and get the normalized height for adjusted offsets - scaleSkeleton(step.skeleton1) - scaleSkeleton(step.skeleton2) - adjustedHeightNormalized = sumAdjustedHeightOffsets(step.skeleton1) - - // Normalize offsets based on the initial normalized skeleton - scaleOffsets() - - // Apply the initial normalized config values - applyConfig(step.skeleton1) - applyConfig(step.skeleton2) - - // Initialize normalization to the set target height (also updates skeleton) - estimatedHeight = targetHmdHeight - updateRecordingScale(step, 1f / targetHmdHeight) - - if (config.useFrameFiltering) { - filterFrames(frames, step) - } - - // Iterate frames now that it's set up - PoseFrameIterator.iterateFrames(step) - - // Scale the normalized offsets to the estimated height for the final result - for (entry in offsets.entries) { - entry.setValue(entry.value * estimatedHeight) - } - - LogManager - .info( - "[AutoBone] Target height: ${step.data.targetHmdHeight}, Final height: $estimatedHeight", - ) - if (step.data.errorStats.mean > config.maxFinalError) { - throw AutoBoneException("The final epoch error value (${step.data.errorStats.mean}) has exceeded the maximum allowed value (${config.maxFinalError}).") - } - - return AutoBoneResults( - estimatedHeight, - step.data.targetHmdHeight, - offsets, - ) - } - - private fun epoch( - step: PoseFrameStep, - epochCallback: Consumer? = null, - ) { - val config = step.config - val epoch = step.epoch - - // Calculate average error over the epoch - if (epoch <= 0 || epoch >= config.numEpochs - 1 || (epoch + 1) % config.printEveryNumEpochs == 0) { - LogManager - .info( - "[AutoBone] Epoch: ${epoch + 1}, Mean error: ${step.data.errorStats.mean} (SD ${step.data.errorStats.standardDeviation}), Adjust rate: ${step.data.adjustRate}", - ) - LogManager - .info( - "[AutoBone] Target height: ${step.data.targetHmdHeight}, Estimated height: $estimatedHeight", - ) - } - - if (epochCallback != null) { - // Scale the normalized offsets to the estimated height for the callback - val scaledOffsets = EnumMap(offsets) - for (entry in scaledOffsets.entries) { - entry.setValue(entry.value * estimatedHeight) - } - epochCallback.accept(Epoch(epoch + 1, config.numEpochs, step.data.errorStats, scaledOffsets)) - } - } - - private fun step(step: PoseFrameStep) { - // Pull frequently used variables out of trainingStep to reduce call length - val skeleton1 = step.skeleton1 - val skeleton2 = step.skeleton2 - - // Scaling each step used to mean enforcing the target height, so keep that - // behaviour to retain predictability - if (!step.config.scaleEachStep) { - // Try to estimate a new height by calculating the height with the lowest - // error between adding or subtracting from the height - val maxHeight = step.data.targetHmdHeight + 0.2f - val minHeight = step.data.targetHmdHeight - 0.2f - - step.data.hmdHeight = estimatedHeight - val heightErrorDeriv = getErrorDeriv(step) - val heightAdjust = errorFunc(heightErrorDeriv) * step.data.adjustRate - - val negHeight = (estimatedHeight - heightAdjust).coerceIn(minHeight, maxHeight) - updateRecordingScale(step, 1f / negHeight) - step.data.hmdHeight = negHeight - val negHeightErrorDeriv = getErrorDeriv(step) - - val posHeight = (estimatedHeight + heightAdjust).coerceIn(minHeight, maxHeight) - updateRecordingScale(step, 1f / posHeight) - step.data.hmdHeight = posHeight - val posHeightErrorDeriv = getErrorDeriv(step) - - if (negHeightErrorDeriv < heightErrorDeriv && negHeightErrorDeriv < posHeightErrorDeriv) { - estimatedHeight = negHeight - // Apply the negative height scale - updateRecordingScale(step, 1f / negHeight) - } else if (posHeightErrorDeriv < heightErrorDeriv) { - estimatedHeight = posHeight - // The last estimated height set was the positive adjustment, so no need to apply it again - } else { - // Reset to the initial scale - updateRecordingScale(step, 1f / estimatedHeight) - } - } - - // Update the heights used for error calculations - step.data.hmdHeight = estimatedHeight - - val errorDeriv = getErrorDeriv(step) - val error = errorFunc(errorDeriv) - - // In case of fire - if (java.lang.Float.isNaN(error) || java.lang.Float.isInfinite(error)) { - // Extinguish - LogManager - .warning( - "[AutoBone] Error value is invalid, resetting variables to recover", - ) - // Reset adjustable config values - loadConfigValues() - - // Reset error sum values - step.data.errorStats.reset() - - // Continue on new data - return - } - - // Store the error count for logging purposes - step.data.errorStats.addValue(errorDeriv) - val adjustVal = error * step.data.adjustRate - - // If there is no adjustment whatsoever, skip this - if (adjustVal == 0f) { - return - } - - val slideL = skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position - - skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position - val slideLLen = slideL.len() - val slideLUnit: Vector3? = if (slideLLen > MIN_SLIDE_DIST) slideL / slideLLen else null - - val slideR = skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position - - skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position - val slideRLen = slideR.len() - val slideRUnit: Vector3? = if (slideRLen > MIN_SLIDE_DIST) slideR / slideRLen else null - - val intermediateOffsets = EnumMap(offsets) - for (entry in intermediateOffsets.entries) { - // Skip adjustment if the epoch is before starting (for logging only) or - // if there are no BoneTypes for this value - if (step.epoch < 0 || entry.key.affectedOffsets.isEmpty()) { - break - } - val originalLength = entry.value - - // Calculate the total effect of the bone based on change in rotation - val slideDot = BoneContribution.getSlideDot( - skeleton1, - skeleton2, - entry.key, - slideLUnit, - slideRUnit, - ) - val dotLength = originalLength * slideDot - - // Scale by the total effect of the bone - val curAdjustVal = adjustVal * -dotLength - if (curAdjustVal == 0f) { - continue - } - - val newLength = originalLength + curAdjustVal - // No small or negative numbers!!! Bad algorithm! - if (newLength < 0.01f) { - continue - } - - // Apply new offset length - skeleton1.setOffset(entry.key, newLength) - skeleton2.setOffset(entry.key, newLength) - scaleSkeleton(skeleton1, onlyAdjustedHeight = true) - scaleSkeleton(skeleton2, onlyAdjustedHeight = true) - - // Update the skeleton poses for the new offset length - skeleton1.update() - skeleton2.update() - - val newErrorDeriv = getErrorDeriv(step) - if (newErrorDeriv < errorDeriv) { - // Apply the adjusted length to the current adjusted offsets - entry.setValue(newLength) - } - - // Reset the skeleton values to minimize bias in other variables, it's applied later - applyConfig(skeleton1) - applyConfig(skeleton2) - } - - // Update the offsets from the adjusted ones - offsets.putAll(intermediateOffsets) - - // Normalize the scale, it will be upscaled to the target height later - // We only need to scale height offsets, as other offsets are not affected by height - scaleOffsets(onlyHeightOffsets = true) - - // Apply the normalized offsets to the skeleton - applyConfig(skeleton1) - applyConfig(skeleton2) - } - - /** - * Sums only the adjusted height offsets of the provided HumanPoseManager - */ - private fun sumAdjustedHeightOffsets(humanPoseManager: HumanPoseManager): Float { - var sum = 0f - SkeletonConfigManager.HEIGHT_OFFSETS.forEach { - if (!adjustOffsets.contains(it)) return@forEach - sum += humanPoseManager.getOffset(it) - } - return sum - } - - /** - * Sums only the height offsets of the provided offset map - */ - private fun sumHeightOffsets(offsets: EnumMap = this.offsets): Float { - var sum = 0f - SkeletonConfigManager.HEIGHT_OFFSETS.forEach { - sum += offsets[it] ?: return@forEach - } - return sum - } - - private fun scaleSkeleton(humanPoseManager: HumanPoseManager, targetHeight: Float = 1f, onlyAdjustedHeight: Boolean = false) { - // Get the scale to apply for the appropriate offsets - val scale = if (onlyAdjustedHeight) { - // Only adjusted height offsets - val adjHeight = sumAdjustedHeightOffsets(humanPoseManager) - // Remove the constant from the target, leaving only the target for adjusted height offsets - val adjTarget = targetHeight - (humanPoseManager.userHeightFromConfig - adjHeight) - // Return only the scale for adjusted offsets - adjTarget / adjHeight - } else { - targetHeight / humanPoseManager.userHeightFromConfig - } - - val offsets = if (onlyAdjustedHeight) SkeletonConfigManager.HEIGHT_OFFSETS else SkeletonConfigOffsets.values - for (offset in offsets) { - if (onlyAdjustedHeight && !adjustOffsets.contains(offset)) continue - humanPoseManager.setOffset(offset, humanPoseManager.getOffset(offset) * scale) - } - } - - private fun scaleOffsets(offsets: EnumMap = this.offsets, targetHeight: Float = adjustedHeightNormalized, onlyHeightOffsets: Boolean = false) { - // Get the scale to apply for the appropriate offsets - val scale = targetHeight / sumHeightOffsets(offsets) - - for (entry in offsets.entries) { - if (onlyHeightOffsets && !SkeletonConfigManager.HEIGHT_OFFSETS.contains(entry.key)) continue - entry.setValue(entry.value * scale) - } - } - - @Throws(AutoBoneException::class) - private fun getErrorDeriv(step: PoseFrameStep): Float { - val config = step.config - var sumError = 0f - if (config.slideErrorFactor > 0f) { - sumError += slideError.getStepError(step) * config.slideErrorFactor - } - if (config.offsetSlideErrorFactor > 0f) { - sumError += ( - offsetSlideError.getStepError(step) * - config.offsetSlideErrorFactor - ) - } - if (config.footHeightOffsetErrorFactor > 0f) { - sumError += ( - footHeightOffsetError.getStepError(step) * - config.footHeightOffsetErrorFactor - ) - } - if (config.bodyProportionErrorFactor > 0f) { - sumError += ( - bodyProportionError.getStepError(step) * - config.bodyProportionErrorFactor - ) - } - if (config.heightErrorFactor > 0f) { - sumError += heightError.getStepError(step) * config.heightErrorFactor - } - if (config.positionErrorFactor > 0f) { - sumError += ( - positionError.getStepError(step) * - config.positionErrorFactor - ) - } - if (config.positionOffsetErrorFactor > 0f) { - sumError += ( - positionOffsetError.getStepError(step) * - config.positionOffsetErrorFactor - ) - } - return sumError - } - - val lengthsString: String - get() { - val configInfo = StringBuilder() - offsets.forEach { (key, value) -> - if (configInfo.isNotEmpty()) { - configInfo.append(", ") - } - configInfo - .append(key.configKey) - .append(": ") - .append(StringUtils.prettyNumber(value * 100f, 2)) - } - return configInfo.toString() - } - - fun saveRecording(frames: PoseFrames, recordingFile: File) { - if (saveDir.isDirectory || saveDir.mkdirs()) { - LogManager - .info("[AutoBone] Exporting frames to \"${recordingFile.path}\"...") - if (PfsIO.tryWriteToFile(recordingFile, frames)) { - LogManager - .info( - "[AutoBone] Done exporting! Recording can be found at \"${recordingFile.path}\".", - ) - } else { - LogManager - .severe( - "[AutoBone] Failed to export the recording to \"${recordingFile.path}\".", - ) - } - } else { - LogManager - .severe( - "[AutoBone] Failed to create the recording directory \"${saveDir.path}\".", - ) - } - } - - fun saveRecording(frames: PoseFrames, recordingFileName: String) { - saveRecording(frames, File(saveDir, recordingFileName)) - } - - fun saveRecording(frames: PoseFrames) { - var recordingFile: File - var recordingIndex = 1 - do { - recordingFile = File(saveDir, "ABRecording${recordingIndex++}.pfs") - } while (recordingFile.exists()) - saveRecording(frames, recordingFile) - } - - fun loadRecordings(): FastList> { - val recordings = FastList>() - - loadDir.listFiles()?.forEach { file -> - if (!file.isFile) return@forEach - - val frames = if (file.name.endsWith(".pfs", ignoreCase = true)) { - LogManager.info("[AutoBone] Loading PFS recording from \"${file.path}\"...") - PfsIO.tryReadFromFile(file) - } else if (file.name.endsWith(".pfr", ignoreCase = true)) { - LogManager.info("[AutoBone] Loading PFR recording from \"${file.path}\"...") - PfrIO.tryReadFromFile(file) - } else { - return@forEach - } - - if (frames == null) { - LogManager.severe("[AutoBone] Failed to load recording from \"${file.path}\".") - } else { - recordings.add(Pair.of(file.name, frames)) - LogManager.info("[AutoBone] Loaded recording from \"${file.path}\".") - } - } - - return recordings - } - - inner class Epoch( - val epoch: Int, - val totalEpochs: Int, - val epochError: StatsCalculator, - val configValues: EnumMap, - ) { - override fun toString(): String = "Epoch: $epoch, Epoch error: $epochError" - } - - inner class AutoBoneResults( - val finalHeight: Float, - val targetHeight: Float, - val configValues: EnumMap, - ) { - val heightDifference: Float - get() = abs(targetHeight - finalHeight) - } - - companion object { - const val MIN_HEIGHT = 0.4f - const val MIN_SLIDE_DIST = 0.002f - const val AUTOBONE_FOLDER = "AutoBone Recordings" - const val LOADAUTOBONE_FOLDER = "Load AutoBone Recordings" - - // FIXME: Won't work on iOS and Android, maybe fix resolveConfigDirectory more than this - val saveDir = File( - OperatingSystem.resolveConfigDirectory(SLIMEVR_IDENTIFIER)?.resolve( - AUTOBONE_FOLDER, - )?.toString() ?: AUTOBONE_FOLDER, - ) - val loadDir = File( - OperatingSystem.resolveConfigDirectory(SLIMEVR_IDENTIFIER)?.resolve( - LOADAUTOBONE_FOLDER, - )?.toString() ?: LOADAUTOBONE_FOLDER, - ) - - // Mean square error function - private fun errorFunc(errorDeriv: Float): Float = 0.5f * (errorDeriv * errorDeriv) - - private fun decayFunc(initialAdjustRate: Float, adjustRateDecay: Float, epoch: Int): Float = if (epoch >= 0) initialAdjustRate / (1 + (adjustRateDecay * epoch)) else 0.0f - - val SYMM_CONFIGS = arrayOf( - SkeletonConfigOffsets.HIPS_WIDTH, - SkeletonConfigOffsets.SHOULDERS_WIDTH, - SkeletonConfigOffsets.SHOULDERS_DISTANCE, - SkeletonConfigOffsets.UPPER_ARM, - SkeletonConfigOffsets.LOWER_ARM, - SkeletonConfigOffsets.UPPER_LEG, - SkeletonConfigOffsets.LOWER_LEG, - SkeletonConfigOffsets.FOOT_LENGTH, - ) - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneHandler.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneHandler.kt deleted file mode 100644 index 2795239370..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneHandler.kt +++ /dev/null @@ -1,408 +0,0 @@ -package dev.slimevr.autobone - -import dev.slimevr.VRServer -import dev.slimevr.autobone.AutoBone.AutoBoneResults -import dev.slimevr.autobone.AutoBone.Companion.loadDir -import dev.slimevr.autobone.errors.AutoBoneException -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.poseframeformat.PoseRecorder -import dev.slimevr.poseframeformat.PoseRecorder.RecordingProgress -import dev.slimevr.poseframeformat.trackerdata.TrackerFrameData -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.processor.config.SkeletonConfigManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import io.eiren.util.StringUtils -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import org.apache.commons.lang3.tuple.Pair -import java.util.* -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.thread -import kotlin.concurrent.withLock - -class AutoBoneHandler(private val server: VRServer) { - private val poseRecorder: PoseRecorder = PoseRecorder(server) - private val autoBone: AutoBone = AutoBone(server) - - private val recordingLock = ReentrantLock() - private var recordingThread: Thread? = null - private val saveRecordingLock = ReentrantLock() - private var saveRecordingThread: Thread? = null - private val autoBoneLock = ReentrantLock() - private var autoBoneThread: Thread? = null - - private val listeners = CopyOnWriteArrayList() - - fun addListener(listener: AutoBoneListener) { - listeners.add(listener) - } - - fun removeListener(listener: AutoBoneListener) { - listeners.removeIf { listener == it } - } - - private fun announceProcessStatus( - processType: AutoBoneProcessType, - message: String? = null, - current: Long = -1L, - total: Long = -1L, - eta: Float = -1f, - completed: Boolean = false, - success: Boolean = true, - ) { - listeners.forEach { - it.onAutoBoneProcessStatus( - processType, - message, - current, - total, - eta, - completed, - success, - ) - } - } - - @Throws(AutoBoneException::class) - private fun processFrames(frames: PoseFrames): AutoBoneResults = autoBone - .processFrames(frames) { epoch -> - listeners.forEach { listener -> listener.onAutoBoneEpoch(epoch) } - } - - fun startProcessByType(processType: AutoBoneProcessType?): Boolean { - when (processType) { - AutoBoneProcessType.RECORD -> startRecording() - - AutoBoneProcessType.SAVE -> saveRecording() - - AutoBoneProcessType.PROCESS -> processRecording() - - else -> { - return false - } - } - return true - } - - fun startRecording() { - recordingLock.withLock { - // Prevent running multiple times - if (recordingThread != null) { - return - } - recordingThread = thread(start = true) { startRecordingThread() } - } - } - - private fun startRecordingThread() { - try { - if (poseRecorder.isReadyToRecord) { - announceProcessStatus(AutoBoneProcessType.RECORD, "Recording...") - - // ex. 1000 samples at 20 ms per sample is 20 seconds - val sampleCount = autoBone.globalConfig.sampleCount - val sampleRate = autoBone.globalConfig.sampleRateMs / 1000f - // Calculate total time in seconds - val totalTime: Float = sampleCount * sampleRate - - val framesFuture = poseRecorder - .startFrameRecording( - sampleCount, - sampleRate, - ) { progress: RecordingProgress -> - announceProcessStatus( - AutoBoneProcessType.RECORD, - current = progress.frame.toLong(), - total = progress.totalFrames.toLong(), - eta = totalTime - (progress.frame * totalTime / progress.totalFrames), - ) - } - val frames = framesFuture.get() - LogManager.info("[AutoBone] Done recording!") - - // Save a recurring recording for users to send as debug info - announceProcessStatus(AutoBoneProcessType.RECORD, "Saving recording...") - autoBone.saveRecording(frames, "LastABRecording.pfs") - if (autoBone.globalConfig.saveRecordings) { - announceProcessStatus( - AutoBoneProcessType.RECORD, - "Saving recording (from config option)...", - ) - autoBone.saveRecording(frames) - } - listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneRecordingEnd(frames) } - announceProcessStatus( - AutoBoneProcessType.RECORD, - "Done recording!", - completed = true, - success = true, - ) - } else { - announceProcessStatus( - AutoBoneProcessType.RECORD, - "The server is not ready to record", - completed = true, - success = false, - ) - LogManager.severe("[AutoBone] Unable to record...") - return - } - } catch (e: Exception) { - announceProcessStatus( - AutoBoneProcessType.RECORD, - "Recording failed: ${e.message}", - completed = true, - success = false, - ) - LogManager.severe("[AutoBone] Failed recording!", e) - } finally { - recordingThread = null - } - } - - fun stopRecording() { - if (poseRecorder.isRecording) { - poseRecorder.stopFrameRecording() - } - } - - fun cancelRecording() { - if (poseRecorder.isRecording) { - poseRecorder.cancelFrameRecording() - } - } - - fun saveRecording() { - saveRecordingLock.withLock { - // Prevent running multiple times - if (saveRecordingThread != null) { - return - } - saveRecordingThread = thread(start = true) { saveRecordingThread() } - } - } - - private fun saveRecordingThread() { - try { - val framesFuture = poseRecorder.framesAsync - if (framesFuture != null) { - announceProcessStatus(AutoBoneProcessType.SAVE, "Waiting for recording...") - val frames = framesFuture.get() - check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." } - check(frames.maxFrameCount > 0) { "Recording has no frames." } - announceProcessStatus(AutoBoneProcessType.SAVE, "Saving recording...") - autoBone.saveRecording(frames) - announceProcessStatus( - AutoBoneProcessType.SAVE, - "Recording saved!", - completed = true, - success = true, - ) - } else { - announceProcessStatus( - AutoBoneProcessType.SAVE, - "No recording found", - completed = true, - success = false, - ) - LogManager.severe("[AutoBone] Unable to save, no recording was done...") - return - } - } catch (e: Exception) { - announceProcessStatus( - AutoBoneProcessType.SAVE, - "Failed to save recording: ${e.message}", - completed = true, - success = false, - ) - LogManager.severe("[AutoBone] Failed to save recording!", e) - } finally { - saveRecordingThread = null - } - } - - fun processRecording() { - autoBoneLock.withLock { - // Prevent running multiple times - if (autoBoneThread != null) { - return - } - autoBoneThread = thread(start = true) { processRecordingThread() } - } - } - - private fun processRecordingThread() { - try { - announceProcessStatus(AutoBoneProcessType.PROCESS, "Loading recordings...") - val frameRecordings = autoBone.loadRecordings() - if (!frameRecordings.isEmpty()) { - LogManager.info("[AutoBone] Done loading frames!") - } else { - val framesFuture = poseRecorder.framesAsync - if (framesFuture != null) { - announceProcessStatus(AutoBoneProcessType.PROCESS, "Waiting for recording...") - val frames = framesFuture.get() - frameRecordings.add(Pair.of("", frames)) - } else { - announceProcessStatus( - AutoBoneProcessType.PROCESS, - "No recordings found...", - completed = true, - success = false, - ) - LogManager - .severe( - "[AutoBone] No recordings found in \"${loadDir.path}\" and no recording was done...", - ) - return - } - } - announceProcessStatus(AutoBoneProcessType.PROCESS, "Processing recording(s)...") - LogManager.info("[AutoBone] Processing frames...") - val errorStats = StatsCalculator() - val offsetStats = EnumMap( - SkeletonConfigOffsets::class.java, - ) - val skeletonConfigManagerBuffer = SkeletonConfigManager(false) - for ((key, value) in frameRecordings) { - LogManager.info("[AutoBone] Processing frames from \"$key\"...") - // Output tracker info for the recording - printTrackerInfo(value.frameHolders) - - // Actually process the recording - val autoBoneResults = processFrames(value) - LogManager.info("[AutoBone] Done processing!") - - // #region Stats/Values - // Accumulate height error - errorStats.addValue(autoBoneResults.heightDifference) - - // Accumulate length values - for (offset in autoBoneResults.configValues) { - val statCalc = offsetStats.getOrPut(offset.key) { - StatsCalculator() - } - // Multiply by 100 to get cm - statCalc.addValue(offset.value * 100f) - } - - // Calculate and output skeleton ratios - skeletonConfigManagerBuffer.setOffsets(autoBoneResults.configValues) - printSkeletonRatios(skeletonConfigManagerBuffer) - - LogManager.info("[AutoBone] Length values: ${autoBone.lengthsString}") - } - // Length value stats - val averageLengthVals = StringBuilder() - offsetStats.forEach { (key, value) -> - if (averageLengthVals.isNotEmpty()) { - averageLengthVals.append(", ") - } - averageLengthVals - .append(key.configKey) - .append(": ") - .append(StringUtils.prettyNumber(value.mean, 2)) - .append(" (SD ") - .append(StringUtils.prettyNumber(value.standardDeviation, 2)) - .append(")") - } - LogManager.info("[AutoBone] Average length values: $averageLengthVals") - - // Height error stats - LogManager - .info( - "[AutoBone] Average height error: ${ - StringUtils.prettyNumber(errorStats.mean, 6) - } (SD ${StringUtils.prettyNumber(errorStats.standardDeviation, 6)})", - ) - // #endregion - listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneEnd(autoBone.offsets) } - announceProcessStatus( - AutoBoneProcessType.PROCESS, - "Done processing!", - completed = true, - success = true, - ) - } catch (e: Exception) { - announceProcessStatus( - AutoBoneProcessType.PROCESS, - "Processing failed: ${e.message}", - completed = true, - success = false, - ) - LogManager.severe("[AutoBone] Failed adjustment!", e) - } finally { - autoBoneThread = null - } - } - - private fun printTrackerInfo(trackers: FastList) { - val trackerInfo = StringBuilder() - for (tracker in trackers) { - val frame = tracker?.tryGetFrame(0) ?: continue - - // Add a comma if this is not the first item listed - if (trackerInfo.isNotEmpty()) { - trackerInfo.append(", ") - } - - trackerInfo.append(frame.tryGetTrackerPosition()?.designation ?: "unassigned") - - // Represent the data flags - val trackerFlags = StringBuilder() - if (frame.hasData(TrackerFrameData.ROTATION)) { - trackerFlags.append("R") - } - if (frame.hasData(TrackerFrameData.POSITION)) { - trackerFlags.append("P") - } - if (frame.hasData(TrackerFrameData.ACCELERATION)) { - trackerFlags.append("A") - } - if (frame.hasData(TrackerFrameData.RAW_ROTATION)) { - trackerFlags.append("r") - } - - // If there are data flags, print them in brackets after the designation - if (trackerFlags.isNotEmpty()) { - trackerInfo.append(" (").append(trackerFlags).append(")") - } - } - LogManager.info("[AutoBone] (${trackers.size} trackers) [$trackerInfo]") - } - - private fun printSkeletonRatios(skeleton: SkeletonConfigManager) { - val neckLength = skeleton.getOffset(SkeletonConfigOffsets.NECK) - val upperChestLength = skeleton.getOffset(SkeletonConfigOffsets.UPPER_CHEST) - val chestLength = skeleton.getOffset(SkeletonConfigOffsets.CHEST) - val waistLength = skeleton.getOffset(SkeletonConfigOffsets.WAIST) - val hipLength = skeleton.getOffset(SkeletonConfigOffsets.HIP) - val torsoLength = upperChestLength + chestLength + waistLength + hipLength - val hipWidth = skeleton.getOffset(SkeletonConfigOffsets.HIPS_WIDTH) - val legLength = skeleton.getOffset(SkeletonConfigOffsets.UPPER_LEG) + - skeleton.getOffset(SkeletonConfigOffsets.LOWER_LEG) - val lowerLegLength = skeleton.getOffset(SkeletonConfigOffsets.LOWER_LEG) - - val neckTorso = neckLength / torsoLength - val chestTorso = (upperChestLength + chestLength) / torsoLength - val torsoWaist = hipWidth / torsoLength - val legTorso = legLength / torsoLength - val legBody = legLength / (torsoLength + neckLength) - val kneeLeg = lowerLegLength / legLength - - LogManager.info( - "[AutoBone] Ratios: [{Neck-Torso: ${ - StringUtils.prettyNumber(neckTorso)}}, {Chest-Torso: ${ - StringUtils.prettyNumber(chestTorso)}}, {Torso-Waist: ${ - StringUtils.prettyNumber(torsoWaist)}}, {Leg-Torso: ${ - StringUtils.prettyNumber(legTorso)}}, {Leg-Body: ${ - StringUtils.prettyNumber(legBody)}}, {Knee-Leg: ${ - StringUtils.prettyNumber(kneeLeg)}}]", - ) - } - - fun applyValues() { - autoBone.applyAndSaveConfig() - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneListener.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneListener.kt deleted file mode 100644 index 5101167db1..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneListener.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.slimevr.autobone - -import dev.slimevr.autobone.AutoBone.Epoch -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import java.util.* - -interface AutoBoneListener { - fun onAutoBoneProcessStatus( - processType: AutoBoneProcessType, - message: String?, - current: Long, - total: Long, - eta: Float, - completed: Boolean, - success: Boolean, - ) - - fun onAutoBoneRecordingEnd(recording: PoseFrames) - fun onAutoBoneEpoch(epoch: Epoch) - fun onAutoBoneEnd(configValues: EnumMap) -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneProcessType.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneProcessType.kt deleted file mode 100644 index 17759cb71a..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneProcessType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.slimevr.autobone - -enum class AutoBoneProcessType(val id: Int) { - NONE(0), - RECORD(1), - SAVE(2), - PROCESS(3), - ; - - companion object { - fun getById(id: Int): AutoBoneProcessType? = byId[id] - } -} - -private val byId = AutoBoneProcessType.values().associateBy { it.id } diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt deleted file mode 100644 index b0fe0df77c..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.slimevr.autobone - -class AutoBoneStep( - var hmdHeight: Float = 1f, - val targetHmdHeight: Float = 1f, - var adjustRate: Float = 0f, -) { - - val errorStats = StatsCalculator() - - val heightOffset: Float - get() = targetHmdHeight - hmdHeight -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/BoneContribution.kt b/server/core/src/main/java/dev/slimevr/autobone/BoneContribution.kt deleted file mode 100644 index 5a8ce46d38..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/BoneContribution.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.slimevr.autobone - -import dev.slimevr.autobone.AutoBone.Companion.MIN_SLIDE_DIST -import dev.slimevr.autobone.AutoBone.Companion.SYMM_CONFIGS -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import io.github.axisangles.ktmath.Vector3 - -object BoneContribution { - /** - * Computes the local tail position of the bone after rotation. - */ - fun getBoneLocalTail( - skeleton: HumanPoseManager, - boneType: BoneType, - ): Vector3 { - val bone = skeleton.getBone(boneType) - return bone.getTailPosition() - bone.getPosition() - } - - /** - * Computes the direction of the bone tail's movement between skeletons 1 and 2. - */ - fun getBoneLocalTailDir( - skeleton1: HumanPoseManager, - skeleton2: HumanPoseManager, - boneType: BoneType, - ): Vector3? { - val boneOff = getBoneLocalTail(skeleton2, boneType) - getBoneLocalTail(skeleton1, boneType) - val boneOffLen = boneOff.len() - // If the offset is approx 0, just return null so it can be easily ignored - return if (boneOffLen > MIN_SLIDE_DIST) boneOff / boneOffLen else null - } - - /** - * Predicts how much the provided config should be affecting the slide offsets - * of the left and right ankles. - */ - fun getSlideDot( - skeleton1: HumanPoseManager, - skeleton2: HumanPoseManager, - config: SkeletonConfigOffsets, - slideL: Vector3?, - slideR: Vector3?, - ): Float { - var slideDot = 0f - // Used for right offset if not a symmetric bone - var boneOffL: Vector3? = null - - // Treat null as 0 - if (slideL != null) { - boneOffL = getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0]) - - // Treat null as 0 - if (boneOffL != null) { - slideDot += slideL.dot(boneOffL) - } - } - - // Treat null as 0 - if (slideR != null) { - // IMPORTANT: This assumption for acquiring BoneType only works if - // SkeletonConfigOffsets is set up to only affect one BoneType, make sure no - // changes to SkeletonConfigOffsets goes against this assumption, please! - val boneOffR = if (SYMM_CONFIGS.contains(config)) { - getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[1]) - } else if (slideL != null) { - // Use cached offset if slideL was used - boneOffL - } else { - // Compute offset if missing because of slideL - getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0]) - } - - // Treat null as 0 - if (boneOffR != null) { - slideDot += slideR.dot(boneOffR) - } - } - - return slideDot / 2f - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/PoseFrameIterator.kt b/server/core/src/main/java/dev/slimevr/autobone/PoseFrameIterator.kt deleted file mode 100644 index a5f8e0af09..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/PoseFrameIterator.kt +++ /dev/null @@ -1,90 +0,0 @@ -package dev.slimevr.autobone - -import kotlin.random.Random - -object PoseFrameIterator { - fun iterateFrames( - step: PoseFrameStep, - ) { - check(step.frames.frameHolders.isNotEmpty()) { "Recording has no trackers." } - check(step.maxFrameCount > 0) { "Recording has no frames." } - - // Epoch loop, each epoch is one full iteration over the full dataset - for (epoch in (if (step.config.calcInitError) -1 else 0) until step.config.numEpochs) { - // Set the current epoch to process - step.epoch = epoch - // Process the epoch - epoch(step) - } - } - - private fun randomIndices(count: Int, random: Random): IntArray { - val randIndices = IntArray(count) - - var zeroPos = -1 - for (i in 0 until count) { - var index = random.nextInt(count) - if (i > 0) { - while (index == zeroPos || randIndices[index] > 0) { - index = random.nextInt(count) - } - } else { - zeroPos = index - } - randIndices[index] = i - } - - return randIndices - } - - private fun epoch(step: PoseFrameStep) { - val config = step.config - val frameCount = step.maxFrameCount - - // Perform any setup that needs to be done before the current epoch - step.preEpoch?.accept(step) - - val randIndices = if (config.randomizeFrameOrder) { - randomIndices(step.maxFrameCount, step.random) - } else { - null - } - - // Iterate over the frames using a cursor and an offset for comparing - // frames a certain number of frames apart - var cursorOffset = config.minDataDistance - while (cursorOffset <= config.maxDataDistance && - cursorOffset < frameCount - ) { - var frameCursor = 0 - while (frameCursor < frameCount - cursorOffset) { - val frameCursor2 = frameCursor + cursorOffset - - // Then set the frame cursors and apply them to both skeletons - if (config.randomizeFrameOrder && randIndices != null) { - step - .setCursors( - randIndices[frameCursor], - randIndices[frameCursor2], - updatePlayerCursors = true, - ) - } else { - step.setCursors( - frameCursor, - frameCursor2, - updatePlayerCursors = true, - ) - } - - // Process the iteration - step.onStep.accept(step) - - // Move on to the next iteration - frameCursor += config.cursorIncrement - } - cursorOffset++ - } - - step.postEpoch?.accept(step) - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/PoseFrameStep.kt b/server/core/src/main/java/dev/slimevr/autobone/PoseFrameStep.kt deleted file mode 100644 index 0140c607a1..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/PoseFrameStep.kt +++ /dev/null @@ -1,70 +0,0 @@ -package dev.slimevr.autobone - -import dev.slimevr.config.AutoBoneConfig -import dev.slimevr.config.ConfigManager -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.poseframeformat.player.TrackerFramesPlayer -import dev.slimevr.tracking.processor.HumanPoseManager -import java.util.function.Consumer -import kotlin.random.Random - -class PoseFrameStep( - val config: AutoBoneConfig, - /** The config to initialize skeletons. */ - serverConfig: ConfigManager? = null, - val frames: PoseFrames, - /** The consumer run before each epoch. */ - val preEpoch: Consumer>? = null, - /** The consumer run for each step. */ - val onStep: Consumer>, - /** The consumer run after each epoch. */ - val postEpoch: Consumer>? = null, - /** The current epoch. */ - var epoch: Int = 0, - /** The current frame cursor position in [frames] for skeleton1. */ - var cursor1: Int = 0, - /** The current frame cursor position in [frames] for skeleton2. */ - var cursor2: Int = 0, - randomSeed: Long = 0, - val data: T, -) { - var maxFrameCount = frames.maxFrameCount - - val framePlayer1 = TrackerFramesPlayer(frames) - val framePlayer2 = TrackerFramesPlayer(frames) - - val trackers1 = framePlayer1.trackers.toList() - val trackers2 = framePlayer2.trackers.toList() - - val skeleton1 = HumanPoseManager(trackers1) - val skeleton2 = HumanPoseManager(trackers2) - - val random = Random(randomSeed) - - init { - // Load server configs into the skeleton - if (serverConfig != null) { - skeleton1.loadFromConfig(serverConfig) - skeleton2.loadFromConfig(serverConfig) - } - // Disable leg tweaks and IK solver, these will mess with the resulting positions - skeleton1.setLegTweaksEnabled(false) - skeleton2.setLegTweaksEnabled(false) - } - - fun setCursors(cursor1: Int, cursor2: Int, updatePlayerCursors: Boolean) { - this.cursor1 = cursor1 - this.cursor2 = cursor2 - - if (updatePlayerCursors) { - updatePlayerCursors() - } - } - - fun updatePlayerCursors() { - framePlayer1.setCursors(cursor1) - framePlayer2.setCursors(cursor2) - skeleton1.update() - skeleton2.update() - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/StatsCalculator.kt b/server/core/src/main/java/dev/slimevr/autobone/StatsCalculator.kt deleted file mode 100644 index 3fb53c18e7..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/StatsCalculator.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.slimevr.autobone - -import kotlin.math.* - -/** - * This is a stat calculator based on Welford's online algorithm - * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford%27s_online_algorithm - */ -class StatsCalculator { - private var count = 0 - var mean = 0f - private set - private var m2 = 0f - - fun reset() { - count = 0 - mean = 0f - m2 = 0f - } - - fun addValue(newValue: Float) { - count += 1 - val delta = newValue - mean - mean += delta / count - val delta2 = newValue - mean - m2 += delta * delta2 - } - - val variance: Float - get() = if (count < 1) { - Float.NaN - } else { - m2 / count - } - val sampleVariance: Float - get() = if (count < 2) { - Float.NaN - } else { - m2 / (count - 1) - } - val standardDeviation: Float - get() = sqrt(variance) -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/AutoBoneException.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/AutoBoneException.kt deleted file mode 100644 index 39646e552f..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/AutoBoneException.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.slimevr.autobone.errors - -class AutoBoneException : Exception { - constructor() - constructor(message: String?) : super(message) - constructor(cause: Throwable?) : super(cause) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor( - message: String?, - cause: Throwable?, - enableSuppression: Boolean, - writableStackTrace: Boolean, - ) : super(message, cause, enableSuppression, writableStackTrace) -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt deleted file mode 100644 index c0fa7f2b80..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt +++ /dev/null @@ -1,123 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.autobone.errors.proportions.ProportionLimiter -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import kotlin.math.* - -// The distance from average human proportions -class BodyProportionError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float = getBodyProportionError( - step.skeleton1, - // Skeletons are now normalized to reduce bias, so height is always 1 - 1f, - ) - - fun getBodyProportionError(humanPoseManager: HumanPoseManager, fullHeight: Float): Float { - var sum = 0f - for (limiter in proportionLimits) { - sum += abs(limiter.getProportionError(humanPoseManager, fullHeight)) - } - return sum - } - - companion object { - // The headset height is not the full height! This value compensates for the - // offset from the headset height to the user full height - // From Drillis and Contini (1966) - @JvmField - var eyeHeightToHeightRatio = 0.936f - - val defaultHeight = SkeletonConfigManager.HEIGHT_OFFSETS.sumOf { - it.defaultValue.toDouble() - }.toFloat() - - private fun makeLimiter( - offset: SkeletonConfigOffsets, - range: Float, - scaleByHeight: Boolean = true, - ) = ProportionLimiter( - if (scaleByHeight) { - offset.defaultValue / defaultHeight - } else { - offset.defaultValue - }, - offset, - range, - scaleByHeight, - ) - - // "Expected" are values from Drillis and Contini (1966) - // Default are values from experimentation by the SlimeVR community - - /** - * Proportions are based off the headset height (or eye height), not the total height of the user. - * To use the total height of the user, multiply it by [eyeHeightToHeightRatio] and use that in the limiters. - */ - val proportionLimits = arrayOf( - makeLimiter( - SkeletonConfigOffsets.HEAD, - 0.01f, - scaleByHeight = false, - ), - // Expected: 0.052 - makeLimiter( - SkeletonConfigOffsets.NECK, - 0.002f, - ), - makeLimiter( - SkeletonConfigOffsets.SHOULDERS_WIDTH, - 0.04f, - scaleByHeight = false, - ), - makeLimiter( - SkeletonConfigOffsets.UPPER_ARM, - 0.02f, - ), - makeLimiter( - SkeletonConfigOffsets.LOWER_ARM, - 0.02f, - ), - makeLimiter( - SkeletonConfigOffsets.UPPER_CHEST, - 0.01f, - ), - makeLimiter( - SkeletonConfigOffsets.CHEST, - 0.01f, - ), - makeLimiter( - SkeletonConfigOffsets.WAIST, - 0.05f, - ), - makeLimiter( - SkeletonConfigOffsets.HIP, - 0.01f, - ), - // Expected: 0.191 - makeLimiter( - SkeletonConfigOffsets.HIPS_WIDTH, - 0.04f, - scaleByHeight = false, - ), - // Expected: 0.245 - makeLimiter( - SkeletonConfigOffsets.UPPER_LEG, - 0.02f, - ), - // Expected: 0.246 (0.285 including below ankle, could use a separate - // offset?) - makeLimiter( - SkeletonConfigOffsets.LOWER_LEG, - 0.02f, - ), - ) - - @JvmStatic - val proportionLimitMap = proportionLimits.associateBy { it.skeletonConfigOffset } - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/FootHeightOffsetError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/FootHeightOffsetError.kt deleted file mode 100644 index 0121263755..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/FootHeightOffsetError.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -// The offset between the height both feet at one instant and over time -class FootHeightOffsetError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float = getSlideError( - step.skeleton1.skeleton, - step.skeleton2.skeleton, - ) - - companion object { - fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float = getFootHeightError( - skeleton1.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(), - skeleton1.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(), - skeleton2.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(), - skeleton2.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(), - ) - - fun getFootHeightError( - leftFoot1: Vector3, - rightFoot1: Vector3, - leftFoot2: Vector3, - rightFoot2: Vector3, - ): Float { - val lFoot1Y = leftFoot1.y - val rFoot1Y = rightFoot1.y - val lFoot2Y = leftFoot2.y - val rFoot2Y = rightFoot2.y - - // Compute all combinations of heights - val dist1 = abs(lFoot1Y - rFoot1Y) - val dist2 = abs(lFoot1Y - lFoot2Y) - val dist3 = abs(lFoot1Y - rFoot2Y) - val dist4 = abs(rFoot1Y - lFoot2Y) - val dist5 = abs(rFoot1Y - rFoot2Y) - val dist6 = abs(lFoot2Y - rFoot2Y) - - // Divide by 12 (6 values * 2 to halve) to halve and average, it's - // halved because you want to approach a midpoint, not the other point - return (dist1 + dist2 + dist3 + dist4 + dist5 + dist6) / 12f - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/HeightError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/HeightError.kt deleted file mode 100644 index 1303d20dbf..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/HeightError.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import kotlin.math.* - -// The difference from the current height to the target height -class HeightError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float = getHeightError( - step.data.hmdHeight, - step.data.targetHmdHeight, - ) - - fun getHeightError(currentHeight: Float, targetHeight: Float): Float = abs(targetHeight - currentHeight) -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/IAutoBoneError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/IAutoBoneError.kt deleted file mode 100644 index e4667877e9..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/IAutoBoneError.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep - -interface IAutoBoneError { - @Throws(AutoBoneException::class) - fun getStepError(step: PoseFrameStep): Float -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/OffsetSlideError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/OffsetSlideError.kt deleted file mode 100644 index f3b5233eb4..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/OffsetSlideError.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -// The change in distance between both of the ankles over time -class OffsetSlideError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float = getSlideError( - step.skeleton1.skeleton, - step.skeleton2.skeleton, - ) - - companion object { - fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float = getSlideError( - skeleton1.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(), - skeleton1.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(), - skeleton2.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(), - skeleton2.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(), - ) - - fun getSlideError( - leftFoot1: Vector3, - rightFoot1: Vector3, - leftFoot2: Vector3, - rightFoot2: Vector3, - ): Float { - val slideDist1 = (rightFoot1 - leftFoot1).len() - val slideDist2 = (rightFoot2 - leftFoot2).len() - val slideDist3 = (rightFoot2 - leftFoot1).len() - val slideDist4 = (rightFoot1 - leftFoot2).len() - - // Compute all combinations of distances - val dist1 = abs(slideDist1 - slideDist2) - val dist2 = abs(slideDist1 - slideDist3) - val dist3 = abs(slideDist1 - slideDist4) - val dist4 = abs(slideDist2 - slideDist3) - val dist5 = abs(slideDist2 - slideDist4) - val dist6 = abs(slideDist3 - slideDist4) - - // Divide by 12 (6 values * 2 to halve) to halve and average, it's - // halved because you want to approach a midpoint, not the other point - return (dist1 + dist2 + dist3 + dist4 + dist5 + dist6) / 12f - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt deleted file mode 100644 index d5cedc4e9f..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton - -// The distance of any points to the corresponding absolute position -class PositionError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float { - val trackers = step.frames.frameHolders - return ( - ( - getPositionError( - trackers, - step.cursor1, - step.skeleton1.skeleton, - ) + - getPositionError( - trackers, - step.cursor2, - step.skeleton2.skeleton, - ) - ) / - 2f - ) - } - - companion object { - fun getPositionError( - trackers: List, - cursor: Int, - skeleton: HumanSkeleton, - ): Float { - var offset = 0f - var offsetCount = 0 - for (tracker in trackers) { - val trackerFrame = tracker.tryGetFrame(cursor) ?: continue - val position = trackerFrame.tryGetPosition() ?: continue - val trackerRole = trackerFrame.tryGetTrackerPosition()?.trackerRole ?: continue - - try { - val computedTracker = skeleton.getComputedTracker(trackerRole) - - offset += (position - computedTracker.position).len() - offsetCount++ - } catch (_: Exception) { - // Ignore unsupported positions - } - } - return if (offsetCount > 0) offset / offsetCount else 0f - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt deleted file mode 100644 index e5cb83b7bb..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import kotlin.math.* - -// The difference between offset of absolute position and the corresponding point over time -class PositionOffsetError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float { - val trackers = step.frames.frameHolders - return getPositionOffsetError( - trackers, - step.cursor1, - step.cursor2, - step.skeleton1.skeleton, - step.skeleton2.skeleton, - ) - } - - fun getPositionOffsetError( - trackers: List, - cursor1: Int, - cursor2: Int, - skeleton1: HumanSkeleton, - skeleton2: HumanSkeleton, - ): Float { - var offset = 0f - var offsetCount = 0 - for (tracker in trackers) { - val trackerFrame1 = tracker.tryGetFrame(cursor1) ?: continue - val position1 = trackerFrame1.tryGetPosition() ?: continue - val trackerRole1 = trackerFrame1.tryGetTrackerPosition()?.trackerRole ?: continue - - val trackerFrame2 = tracker.tryGetFrame(cursor2) ?: continue - val position2 = trackerFrame2.tryGetPosition() ?: continue - val trackerRole2 = trackerFrame2.tryGetTrackerPosition()?.trackerRole ?: continue - - try { - val computedTracker1 = skeleton1.getComputedTracker(trackerRole1) - val computedTracker2 = skeleton2.getComputedTracker(trackerRole2) - - val dist1 = (position1 - computedTracker1.position).len() - val dist2 = (position2 - computedTracker2.position).len() - offset += abs(dist2 - dist1) - offsetCount++ - } catch (_: Exception) { - // Ignore unsupported positions - } - } - return if (offsetCount > 0) offset / offsetCount else 0f - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/SlideError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/SlideError.kt deleted file mode 100644 index df245736cf..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/SlideError.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.slimevr.autobone.errors - -import dev.slimevr.autobone.AutoBoneStep -import dev.slimevr.autobone.PoseFrameStep -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton - -// The change in position of the ankle over time -class SlideError : IAutoBoneError { - @Throws(AutoBoneException::class) - override fun getStepError(step: PoseFrameStep): Float = getSlideError( - step.skeleton1.skeleton, - step.skeleton2.skeleton, - ) - - companion object { - fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float { - // Calculate and average between both feet - return ( - getSlideError(skeleton1, skeleton2, BoneType.LEFT_LOWER_LEG) + - getSlideError(skeleton1, skeleton2, BoneType.RIGHT_LOWER_LEG) - ) / - 2f - } - - fun getSlideError( - skeleton1: HumanSkeleton, - skeleton2: HumanSkeleton, - bone: BoneType, - ): Float { - // Calculate and average between both feet - return getSlideError( - skeleton1.getBone(bone), - skeleton2.getBone(bone), - ) - } - - fun getSlideError(bone1: Bone, bone2: Bone): Float { - // Return the midpoint distance - return (bone2.getTailPosition() - bone1.getTailPosition()).len() / 2f - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/proportions/ProportionLimiter.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/proportions/ProportionLimiter.kt deleted file mode 100644 index 711a6f44b2..0000000000 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/proportions/ProportionLimiter.kt +++ /dev/null @@ -1,80 +0,0 @@ -package dev.slimevr.autobone.errors.proportions - -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import kotlin.math.* - -class ProportionLimiter { - val targetRatio: Float - val skeletonConfigOffset: SkeletonConfigOffsets - val scaleByHeight: Boolean - - val positiveRange: Float - val negativeRange: Float - - /** - * @param targetRatio The bone to height ratio to target - * @param skeletonConfigOffset The SkeletonConfigOffset to use for the length - * @param range The range from the target ratio to accept (ex. 0.1) - * @param scaleByHeight True if the bone length will be scaled by the height - */ - constructor( - targetRatio: Float, - skeletonConfigOffset: SkeletonConfigOffsets, - range: Float, - scaleByHeight: Boolean = true, - ) { - this.targetRatio = targetRatio - this.skeletonConfigOffset = skeletonConfigOffset - this.scaleByHeight = scaleByHeight - - // Handle if someone puts in a negative value - val absRange = abs(range) - positiveRange = absRange - negativeRange = -absRange - } - - /** - * @param targetRatio The bone to height ratio to target - * @param skeletonConfigOffset The SkeletonConfigOffset to use for the length - * @param positiveRange The positive range from the target ratio to accept - * (ex. 0.1) - * @param negativeRange The negative range from the target ratio to accept - * (ex. -0.1) - * @param scaleByHeight True if the bone length will be scaled by the height - */ - constructor( - targetRatio: Float, - skeletonConfigOffset: SkeletonConfigOffsets, - positiveRange: Float, - negativeRange: Float, - scaleByHeight: Boolean = true, - ) { - // If the positive range is less than the negative range, something is wrong - require(positiveRange >= negativeRange) { "positiveRange must not be less than negativeRange" } - - this.targetRatio = targetRatio - this.skeletonConfigOffset = skeletonConfigOffset - this.scaleByHeight = scaleByHeight - - this.positiveRange = positiveRange - this.negativeRange = negativeRange - } - - fun getProportionError(humanPoseManager: HumanPoseManager, height: Float): Float { - val boneLength = humanPoseManager.getOffset(skeletonConfigOffset) - val ratioOffset = if (scaleByHeight) { - targetRatio - boneLength / height - } else { - targetRatio - boneLength - } - - // If the range is exceeded, return the offset from the range limit - if (ratioOffset > positiveRange) { - return ratioOffset - positiveRange - } else if (ratioOffset < negativeRange) { - return ratioOffset - negativeRange - } - return 0f - } -} diff --git a/server/core/src/main/java/dev/slimevr/behaviours.kt b/server/core/src/main/java/dev/slimevr/behaviours.kt new file mode 100644 index 0000000000..98d6bc2c2f --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/behaviours.kt @@ -0,0 +1,24 @@ +package dev.slimevr + +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +object BaseBehaviour : VRServerBehaviour { + override fun reduce(state: VRServerState, action: VRServerActions): VRServerState = when (action) { + is VRServerActions.NewTracker -> state.copy(trackers = state.trackers + (action.trackerId to action.context)) + is VRServerActions.NewDevice -> state.copy(devices = state.devices + (action.deviceId to action.context)) + is VRServerActions.DriverConnected -> state.copy(drivers = state.drivers + (action.bridge.id to action.bridge)) + is VRServerActions.DriverDisconnected -> state.copy(drivers = state.drivers - action.bridgeId) + is VRServerActions.FeederConnected -> state.copy(feeders = state.feeders + (action.bridge.id to action.bridge)) + is VRServerActions.FeederDisconnected -> state.copy(feeders = state.feeders - action.bridgeId) + is VRServerActions.SolarXRConnected -> state.copy(solarxr = state.solarxr + (action.connection.id to action.connection)) + is VRServerActions.SolarXRDisconnected -> state.copy(solarxr = state.solarxr - action.connectionId) + } + + override fun observe(receiver: VRServer) { + receiver.context.state.distinctUntilChangedBy { it.trackers.size }.onEach { + println("tracker list size changed") + }.launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt b/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt deleted file mode 100644 index 5750b53b3b..0000000000 --- a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.slimevr.bridge - -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerRole -import dev.slimevr.util.ann.VRServerThread - -/** - * Bridge handles sending and receiving tracker data between SlimeVR and other - * systems like VR APIs (SteamVR, OpenXR, etc), apps and protocols (VMC, - * WebSocket, TIP). It can create and manage tracker received from the **remote - * side** or send shared **local trackers** to the other side. - */ -interface Bridge { - @VRServerThread - fun dataRead() - - @VRServerThread - fun dataWrite() - - /** - * Adds shared tracker to the bridge. Bridge should notify the other side of - * this tracker, if it's the type of tracker this bridge serves, and start - * sending data each update - * - * @param tracker - */ - @VRServerThread - fun addSharedTracker(tracker: Tracker?) - - /** - * Removes tracker from a bridge. If the other side supports tracker - * removal, bridge should notify it and stop sending new data. If it doesn't - * support tracker removal, the bridge can either stop sending new data, or - * keep sending it if it's available. - * - * @param tracker - */ - @VRServerThread - fun removeSharedTracker(tracker: Tracker?) - - @VRServerThread - fun startBridge() - - fun isConnected(): Boolean -} - -interface ISteamVRBridge : Bridge { - fun getShareSetting(role: TrackerRole): Boolean - - fun changeShareSettings(role: TrackerRole?, share: Boolean) - - fun updateShareSettingsAutomatically(): Boolean - - fun getAutomaticSharedTrackers(): Boolean - fun setAutomaticSharedTrackers(value: Boolean) - - fun getBridgeConfigKey(): String -} diff --git a/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java b/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java deleted file mode 100644 index 52f7224241..0000000000 --- a/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slimevr.bridge; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -@Retention(value = RetentionPolicy.SOURCE) -public @interface BridgeThread { -} diff --git a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt deleted file mode 100644 index 3d3c62f22a..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.slimevr.config - -class AutoBoneConfig { - var cursorIncrement = 2 - var minDataDistance = 1 - var maxDataDistance = 1 - var numEpochs = 50 - var printEveryNumEpochs = 25 - var initialAdjustRate = 10.0f - var adjustRateDecay = 1.0f - var slideErrorFactor = 1.0f - var offsetSlideErrorFactor = 0.0f - var footHeightOffsetErrorFactor = 0.0f - var bodyProportionErrorFactor = 0.05f - var heightErrorFactor = 0.0f - var positionErrorFactor = 0.0f - var positionOffsetErrorFactor = 0.0f - var calcInitError = false - var randomizeFrameOrder = true - var scaleEachStep = true - var sampleCount = 1500 - var sampleRateMs = 20L - var saveRecordings = false - var useSkeletonHeight = false - var randSeed = 4L - var useFrameFiltering = false - var maxFinalError = 0.03f -} diff --git a/server/core/src/main/java/dev/slimevr/config/BridgeConfig.java b/server/core/src/main/java/dev/slimevr/config/BridgeConfig.java deleted file mode 100644 index 065ffca468..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/BridgeConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.slimevr.config; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.StdKeySerializers; -import dev.slimevr.config.serializers.BooleanMapDeserializer; -import dev.slimevr.tracking.trackers.TrackerRole; -import java.util.HashMap; -import java.util.Map; - - -public class BridgeConfig { - - @JsonDeserialize(using = BooleanMapDeserializer.class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) - public Map trackers = new HashMap<>(); - public boolean automaticSharedTrackersToggling = true; - - public BridgeConfig() { - } - - public boolean getBridgeTrackerRole(TrackerRole role, boolean def) { - return trackers.getOrDefault(role.name().toLowerCase(), def); - } - - public void setBridgeTrackerRole(TrackerRole role, boolean val) { - this.trackers.put(role.name().toLowerCase(), val); - } - - public Map getTrackers() { - return trackers; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/ConfigManager.java b/server/core/src/main/java/dev/slimevr/config/ConfigManager.java deleted file mode 100644 index 51bfcd7b04..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/ConfigManager.java +++ /dev/null @@ -1,176 +0,0 @@ -package dev.slimevr.config; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import com.github.jonpeterson.jackson.module.versioning.VersioningModule; -import dev.slimevr.config.serializers.QuaternionDeserializer; -import dev.slimevr.config.serializers.QuaternionSerializer; -import io.eiren.util.ann.ThreadSafe; -import io.eiren.util.logging.LogManager; -import io.github.axisangles.ktmath.ObjectQuaternion; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.*; -import java.util.Comparator; -import java.util.stream.Collectors; -import java.util.stream.Stream; - - -public class ConfigManager { - - private final String configPath; - - private final ObjectMapper om; - - private VRConfig vrConfig; - - - public ConfigManager(String configPath) { - this.configPath = configPath; - om = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.SPLIT_LINES)); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - om.registerModule(new VersioningModule()); - SimpleModule quaternionModule = new SimpleModule(); - quaternionModule.addSerializer(ObjectQuaternion.class, new QuaternionSerializer()); - quaternionModule.addDeserializer(ObjectQuaternion.class, new QuaternionDeserializer()); - om.registerModule(quaternionModule); - } - - public void loadConfig() { - try { - this.vrConfig = om - .readValue(new FileInputStream(configPath), VRConfig.class); - } catch (FileNotFoundException e) { - // Config file didn't exist, is not an error - } catch (IOException e) { - // Log the exception - LogManager.severe("Config failed to load: " + e); - // Make a backup of the erroneous config - backupConfig(); - } - - if (this.vrConfig == null) { - this.vrConfig = new VRConfig(); - } - } - - static public void atomicMove(Path from, Path to) throws IOException { - try { - // Atomic move to overwrite - Files.move(from, to, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException | FileAlreadyExistsException e) { - // Atomic move not supported or does not replace, try just replacing - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); - } - } - - public void backupConfig() { - Path cfgFile = Paths.get(configPath); - Path tmpBakCfgFile = Paths.get(configPath + ".bak.tmp"); - Path bakCfgFile = Paths.get(configPath + ".bak"); - - try { - Files - .copy( - cfgFile, - tmpBakCfgFile, - StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.COPY_ATTRIBUTES - ); - LogManager.info("Made a backup copy of config to \"" + tmpBakCfgFile + "\""); - } catch (IOException e) { - LogManager - .severe( - "Unable to make backup copy of config from \"" - + cfgFile - + "\" to \"" - + tmpBakCfgFile - + "\"", - e - ); - return; // Abort write - } - - try { - atomicMove(tmpBakCfgFile, bakCfgFile); - } catch (IOException e) { - LogManager - .severe( - "Unable to move backup config from \"" - + tmpBakCfgFile - + "\" to \"" - + bakCfgFile - + "\"", - e - ); - } - } - - @ThreadSafe - public synchronized void saveConfig() { - Path tmpCfgFile = Paths.get(configPath + ".tmp"); - Path cfgFile = Paths.get(configPath); - - // Serialize config - try { - // delete accidental folder caused by PR - // https://github.com/SlimeVR/SlimeVR-Server/pull/1176 - var cfgFileMaybeFolder = cfgFile.toFile(); - if (cfgFileMaybeFolder.isDirectory()) { - try (Stream pathStream = Files.walk(cfgFile)) { - // Can't use .toList() on Android - var list = pathStream - .sorted(Comparator.reverseOrder()) - .collect(Collectors.toList()); - for (var path : list) { - Files.delete(path); - } - } catch (IOException e) { - LogManager - .severe( - "Unable to delete folder that has same name as the config file on path \"" - + cfgFile - + "\"" - ); - return; - } - - } - var cfgFolder = cfgFile.toAbsolutePath().getParent().toFile(); - if (!cfgFolder.exists() && !cfgFolder.mkdirs()) { - LogManager - .severe("Unable to create folders for config on path \"" + cfgFile + "\""); - return; - } - om.writeValue(tmpCfgFile.toFile(), this.vrConfig); - } catch (IOException e) { - LogManager.severe("Unable to write serialized config to \"" + tmpCfgFile + "\"", e); - return; // Abort write - } - - // Overwrite old config - try { - atomicMove(tmpCfgFile, cfgFile); - } catch (IOException e) { - LogManager - .severe( - "Unable to move new config from \"" + tmpCfgFile + "\" to \"" + cfgFile + "\"", - e - ); - } - } - - public void resetConfig() { - this.vrConfig = new VRConfig(); - saveConfig(); - } - - public VRConfig getVrConfig() { - return vrConfig; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java deleted file mode 100644 index 77a1a777fb..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java +++ /dev/null @@ -1,357 +0,0 @@ -package dev.slimevr.config; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.*; -import com.github.jonpeterson.jackson.module.versioning.VersionedModelConverter; -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets; -import dev.slimevr.tracking.trackers.TrackerPosition; -import io.eiren.util.logging.LogManager; - -import java.util.Map; -import java.util.regex.Pattern; - - -public class CurrentVRConfigConverter implements VersionedModelConverter { - - @Override - public ObjectNode convert( - ObjectNode modelData, - String modelVersion, - String targetModelVersion, - JsonNodeFactory nodeFactory - ) { - try { - int version = Integer.parseInt(modelVersion); - - // Configs with old versions need a migration to the latest config - if (version < 2) { - // Move zoom to the window config - ObjectNode windowNode = (ObjectNode) modelData.get("window"); - DoubleNode zoomNode = (DoubleNode) modelData.get("zoom"); - if (windowNode != null && zoomNode != null) { - windowNode.set("zoom", zoomNode); - modelData.remove("zoom"); - } - - // Change trackers list to map - ArrayNode oldTrackersNode = modelData.withArray("trackers"); - if (oldTrackersNode != null) { - var trackersIter = oldTrackersNode.iterator(); - ObjectNode trackersNode = nodeFactory.objectNode(); - while (trackersIter.hasNext()) { - JsonNode node = trackersIter.next(); - JsonNode resultNode = TrackerConfig.toV2(node, nodeFactory); - trackersNode.set(node.get("name").asText(), resultNode); - } - modelData.set("trackers", trackersNode); - } - - // Rename bridge to bridges - ObjectNode bridgeNode = (ObjectNode) modelData.get("bridge"); - if (bridgeNode != null) { - modelData.set("bridges", bridgeNode); - modelData.remove("bridge"); - } - - // Move body to skeleton (and merge it to current skeleton) - ObjectNode bodyNode = (ObjectNode) modelData.get("body"); - if (bodyNode != null) { - var bodyIter = bodyNode.fields(); - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode == null) { - skeletonNode = nodeFactory.objectNode(); - } - - ObjectNode offsetsNode = nodeFactory.objectNode(); - while (bodyIter.hasNext()) { - Map.Entry node = bodyIter.next(); - // Filter only number values because other types would - // be stuff that didn't get migrated correctly before - if (node.getValue().isNumber()) { - offsetsNode.set(node.getKey(), node.getValue()); - } - } - - // Fix calibration wolf typos - offsetsNode.set("shouldersWidth", bodyNode.get("shoulersWidth")); - offsetsNode.set("shouldersDistance", bodyNode.get("shoulersDistance")); - offsetsNode.remove("shoulersWidth"); - offsetsNode.remove("shoulersDistance"); - skeletonNode.set("offsets", offsetsNode); - modelData.set("skeleton", skeletonNode); - modelData.remove("body"); - } - } - if (version < 3) { - // Check for out-of-bound filtering amount - ObjectNode filtersNode = (ObjectNode) modelData.get("filters"); - if (filtersNode != null && filtersNode.get("amount").floatValue() > 2f) { - filtersNode.set("amount", new FloatNode(0.2f)); - } - } - if (version < 4) { - // Change mountingRotation to mountingOrientation - ObjectNode oldTrackersNode = (ObjectNode) modelData.get("trackers"); - if (oldTrackersNode != null) { - var trackersIter = oldTrackersNode.iterator(); - var fieldNamesIter = oldTrackersNode.fieldNames(); - ObjectNode trackersNode = nodeFactory.objectNode(); - String fieldName; - while (trackersIter.hasNext()) { - ObjectNode node = (ObjectNode) trackersIter.next(); - fieldName = fieldNamesIter.next(); - node.set("mountingOrientation", node.get("mountingRotation")); - node.remove("mountingRotation"); - trackersNode.set(fieldName, node); - } - modelData.set("trackers", trackersNode); - } - } - if (version < 5) { - // Migrate old skeleton offsets to new ones - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode != null) { - ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets"); - if (offsetsNode != null) { - // torsoLength, chestDistance and waistDistance become - // chestLength, waistLength and hipLength. - float torsoLength = SkeletonConfigOffsets.CHEST.defaultValue - + SkeletonConfigOffsets.WAIST.defaultValue - + SkeletonConfigOffsets.HIP.defaultValue; - float chestDistance = SkeletonConfigOffsets.CHEST.defaultValue; - float waistDistance = SkeletonConfigOffsets.HIP.defaultValue; - JsonNode torsoNode = offsetsNode.get("torsoLength"); - if (torsoNode != null) - torsoLength = torsoNode.floatValue(); - JsonNode chestNode = offsetsNode.get("chestDistance"); - if (chestNode != null) - chestDistance = chestNode.floatValue(); - JsonNode waistNode = offsetsNode.get("waistDistance"); - if (waistNode != null) - waistDistance = waistNode.floatValue(); - offsetsNode.set("chestLength", offsetsNode.get("chestDistance")); - offsetsNode - .set( - "waistLength", - new FloatNode(torsoLength - chestDistance - waistDistance) - ); - offsetsNode.set("hipLength", offsetsNode.get("waistDistance")); - offsetsNode.remove("torsoLength"); - offsetsNode.remove("chestDistance"); - offsetsNode.remove("waistDistance"); - - // legsLength and kneeHeight become - // upperLegLength and lowerLegLength - float legsLength = SkeletonConfigOffsets.UPPER_LEG.defaultValue - + SkeletonConfigOffsets.LOWER_LEG.defaultValue; - float kneeHeight = SkeletonConfigOffsets.LOWER_LEG.defaultValue; - JsonNode legsNode = offsetsNode.get("legsLength"); - if (legsNode != null) - legsLength = legsNode.floatValue(); - JsonNode kneesNode = offsetsNode.get("kneeHeight"); - if (kneesNode != null) - kneeHeight = kneesNode.floatValue(); - offsetsNode.set("upperLegLength", new FloatNode(legsLength - kneeHeight)); - offsetsNode.set("lowerLegLength", new FloatNode(kneeHeight)); - offsetsNode.remove("legsLength"); - offsetsNode.remove("kneeHeight"); - - skeletonNode.set("offsets", offsetsNode); - modelData.set("skeleton", skeletonNode); - } - } - } - if (version < 6) { - // Migrate controllers offsets to hands offsets - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode != null) { - ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets"); - if (offsetsNode != null) { - offsetsNode.set("handDistanceY", offsetsNode.get("controllerDistanceY")); - offsetsNode.set("handDistanceZ", offsetsNode.get("controllerDistanceZ")); - } - } - } - if (version < 7) { - // Chest, hip, and elbow offsets now go the opposite direction - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode != null) { - ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets"); - if (offsetsNode != null) { - JsonNode chestNode = offsetsNode.get("chestOffset"); - if (chestNode != null) - offsetsNode.set("chestOffset", new FloatNode(-chestNode.floatValue())); - JsonNode hipNode = offsetsNode.get("hipOffset"); - if (hipNode != null) - offsetsNode.set("hipOffset", new FloatNode(-hipNode.floatValue())); - JsonNode elbowNode = offsetsNode.get("elbowOffset"); - if (elbowNode != null) - offsetsNode.set("elbowOffset", new FloatNode(-elbowNode.floatValue())); - } - } - } - if (version < 8) { - // reset > fullReset, quickReset > yawReset - ObjectNode keybindingsNode = (ObjectNode) modelData.get("keybindings"); - if (keybindingsNode != null) { - JsonNode fullResetNode = keybindingsNode.get("resetBinding"); - if (fullResetNode != null) - keybindingsNode.set("fullResetBinding", fullResetNode); - JsonNode yawResetNode = keybindingsNode.get("quickResetBinding"); - if (yawResetNode != null) - keybindingsNode.set("yawResetBinding", yawResetNode); - JsonNode mountingResetNode = keybindingsNode.get("resetMountingBinding"); - if (mountingResetNode != null) - keybindingsNode.set("mountingResetBinding", mountingResetNode); - - JsonNode fullDelayNode = keybindingsNode.get("resetDelay"); - if (fullDelayNode != null) - keybindingsNode.set("fullResetDelay", fullDelayNode); - JsonNode yawDelayNode = keybindingsNode.get("quickResetDelay"); - if (yawDelayNode != null) - keybindingsNode.set("yawResetDelay", yawDelayNode); - JsonNode mountingDelayNode = keybindingsNode.get("resetMountingDelay"); - if (mountingDelayNode != null) - keybindingsNode.set("mountingResetDelay", mountingDelayNode); - } - - ObjectNode tapDetectionNode = (ObjectNode) modelData.get("tapDetection"); - if (tapDetectionNode != null) { - tapDetectionNode.set("yawResetDelay", tapDetectionNode.get("quickResetDelay")); - tapDetectionNode.set("fullResetDelay", tapDetectionNode.get("resetDelay")); - tapDetectionNode - .set("yawResetEnabled", tapDetectionNode.get("quickResetEnabled")); - tapDetectionNode.set("fullResetEnabled", tapDetectionNode.get("resetEnabled")); - tapDetectionNode.set("yawResetTaps", tapDetectionNode.get("quickResetTaps")); - tapDetectionNode.set("fullResetTaps", tapDetectionNode.get("resetTaps")); - } - } - if (version < 9) { - // split chest into 2 offsets - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode != null) { - ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets"); - if (offsetsNode != null) { - JsonNode chestNode = offsetsNode.get("chestLength"); - if (chestNode != null) { - offsetsNode - .set("chestLength", new FloatNode(chestNode.floatValue() / 2f)); - offsetsNode - .set( - "upperChestLength", - new FloatNode(chestNode.floatValue() / 2f) - ); - } - } - } - } - if (version < 10) { - // Change default AutoBone recording length from 20 to 30 - // seconds - ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); - if (autoBoneNode != null) { - JsonNode sampleCountNode = autoBoneNode.get("sampleCount"); - if (sampleCountNode != null && sampleCountNode.intValue() == 1000) { - autoBoneNode.set("sampleCount", new IntNode(1500)); - } - } - } - if (version < 11) { - // Sets HMD's designation to "body:head" - ObjectNode trackersNode = (ObjectNode) modelData.get("trackers"); - if (trackersNode != null) { - ObjectNode HMDNode = (ObjectNode) trackersNode.get("HMD"); - if (HMDNode != null) { - HMDNode - .set( - "designation", - new TextNode(TrackerPosition.HEAD.getDesignation()) - ); - trackersNode.set("HMD", HMDNode); - modelData.set("trackers", trackersNode); - } - } - } - if (version < 12) { - // Update AutoBone defaults - ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); - if (autoBoneNode != null) { - JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor"); - if (offsetSlideNode != null && offsetSlideNode.floatValue() == 2.0f) { - autoBoneNode.set("offsetSlideErrorFactor", new FloatNode(1.0f)); - } - JsonNode bodyProportionsNode = autoBoneNode.get("bodyProportionErrorFactor"); - if (bodyProportionsNode != null && bodyProportionsNode.floatValue() == 0.825f) { - autoBoneNode.set("bodyProportionErrorFactor", new FloatNode(0.25f)); - } - } - } - - if (version < 13) { - ObjectNode oldTrackersNode = (ObjectNode) modelData.get("trackers"); - if (oldTrackersNode != null) { - var fieldNamesIter = oldTrackersNode.fieldNames(); - String trackerId; - final String macAddressRegex = "udp://((?:[a-zA-Z\\d]{2}:){5}[a-zA-Z\\d]{2})/0"; - final Pattern pattern = Pattern.compile(macAddressRegex); - while (fieldNamesIter.hasNext()) { - trackerId = fieldNamesIter.next(); - var matcher = pattern.matcher(trackerId); - if (!matcher.find()) - continue; - - modelData.withArray("knownDevices").add(matcher.group(1)); - } - } - } - - if (version < 14) { - // Update AutoBone defaults - ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); - if (autoBoneNode != null) { - // Move HMD height to skeleton - ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); - if (skeletonNode != null) { - JsonNode targetHmdHeight = autoBoneNode.get("targetHmdHeight"); - if (targetHmdHeight != null) { - skeletonNode.set("hmdHeight", targetHmdHeight); - } - } - - JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor"); - JsonNode slideNode = autoBoneNode.get("slideErrorFactor"); - if ( - offsetSlideNode != null - && slideNode != null - && offsetSlideNode.floatValue() == 1.0f - && slideNode.floatValue() == 0.0f - ) { - autoBoneNode.set("offsetSlideErrorFactor", new FloatNode(0.0f)); - autoBoneNode.set("slideErrorFactor", new FloatNode(1.0f)); - } - JsonNode bodyProportionsNode = autoBoneNode.get("bodyProportionErrorFactor"); - if (bodyProportionsNode != null && bodyProportionsNode.floatValue() == 0.25f) { - autoBoneNode.set("bodyProportionErrorFactor", new FloatNode(0.05f)); - } - JsonNode numEpochsNode = autoBoneNode.get("numEpochs"); - if (numEpochsNode != null && numEpochsNode.intValue() == 100) { - autoBoneNode.set("numEpochs", new IntNode(50)); - } - } - } - - if (version < 15) { - ObjectNode checklistNode = (ObjectNode) modelData.get("trackingChecklist"); - if (checklistNode != null) { - ArrayNode ignoredStepsArray = (ArrayNode) checklistNode.get("ignoredStepsIds"); - if (ignoredStepsArray != null) - ignoredStepsArray.removeAll(); - } - } - } catch (Exception e) { - LogManager.severe("Error during config migration: " + e); - } - - return modelData; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/DriftCompensationConfig.kt b/server/core/src/main/java/dev/slimevr/config/DriftCompensationConfig.kt deleted file mode 100644 index adced65a46..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/DriftCompensationConfig.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.slimevr.config - -import dev.slimevr.VRServer - -class DriftCompensationConfig { - - // Is drift compensation enabled - var enabled = false - - // Is drift prediction enabled - var prediction = false - - // Amount of drift compensation applied - var amount = 0.8f - - // Max resets for the calculated average drift - var maxResets = 6 - - fun updateTrackersDriftCompensation() { - for (t in VRServer.instance.allTrackers) { - if (t.isImu()) { - t.resetsHandler.readDriftCompensationConfig(this) - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/FiltersConfig.kt b/server/core/src/main/java/dev/slimevr/config/FiltersConfig.kt deleted file mode 100644 index 4ba5303142..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/FiltersConfig.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slimevr.config - -import dev.slimevr.VRServer - -class FiltersConfig { - - // Type of filtering applied (none, smoothing or prediction) - var type = "prediction" - - // Amount/Intensity of the specified filtering (0 to 1) - var amount = 0.2f - - fun updateTrackersFilters() { - for (tracker in VRServer.instance.allTrackers) { - if (tracker.allowFiltering) { - tracker.filteringHandler.readFilteringConfig(this, tracker.getRotation()) - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/HIDConfig.kt b/server/core/src/main/java/dev/slimevr/config/HIDConfig.kt deleted file mode 100644 index 2945ee3144..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/HIDConfig.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slimevr.config - -import com.fasterxml.jackson.annotation.JsonIgnore - -class HIDConfig { - var trackersOverHID = false -} diff --git a/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java b/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java deleted file mode 100644 index a3c1b0bfe3..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/KeybindingsConfig.java +++ /dev/null @@ -1,88 +0,0 @@ -package dev.slimevr.config; - -public class KeybindingsConfig { - - private String fullResetBinding = "CTRL+ALT+SHIFT+Y"; - - private String yawResetBinding = "CTRL+ALT+SHIFT+U"; - - private String mountingResetBinding = "CTRL+ALT+SHIFT+I"; - - private String feetMountingResetBinding = "CTRL+ALT+SHIFT+P"; - - private String pauseTrackingBinding = "CTRL+ALT+SHIFT+O"; - - private long fullResetDelay = 0L; - - private long yawResetDelay = 0L; - - private long mountingResetDelay = 0L; - - private long feetMountingResetDelay = 0L; - - private long pauseTrackingDelay = 0L; - - - public KeybindingsConfig() { - } - - public String getFullResetBinding() { - return fullResetBinding; - } - - public String getYawResetBinding() { - return yawResetBinding; - } - - public String getMountingResetBinding() { - return mountingResetBinding; - } - - public String getFeetMountingResetBinding() { - return feetMountingResetBinding; - } - - public String getPauseTrackingBinding() { - return pauseTrackingBinding; - } - - public long getFullResetDelay() { - return fullResetDelay; - } - - public void setFullResetDelay(long delay) { - fullResetDelay = delay; - } - - public long getYawResetDelay() { - return yawResetDelay; - } - - public void setYawResetDelay(long delay) { - yawResetDelay = delay; - } - - public long getMountingResetDelay() { - return mountingResetDelay; - } - - public void setMountingResetDelay(long delay) { - mountingResetDelay = delay; - } - - public long getFeetMountingResetDelay() { - return feetMountingResetDelay; - } - - public void setFeetMountingResetDelay(long delay) { - feetMountingResetDelay = delay; - } - - public long getPauseTrackingDelay() { - return pauseTrackingDelay; - } - - public void setPauseTrackingDelay(long delay) { - pauseTrackingDelay = delay; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/LegTweaksConfig.kt b/server/core/src/main/java/dev/slimevr/config/LegTweaksConfig.kt deleted file mode 100644 index 97250faa00..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/LegTweaksConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.config - -class LegTweaksConfig { - var correctionStrength = 0.3f - var alwaysUseFloorclip = false -} diff --git a/server/core/src/main/java/dev/slimevr/config/OSCConfig.kt b/server/core/src/main/java/dev/slimevr/config/OSCConfig.kt deleted file mode 100644 index 89b317fa37..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/OSCConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.slimevr.config - -open class OSCConfig { - - // Are the OSC receiver and sender enabled? - var enabled = false - - // Port to receive OSC messages from - var portIn = 0 - - // Port to send out OSC messages at - var portOut = 0 - - // Address to send out OSC messages at - var address = "127.0.0.1" -} diff --git a/server/core/src/main/java/dev/slimevr/config/OverlayConfig.java b/server/core/src/main/java/dev/slimevr/config/OverlayConfig.java deleted file mode 100644 index 7f564e57d5..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/OverlayConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.config; - -public class OverlayConfig { - - private boolean isMirrored = false; - private boolean isVisible = false; - - - public boolean isMirrored() { - return isMirrored; - } - - public boolean isVisible() { - return isVisible; - } - - public void setMirrored(boolean mirrored) { - isMirrored = mirrored; - } - - public void setVisible(boolean visible) { - isVisible = visible; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt deleted file mode 100644 index 65cc761d84..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt +++ /dev/null @@ -1,78 +0,0 @@ -package dev.slimevr.config - -import dev.slimevr.VRServer - -enum class ArmsResetModes(val id: Int) { - // Upper arm going back and forearm going forward - BACK(0), - - // Arms going forward - FORWARD(1), - - // Arms going up to the sides into a tpose - TPOSE_UP(2), - - // Arms going down to the sides from a tpose - TPOSE_DOWN(3), - ; - - companion object { - val values = entries.toTypedArray() - - @JvmStatic - fun fromId(id: Int): ArmsResetModes? { - for (filter in values) { - if (filter.id == id) return filter - } - return null - } - } -} - -enum class MountingMethods(val id: Int) { - MANUAL(0), - AUTOMATIC(1), - ; - - companion object { - val values = MountingMethods.entries.toTypedArray() - - @JvmStatic - fun fromId(id: Int): MountingMethods? { - for (filter in values) { - if (filter.id == id) return filter - } - return null - } - } -} - -class ResetsConfig { - - // Always reset mounting for feet - var resetMountingFeet = false - - // Reset mode used for the arms - var mode = ArmsResetModes.BACK - - // Yaw reset smoothing time in seconds - var yawResetSmoothTime = 0.0f - - // Save automatic mounting reset calibration - var saveMountingReset = false - - // Reset the HMD's pitch upon full reset - var resetHmdPitch = false - - var lastMountingMethod = MountingMethods.AUTOMATIC - - var yawResetDelay = 0.0f - var fullResetDelay = 3.0f - var mountingResetDelay = 3.0f - - fun updateTrackersResetsSettings() { - for (t in VRServer.instance.allTrackers) { - t.resetsHandler.readResetConfig(this) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/ServerConfig.kt b/server/core/src/main/java/dev/slimevr/config/ServerConfig.kt deleted file mode 100644 index c2bbc86183..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/ServerConfig.kt +++ /dev/null @@ -1,66 +0,0 @@ -package dev.slimevr.config - -import dev.slimevr.VRServer -import dev.slimevr.tracking.trackers.udp.MagnetometerStatus -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex - -class ServerConfig { - val trackerPort: Int = 6969 - - var useMagnetometerOnAllTrackers: Boolean = false - private set - - private val magMutex = Mutex() - suspend fun defineMagOnAllTrackers(state: Boolean) = coroutineScope { - magMutex.lock() - try { - if (useMagnetometerOnAllTrackers == state) return@coroutineScope - - VRServer.instance.deviceManager.devices.filter { it.magSupport }.map { - async { - // Not using 255 as it sometimes could make one of the sensors go into - // error mode (if there is more than one sensor inside the device) - if (!state) { - val trackers = it.trackers.filterValues { - it.magStatus != MagnetometerStatus.NOT_SUPPORTED - } -// if(trackers.size == it.trackers.size) { -// it.setMag(false) -// } else { - trackers.map { (_, t) -> - async { it.setMag(false, t.trackerNum) } - }.awaitAll() -// } - return@async - } - -// val every = it.trackers.all { (_, t) -> t.config.shouldHaveMagEnabled == true -// && t.magStatus != MagnetometerStatus.NOT_SUPPORTED } -// if (every) { -// it.setMag(true) -// return@async -// } - - it.trackers.filterValues { - it.config.shouldHaveMagEnabled == true && - it.magStatus != MagnetometerStatus.NOT_SUPPORTED - } - .map { (_, t) -> - async { - // FIXME: Tracker gets restarted after each setMag, what will happen for devices with 3 trackers? - it.setMag(true, t.trackerNum) - } - }.awaitAll() - } - }.awaitAll() - - useMagnetometerOnAllTrackers = state - VRServer.instance.configManager.saveConfig() - } finally { - magMutex.unlock() - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java b/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java deleted file mode 100644 index 27e6b3c3e8..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.slimevr.config; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.StdKeySerializers; -import dev.slimevr.config.serializers.BooleanMapDeserializer; -import dev.slimevr.config.serializers.FloatMapDeserializer; - -import java.util.HashMap; -import java.util.Map; - - -public class SkeletonConfig { - - @JsonDeserialize(using = BooleanMapDeserializer.class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) - public Map toggles = new HashMap<>(); - - @JsonDeserialize(using = FloatMapDeserializer.class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) - public Map values = new HashMap<>(); - - @JsonDeserialize(using = FloatMapDeserializer.class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) - public Map offsets = new HashMap<>(); - - private float hmdHeight = 0f; - private float floorHeight = 0f; - - public Map getToggles() { - return toggles; - } - - public Map getOffsets() { - return offsets; - } - - public Map getValues() { - return values; - } - - public float getHmdHeight() { - return hmdHeight; - } - - public void setHmdHeight(float hmdHeight) { - this.hmdHeight = hmdHeight; - } - - public float getFloorHeight() { - return floorHeight; - } - - public void setFloorHeight(float hmdHeight) { - this.floorHeight = hmdHeight; - } - - @JsonIgnore - public float getUserHeight() { - return hmdHeight - floorHeight; - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/StayAlignedConfig.kt b/server/core/src/main/java/dev/slimevr/config/StayAlignedConfig.kt deleted file mode 100644 index 318223dbbb..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/StayAlignedConfig.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.slimevr.config - -import com.fasterxml.jackson.annotation.JsonIgnore - -class StayAlignedConfig { - - /** - * Apply yaw correction - */ - var enabled = false - - /** - * Temporarily hide the yaw correction from Stay Aligned. - * - * Players can enable this to compare to when Stay Aligned is not enabled. Useful to - * verify if Stay Aligned improved the situation. Also useful to prevent players - * from saying "Stay Aligned screwed up my trackers!!" when it's actually a tracker - * that is drifting extremely badly. - * - * Do not serialize to config so that when the server restarts, it is always false. - */ - @JsonIgnore - var hideYawCorrection = false - - /** - * Standing relaxed pose - */ - val standingRelaxedPose = StayAlignedRelaxedPoseConfig() - - /** - * Sitting relaxed pose - */ - val sittingRelaxedPose = StayAlignedRelaxedPoseConfig() - - /** - * Flat relaxed pose - */ - val flatRelaxedPose = StayAlignedRelaxedPoseConfig() - - /** - * Whether setup has been completed - */ - var setupComplete = false -} diff --git a/server/core/src/main/java/dev/slimevr/config/StayAlignedRelaxedPoseConfig.kt b/server/core/src/main/java/dev/slimevr/config/StayAlignedRelaxedPoseConfig.kt deleted file mode 100644 index 6227a655bd..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/StayAlignedRelaxedPoseConfig.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.slimevr.config - -class StayAlignedRelaxedPoseConfig { - - /** - * Whether Stay Aligned should adjust the tracker yaws when the player is in this - * pose. - */ - var enabled = false - - /** - * Angle between the upper leg yaw and the center yaw. - */ - var upperLegAngleInDeg = 0.0f - - /** - * Angle between the lower leg yaw and the center yaw. - */ - var lowerLegAngleInDeg = 0.0f - - /** - * Angle between the foot and the center yaw. - */ - var footAngleInDeg = 0.0f -} diff --git a/server/core/src/main/java/dev/slimevr/config/TapDetectionConfig.kt b/server/core/src/main/java/dev/slimevr/config/TapDetectionConfig.kt deleted file mode 100644 index 0e5a83cdef..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/TapDetectionConfig.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slimevr.config - -import com.jme3.math.FastMath - -// handles the tap detection config -// this involves the number of taps, the delay, and whether or not the feature is enabled -// for each reset type -class TapDetectionConfig { - var yawResetDelay = 0.2f - var fullResetDelay = 1.0f - var mountingResetDelay = 1.0f - var yawResetEnabled = true - var fullResetEnabled = true - var mountingResetEnabled = true - var setupMode = false - var yawResetTaps = 2 - set(yawResetTaps) { - field = yawResetTaps.coerceIn(2, 10) - } - var fullResetTaps = 3 - set(fullResetTaps) { - field = fullResetTaps.coerceIn(2, 10) - } - var mountingResetTaps = 3 - set(mountingResetTaps) { - field = mountingResetTaps.coerceIn(2, 10) - } - var numberTrackersOverThreshold = 1 -} diff --git a/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt b/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt deleted file mode 100644 index 7770bec6be..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/TrackerConfig.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.slimevr.config - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import dev.slimevr.VRServer -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.ObjectQuaternion - -class TrackerConfig { - var customName: String? = null - var designation: String? = null - - @get:JvmName("isHide") - var hide: Boolean = false - var adjustment: ObjectQuaternion? = null - var mountingOrientation: ObjectQuaternion? = null - var mountingResetOrientation: ObjectQuaternion? = null - var allowDriftCompensation: Boolean? = null - - /** - * Only checked if [ServerConfig.useMagnetometerOnAllTrackers] enabled - */ - var shouldHaveMagEnabled: Boolean? = null - - constructor() - - constructor(tracker: Tracker) { - this.designation = if (tracker.trackerPosition != null) tracker.trackerPosition!!.designation else null - this.customName = tracker.customName - allowDriftCompensation = tracker.isImu() - shouldHaveMagEnabled = tracker.isImu() - } - - companion object { - @JvmStatic - fun toV2(v1: JsonNode, factory: JsonNodeFactory): JsonNode { - val node = factory.objectNode() - if (v1.has("customName")) node.set("customName", v1["customName"]) - if (v1.has("designation")) node.set("designation", v1["designation"]) - if (v1.has("hide")) node.set("hide", v1["hide"]) - if (v1.has("mountingRotation")) node.set("mountingRotation", v1["mountingRotation"]) - if (v1.has("adjustment")) node.set("adjustment", v1["adjustment"]) - return node - } - } -} - -val Tracker.config: TrackerConfig - get() = VRServer.instance.configManager.vrConfig.getTracker(this) diff --git a/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt b/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt deleted file mode 100644 index 182a97bfff..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slimevr.config - -class TrackingChecklistConfig { - val ignoredStepsIds: MutableList = mutableListOf() -} diff --git a/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt b/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt deleted file mode 100644 index b48e4d70a2..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/VMCConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.slimevr.config - -class VMCConfig : OSCConfig() { - - // Anchor the tracking at the hip? - var anchorHip = true - - // JSON part of the VRM to be used - var vrmJson: String? = null - - // Mirror the tracking before sending it (turn left <=> turn right, left leg <=> right leg) - var mirrorTracking = false -} diff --git a/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt deleted file mode 100644 index fd40b13dda..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.config - -class VRCConfig { - // List of fields ignored in vrc warnings - @see VRCConfigValidity - val mutedWarnings: MutableList = mutableListOf() -} diff --git a/server/core/src/main/java/dev/slimevr/config/VRCOSCConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRCOSCConfig.kt deleted file mode 100644 index 7d0ed248dc..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/VRCOSCConfig.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.config - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.ser.std.StdKeySerializers -import dev.slimevr.config.serializers.BooleanMapDeserializer -import dev.slimevr.tracking.trackers.TrackerRole -import java.util.* - -class VRCOSCConfig : OSCConfig() { - - // Which trackers' data to send - @JsonDeserialize(using = BooleanMapDeserializer::class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class) - var trackers: MutableMap = HashMap() - - var oscqueryEnabled: Boolean = true - - fun getOSCTrackerRole(role: TrackerRole, def: Boolean): Boolean = trackers.getOrDefault(role.name.lowercase(Locale.getDefault()), def) - - fun setOSCTrackerRole(role: TrackerRole, `val`: Boolean) { - trackers[role.name.lowercase(Locale.getDefault())] = `val` - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt deleted file mode 100644 index 9ef33f5e00..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ /dev/null @@ -1,145 +0,0 @@ -package dev.slimevr.config - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.ser.std.StdKeySerializers -import com.github.jonpeterson.jackson.module.versioning.JsonVersionedModel -import dev.slimevr.config.serializers.BridgeConfigMapDeserializer -import dev.slimevr.config.serializers.TrackerConfigMapDeserializer -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerRole - -@JsonVersionedModel( - currentVersion = "15", - defaultDeserializeToVersion = "15", - toCurrentConverterClass = CurrentVRConfigConverter::class, -) -class VRConfig { - val server: ServerConfig = ServerConfig() - - val filters: FiltersConfig = FiltersConfig() - - val driftCompensation: DriftCompensationConfig = DriftCompensationConfig() - - val oscRouter: OSCConfig = OSCConfig() - - val vrcOSC: VRCOSCConfig = VRCOSCConfig() - - @get:JvmName("getVMC") - val vmc: VMCConfig = VMCConfig() - - val autoBone: AutoBoneConfig = AutoBoneConfig() - - val keybindings: KeybindingsConfig = KeybindingsConfig() - - val skeleton: SkeletonConfig = SkeletonConfig() - - val legTweaks: LegTweaksConfig = LegTweaksConfig() - - val tapDetection: TapDetectionConfig = TapDetectionConfig() - - val resetsConfig: ResetsConfig = ResetsConfig() - - val stayAlignedConfig = StayAlignedConfig() - - val hidConfig = HIDConfig() - - @JsonDeserialize(using = TrackerConfigMapDeserializer::class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class) - private val trackers: MutableMap = HashMap() - - @JsonDeserialize(using = BridgeConfigMapDeserializer::class) - @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class) - private val bridges: MutableMap = HashMap() - - val knownDevices: MutableSet = mutableSetOf() - - val overlay: OverlayConfig = OverlayConfig() - - val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig() - - val vrcConfig: VRCConfig = VRCConfig() - - init { - // Initialize default settings for OSC Router - oscRouter.portIn = 9002 - oscRouter.portOut = 9000 - - // Initialize default settings for VRC OSC - vrcOSC.portIn = 9001 - vrcOSC.portOut = 9000 - vrcOSC - .setOSCTrackerRole( - TrackerRole.WAIST, - vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true), - ) - vrcOSC - .setOSCTrackerRole( - TrackerRole.LEFT_FOOT, - vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true), - ) - vrcOSC - .setOSCTrackerRole( - TrackerRole.RIGHT_FOOT, - vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true), - ) - - // Initialize default settings for VMC - vmc.portIn = 39540 - vmc.portOut = 39539 - } - - fun getTrackers(): Map = trackers - - fun getBridges(): Map = bridges - - fun hasTrackerByName(name: String): Boolean = trackers.containsKey(name) - - fun getTracker(tracker: Tracker): TrackerConfig { - var config = trackers[tracker.name] - if (config == null) { - config = TrackerConfig(tracker) - trackers[tracker.name] = config - } - return config - } - - fun readTrackerConfig(tracker: Tracker) { - if (tracker.userEditable) { - val config = getTracker(tracker) - tracker.readConfig(config) - if (tracker.isImu()) tracker.resetsHandler.readDriftCompensationConfig(driftCompensation) - tracker.resetsHandler.readResetConfig(resetsConfig) - if (tracker.allowReset) { - tracker.saveMountingResetOrientation(config) - } - if (tracker.allowFiltering) { - tracker - .filteringHandler - .readFilteringConfig(filters, tracker.getRotation()) - } - } - } - - fun writeTrackerConfig(tracker: Tracker?) { - if (tracker?.userEditable == true) { - val tc = getTracker(tracker) - tracker.writeConfig(tc) - } - } - - fun getBridge(bridgeKey: String): BridgeConfig { - var config = bridges[bridgeKey] - if (config == null) { - config = BridgeConfig() - bridges[bridgeKey] = config - } - return config - } - - fun isKnownDevice(mac: String?): Boolean = knownDevices.contains(mac) - - fun addKnownDevice(mac: String): Boolean = knownDevices.add(mac) - - fun forgetKnownDevice(mac: String): Boolean = knownDevices.remove(mac) -} diff --git a/server/core/src/main/java/dev/slimevr/config/behaviours.kt b/server/core/src/main/java/dev/slimevr/config/behaviours.kt new file mode 100644 index 0000000000..bdc7289862 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/behaviours.kt @@ -0,0 +1,28 @@ +package dev.slimevr.config + +object DefaultGlobalConfigBehaviour : GlobalConfigBehaviour { + override fun reduce(state: GlobalConfigState, action: GlobalConfigActions) = when (action) { + is GlobalConfigActions.SetUserProfile -> state.copy(selectedUserProfile = action.name) + is GlobalConfigActions.SetSettingsProfile -> state.copy(selectedSettingsProfile = action.name) + } +} + +object DefaultSettingsBehaviour : SettingsBehaviour { + override fun reduce(state: SettingsState, action: SettingsActions) = when (action) { + is SettingsActions.Update -> state.copy(data = action.transform(state.data)) + + is SettingsActions.LoadProfile -> action.newState + + is SettingsActions.UpdateTracker -> { + val existing = state.data.trackers[action.hardwareId] ?: TrackerConfig() + state.copy(data = state.data.copy(trackers = state.data.trackers + (action.hardwareId to action.transform(existing)))) + } + } +} + +object DefaultUserBehaviour : UserConfigBehaviour { + override fun reduce(state: UserConfigState, action: UserConfigActions) = when (action) { + is UserConfigActions.Update -> state.copy(data = action.transform(state.data)) + is UserConfigActions.LoadProfile -> action.newState + } +} diff --git a/server/core/src/main/java/dev/slimevr/config/io.kt b/server/core/src/main/java/dev/slimevr/config/io.kt new file mode 100644 index 0000000000..d32f41769c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/io.kt @@ -0,0 +1,92 @@ +package dev.slimevr.config + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +val jsonConfig = Json { + prettyPrint = true + ignoreUnknownKeys = true + encodeDefaults = true +} + +suspend fun atomicWriteFile(file: File, content: String) = withContext(Dispatchers.IO) { + file.parentFile?.mkdirs() + val tmp = File(file.parent, "${file.name}.tmp") + tmp.writeText(content) + Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) + Unit +} + +suspend inline fun loadFileWithBackup(file: File, default: T, crossinline deserialize: (String) -> T): T = withContext(Dispatchers.IO) { + if (!file.exists()) { + atomicWriteFile(file, jsonConfig.encodeToString(default)) + return@withContext default + } + + try { + deserialize(file.readText()) + } catch (e: Exception) { + e.printStackTrace() + System.err.println("Failed to load ${file.absolutePath}: ${e.message}") + if (file.exists()) { + try { + val bakTmp = File(file.parent, "${file.name}.bak.tmp") + file.copyTo(bakTmp, overwrite = true) + Files.move( + bakTmp.toPath(), + File(file.parent, "${file.name}.bak").toPath(), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING, + ) + } catch (e2: Exception) { + System.err.println("Failed to back up corrupted file: ${e2.message}") + } + } + default + } +} + +/** + * Launches a debounced autosave coroutine. Skips the initial state (already on + * disk at start time) and any state that was already successfully persisted. + * Cancel and restart to switch profiles. the new job treats the current state + * as already saved. + */ +@OptIn(FlowPreview::class) +fun launchAutosave( + scope: CoroutineScope, + state: StateFlow, + toFile: (S) -> File, + serialize: (S) -> String, +): Job { + var lastSaved = state.value + return merge(state.debounce(500L), state.sample(2000L)) + .distinctUntilChanged() + .filter { it != lastSaved } + .onEach { s -> + try { + val file = toFile(s) + atomicWriteFile(file, serialize(s)) + lastSaved = s + println("Saved ${file.absolutePath}") + } catch (e: Exception) { + System.err.println("Failed to save: ${e.message}") + } + } + .launchIn(scope) +} diff --git a/server/core/src/main/java/dev/slimevr/config/module.kt b/server/core/src/main/java/dev/slimevr/config/module.kt new file mode 100644 index 0000000000..96a88e91bf --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/module.kt @@ -0,0 +1,90 @@ +package dev.slimevr.config + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.io.File + +private const val GLOBAL_CONFIG_VERSION = 1 + +@Serializable +data class GlobalConfigState( + val selectedUserProfile: String = "default", + val selectedSettingsProfile: String = "default", + val version: Int = GLOBAL_CONFIG_VERSION, +) + +sealed interface GlobalConfigActions { + data class SetUserProfile(val name: String) : GlobalConfigActions + data class SetSettingsProfile(val name: String) : GlobalConfigActions +} + +typealias GlobalConfigContext = Context +typealias GlobalConfigBehaviour = Behaviour + +private fun migrateGlobalConfig(json: JsonObject): JsonObject { + val version = json["version"]?.jsonPrimitive?.intOrNull ?: 0 + return when { + // add migration branches here as: version < N -> migrateGlobalConfig(...) + else -> json + } +} + +private fun parseAndMigrateGlobalConfig(raw: String): GlobalConfigState { + val json = jsonConfig.parseToJsonElement(raw).jsonObject + return jsonConfig.decodeFromJsonElement(migrateGlobalConfig(json)) +} + +class AppConfig( + val globalContext: GlobalConfigContext, + val userConfig: UserConfig, + val settings: Settings, +) { + suspend fun switchUserProfile(name: String) { + globalContext.dispatch(GlobalConfigActions.SetUserProfile(name)) + userConfig.swap(name) + } + + suspend fun switchSettingsProfile(name: String) { + globalContext.dispatch(GlobalConfigActions.SetSettingsProfile(name)) + settings.swap(name) + } + + companion object { + suspend fun create(scope: CoroutineScope, configFolder: File): AppConfig { + val initialGlobal = loadFileWithBackup(File(configFolder, "global.json"), GlobalConfigState()) { + parseAndMigrateGlobalConfig(it) + } + + val behaviours = listOf(DefaultGlobalConfigBehaviour) + val globalContext = Context.create( + initialState = initialGlobal, + scope = scope, + behaviours = behaviours, + ) + behaviours.forEach { it.observe(globalContext) } + + launchAutosave( + scope = scope, + state = globalContext.state, + toFile = { File(configFolder, "global.json") }, + serialize = { jsonConfig.encodeToString(it) }, + ) + + val userConfig = UserConfig.create(scope, configFolder, initialGlobal.selectedUserProfile) + val settings = Settings.create(scope, configFolder, initialGlobal.selectedSettingsProfile) + + return AppConfig( + globalContext = globalContext, + userConfig = userConfig, + settings = settings, + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers.kt b/server/core/src/main/java/dev/slimevr/config/serializers.kt new file mode 100644 index 0000000000..9161888a86 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/serializers.kt @@ -0,0 +1,49 @@ +package dev.slimevr.config + +import io.github.axisangles.ktmath.Quaternion +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import solarxr_protocol.datatypes.BodyPart + +object BodyPartSerializer : KSerializer { + @OptIn(ExperimentalSerializationApi::class) + override val descriptor = PrimitiveSerialDescriptor("BodyPart", PrimitiveKind.STRING).nullable + + override fun serialize(encoder: Encoder, value: BodyPart?) { + val jsonEncoder = encoder as JsonEncoder + if (value == null) { + jsonEncoder.encodeJsonElement(JsonNull) + } else { + jsonEncoder.encodeJsonElement(JsonPrimitive(value.name)) + } + } + + override fun deserialize(decoder: Decoder): BodyPart? { + val element = (decoder as JsonDecoder).decodeJsonElement() + if (element is JsonNull) return null + return BodyPart.entries.firstOrNull { it.name == element.jsonPrimitive.content } + } +} + +@Serializable +private data class QuaternionSurrogate(val w: Float, val x: Float, val y: Float, val z: Float) + +object QuaternionSerializer : KSerializer { + override val descriptor = QuaternionSurrogate.serializer().descriptor + override fun serialize(encoder: Encoder, value: Quaternion) = encoder.encodeSerializableValue(QuaternionSurrogate.serializer(), QuaternionSurrogate(value.w, value.x, value.y, value.z)) + override fun deserialize(decoder: Decoder): Quaternion { + val s = decoder.decodeSerializableValue(QuaternionSurrogate.serializer()) + return Quaternion(s.w, s.x, s.y, s.z) + } +} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/BooleanMapDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/BooleanMapDeserializer.java deleted file mode 100644 index 7ef56dc848..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/BooleanMapDeserializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.slimevr.config.serializers; - -/** - * This class allows the use of the utility super class MapDeserializer that - * takes the Value of a map as its Generic parameter. It is so you can use that - * class in a @JsonDeserialize annotation on the Map field inside the config - * instance - * - * @see dev.slimevr.config.VRConfig - */ -public class BooleanMapDeserializer extends MapDeserializer { - public BooleanMapDeserializer() { - super(Boolean.class); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/BridgeConfigMapDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/BridgeConfigMapDeserializer.java deleted file mode 100644 index 6f4ddef52f..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/BridgeConfigMapDeserializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.slimevr.config.serializers; - -import dev.slimevr.config.BridgeConfig; - - -/** - * This class allows the use of the utility super class MapDeserializer that - * takes the Value of a map as its Generic parameter. It is so you can use that - * class in a @JsonDeserialize annotation on the Map field inside the config - * instance - * - * @see dev.slimevr.config.VRConfig - */ -public class BridgeConfigMapDeserializer extends MapDeserializer { - public BridgeConfigMapDeserializer() { - super(BridgeConfig.class); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/FloatMapDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/FloatMapDeserializer.java deleted file mode 100644 index c7340db17f..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/FloatMapDeserializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.slimevr.config.serializers; - -/** - * This class allows the use of the utility super class MapDeserializer that - * takes the Value of a map as its Generic parameter. It is so you can use that - * class in a @JsonDeserialize annotation on the Map field inside the config - * instance - * - * @see dev.slimevr.config.VRConfig - */ -public class FloatMapDeserializer extends MapDeserializer { - public FloatMapDeserializer() { - super(Float.class); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/MapDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/MapDeserializer.java deleted file mode 100644 index 4c8ecfc11a..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/MapDeserializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.slimevr.config.serializers; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.type.MapType; -import com.fasterxml.jackson.databind.type.TypeFactory; - -import java.io.IOException; -import java.util.HashMap; - - -/** - * This class is a utility class that allows to write Map serializers easily to - * be used in the VRConfig (@see {@link dev.slimevr.config.VRConfig}) - * - * @see BooleanMapDeserializer to see how it is used - */ -public abstract class MapDeserializer extends JsonDeserializer> { - - private final Class valueClass; - - public MapDeserializer(Class valueClass) { - super(); - this.valueClass = valueClass; - } - - @Override - public HashMap deserialize(JsonParser p, DeserializationContext dc) - throws IOException { - TypeFactory typeFactory = dc.getTypeFactory(); - MapType mapType = typeFactory - .constructMapType(HashMap.class, String.class, valueClass); - return dc.readValue(p, mapType); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionDeserializer.java deleted file mode 100644 index 346c374a51..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionDeserializer.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.slimevr.config.serializers; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import io.github.axisangles.ktmath.ObjectQuaternion; - -import java.io.IOException; - - -public class QuaternionDeserializer extends JsonDeserializer { - @Override - public ObjectQuaternion deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JacksonException { - JsonNode node = p.getCodec().readTree(p); - - return new ObjectQuaternion( - (float) node.get("w").asDouble(), - (float) node.get("x").asDouble(), - (float) node.get("y").asDouble(), - (float) node.get("z").asDouble() - ); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionSerializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionSerializer.java deleted file mode 100644 index bbd0faf82b..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/QuaternionSerializer.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.slimevr.config.serializers; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import io.github.axisangles.ktmath.ObjectQuaternion; - -import java.io.IOException; - - -public class QuaternionSerializer extends JsonSerializer { - - @Override - public void serialize(ObjectQuaternion value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeStartObject(); - gen.writeNumberField("x", value.getX()); - gen.writeNumberField("y", value.getY()); - gen.writeNumberField("z", value.getZ()); - gen.writeNumberField("w", value.getW()); - gen.writeEndObject(); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/serializers/TrackerConfigMapDeserializer.java b/server/core/src/main/java/dev/slimevr/config/serializers/TrackerConfigMapDeserializer.java deleted file mode 100644 index 2e624273f9..0000000000 --- a/server/core/src/main/java/dev/slimevr/config/serializers/TrackerConfigMapDeserializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.slimevr.config.serializers; - -import dev.slimevr.config.TrackerConfig; - - -/** - * This class allows the use of the utility super class MapDeserializer that - * takes the Value of a map as its Generic parameter. It is so you can use that - * class in a @JsonDeserialize annotation on the Map field inside the config - * instance - * - * @see dev.slimevr.config.VRConfig - */ -public class TrackerConfigMapDeserializer extends MapDeserializer { - public TrackerConfigMapDeserializer() { - super(TrackerConfig.class); - } -} diff --git a/server/core/src/main/java/dev/slimevr/config/settings.kt b/server/core/src/main/java/dev/slimevr/config/settings.kt new file mode 100644 index 0000000000..8553c2c132 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/settings.kt @@ -0,0 +1,120 @@ +package dev.slimevr.config + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import io.github.axisangles.ktmath.Quaternion +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import solarxr_protocol.datatypes.BodyPart +import java.io.File + +private const val SETTINGS_CONFIG_VERSION = 1 + +@Serializable +data class TrackerConfig( + @Serializable(with = BodyPartSerializer::class) + val bodyPart: BodyPart? = null, + val customName: String? = null, + @Serializable(with = QuaternionSerializer::class) + val mountingOrientation: Quaternion? = null, + val magEnabled: Boolean? = null, +) + +@Serializable +data class SettingsConfigState( + val trackerPort: Int = 6969, + val mutedVRCWarnings: List = listOf(), + val mutedChecklistSteps: Set = emptySet(), + val trackers: Map = emptyMap(), + val globalMagEnabled: Boolean = true, + val version: Int = SETTINGS_CONFIG_VERSION, +) + +private fun migrateSettingsConfig(json: JsonObject): JsonObject { + val version = json["version"]?.jsonPrimitive?.intOrNull ?: 0 + return when { + // add migration branches here as: version < N -> migrateSettingsConfig(...) + else -> json + } +} + +private fun parseAndMigrateSettingsConfig(raw: String): SettingsConfigState { + val json = jsonConfig.parseToJsonElement(raw).jsonObject + return jsonConfig.decodeFromJsonElement(migrateSettingsConfig(json)) +} + +data class SettingsState( + val data: SettingsConfigState, + val name: String, +) + +sealed interface SettingsActions { + data class Update(val transform: SettingsConfigState.() -> SettingsConfigState) : SettingsActions + data class LoadProfile(val newState: SettingsState) : SettingsActions + data class UpdateTracker(val hardwareId: String, val transform: TrackerConfig.() -> TrackerConfig) : SettingsActions +} + +typealias SettingsContext = Context +typealias SettingsBehaviour = Behaviour + +class Settings( + val context: SettingsContext, + private val scope: CoroutineScope, + private val settingsDir: File, +) { + private var autosaveJob: Job = startAutosave() + + private fun startAutosave() = launchAutosave( + scope = scope, + state = context.state, + toFile = { state -> File(settingsDir, "${state.name}.json") }, + serialize = { state -> jsonConfig.encodeToString(state.data) }, + ) + + suspend fun swap(newName: String) { + autosaveJob.cancelAndJoin() + + val newData = loadFileWithBackup(File(settingsDir, "$newName.json"), SettingsConfigState()) { + parseAndMigrateSettingsConfig(it) + } + val newState = SettingsState(name = newName, data = newData) + context.dispatch(SettingsActions.LoadProfile(newState)) + + autosaveJob = startAutosave() + } + + companion object { + suspend fun create(scope: CoroutineScope, configDir: File, name: String): Settings { + val settingsDir = File(configDir, "settings") + + val initialData = loadFileWithBackup(File(settingsDir, "$name.json"), SettingsConfigState()) { + parseAndMigrateSettingsConfig(it) + } + val initialState = SettingsState(name = name, data = initialData) + + val behaviours = listOf(DefaultSettingsBehaviour) + val context = Context.create(initialState = initialState, scope = scope, behaviours = behaviours) + val settings = Settings(context, scope = scope, settingsDir = settingsDir) + behaviours.forEach { it.observe(settings) } + return settings + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/config/user.kt b/server/core/src/main/java/dev/slimevr/config/user.kt new file mode 100644 index 0000000000..54161b3836 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/user.kt @@ -0,0 +1,92 @@ +package dev.slimevr.config + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.io.File + +private const val USER_CONFIG_VERSION = 1 + +@Serializable +data class UserConfigData( + val userHeight: Float = 1.6f, + val proportions: Map = emptyMap(), + val version: Int = USER_CONFIG_VERSION, +) + +private fun migrateUserConfig(json: JsonObject): JsonObject { + val version = json["version"]?.jsonPrimitive?.intOrNull ?: 0 + return when { + // add migration branches here as: version < N -> migrateUserConfig(...) + else -> json + } +} + +private fun parseAndMigrateUserConfig(raw: String): UserConfigData { + val json = jsonConfig.parseToJsonElement(raw).jsonObject + return jsonConfig.decodeFromJsonElement(migrateUserConfig(json)) +} + +data class UserConfigState( + val data: UserConfigData, + val name: String, +) + +sealed interface UserConfigActions { + data class Update(val transform: UserConfigData.() -> UserConfigData) : UserConfigActions + data class LoadProfile(val newState: UserConfigState) : UserConfigActions +} + +typealias UserConfigContext = Context +typealias UserConfigBehaviour = Behaviour + +class UserConfig( + val context: UserConfigContext, + private val scope: CoroutineScope, + private val userConfigDir: File, +) { + private var autosaveJob: Job = startAutosave() + + private fun startAutosave() = launchAutosave( + scope = scope, + state = context.state, + toFile = { state -> File(userConfigDir, "${state.name}.json") }, + serialize = { state -> jsonConfig.encodeToString(state.data) }, + ) + + suspend fun swap(newName: String) { + autosaveJob.cancelAndJoin() + + val newData = loadFileWithBackup(File(userConfigDir, "$newName.json"), UserConfigData()) { + parseAndMigrateUserConfig(it) + } + val newState = UserConfigState(name = newName, data = newData) + context.dispatch(UserConfigActions.LoadProfile(newState)) + + autosaveJob = startAutosave() + } + + companion object { + suspend fun create(scope: CoroutineScope, configDir: File, name: String): UserConfig { + val userConfigDir = File(configDir, "user") + + val initialData = loadFileWithBackup(File(userConfigDir, "$name.json"), UserConfigData()) { + parseAndMigrateUserConfig(it) + } + val initialState = UserConfigState(name = name, data = initialData) + + val context = Context.create(initialState = initialState, scope = scope, behaviours = listOf(DefaultUserBehaviour)) + val userConfig = UserConfig(context, scope = scope, userConfigDir = userConfigDir) + context.observeAll(userConfig) + return userConfig + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/context/context.kt b/server/core/src/main/java/dev/slimevr/context/context.kt new file mode 100644 index 0000000000..0b16ce7e2d --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/context/context.kt @@ -0,0 +1,80 @@ +package dev.slimevr.context + +import dev.slimevr.context.debug.DebugMiddleware +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.concurrent.CopyOnWriteArrayList + +interface Behaviour { + fun reduce(state: S, action: A): S = state + fun observe(receiver: C) {} +} + +class Context( + private val mutableStateFlow: MutableStateFlow, + val reducer: (S, A) -> S, + val scope: CoroutineScope, + val behaviours: CopyOnWriteArrayList>, + private val debugMiddleware: DebugMiddleware? = null, +) { + val state: StateFlow = mutableStateFlow.asStateFlow() + + fun dispatch(action: A) { + if (debugMiddleware == null) { + mutableStateFlow.update { currentState -> reducer(currentState, action) } + return + } + val caller = captureCallerBehaviour() + val before = mutableStateFlow.value + mutableStateFlow.update { currentState -> reducer(currentState, action) } + debugMiddleware.onDispatch(caller, before, action, mutableStateFlow.value) + } + + fun dispatchAll(actions: List) { + if (debugMiddleware == null) { + mutableStateFlow.update { currentState -> + actions.fold(currentState) { s, action -> reducer(s, action) } + } + return + } + val caller = captureCallerBehaviour() + val before = mutableStateFlow.value + mutableStateFlow.update { currentState -> + actions.fold(currentState) { s, action -> reducer(s, action) } + } + debugMiddleware.onDispatchAll(caller, before, actions, mutableStateFlow.value) + } + + fun observeAll(receiver: C) = behaviours.forEach { behaviour -> + @Suppress("UNCHECKED_CAST") + (behaviour as Behaviour).observe(receiver) + } + + private fun captureCallerBehaviour(): String? { + val knownBehaviourClasses = behaviours.mapTo(HashSet()) { b -> b::class.java.name } + return Thread.currentThread().stackTrace + .firstOrNull { frame -> frame.className in knownBehaviourClasses } + ?.className?.substringAfterLast('.') + } + + companion object { + val debugEnabled: Boolean = System.getProperty("slimevr.debug.context") == "true" || + System.getenv("SLIMEVR_DEBUG_CONTEXT") == "true" + + fun create( + initialState: S, + scope: CoroutineScope, + behaviours: List>, + debugMiddleware: DebugMiddleware? = null, + ): Context { + val mutableStateFlow = MutableStateFlow(initialState) + val reducer: (S, A) -> S = { currentState, action -> + behaviours.fold(currentState) { s, b -> b.reduce(s, action) } + } + return Context(mutableStateFlow, reducer, scope, CopyOnWriteArrayList(behaviours), if (debugEnabled) debugMiddleware else null) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/context/debug/DebugMiddleware.kt b/server/core/src/main/java/dev/slimevr/context/debug/DebugMiddleware.kt new file mode 100644 index 0000000000..a2a0f0a675 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/context/debug/DebugMiddleware.kt @@ -0,0 +1,6 @@ +package dev.slimevr.context.debug + +interface DebugMiddleware { + fun onDispatch(caller: String?, before: S, action: A, after: S) + fun onDispatchAll(caller: String?, before: S, actions: List, after: S) +} diff --git a/server/core/src/main/java/dev/slimevr/context/debug/LoggingMiddleware.kt b/server/core/src/main/java/dev/slimevr/context/debug/LoggingMiddleware.kt new file mode 100644 index 0000000000..4dbbe1a9d7 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/context/debug/LoggingMiddleware.kt @@ -0,0 +1,124 @@ +package dev.slimevr.context.debug + +import kotlin.reflect.KClass + +private object Ansi { + const val RESET = "\u001B[0m" + const val DIM = "\u001B[2m" + const val CYAN = "\u001B[36m" + const val YELLOW = "\u001B[33m" + const val GREEN = "\u001B[32m" + const val RED = "\u001B[31m" + const val MAGENTA = "\u001B[35m" + const val ARROW = "\u001B[2m→\u001B[0m" +} + +enum class DiffStyle { INLINE, MULTILINE } + +class LoggingMiddleware( + moduleName: String, + private val logNoOps: Boolean = false, + private val diffStyle: DiffStyle = DiffStyle.MULTILINE, + private val allow: Set>? = null, + private val block: Set> = emptySet(), +) : DebugMiddleware { + private val tag = "${Ansi.CYAN}[$moduleName]${Ansi.RESET}" + + private fun isAllowed(action: A): Boolean { + val klass = action!!::class + if (klass in block) return false + if (allow != null && klass !in allow) return false + return true + } + + override fun onDispatch(caller: String?, before: S, action: A, after: S) { + if (!isAllowed(action)) return + if (!logNoOps && before == after) return + val actionName = "${Ansi.YELLOW}${action!!::class.simpleName ?: "UnknownAction"}${Ansi.RESET}" + val callerSuffix = if (caller != null) " ${Ansi.DIM}(from $caller)${Ansi.RESET}" else "" + println(formatLine("$tag $actionName$callerSuffix", before, after)) + } + + override fun onDispatchAll(caller: String?, before: S, actions: List, after: S) { + val visible = actions.filter { action -> isAllowed(action) } + if (visible.isEmpty()) return + if (!logNoOps && before == after) return + val names = visible.joinToString(", ") { action -> action!!::class.simpleName ?: "?" } + val actionName = "${Ansi.MAGENTA}batch${Ansi.RESET} ${Ansi.YELLOW}[$names]${Ansi.RESET}" + val callerSuffix = if (caller != null) " ${Ansi.DIM}(from $caller)${Ansi.RESET}" else "" + println(formatLine("$tag $actionName$callerSuffix", before, after)) + } + + private fun formatLine(header: String, before: S, after: S): String { + if (before == after) return "$header ${Ansi.DIM}(no-op)${Ansi.RESET}" + val changes = fieldChanges(before.toString(), after.toString()) + if (changes.isEmpty()) return "$header ${Ansi.DIM}(no-op)${Ansi.RESET}" + if (diffStyle == DiffStyle.INLINE) { + val inline = changes.joinToString(", ") { c -> + "${Ansi.DIM}${c.name}:${Ansi.RESET} ${Ansi.RED}${c.before}${Ansi.RESET} ${Ansi.ARROW} ${Ansi.GREEN}${c.after}${Ansi.RESET}" + } + return "$header $inline" + } + if (changes.size == 1 && !isNestedObject(changes[0].before)) { + val c = changes[0] + return "$header ${Ansi.DIM}${c.name}:${Ansi.RESET} ${Ansi.RED}${c.before}${Ansi.RESET} ${Ansi.ARROW} ${Ansi.GREEN}${c.after}${Ansi.RESET}" + } + val nameWidth = changes.maxOf { c -> c.name.length } + val rows = changes.joinToString("\n") { c -> formatChange(c, nameWidth) } + return "$header\n$rows" + } +} + +private fun isNestedObject(value: String): Boolean = value.contains('(') && value.endsWith(')') + +private fun formatChange(c: FieldChange, nameWidth: Int): String { + if (isNestedObject(c.before) && isNestedObject(c.after)) { + val subChanges = fieldChanges(c.before, c.after) + if (subChanges.isNotEmpty()) { + val subNameWidth = subChanges.maxOf { s -> s.name.length } + val subOldWidth = subChanges.maxOf { s -> s.before.length } + val rows = subChanges.joinToString("\n") { s -> + val name = s.name.padEnd(subNameWidth) + val old = s.before.padEnd(subOldWidth) + " ${Ansi.DIM}$name${Ansi.RESET} ${Ansi.RED}$old${Ansi.RESET} ${Ansi.ARROW} ${Ansi.GREEN}${s.after}${Ansi.RESET}" + } + val typeName = c.before.substringBefore('(') + return " ${Ansi.DIM}${c.name.padEnd(nameWidth)}${Ansi.RESET} ${Ansi.DIM}$typeName${Ansi.RESET}\n$rows" + } + } + return " ${Ansi.DIM}${c.name.padEnd(nameWidth)}${Ansi.RESET} ${Ansi.RED}${c.before}${Ansi.RESET} ${Ansi.ARROW} ${Ansi.GREEN}${c.after}${Ansi.RESET}" +} + +private data class FieldChange(val name: String, val before: String, val after: String) + +private fun parseFields(s: String): Map { + val inner = s.substringAfter('(').dropLast(1) + val fields = mutableListOf() + var depth = 0 + var start = 0 + for (i in inner.indices) { + when (inner[i]) { + '(' -> depth++ + + ')' -> depth-- + + ',' -> if (depth == 0) { + fields.add(inner.substring(start, i).trim()) + start = i + 1 + } + } + } + if (start < inner.length) fields.add(inner.substring(start).trim()) + return fields.associate { field -> + val eq = field.indexOf('=') + field.substring(0, eq) to field.substring(eq + 1) + } +} + +private fun fieldChanges(before: String, after: String): List { + val beforeMap = parseFields(before) + val afterMap = parseFields(after) + return beforeMap.keys.intersect(afterMap.keys) + .filter { key -> beforeMap[key] != afterMap[key] } + .map { key -> FieldChange(key, beforeMap[key]!!, afterMap[key]!!) } +} diff --git a/server/core/src/main/java/dev/slimevr/device/behaviours.kt b/server/core/src/main/java/dev/slimevr/device/behaviours.kt new file mode 100644 index 0000000000..fba8c2f071 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/device/behaviours.kt @@ -0,0 +1,17 @@ +package dev.slimevr.device + +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +object DeviceStatsBehaviour : DeviceBehaviour { + override fun reduce(state: DeviceState, action: DeviceActions) = when (action) { + is DeviceActions.Update -> action.transform(state) + is DeviceActions.PacketStats -> state.copy(packetsReceived = action.packetsReceived, packetsLost = action.packetsLost) + } + + override fun observe(receiver: DeviceContext) { + receiver.state.onEach { +// AppLogger.device.info("Device state changed", it) + }.launchIn(receiver.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/device/module.kt b/server/core/src/main/java/dev/slimevr/device/module.kt new file mode 100644 index 0000000000..29e8c3bcfa --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/device/module.kt @@ -0,0 +1,92 @@ +package dev.slimevr.device + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.context.debug.DiffStyle +import dev.slimevr.context.debug.LoggingMiddleware +import kotlinx.coroutines.CoroutineScope +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.datatypes.hardware_info.BoardType +import solarxr_protocol.datatypes.hardware_info.McuType + +enum class DeviceOrigin { + DRIVER, + FEEDER, + UDP, + HID, +} + +data class DeviceState( + val id: Int, + val name: String, + val address: String, + val macAddress: String?, + val batteryLevel: Float, + val batteryVoltage: Float, + val ping: Long?, + val signalStrength: Int?, + val firmware: String?, + val boardType: BoardType, + val mcuType: McuType, + val protocolVersion: Int, + val status: TrackerStatus, + val origin: DeviceOrigin, + val packetsReceived: Long, + val packetsLost: Long, +) + +sealed interface DeviceActions { + data class Update(val transform: DeviceState.() -> DeviceState) : DeviceActions + data class PacketStats(val packetsReceived: Long, val packetsLost: Long) : DeviceActions +} + +typealias DeviceContext = Context +typealias DeviceBehaviour = Behaviour + +class Device( + val context: DeviceContext, +) { + companion object { + fun create( + scope: CoroutineScope, + id: Int, + address: String, + macAddress: String? = null, + origin: DeviceOrigin, + protocolVersion: Int, + ): Device { + val deviceState = DeviceState( + id = id, + name = "Device $id", + batteryLevel = 0f, + batteryVoltage = 0f, + origin = origin, + address = address, + macAddress = macAddress, + protocolVersion = protocolVersion, + ping = null, + signalStrength = null, + status = TrackerStatus.DISCONNECTED, + mcuType = McuType.Other, + boardType = BoardType.UNKNOWN, + firmware = null, + packetsReceived = 0L, + packetsLost = 0L, + ) + + val behaviours = listOf(DeviceStatsBehaviour) + val context = Context.create( + initialState = deviceState, + scope = scope, + behaviours = behaviours, + debugMiddleware = LoggingMiddleware( + "Device[$address]", + block = setOf(DeviceActions.PacketStats::class), + diffStyle = DiffStyle.MULTILINE, + ), + ) + behaviours.forEach { it.observe(context) } + return Device(context = context) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/driver/behaviours.kt b/server/core/src/main/java/dev/slimevr/driver/behaviours.kt new file mode 100644 index 0000000000..e0c2dab276 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/driver/behaviours.kt @@ -0,0 +1,56 @@ +package dev.slimevr.driver + +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.tracker.TrackerActions +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +object DriverBaseBehaviour : DriverBridgeBehaviour { + override fun reduce(state: DriverBridgeState, action: DriverBridgeActions): DriverBridgeState = when (action) { + is DriverBridgeActions.UpdateProtocolVersion -> state.copy(protocolVersion = action.version) + } + + override fun observe(receiver: DriverBridge) { + receiver.inbound.on { event -> + receiver.context.dispatch(DriverBridgeActions.UpdateProtocolVersion(event.protocolVersion)) + } + + receiver.inbound.on { event -> + receiver.appContext.server.getTracker(event.trackerId)?.context?.dispatch( + TrackerActions.SetRotation(rotation = event.rotation), + ) + } + + // Should be safe: StateFlow never delivers two emissions concurrently to the same collector. + val subscribedTrackers = mutableSetOf() + + receiver.appContext.server.context.state + .onEach { state -> + state.trackers.values.forEach { tracker -> + val ts = tracker.context.state.value + if (ts.origin == DeviceOrigin.DRIVER) return@forEach + if (subscribedTrackers.add(ts.id)) { + receiver.outbound.emit( + DriverBridgeOutbound.TrackerAdded( + trackerId = ts.id, + serial = ts.hardwareId, + name = ts.customName ?: ts.name, + ), + ) + tracker.context.state + .onEach { trackerState -> + receiver.outbound.emit( + DriverBridgeOutbound.TrackerPosition( + trackerId = trackerState.id, + rotation = trackerState.rawRotation, + position = trackerState.position, + ), + ) + } + .launchIn(receiver.context.scope) + } + } + } + .launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/driver/module.kt b/server/core/src/main/java/dev/slimevr/driver/module.kt new file mode 100644 index 0000000000..67e1c687cd --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/driver/module.kt @@ -0,0 +1,63 @@ +package dev.slimevr.driver + +import dev.slimevr.AppContextProvider +import dev.slimevr.EventDispatcher +import dev.slimevr.VRServerActions +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.CoroutineScope + +data class DriverBridgeState( + val protocolVersion: Int, +) + +sealed interface DriverBridgeActions { + data class UpdateProtocolVersion(val version: Int) : DriverBridgeActions +} + +sealed interface DriverBridgeInbound { + data class Version(val protocolVersion: Int) : DriverBridgeInbound + data class TrackerPosition(val trackerId: Int, val rotation: Quaternion, val position: Vector3?) : DriverBridgeInbound +} + +sealed interface DriverBridgeOutbound { + data class TrackerAdded(val trackerId: Int, val serial: String, val name: String) : DriverBridgeOutbound + data class TrackerPosition(val trackerId: Int, val rotation: Quaternion, val position: Vector3?) : DriverBridgeOutbound +} + +typealias DriverBridgeContext = Context +typealias DriverBridgeBehaviour = Behaviour + +class DriverBridge( + val id: Int, + val context: DriverBridgeContext, + val appContext: AppContextProvider, + val inbound: EventDispatcher = EventDispatcher(), + val outbound: EventDispatcher = EventDispatcher(), +) { + fun disconnect() { + appContext.server.context.dispatch(VRServerActions.DriverDisconnected(id)) + } + + fun startObserving() = context.observeAll(this) + + companion object { + fun create(id: Int, appContext: AppContextProvider, scope: CoroutineScope): DriverBridge { + val behaviours = listOf(DriverBaseBehaviour) + + val context = Context.create( + initialState = DriverBridgeState(protocolVersion = 0), + scope = scope, + behaviours = behaviours, + ) + + val bridge = DriverBridge(id = id, context = context, appContext = appContext) + bridge.startObserving() + appContext.server.context.dispatch(VRServerActions.DriverConnected(bridge)) + + return bridge + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/event-dispatcher.kt b/server/core/src/main/java/dev/slimevr/event-dispatcher.kt new file mode 100644 index 0000000000..97b7e587f1 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/event-dispatcher.kt @@ -0,0 +1,33 @@ +package dev.slimevr + +import kotlin.reflect.KClass + +class EventDispatcher(private val keyOf: (T) -> KClass<*> = { it::class }) { + @Volatile var listeners: Map, List Unit>> = emptyMap() + + @Volatile private var globalListeners: List Unit> = emptyList() + + fun register(key: KClass<*>, callback: suspend (T) -> Unit) { + synchronized(this) { + val updated = listeners.toMutableMap() + updated[key] = (updated[key] ?: emptyList()) + callback + listeners = updated + } + } + + @Suppress("UNCHECKED_CAST") + inline fun on(crossinline callback: suspend (P) -> Unit) { + register(P::class) { callback(it as P) } + } + + fun onAny(callback: suspend (T) -> Unit) { + synchronized(this) { + globalListeners = globalListeners + callback + } + } + + suspend fun emit(event: T) { + globalListeners.forEach { it(event) } + listeners[keyOf(event)]?.forEach { it(event) } + } +} diff --git a/server/core/src/main/java/dev/slimevr/feeder/behaviours.kt b/server/core/src/main/java/dev/slimevr/feeder/behaviours.kt new file mode 100644 index 0000000000..76b679caa0 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/feeder/behaviours.kt @@ -0,0 +1,72 @@ +package dev.slimevr.feeder + +import dev.slimevr.VRServerActions +import dev.slimevr.device.Device +import dev.slimevr.device.DeviceActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.tracker.Tracker +import dev.slimevr.tracker.TrackerActions +import solarxr_protocol.datatypes.hardware_info.ImuType + +object FeederBaseBehaviour : FeederBridgeBehaviour { + override fun reduce(state: FeederBridgeState, action: FeederBridgeActions): FeederBridgeState = when (action) { + is FeederBridgeActions.UpdateProtocolVersion -> state.copy(protocolVersion = action.version, firmware = action.firmware) + } + + override fun observe(receiver: FeederBridge) { + receiver.inbound.on { event -> + receiver.context.dispatch(FeederBridgeActions.UpdateProtocolVersion(event.protocolVersion, event.firmware)) + } + + receiver.inbound.on { event -> + handleTrackerAdded(receiver, event.serial) + } + + receiver.inbound.on { event -> + receiver.appContext.server.getTracker(event.trackerId)?.context?.dispatch( + TrackerActions.SetRotation(rotation = event.rotation), + ) + } + } + + private fun handleTrackerAdded(receiver: FeederBridge, serial: String) { + val server = receiver.appContext.server + val settings = receiver.appContext.config.settings + val scope = server.context.scope + val existingTracker = server.context.state.value.trackers.values + .find { tracker -> tracker.context.state.value.hardwareId == serial } + + val device = if (existingTracker != null) { + server.getDevice(existingTracker.context.state.value.deviceId) + ?: error("could not find existing device for serial $serial") + } else { + val deviceId = server.nextHandle() + val newDevice = Device.create( + scope = scope, + id = deviceId, + address = serial, + macAddress = serial, + origin = DeviceOrigin.FEEDER, + protocolVersion = 0, + ) + server.context.dispatch(VRServerActions.NewDevice(deviceId, newDevice)) + + val trackerId = server.nextHandle() + val tracker = Tracker.create( + scope = scope, + id = trackerId, + deviceId = deviceId, + sensorType = ImuType.MPU9250, + hardwareId = serial, + origin = DeviceOrigin.FEEDER, + server = server, + settings = settings, + ) + server.context.dispatch(VRServerActions.NewTracker(trackerId, tracker)) + + newDevice + } + + device.context.dispatch(DeviceActions.Update { copy(protocolVersion = 0) }) + } +} diff --git a/server/core/src/main/java/dev/slimevr/feeder/module.kt b/server/core/src/main/java/dev/slimevr/feeder/module.kt new file mode 100644 index 0000000000..50f380d575 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/feeder/module.kt @@ -0,0 +1,59 @@ +package dev.slimevr.feeder + +import dev.slimevr.AppContextProvider +import dev.slimevr.EventDispatcher +import dev.slimevr.VRServerActions +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.CoroutineScope + +data class FeederBridgeState( + val protocolVersion: Int, + val firmware: String?, +) + +sealed interface FeederBridgeActions { + data class UpdateProtocolVersion(val version: Int, val firmware: String?) : FeederBridgeActions +} + +sealed interface FeederBridgeInbound { + data class Version(val protocolVersion: Int, val firmware: String?) : FeederBridgeInbound + data class TrackerAdded(val serial: String) : FeederBridgeInbound + data class TrackerPosition(val trackerId: Int, val rotation: Quaternion, val position: Vector3?) : FeederBridgeInbound +} + +typealias FeederBridgeContext = Context +typealias FeederBridgeBehaviour = Behaviour + +class FeederBridge( + val id: Int, + val context: FeederBridgeContext, + val appContext: AppContextProvider, + val inbound: EventDispatcher = EventDispatcher(), +) { + fun disconnect() { + appContext.server.context.dispatch(VRServerActions.FeederDisconnected(id)) + } + + fun startObserving() = context.observeAll(this) + + companion object { + fun create(id: Int, appContext: AppContextProvider, scope: CoroutineScope): FeederBridge { + val behaviours = listOf(FeederBaseBehaviour) + + val context = Context.create( + initialState = FeederBridgeState(protocolVersion = 0, firmware = null), + scope = scope, + behaviours = behaviours, + ) + + val bridge = FeederBridge(id = id, context = context, appContext = appContext) + bridge.startObserving() + appContext.server.context.dispatch(VRServerActions.FeederConnected(bridge)) + + return bridge + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/filtering/CircularArrayList.java b/server/core/src/main/java/dev/slimevr/filtering/CircularArrayList.java deleted file mode 100644 index cccd08342f..0000000000 --- a/server/core/src/main/java/dev/slimevr/filtering/CircularArrayList.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.slimevr.filtering; - -import java.util.*; - - -/** - * If you use this code, please consider notifying isak at du-preez dot com with - * a brief description of your application. - *

- * This is free and unencumbered software released into the public domain. - * Anyone is free to copy, modify, publish, use, compile, sell, or distribute - * this software, either in source code form or as a compiled binary, for any - * purpose, commercial or non-commercial, and by any means. - */ - -public class CircularArrayList extends AbstractList implements RandomAccess { - - private final int n; // buffer length - private final List buf; // a List implementing RandomAccess - private int head = 0; - private int tail = 0; - - public CircularArrayList(int capacity) { - n = capacity + 1; - buf = new ArrayList<>(Collections.nCopies(n, null)); - } - - public int capacity() { - return n - 1; - } - - private int wrapIndex(int i) { - int m = i % n; - if (m < 0) { // java modulus can be negative - m += n; - } - return m; - } - - // This method is O(n) but will never be called if the - // CircularArrayList is used in its typical/intended role. - private void shiftBlock(int startIndex, int endIndex) { - assert (endIndex > startIndex); - for (int i = endIndex - 1; i >= startIndex; i--) { - set(i + 1, get(i)); - } - } - - @Override - public int size() { - return tail - head + (tail < head ? n : 0); - } - - @Override - public E get(int i) { - if (i < 0 || i >= size()) { - throw new IndexOutOfBoundsException(); - } - return buf.get(wrapIndex(head + i)); - } - - public E getLatest() { - return buf.get(wrapIndex(head + size() - 1)); - } - - @Override - public E set(int i, E e) { - if (i < 0 || i >= size()) { - throw new IndexOutOfBoundsException(); - } - return buf.set(wrapIndex(head + i), e); - } - - @Override - public void add(int i, E e) { - int s = size(); - if (s == n - 1) { - throw new IllegalStateException( - "CircularArrayList is filled to capacity. " - + "(You may want to remove from front" - + " before adding more to back.)" - ); - } - if (i < 0 || i > s) { - throw new IndexOutOfBoundsException(); - } - tail = wrapIndex(tail + 1); - if (i < s) { - shiftBlock(i, s); - } - set(i, e); - } - - @Override - public E remove(int i) { - int s = size(); - if (i < 0 || i >= s) { - throw new IndexOutOfBoundsException(); - } - E e = get(i); - if (i > 0) { - shiftBlock(0, i); - } - head = wrapIndex(head + 1); - return e; - } - - public E removeLast() { - int s = size(); - if (0 == s) { - throw new IndexOutOfBoundsException(); - } - E e = get(0); - head = wrapIndex(head + 1); - return e; - } -} diff --git a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt b/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt deleted file mode 100644 index b968068890..0000000000 --- a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt +++ /dev/null @@ -1,127 +0,0 @@ -package dev.slimevr.filtering - -import com.jme3.system.NanoTimer -import dev.slimevr.VRServer -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY - -// influences the range of smoothFactor. -private const val SMOOTH_MULTIPLIER = 42f -private const val SMOOTH_MIN = 11f - -// influences the range of predictFactor -private const val PREDICT_MULTIPLIER = 15f -private const val PREDICT_MIN = 10f - -// how many past rotations are used for prediction. -private const val PREDICT_BUFFER = 6 - -class QuaternionMovingAverage( - val type: TrackerFilters, - var amount: Float = 0f, - initialRotation: Quaternion = IDENTITY, -) { - var filteredQuaternion = IDENTITY - var filteringImpact = 0f - private var smoothFactor = 0f - private var predictFactor = 0f - private var rotBuffer: CircularArrayList? = null - private var latestQuaternion = IDENTITY - private var smoothingQuaternion = IDENTITY - private val fpsTimer = if (VRServer.instanceInitialized) VRServer.instance.fpsTimer else NanoTimer() - private var timeSinceUpdate = 0f - - init { - // amount should range from 0 to 1. - // GUI should clamp it from 0.01 (1%) or 0.1 (10%) - // to 1 (100%). - amount = amount.coerceAtLeast(0f) - if (type == TrackerFilters.SMOOTHING) { - // lower smoothFactor = more smoothing - smoothFactor = SMOOTH_MULTIPLIER * (1 - amount.coerceAtMost(1f)) + SMOOTH_MIN - // Totally a hack - if (amount > 1) { - smoothFactor /= amount - } - } - if (type == TrackerFilters.PREDICTION) { - // higher predictFactor = more prediction - predictFactor = PREDICT_MULTIPLIER * amount + PREDICT_MIN - rotBuffer = CircularArrayList(PREDICT_BUFFER) - } - - // We have no reference at the start, so just use the initial rotation - resetQuats(initialRotation, initialRotation) - } - - // Runs at up to 1000hz. We use a timer to make it framerate-independent - // since it runs a bit below 1000hz in practice. - @Synchronized - fun update() { - if (type == TrackerFilters.PREDICTION) { - val rotBuf = rotBuffer - if (rotBuf != null && rotBuf.isNotEmpty()) { - // Applies the past rotations to the current rotation - val predictRot = rotBuf.fold(latestQuaternion) { buf, rot -> buf * rot } - - // Calculate how much to slerp - // Limit slerp by a reasonable amount so low TPS doesn't break tracking - val amt = (predictFactor * fpsTimer.timePerFrame).coerceAtMost(1f) - - // Slerps the target rotation to that predicted rotation by amt - filteredQuaternion = filteredQuaternion.interpQ(predictRot, amt) - } - } else if (type == TrackerFilters.SMOOTHING) { - // Make it framerate-independent - timeSinceUpdate += fpsTimer.timePerFrame - - // Calculate the slerp factor based off the smoothFactor and smoothingCounter - // limit to 1 to not overshoot - val amt = (smoothFactor * timeSinceUpdate).coerceAtMost(1f) - - // Smooth towards the target rotation by the slerp factor - filteredQuaternion = smoothingQuaternion.interpQ(latestQuaternion, amt) - } - - filteringImpact = latestQuaternion.angleToR(filteredQuaternion) - } - - @Synchronized - fun addQuaternion(q: Quaternion) { - val oldQ = latestQuaternion - val newQ = q.twinNearest(oldQ) - latestQuaternion = newQ - - if (type == TrackerFilters.PREDICTION) { - if (rotBuffer!!.size == rotBuffer!!.capacity()) { - rotBuffer?.removeLast() - } - - // Gets and stores the rotation between the last 2 quaternions - rotBuffer?.add(oldQ.inv().times(newQ)) - } else if (type == TrackerFilters.SMOOTHING) { - timeSinceUpdate = 0f - smoothingQuaternion = filteredQuaternion - } else { - // No filtering; just keep track of rotations (for going over 180 degrees) - filteredQuaternion = newQ - } - } - - /** - * Aligns the quaternion space of [q] to the [reference] and sets the latest - * [filteredQuaternion] immediately - */ - @Synchronized - fun resetQuats(q: Quaternion, reference: Quaternion) { - // Assume a rotation within 180 degrees of the reference - // TODO: Currently the reference is the headset, this restricts all trackers to - // have at most a 180 degree rotation from the HMD during a reset, we can - // probably do better using a hierarchy - val rot = q.twinNearest(reference) - rotBuffer?.clear() - latestQuaternion = rot - filteredQuaternion = rot - addQuaternion(rot) - } -} diff --git a/server/core/src/main/java/dev/slimevr/filtering/TrackerFilters.kt b/server/core/src/main/java/dev/slimevr/filtering/TrackerFilters.kt deleted file mode 100644 index f98553cd01..0000000000 --- a/server/core/src/main/java/dev/slimevr/filtering/TrackerFilters.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.slimevr.filtering - -import java.util.* - -enum class TrackerFilters(val id: Int, val configKey: String) { - NONE(0, "none"), - SMOOTHING(1, "smoothing"), - PREDICTION(2, "prediction"), - ; - - companion object { - private val byConfigkey: MutableMap = HashMap() - - init { - for (configVal in values()) { - byConfigkey[configVal.configKey.lowercase(Locale.getDefault())] = - configVal - } - } - - val values = values() - - @JvmStatic - fun fromId(id: Int): TrackerFilters? { - for (filter in values) { - if (filter.id == id) return filter - } - return null - } - - @JvmStatic - fun getByConfigkey(configKey: String?): TrackerFilters? = if (configKey == null) null else byConfigkey[configKey.lowercase(Locale.getDefault())] - } -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt deleted file mode 100644 index ecc3e160c0..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt +++ /dev/null @@ -1,539 +0,0 @@ -package dev.slimevr.firmware - -import com.mayakapps.kache.InMemoryKache -import com.mayakapps.kache.KacheStrategy -import dev.llelievr.espflashkotlin.Flasher -import dev.llelievr.espflashkotlin.FlashingProgressListener -import dev.slimevr.VRServer -import dev.slimevr.serial.ProvisioningListener -import dev.slimevr.serial.ProvisioningStatus -import dev.slimevr.serial.SerialPort -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.TrackerStatusListener -import dev.slimevr.tracking.trackers.udp.UDPDevice -import io.eiren.util.logging.LogManager -import kotlinx.coroutines.* -import solarxr_protocol.rpc.FirmwarePartT -import solarxr_protocol.rpc.FirmwareUpdateRequestT -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.InputStream -import java.net.URL -import java.security.MessageDigest -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList -import java.util.stream.Collectors -import kotlin.concurrent.scheduleAtFixedRate - -data class DownloadedFirmwarePart( - val firmware: ByteArray, - val offset: Long?, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DownloadedFirmwarePart - - if (!firmware.contentEquals(other.firmware)) return false - if (offset != other.offset) return false - - return true - } - - override fun hashCode(): Int { - var result = firmware.contentHashCode() - result = 31 * result + (offset?.hashCode() ?: 0) - return result - } -} - -class FirmwareUpdateHandler(private val server: VRServer) : - TrackerStatusListener, - ProvisioningListener, - SerialRebootListener { - - private val updateTickTimer = Timer("StatusUpdateTimer") - private val runningJobs: MutableList = CopyOnWriteArrayList() - private val watchRestartQueue: MutableList, () -> Unit>> = - CopyOnWriteArrayList() - private val updatingDevicesStatus: MutableMap, UpdateStatusEvent<*>> = - ConcurrentHashMap() - private val listeners: MutableList = CopyOnWriteArrayList() - private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob()) - private var clearJob: Deferred? = null - - private var serialRebootHandler: SerialRebootHandler = SerialRebootHandler(watchRestartQueue, server, this) - - fun addListener(channel: FirmwareUpdateListener) { - listeners.add(channel) - } - - fun removeListener(channel: FirmwareUpdateListener) { - listeners.removeIf { channel == it } - } - - init { - server.addTrackerStatusListener(this) - server.provisioningHandler.addListener(this) - server.serialHandler.addListener(serialRebootHandler) - - this.updateTickTimer.scheduleAtFixedRate(0, 1000) { - checkUpdateTimeout() - } - } - - private suspend fun startOtaUpdate( - part: DownloadedFirmwarePart, - deviceId: UpdateDeviceId, - ): Unit = suspendCancellableCoroutine { c -> - val udpDevice: UDPDevice? = - (server.deviceManager.devices.find { device -> device is UDPDevice && device.id == deviceId.id }) as UDPDevice? - - if (udpDevice == null) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, - ), - ) - return@suspendCancellableCoroutine - } - val task = OTAUpdateTask( - part.firmware, - deviceId, - udpDevice.ipAddress, - ::onStatusChange, - ) - c.invokeOnCancellation { - task.cancel() - } - task.run() - } - - private fun startSerialUpdate( - firmwares: Array, - deviceId: UpdateDeviceId, - needManualReboot: Boolean, - ssid: String, - password: String, - ) { - // Can't use .toList() on Android - val serialPort = this.server.serialHandler.knownPorts.collect(Collectors.toList()) - .find { port -> deviceId.id == port.portLocation } - - if (serialPort == null) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, - ), - ) - return - } - - val flashingHandler = this.server.serialFlashingHandler - - if (flashingHandler == null) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_UNSUPPORTED_METHOD, - ), - ) - return - } - - try { - val flasher = Flasher(flashingHandler) - - for (part in firmwares) { - if (part.offset == null) { - error("Offset is empty") - } - flasher.addBin(part.firmware, part.offset.toInt()) - } - - flasher.addProgressListener(object : FlashingProgressListener { - override fun progress(progress: Float) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.UPLOADING, - (progress * 100).toInt(), - ), - ) - } - }) - - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.SYNCING_WITH_MCU, - ), - ) - flasher.flash(serialPort) - if (needManualReboot) { - if (watchRestartQueue.find { it.first == deviceId } != null) { - LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping") - } - - onStatusChange(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.NEED_MANUAL_REBOOT)) - server.serialHandler.openSerial(deviceId.id, false) - watchRestartQueue.add( - Pair(deviceId) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.REBOOTING, - ), - ) - server.provisioningHandler.start( - ssid, - password, - serialPort.portLocation, - ) - }, - ) - } else { - onStatusChange(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.REBOOTING)) - server.provisioningHandler.start(ssid, password, serialPort.portLocation) - } - } catch (e: Exception) { - LogManager.severe("[FirmwareUpdateHandler] Upload failed", e) - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_UPLOAD_FAILED, - ), - ) - } - } - - fun queueFirmwareUpdate( - request: FirmwareUpdateRequestT, - deviceId: UpdateDeviceId<*>, - ) = mainScope.launch { - val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method") - - clearJob?.await() - if (method == FirmwareUpdateMethod.OTA) { - if (watchRestartQueue.find { it.first == deviceId } != null) { - LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping") - } - - val udpDevice: UDPDevice? = - (server.deviceManager.devices.find { device -> device is UDPDevice && device.id == deviceId.id }) as UDPDevice? - if (udpDevice === null) { - error("invalid state - device does not exist") - } - - if (udpDevice.protocolVersion <= 20) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.NEED_MANUAL_REBOOT, - ), - ) - watchRestartQueue.add( - Pair(deviceId) { - mainScope.launch { - startFirmwareUpdateJob( - request, - deviceId, - ) - } - }, - ) - } else { - startFirmwareUpdateJob( - request, - deviceId, - ) - } - } else { - if (updatingDevicesStatus[deviceId] != null) { - LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping") - return@launch - } - - startFirmwareUpdateJob( - request, - deviceId, - ) - } - } - - fun cancelUpdates() { - val oldClearJob = clearJob - clearJob = mainScope.async { - oldClearJob?.await() - watchRestartQueue.clear() - runningJobs.forEach { it.cancelAndJoin() } - runningJobs.clear() - LogManager.info("[FirmwareUpdateHandler] Update jobs canceled") - } - } - - private fun getFirmwareParts(request: FirmwareUpdateRequestT): ArrayList { - val parts = ArrayList() - val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method") - when (method) { - FirmwareUpdateMethod.OTA -> { - val updateReq = request.method.asOTAFirmwareUpdate() - parts.add(updateReq.firmwarePart) - } - - FirmwareUpdateMethod.SERIAL -> { - val updateReq = request.method.asSerialFirmwareUpdate() - parts.addAll(updateReq.firmwarePart) - } - - FirmwareUpdateMethod.NONE -> error("Method should not be NONE") - } - return parts - } - - private suspend fun startFirmwareUpdateJob( - request: FirmwareUpdateRequestT, - deviceId: UpdateDeviceId<*>, - ) = coroutineScope { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.DOWNLOADING, - ), - ) - - try { - val toDownloadParts = getFirmwareParts(request) - val firmwareParts = try { - withTimeoutOrNull(30_000) { - toDownloadParts.map { - val firmware = downloadFirmware(it.url, it.digest) - DownloadedFirmwarePart( - firmware, - it.offset, - ) - }.toTypedArray() - } - } catch (e: Exception) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED, - ), - ) - LogManager.severe("[FirmwareUpdateHandler] Unable to download firmware", e) - return@coroutineScope - } - - val job = launch { - withTimeout(2 * 60 * 1000) { - if (firmwareParts.isNullOrEmpty()) { - onStatusChange( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED, - ), - ) - return@withTimeout - } - - val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method") - when (method) { - FirmwareUpdateMethod.NONE -> error("unsupported method") - - FirmwareUpdateMethod.OTA -> { - if (deviceId.id !is Int) { - error("invalid state, the device id is not an int") - } - if (firmwareParts.size > 1) { - error("invalid state, ota only use one firmware file") - } - startOtaUpdate( - firmwareParts.first(), - UpdateDeviceId( - FirmwareUpdateMethod.OTA, - deviceId.id, - ), - ) - } - - FirmwareUpdateMethod.SERIAL -> { - val req = request.method.asSerialFirmwareUpdate() - if (deviceId.id !is String) { - error("invalid state, the device id is not a string") - } - startSerialUpdate( - firmwareParts, - UpdateDeviceId( - FirmwareUpdateMethod.SERIAL, - deviceId.id, - ), - req.needManualReboot, - req.ssid, - req.password, - ) - } - } - } - } - runningJobs.add(job) - } catch (e: Exception) { - onStatusChange( - UpdateStatusEvent( - deviceId, - if (e is TimeoutCancellationException) FirmwareUpdateStatus.ERROR_TIMEOUT else FirmwareUpdateStatus.ERROR_UNKNOWN, - ), - ) - if (e !is TimeoutCancellationException) { - LogManager.severe("[FirmwareUpdateHandler] Update process timed out", e) - e.printStackTrace() - } - return@coroutineScope - } - } - - private fun onStatusChange(event: UpdateStatusEvent) { - this.updatingDevicesStatus[event.deviceId] = event - - if (event.status == FirmwareUpdateStatus.DONE || event.status.isError()) { - this.updatingDevicesStatus.remove(event.deviceId) - - // we remove the device from the restart queue - val queuedDevice = watchRestartQueue.find { it.first.id == event.deviceId } - if (queuedDevice != null) { - watchRestartQueue.remove(queuedDevice) - if (event.deviceId.type == FirmwareUpdateMethod.SERIAL && server.serialHandler.isConnected) { - server.serialHandler.closeSerial() - } - } - - // We make sure to stop the provisioning routine if the tracker is done - // flashing - if (event.deviceId.type == FirmwareUpdateMethod.SERIAL) { - this.server.provisioningHandler.stop() - } - } - listeners.forEach { l -> l.onUpdateStatusChange(event) } - } - - private fun checkUpdateTimeout() { - updatingDevicesStatus.forEach { (id, device) -> - // if more than 30s between two events, consider the update as stuck - // We do not timeout on the Downloading step as it has it own timeout - // We do not timeout on the Done step as it is the end of the update process - if (!device.status.isError() && - !intArrayOf(FirmwareUpdateStatus.DONE.id, FirmwareUpdateStatus.DOWNLOADING.id).contains(device.status.id) && - System.currentTimeMillis() - device.time > 30 * 1000 - ) { - onStatusChange( - UpdateStatusEvent( - id, - FirmwareUpdateStatus.ERROR_TIMEOUT, - ), - ) - } - } - } - - // this only works for OTA trackers as the device id - // only exists when the usb connection is created - override fun onTrackerStatusChanged( - tracker: Tracker, - oldStatus: TrackerStatus, - newStatus: TrackerStatus, - ) { - val device = tracker.device - if (device !is UDPDevice) return - - if (oldStatus == TrackerStatus.DISCONNECTED && newStatus == TrackerStatus.OK) { - val queuedDevice = watchRestartQueue.find { it.first.id == device.id } - - if (queuedDevice != null) { - queuedDevice.second() // we start the queued update task - watchRestartQueue.remove(queuedDevice) // then we remove it from the queue - return - } - - // We can only filter OTA method here as the device id is only provided when using Wi-Fi - val deviceStatusKey = - updatingDevicesStatus.keys.find { it.type == FirmwareUpdateMethod.OTA && it.id == device.id } - ?: return - val updateStatus = updatingDevicesStatus[deviceStatusKey] ?: return - // We check for the reconnection of the tracker, once the tracker reconnected we notify the user that the update is completed - if (updateStatus.status == FirmwareUpdateStatus.REBOOTING) { - onStatusChange( - UpdateStatusEvent( - updateStatus.deviceId, - FirmwareUpdateStatus.DONE, - ), - ) - } - } - } - - override fun onProvisioningStatusChange( - status: ProvisioningStatus, - port: SerialPort?, - ) { - fun update(s: FirmwareUpdateStatus) { - val deviceStatusKey = - updatingDevicesStatus.keys.find { it.type == FirmwareUpdateMethod.SERIAL && it.id == port?.portLocation } - ?: return - val updateStatus = updatingDevicesStatus[deviceStatusKey] ?: return - onStatusChange(UpdateStatusEvent(updateStatus.deviceId, s)) - } - - when (status) { - ProvisioningStatus.PROVISIONING -> update(FirmwareUpdateStatus.PROVISIONING) - ProvisioningStatus.DONE -> update(FirmwareUpdateStatus.DONE) - ProvisioningStatus.CONNECTION_ERROR, ProvisioningStatus.COULD_NOT_FIND_SERVER -> update(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED) - else -> {} - } - } - - override fun onSerialDeviceReconnect(deviceHandle: Pair, () -> Unit>) { - deviceHandle.second() - watchRestartQueue.remove(deviceHandle) - } -} - -fun downloadFirmware(url: String, expectedDigest: String): ByteArray { - val outputStream = ByteArrayOutputStream() - - val chunk = ByteArray(4096) - var bytesRead: Int - val stream: InputStream = URL(url).openStream() - while (stream.read(chunk).also { bytesRead = it } > 0) { - outputStream.write(chunk, 0, bytesRead) - } - - val downloadedData = outputStream.toByteArray() - - if (!verifyChecksum(downloadedData, expectedDigest)) { - error("Checksum verification failed for $url") - } - - return downloadedData -} - -fun verifyChecksum(data: ByteArray, expectedDigest: String): Boolean { - val parts = expectedDigest.split(":", limit = 2) - if (parts.size != 2) { - error("Invalid digest format. Expected 'algorithm:hash' got $expectedDigest") - } - - val algorithm = parts[0].uppercase().replace("-", "") - val expectedHash = parts[1].lowercase() - - val messageDigest = MessageDigest.getInstance(algorithm) - val actualHash = messageDigest.digest(data).joinToString("") { - "%02x".format(it) - } - - return actualHash == expectedHash -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateListener.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateListener.kt deleted file mode 100644 index 3f048828ac..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slimevr.firmware - -interface FirmwareUpdateListener { - fun onUpdateStatusChange(event: UpdateStatusEvent<*>) -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateMethod.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateMethod.kt deleted file mode 100644 index 7aaddf974e..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateMethod.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.slimevr.firmware - -enum class FirmwareUpdateMethod(val id: Byte) { - NONE(solarxr_protocol.rpc.FirmwareUpdateMethod.NONE), - OTA(solarxr_protocol.rpc.FirmwareUpdateMethod.OTAFirmwareUpdate), - SERIAL(solarxr_protocol.rpc.FirmwareUpdateMethod.SerialFirmwareUpdate), - ; - - companion object { - fun getById(id: Byte): FirmwareUpdateMethod? = byId[id] - } -} - -private val byId = FirmwareUpdateMethod.entries.associateBy { it.id } diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateStatus.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateStatus.kt deleted file mode 100644 index 4edb320b44..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateStatus.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slimevr.firmware - -enum class FirmwareUpdateStatus(val id: Int) { - DOWNLOADING(solarxr_protocol.rpc.FirmwareUpdateStatus.DOWNLOADING), - AUTHENTICATING(solarxr_protocol.rpc.FirmwareUpdateStatus.AUTHENTICATING), - UPLOADING(solarxr_protocol.rpc.FirmwareUpdateStatus.UPLOADING), - SYNCING_WITH_MCU(solarxr_protocol.rpc.FirmwareUpdateStatus.SYNCING_WITH_MCU), - REBOOTING(solarxr_protocol.rpc.FirmwareUpdateStatus.REBOOTING), - NEED_MANUAL_REBOOT(solarxr_protocol.rpc.FirmwareUpdateStatus.NEED_MANUAL_REBOOT), - PROVISIONING(solarxr_protocol.rpc.FirmwareUpdateStatus.PROVISIONING), - DONE(solarxr_protocol.rpc.FirmwareUpdateStatus.DONE), - ERROR_DEVICE_NOT_FOUND(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND), - ERROR_TIMEOUT(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_TIMEOUT), - ERROR_DOWNLOAD_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED), - ERROR_AUTHENTICATION_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED), - ERROR_UPLOAD_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UPLOAD_FAILED), - ERROR_PROVISIONING_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED), - ERROR_UNSUPPORTED_METHOD(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UNSUPPORTED_METHOD), - ERROR_UNKNOWN(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UNKNOWN), - ; - - fun isError(): Boolean = id in ERROR_DEVICE_NOT_FOUND.id..ERROR_UNKNOWN.id - - companion object { - fun getById(id: Int): FirmwareUpdateStatus? = byId[id] - } -} - -private val byId = FirmwareUpdateStatus.entries.associateBy { it.id } diff --git a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt deleted file mode 100644 index 7d934b8588..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt +++ /dev/null @@ -1,205 +0,0 @@ -package dev.slimevr.firmware - -import io.eiren.util.logging.LogManager -import java.io.DataInputStream -import java.io.DataOutputStream -import java.io.EOFException -import java.io.IOException -import java.net.DatagramPacket -import java.net.DatagramSocket -import java.net.InetAddress -import java.net.ServerSocket -import java.net.Socket -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.* -import java.util.function.Consumer -import kotlin.math.min - -class OTAUpdateTask( - private val firmware: ByteArray, - private val deviceId: UpdateDeviceId, - private val deviceIp: InetAddress, - private val statusCallback: Consumer>, -) { - private val receiveBuffer: ByteArray = ByteArray(38) - var socketServer: ServerSocket? = null - var uploadSocket: Socket? = null - var authSocket: DatagramSocket? = null - var canceled: Boolean = false - - @Throws(NoSuchAlgorithmException::class) - private fun bytesToMd5(bytes: ByteArray): String { - val md5 = MessageDigest.getInstance("MD5") - md5.update(bytes) - val digest = md5.digest() - val md5str = StringBuilder() - for (b in digest) { - md5str.append(String.format("%02x", b)) - } - return md5str.toString() - } - - private fun authenticate(localPort: Int): Boolean { - try { - DatagramSocket().use { socket -> - authSocket = socket - statusCallback.accept(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.AUTHENTICATING)) - LogManager.info("[OTAUpdate] Sending OTA invitation to: $deviceIp") - - val fileMd5 = bytesToMd5(firmware) - val message = "$FLASH $localPort ${firmware.size} $fileMd5\n" - - socket.send(DatagramPacket(message.toByteArray(), message.length, deviceIp, PORT)) - socket.soTimeout = 10000 - - val authPacket = DatagramPacket(receiveBuffer, receiveBuffer.size) - socket.receive(authPacket) - - val data = String(authPacket.data, 0, authPacket.length) - - // if we received OK directly from the MCU, we do not need to authenticate - if (data == "OK") return true - - val args = data.split(" ") - - // The expected auth payload should look like "AUTH AUTH_TOKEN" - // if we have less than those two args it means that we are in an invalid state - if (args.size != 2 || args[0] != "AUTH") return false - - LogManager.info("[OTAUpdate] Authenticating...") - - val authToken = args[1] - val signature = bytesToMd5(UUID.randomUUID().toString().toByteArray()) - val hashedPassword = bytesToMd5(PASSWORD.toByteArray()) - val resultText = "$hashedPassword:$authToken:$signature" - val payload = bytesToMd5(resultText.toByteArray()) - - val authMessage = "$AUTH $signature $payload\n" - - socket.soTimeout = 10000 - socket.send( - DatagramPacket( - authMessage.toByteArray(), - authMessage.length, - deviceIp, - PORT, - ), - ) - - val authResponsePacket = DatagramPacket(receiveBuffer, receiveBuffer.size) - socket.receive(authResponsePacket) - - val authResponse = String(authResponsePacket.data, 0, authResponsePacket.length) - - return authResponse == "OK" - } - } catch (e: Exception) { - LogManager.severe("OTA Authentication exception", e) - return false - } - } - - private fun upload(serverSocket: ServerSocket): Boolean { - var connection: Socket? = null - try { - LogManager.info("[OTAUpdate] Starting on: ${serverSocket.localPort}") - LogManager.info("[OTAUpdate] Waiting for device...") - - connection = serverSocket.accept() - this.uploadSocket = connection - connection.setSoTimeout(1000) - val dos = DataOutputStream(connection.getOutputStream()) - val dis = DataInputStream(connection.getInputStream()) - - LogManager.info("[OTAUpdate] Upload size: ${firmware.size} bytes") - var offset = 0 - val chunkSize = 2048 - while (offset != firmware.size && !canceled) { - statusCallback.accept( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.UPLOADING, - ((offset.toDouble() / firmware.size) * 100).toInt(), - ), - ) - - val chunkLen = min(chunkSize, (firmware.size - offset)) - dos.write(firmware, offset, chunkLen) - dos.flush() - offset += chunkLen - - // Those skipped bytes are the size written to the MCU. We do not really need that information, - // so we simply skip it. - // The reason those bytes are skipped here is to not have to skip all of them when checking - // for the OK response. Saving time - val bytesSkipped = dis.skipBytes(4) - // Replicate behaviour of .skipNBytes() - if (bytesSkipped != 4) { - throw IOException("Unexpected number of bytes skipped: $bytesSkipped") - } - } - if (canceled) return false - - LogManager.info("[OTAUpdate] Waiting for result...") - // We set the timeout of the connection bigger as it can take some time for the MCU - // to confirm that everything is ok - connection.setSoTimeout(10000) - val responseBytes = dis.readBytes() - val response = String(responseBytes) - - return response.contains("OK") - } catch (e: Exception) { - LogManager.severe("Unable to upload the firmware using ota", e) - return false - } finally { - connection?.close() - } - } - - fun run() { - ServerSocket(0).use { serverSocket -> - socketServer = serverSocket - if (!authenticate(serverSocket.localPort)) { - statusCallback.accept( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED, - ), - ) - return - } - - if (!upload(serverSocket)) { - statusCallback.accept( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.ERROR_UPLOAD_FAILED, - ), - ) - return - } - - statusCallback.accept( - UpdateStatusEvent( - deviceId, - FirmwareUpdateStatus.REBOOTING, - ), - ) - } - } - - fun cancel() { - canceled = true - socketServer?.close() - authSocket?.close() - uploadSocket?.close() - } - - companion object { - private const val FLASH = 0 - private const val PORT = 8266 - private const val PASSWORD = "SlimeVR-OTA" - private const val AUTH = 200 - } -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/SerialFlashingHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/SerialFlashingHandler.kt deleted file mode 100644 index 1564ab3341..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/SerialFlashingHandler.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slimevr.firmware - -import dev.llelievr.espflashkotlin.FlasherSerialInterface - -interface SerialFlashingHandler : FlasherSerialInterface diff --git a/server/core/src/main/java/dev/slimevr/firmware/SerialRebootHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/SerialRebootHandler.kt deleted file mode 100644 index 7131e56838..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/SerialRebootHandler.kt +++ /dev/null @@ -1,66 +0,0 @@ -package dev.slimevr.firmware - -import dev.slimevr.VRServer -import dev.slimevr.serial.SerialListener -import dev.slimevr.serial.SerialPort -import java.util.concurrent.CopyOnWriteArrayList - -interface SerialRebootListener { - fun onSerialDeviceReconnect(deviceHandle: Pair, () -> Unit>) -} - -/** - * This class watch for a serial device to disconnect then reconnect. - * This is used to watch the user progress through the firmware update process - */ -class SerialRebootHandler( - private val watchRestartQueue: MutableList, () -> Unit>>, - private val server: VRServer, - // Could be moved to a list of listeners later - private val serialRebootListener: SerialRebootListener, -) : SerialListener { - - private var currentPort: SerialPort? = null - private val disconnectedDevices: MutableList = CopyOnWriteArrayList() - - override fun onSerialConnected(port: SerialPort) { - currentPort = port - } - - override fun onSerialDisconnected() { - currentPort = null - } - - override fun onSerialLog(str: String, ignored: Boolean) { - if (str.contains("starting up...")) { - val foundPort = watchRestartQueue.find { it.first.id == currentPort?.portLocation } - if (foundPort != null) { - disconnectedDevices.remove(currentPort) - serialRebootListener.onSerialDeviceReconnect(foundPort) - // once the restart detected we close the connection - if (server.serialHandler.isConnected) { - server.serialHandler.closeSerial() - } - } - } - } - - override fun onNewSerialDevice(port: SerialPort) { - val foundPort = watchRestartQueue.find { it.first.id == port.portLocation } - if (foundPort != null && disconnectedDevices.contains(port)) { - disconnectedDevices.remove(port) - serialRebootListener.onSerialDeviceReconnect(foundPort) - // once the restart detected we close the connection - if (server.serialHandler.isConnected) { - server.serialHandler.closeSerial() - } - } - } - - override fun onSerialDeviceDeleted(port: SerialPort) { - val foundPort = watchRestartQueue.find { it.first.id == port.portLocation } - if (foundPort != null) { - disconnectedDevices.add(port) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/UpdateDeviceId.kt b/server/core/src/main/java/dev/slimevr/firmware/UpdateDeviceId.kt deleted file mode 100644 index 5bc6dc5899..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/UpdateDeviceId.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.firmware - -data class UpdateDeviceId( - val type: FirmwareUpdateMethod, - val id: T, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UpdateDeviceId<*> - - if (type != other.type) return false - if (id != other.id) return false - - return true - } - - override fun hashCode(): Int { - var result = type.hashCode() - result = 31 * result + (id?.hashCode() ?: 0) - return result - } -} diff --git a/server/core/src/main/java/dev/slimevr/firmware/UpdateStatusEvent.kt b/server/core/src/main/java/dev/slimevr/firmware/UpdateStatusEvent.kt deleted file mode 100644 index 85b485a95b..0000000000 --- a/server/core/src/main/java/dev/slimevr/firmware/UpdateStatusEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.slimevr.firmware - -data class UpdateStatusEvent( - val deviceId: UpdateDeviceId, - val status: FirmwareUpdateStatus, - val progress: Int = 0, - val time: Long = System.currentTimeMillis(), -) diff --git a/server/core/src/main/java/dev/slimevr/firmware/behaviours.kt b/server/core/src/main/java/dev/slimevr/firmware/behaviours.kt new file mode 100644 index 0000000000..a052831261 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/firmware/behaviours.kt @@ -0,0 +1,13 @@ +package dev.slimevr.firmware + +object FirmwareManagerBaseBehaviour : FirmwareManagerBehaviour { + override fun reduce(state: FirmwareManagerState, action: FirmwareManagerActions) = when (action) { + is FirmwareManagerActions.UpdateJob -> state.copy( + jobs = state.jobs + (action.jobStatus.portLocation to action.jobStatus), + ) + + is FirmwareManagerActions.RemoveJob -> state.copy(jobs = state.jobs - action.portLocation) + + is FirmwareManagerActions.ClearJobs -> state.copy(jobs = mapOf()) + } +} diff --git a/server/core/src/main/java/dev/slimevr/firmware/download.kt b/server/core/src/main/java/dev/slimevr/firmware/download.kt new file mode 100644 index 0000000000..ebc27e5ce8 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/firmware/download.kt @@ -0,0 +1,48 @@ +package dev.slimevr.firmware + +import java.io.ByteArrayOutputStream +import java.net.URL +import java.security.MessageDigest + +data class DownloadedFirmwarePart( + val data: ByteArray, + val offset: Int, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as DownloadedFirmwarePart + if (!data.contentEquals(other.data)) return false + return offset == other.offset + } + + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + offset + return result + } +} + +fun downloadFirmware(url: String, digest: String): ByteArray { + val output = ByteArrayOutputStream() + val chunk = ByteArray(4096) + URL(url).openStream().use { stream -> + while (true) { + val read = stream.read(chunk) + if (read <= 0) break + output.write(chunk, 0, read) + } + } + val data = output.toByteArray() + check(verifyChecksum(data, digest)) { "Checksum verification failed for $url" } + return data +} + +fun verifyChecksum(data: ByteArray, expectedDigest: String): Boolean { + val parts = expectedDigest.split(":", limit = 2) + check(parts.size == 2) { "Invalid digest format '$expectedDigest', expected 'algorithm:hash'" } + val algorithm = parts[0].uppercase().replace("-", "") + val expectedHash = parts[1].lowercase() + val actualHash = MessageDigest.getInstance(algorithm).digest(data).joinToString("") { "%02x".format(it) } + return actualHash == expectedHash +} diff --git a/server/core/src/main/java/dev/slimevr/firmware/module.kt b/server/core/src/main/java/dev/slimevr/firmware/module.kt new file mode 100644 index 0000000000..47b813ddd6 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/firmware/module.kt @@ -0,0 +1,134 @@ +package dev.slimevr.firmware + +import dev.slimevr.Phase1ContextProvider +import dev.slimevr.VRServer +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import solarxr_protocol.datatypes.DeviceIdTable +import solarxr_protocol.rpc.FirmwarePart +import solarxr_protocol.rpc.FirmwareUpdateDeviceId +import solarxr_protocol.rpc.FirmwareUpdateStatus +import solarxr_protocol.rpc.SerialDevicePort + +data class FirmwareJobStatus( + val portLocation: String, + val firmwareDeviceId: FirmwareUpdateDeviceId, + val status: FirmwareUpdateStatus, + val progress: Int = 0, +) + +data class FirmwareManagerState( + val jobs: Map, +) + +sealed interface FirmwareManagerActions { + data class UpdateJob( + val jobStatus: FirmwareJobStatus, + ) : FirmwareManagerActions + + data class RemoveJob(val portLocation: String) : FirmwareManagerActions + + data object ClearJobs : FirmwareManagerActions +} + +typealias FirmwareManagerContext = Context +typealias FirmwareManagerBehaviour = Behaviour + +class FirmwareManager( + val context: FirmwareManagerContext, + private val serialServer: SerialServer, + private val scope: CoroutineScope, +) { + fun startObserving() = context.observeAll(this) + + private val runningJobs = mutableMapOf() + + suspend fun flash( + portLocation: String, + parts: List, + needManualReboot: Boolean, + ssid: String?, + password: String?, + server: VRServer, + ) { + runningJobs[portLocation]?.cancelAndJoin() + runningJobs[portLocation] = scope.launch { + doSerialFlash( + portLocation = portLocation, + parts = parts, + needManualReboot = needManualReboot, + ssid = ssid, + password = password, + serialServer = serialServer, + server = server, + onStatus = { status, progress -> + context.dispatch( + FirmwareManagerActions.UpdateJob( + FirmwareJobStatus( + portLocation = portLocation, + firmwareDeviceId = SerialDevicePort(port = portLocation), + status = status, + progress = progress, + ), + ), + ) + }, + scope = scope, + ) + context.dispatch(FirmwareManagerActions.RemoveJob(portLocation)) + } + } + + suspend fun otaFlash( + deviceIp: String, + firmwareDeviceId: FirmwareUpdateDeviceId, + part: FirmwarePart, + server: VRServer, + ) { + runningJobs[deviceIp]?.cancelAndJoin() + runningJobs[deviceIp] = scope.launch { + doOtaFlash( + deviceIp = deviceIp, + deviceId = (firmwareDeviceId as? DeviceIdTable)?.id ?: error("device id should exist"), + part = part, + server = server, + onStatus = { status, progress -> + context.dispatch( + FirmwareManagerActions.UpdateJob( + FirmwareJobStatus( + portLocation = deviceIp, + firmwareDeviceId = firmwareDeviceId, + status = status, + progress = progress, + ), + ), + ) + }, + ) + context.dispatch(FirmwareManagerActions.RemoveJob(deviceIp)) + } + } + + suspend fun cancelAll() { + context.dispatch(FirmwareManagerActions.ClearJobs) + runningJobs.values.forEach { it.cancelAndJoin() } + runningJobs.clear() + } + + companion object { + fun create(ctx: Phase1ContextProvider, scope: CoroutineScope): FirmwareManager { + val behaviours = listOf(FirmwareManagerBaseBehaviour) + val context = Context.create( + initialState = FirmwareManagerState(jobs = mapOf()), + scope = scope, + behaviours = behaviours, + ) + return FirmwareManager(context = context, serialServer = ctx.serialServer, scope = scope) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/firmware/ota.kt b/server/core/src/main/java/dev/slimevr/firmware/ota.kt new file mode 100644 index 0000000000..90498f796b --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/firmware/ota.kt @@ -0,0 +1,179 @@ +package dev.slimevr.firmware + +import dev.slimevr.VRServer +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.BoundDatagramSocket +import io.ktor.network.sockets.Datagram +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.aSocket +import io.ktor.utils.io.core.buildPacket +import io.ktor.utils.io.core.readText +import io.ktor.utils.io.core.writeFully +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import solarxr_protocol.datatypes.DeviceId +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.rpc.FirmwarePart +import solarxr_protocol.rpc.FirmwareUpdateStatus +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.net.ServerSocket +import java.security.MessageDigest +import java.util.UUID +import kotlin.math.min + +private const val OTA_PORT = 8266 +private const val OTA_PASSWORD = "SlimeVR-OTA" +private const val OTA_CHUNK_SIZE = 2048 + +private fun bytesToMd5(bytes: ByteArray): String = MessageDigest.getInstance("MD5").digest(bytes).joinToString("") { "%02x".format(it) } + +private suspend fun sendDatagram(socket: BoundDatagramSocket, message: String, target: InetSocketAddress) = socket.send(Datagram(buildPacket { writeFully(message.toByteArray()) }, target)) + +/** + * Sends the OTA invitation over UDP and performs the optional AUTH challenge-response. + * Returns true if authentication succeeded (or was not required). + */ +private suspend fun otaAuthenticate( + selectorManager: SelectorManager, + deviceIp: String, + localPort: Int, + firmware: ByteArray, +): Boolean { + val fileMd5 = bytesToMd5(firmware) + val target = InetSocketAddress(deviceIp, OTA_PORT) + + aSocket(selectorManager).udp().bind(InetSocketAddress("0.0.0.0", 0)).use { socket -> + sendDatagram(socket, "0 $localPort ${firmware.size} $fileMd5\n", target) + + val responseData = withTimeout(10_000) { socket.receive() }.packet.readText() + if (responseData == "OK") return true + + val args = responseData.split(" ") + if (args.size != 2 || args[0] != "AUTH") return false + + val authToken = args[1] + val signature = bytesToMd5(UUID.randomUUID().toString().toByteArray()) + val hashedPassword = bytesToMd5(OTA_PASSWORD.toByteArray()) + val payload = bytesToMd5("$hashedPassword:$authToken:$signature".toByteArray()) + + sendDatagram(socket, "200 $signature $payload\n", target) + + val authResponseData = withTimeout(10_000) { socket.receive() }.packet.readText() + return authResponseData == "OK" + } +} + +/** + * Accepts a TCP connection from the device and streams the firmware in chunks. + * Returns true if the device confirmed a successful flash with "OK". + */ +private suspend fun otaUpload( + tcpServer: ServerSocket, + firmware: ByteArray, + onProgress: suspend (Int) -> Unit, +): Boolean { + val socket = withContext(Dispatchers.IO) { tcpServer.accept() } + return socket.use { + socket.soTimeout = 1_000 + val dos = DataOutputStream(socket.getOutputStream()) + val dis = DataInputStream(socket.getInputStream()) + + var offset = 0 + while (offset < firmware.size) { + onProgress(((offset.toDouble() / firmware.size) * 100).toInt()) + + val chunkLen = min(OTA_CHUNK_SIZE, firmware.size - offset) + withContext(Dispatchers.IO) { + dos.write(firmware, offset, chunkLen) + dos.flush() + } + offset += chunkLen + + val bytesSkipped = withContext(Dispatchers.IO) { dis.skipBytes(4) } + if (bytesSkipped != 4) throw IOException("Unexpected bytes skipped: $bytesSkipped") + } + + socket.soTimeout = 10_000 + val response = withContext(Dispatchers.IO) { dis.readBytes().decodeToString() } + response.contains("OK") + } +} + +suspend fun doOtaFlash( + deviceIp: String, + deviceId: DeviceId, + part: FirmwarePart, + server: VRServer, + onStatus: suspend (FirmwareUpdateStatus, Int) -> Unit, +) { + onStatus(FirmwareUpdateStatus.DOWNLOADING, 0) + + val firmware = try { + withContext(Dispatchers.IO) { + val url = part.url ?: error("missing url") + val digest = part.digest ?: error("missing digest") + downloadFirmware(url, digest) + } + } catch (_: Exception) { + onStatus(FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED, 0) + return + } + + onStatus(FirmwareUpdateStatus.AUTHENTICATING, 0) + + SelectorManager(Dispatchers.IO).use { selectorManager -> + // Bind TCP server first so we know which port to advertise in the invitation + ServerSocket(0).use { tcpServer -> + tcpServer.soTimeout = 30_000 + val localPort = tcpServer.localPort + + if (!otaAuthenticate(selectorManager, deviceIp, localPort, firmware)) { + onStatus(FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED, 0) + return + } + + val uploaded = runCatching { + otaUpload(tcpServer, firmware) { progress -> + onStatus(FirmwareUpdateStatus.UPLOADING, progress) + } + } + + if (uploaded.isFailure) { + onStatus(FirmwareUpdateStatus.ERROR_UPLOAD_FAILED, 0) + return + } + } + } + + onStatus(FirmwareUpdateStatus.REBOOTING, 0) + + // Wait for the device to come back online after reboot. + // flatMapLatest switches to the matched device's own state flow so that status changes, + // which don't emit a new VRServerState, are also observed. + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + val connected = withTimeoutOrNull(60_000) { + server.context.state + .flatMapLatest { state -> + val device = state.devices.values.find { it.context.state.value.id.toUByte() == deviceId.id } + device?.context?.state?.map { it.status != TrackerStatus.DISCONNECTED } ?: flowOf(false) + } + .filter { it } + .first() + } + + if (connected == null) { + onStatus(FirmwareUpdateStatus.ERROR_TIMEOUT, 0) + return + } + + onStatus(FirmwareUpdateStatus.DONE, 0) +} diff --git a/server/core/src/main/java/dev/slimevr/firmware/serial.kt b/server/core/src/main/java/dev/slimevr/firmware/serial.kt new file mode 100644 index 0000000000..2fd9421ae2 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/firmware/serial.kt @@ -0,0 +1,207 @@ +package dev.slimevr.firmware + +import dev.llelievr.espflashkotlin.Flasher +import dev.llelievr.espflashkotlin.FlashingProgressListener +import dev.slimevr.VRServer +import dev.slimevr.serial.MAC_REGEX +import dev.slimevr.serial.SerialConnection +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.rpc.FirmwarePart +import solarxr_protocol.rpc.FirmwareUpdateStatus + +@OptIn(ExperimentalCoroutinesApi::class) +suspend fun doSerialFlash( + portLocation: String, + parts: List, + needManualReboot: Boolean, + ssid: String?, + password: String?, + serialServer: SerialServer, + server: VRServer, + onStatus: suspend (FirmwareUpdateStatus, Int) -> Unit, + scope: CoroutineScope, +) { + onStatus(FirmwareUpdateStatus.DOWNLOADING, 0) + + val downloadedParts = try { + withContext(Dispatchers.IO) { + parts.map { part -> + val url = part.url ?: error("missing url") + val digest = part.digest ?: error("missing digest") + DownloadedFirmwarePart( + data = downloadFirmware(url, digest), + offset = part.offset.toInt(), + ) + } + } + } catch (_: Exception) { + onStatus(FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED, 0) + return + } + + onStatus(FirmwareUpdateStatus.SYNCING_WITH_MCU, 0) + + val handler = serialServer.openForFlashing(portLocation) ?: run { + onStatus(FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, 0) + return + } + + val flasher = Flasher(handler) + for (part in downloadedParts) { + flasher.addBin(part.data, part.offset) + } + flasher.addProgressListener( + object : FlashingProgressListener { + override fun progress(progress: Float) { + scope.launch { onStatus(FirmwareUpdateStatus.UPLOADING, (progress * 100).toInt()) } + } + }, + ) + + val runFlasher = runCatching { + withContext(Dispatchers.IO) { flasher.flash(portLocation) } + } + + if (runFlasher.isFailure) { + onStatus(FirmwareUpdateStatus.ERROR_UPLOAD_FAILED, 0) + return + } + + doSerialFlashPostFlash( + portLocation = portLocation, + needManualReboot = needManualReboot, + ssid = ssid, + password = password, + serialServer = serialServer, + server = server, + onStatus = onStatus, + ) +} + +/** + * Handles the post-flash provisioning phase: reconnects the serial console, + * reads the device MAC address, sends Wi-Fi credentials, and waits for the + * tracker to appear on the network. + * + * Separated from [doSerialFlash] so it can also be exercised independently for + * unit tests + */ +internal suspend fun doSerialFlashPostFlash( + portLocation: String, + needManualReboot: Boolean, + ssid: String?, + password: String?, + serialServer: SerialServer, + server: VRServer, + onStatus: suspend (FirmwareUpdateStatus, Int) -> Unit, +) { + onStatus( + if (needManualReboot) { + FirmwareUpdateStatus.NEED_MANUAL_REBOOT + } else { + FirmwareUpdateStatus.REBOOTING + }, + 0, + ) + + serialServer.openConnection(portLocation) + val serialConn = serialServer.context.state.value.connections[portLocation] + if (serialConn == null) { + onStatus(FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, 0) + return + } + if (serialConn !is SerialConnection.Console) { + onStatus(FirmwareUpdateStatus.ERROR_UNKNOWN, 0) + return + } + + if (needManualReboot) { + // wait for the device to reboot + val rebooted = withTimeoutOrNull(60_000) { + serialConn.context.state.map { it.logLines } + .filter { logLines -> logLines.any { "starting up" in it.lowercase() } } + .first() + } + + if (rebooted == null) { + onStatus(FirmwareUpdateStatus.ERROR_TIMEOUT, 0) + return + } + } + + // get MAC address by sending GET INFO and parsing the response + serialConn.handle.writeCommand("GET INFO") + + val macAddress = withTimeoutOrNull(10_000) { + serialConn.context.state.map { it.logLines }.mapNotNull { logLines -> + logLines.firstNotNullOfOrNull { line -> + MAC_REGEX.find(line)?.groupValues?.get(1)?.uppercase() + } + }.first() + } + + if (macAddress == null) { + onStatus(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, 0) + return + } + + // provision with Wi-Fi credentials + if (ssid == null || password == null) { + onStatus(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, 0) + return + } + + onStatus(FirmwareUpdateStatus.PROVISIONING, 0) + serialConn.handle.writeCommand("SET WIFI \"$ssid\" \"$password\"\n") + + // Wait for Wi-Fi to connect ("looking for the server") + val provisioned = withTimeoutOrNull(30_000) { + serialConn.context.state.map { it.logLines }.filter { logLines -> + logLines.any { + "looking for the server" in it.lowercase() || "searching for the server" in it.lowercase() + } + }.first() + } + + if (provisioned == null) { + onStatus(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, 0) + return + } + + // wait for the tracker with that MAC to connect to the server via UDP + val connected = waitForConnected(server, macAddress) + + if (connected == null) { + onStatus(FirmwareUpdateStatus.ERROR_TIMEOUT, 0) + return + } + + onStatus(FirmwareUpdateStatus.DONE, 0) +} + +suspend fun waitForConnected(server: VRServer, macAddress: String): Boolean? = + @OptIn(ExperimentalCoroutinesApi::class) + withTimeoutOrNull(30_000) { + server.context.state + .flatMapLatest { state -> + val device = + state.devices.values.find { it.context.state.value.macAddress?.uppercase() == macAddress } + device?.context?.state?.map { it.status != TrackerStatus.DISCONNECTED } + ?: flowOf(false) + } + .filter { it } + .first() + } diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt deleted file mode 100644 index 257c33b3d6..0000000000 --- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt +++ /dev/null @@ -1,217 +0,0 @@ -package dev.slimevr.games.vrchat - -import dev.slimevr.VRServer -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerUtils -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.math.* - -enum class VRCTrackerModel(val value: Int, val id: Int) { - UNKNOWN(-1, solarxr_protocol.rpc.VRCTrackerModel.UNKNOWN), - SPHERE(0, solarxr_protocol.rpc.VRCTrackerModel.SPHERE), - SYSTEM(1, solarxr_protocol.rpc.VRCTrackerModel.SYSTEM), - BOX(2, solarxr_protocol.rpc.VRCTrackerModel.BOX), - AXIS(3, solarxr_protocol.rpc.VRCTrackerModel.AXIS), - ; - - companion object { - private val byValue = VRCTrackerModel.entries.associateBy { it.value } - - fun getByValue(value: Int): VRCTrackerModel? = byValue[value] - } -} - -enum class VRCSpineMode(val value: Int, val id: Int) { - UNKNOWN(-1, solarxr_protocol.rpc.VRCSpineMode.UNKNOWN), - LOCK_HIP(0, solarxr_protocol.rpc.VRCSpineMode.LOCK_HIP), - LOCK_HEAD(1, solarxr_protocol.rpc.VRCSpineMode.LOCK_HEAD), - LOCK_BOTH(2, solarxr_protocol.rpc.VRCSpineMode.LOCK_BOTH), - ; - - companion object { - private val byValue = VRCSpineMode.entries.associateBy { it.value } - - fun getByValue(value: Int): VRCSpineMode? = byValue[value] - } -} - -enum class VRCAvatarMeasurementType(val value: Int, val id: Int) { - UNKNOWN(-1, solarxr_protocol.rpc.VRCAvatarMeasurementType.UNKNOWN), - ARM_SPAN(0, solarxr_protocol.rpc.VRCAvatarMeasurementType.ARM_SPAN), - HEIGHT(1, solarxr_protocol.rpc.VRCAvatarMeasurementType.HEIGHT), - ; - - companion object { - private val byValue = VRCAvatarMeasurementType.entries.associateBy { it.value } - - fun getByValue(value: Int): VRCAvatarMeasurementType? = byValue[value] - } -} - -data class VRCConfigValues( - val legacyMode: Boolean, - val shoulderTrackingDisabled: Boolean, - val shoulderWidthCompensation: Boolean, - val userHeight: Double, - val calibrationRange: Double, - val calibrationVisuals: Boolean, - val trackerModel: VRCTrackerModel, - val spineMode: VRCSpineMode, - val avatarMeasurementType: VRCAvatarMeasurementType, -) - -data class VRCConfigRecommendedValues( - val legacyMode: Boolean, - val shoulderTrackingDisabled: Boolean, - val shoulderWidthCompensation: Boolean, - val userHeight: Double, - val calibrationRange: Double, - val calibrationVisuals: Boolean, - val trackerModel: VRCTrackerModel, - val spineMode: Array, - val avatarMeasurementType: VRCAvatarMeasurementType, -) - -data class VRCConfigValidity( - val legacyModeOk: Boolean, - val shoulderTrackingOk: Boolean, - val shoulderWidthCompensationOk: Boolean, - val userHeightOk: Boolean, - val calibrationRangeOk: Boolean, - val calibrationVisualsOk: Boolean, - val trackerModelOk: Boolean, - val spineModeOk: Boolean, - val avatarMeasurementTypeOk: Boolean, -) - -abstract class VRCConfigHandler { - abstract val isSupported: Boolean - abstract fun initHandler(onChange: (config: VRCConfigValues) -> Unit) -} - -class VRCConfigHandlerStub : VRCConfigHandler() { - override val isSupported: Boolean - get() = false - - override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) {} -} - -interface VRCConfigListener { - fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) -} - -class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) { - - private val listeners: MutableList = CopyOnWriteArrayList() - var currentValues: VRCConfigValues? = null - var currentValidity: VRCConfigValidity? = null - - val isSupported: Boolean - get() = handler.isSupported - - init { - handler.initHandler(::onChange) - } - - fun toggleMuteWarning(key: String) { - val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name } - if (!keys.contains(key)) return - - if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) { - server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key) - } else { - server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key) - } - - server.configManager.saveConfig() - - val recommended = recommendedValues() - val validity = currentValidity ?: return - val values = currentValues ?: return - listeners.forEach { - it.onChange( - validity, - values, - recommended, - server.configManager.vrConfig.vrcConfig.mutedWarnings, - ) - } - } - - /** - * shoulderTrackingDisabled should be true if: - * The user isn't tracking their whole arms from their controllers: - * forceArmsFromHMD is enabled || the user doesn't have hand trackers with position || the user doesn't have lower arms trackers || the user doesn't have upper arm trackers - * And the user isn't tracking their arms from their HMD or doesn't have both shoulders: - * (forceArmsFromHMD is disabled && user has hand trackers with position) || user is missing a shoulder tracker - */ - fun recommendedValues(): VRCConfigRecommendedValues { - val forceArmsFromHMD = server.humanPoseManager.getToggle(SkeletonConfigToggles.FORCE_ARMS_FROM_HMD) - - val hasLeftHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_HAND)?.hasPosition ?: false - val hasRightHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_HAND)?.hasPosition ?: false - - val isMissingAnArmTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_LOWER_ARM) == null || - TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_LOWER_ARM) == null || - TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_UPPER_ARM) == null || - TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_UPPER_ARM) == null - val isMissingAShoulderTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_SHOULDER) == null || - TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_SHOULDER) == null - - return VRCConfigRecommendedValues( - legacyMode = false, - shoulderTrackingDisabled = - ((forceArmsFromHMD || !hasLeftHandWithPosition || !hasRightHandWithPosition) || isMissingAnArmTracker) && // Not tracking shoulders from hands - ((!forceArmsFromHMD && hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker), // Not tracking shoulders from HMD - userHeight = server.humanPoseManager.realUserHeight.toDouble(), - calibrationRange = 0.2, - trackerModel = VRCTrackerModel.AXIS, - spineMode = arrayOf(VRCSpineMode.LOCK_HIP, VRCSpineMode.LOCK_HEAD), - calibrationVisuals = true, - avatarMeasurementType = VRCAvatarMeasurementType.HEIGHT, - shoulderWidthCompensation = true, - ) - } - - fun addListener(listener: VRCConfigListener) { - listeners.add(listener) - val values = currentValues ?: return - val recommended = recommendedValues() - val validity = checkValidity(values, recommended) - listener.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings) - } - - fun removeListener(listener: VRCConfigListener) { - listeners.removeIf { l -> l === listener } - } - - fun checkValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity = VRCConfigValidity( - legacyModeOk = values.legacyMode == recommended.legacyMode, - shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled, - spineModeOk = recommended.spineMode.contains(values.spineMode), - trackerModelOk = values.trackerModel == recommended.trackerModel, - calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1, - userHeightOk = abs(server.humanPoseManager.realUserHeight - values.userHeight) < 0.1, - calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals, - avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType, - shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation, - ) - - fun forceUpdate() { - val values = currentValues - if (values != null) { - this.onChange(values) - } - } - - fun onChange(values: VRCConfigValues) { - val recommended = recommendedValues() - val validity = checkValidity(values, recommended) - currentValidity = validity - currentValues = values - listeners.forEach { - it.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/guards/ServerGuards.kt b/server/core/src/main/java/dev/slimevr/guards/ServerGuards.kt deleted file mode 100644 index 0b522011b1..0000000000 --- a/server/core/src/main/java/dev/slimevr/guards/ServerGuards.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.slimevr.guards - -import java.util.Timer -import java.util.TimerTask -import kotlin.concurrent.schedule - -class ServerGuards { - - var canDoMounting: Boolean = false - var canDoYawReset: Boolean = false - var canDoUserHeightCalibration: Boolean = false - - private val timer = Timer() - private var mountingTimeoutTask: TimerTask? = null - - fun onFullReset() { - canDoMounting = true - canDoYawReset = true - mountingTimeoutTask?.cancel() - mountingTimeoutTask = timer.schedule(MOUNTING_RESET_TIMEOUT) { - canDoMounting = false - } - } - - companion object { - const val MOUNTING_RESET_TIMEOUT = 2 * 60 * 1000L - } -} diff --git a/server/core/src/main/java/dev/slimevr/heightcalibration/behaviours.kt b/server/core/src/main/java/dev/slimevr/heightcalibration/behaviours.kt new file mode 100644 index 0000000000..2ee1d403b6 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/heightcalibration/behaviours.kt @@ -0,0 +1,195 @@ +@file:OptIn(kotlinx.coroutines.FlowPreview::class) + +package dev.slimevr.heightcalibration + +import dev.slimevr.config.UserConfig +import dev.slimevr.config.UserConfigActions +import dev.slimevr.skeleton.computeDefaultProportionsByBone +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.withTimeoutOrNull +import solarxr_protocol.rpc.UserHeightCalibrationStatus +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sqrt + +internal const val SAMPLE_INTERVAL_MS = 16L + +private const val FLOOR_ALPHA = 0.1f +private const val HMD_ALPHA = 0.1f + +private const val CONTROLLER_STABILITY_THRESHOLD = 0.005f +internal const val CONTROLLER_STABILITY_DURATION = 300_000_000L + +private const val HMD_STABILITY_THRESHOLD = 0.003f +internal const val HEAD_STABILITY_DURATION = 600_000_000L + +internal const val MAX_FLOOR_Y = 0.10f +internal const val HMD_RISE_THRESHOLD = 1.2f +internal const val HEIGHT_MIN = 1.4f +internal const val HEIGHT_MAX = 1.936f + +private val HEAD_ANGLE_THRESHOLD = cos((PI / 180.0) * 15.0) +private val CONTROLLER_ANGLE_THRESHOLD = cos((PI / 180.0) * 45.0) + +internal const val TIMEOUT_MS = 30_000L + +private fun UserHeightCalibrationStatus.isTerminal() = when (this) { + UserHeightCalibrationStatus.DONE, + UserHeightCalibrationStatus.ERROR_TOO_HIGH, + UserHeightCalibrationStatus.ERROR_TOO_SMALL, + -> true + + else -> false +} + +private fun isControllerPointingDown(snapshot: TrackerSnapshot): Boolean { + val forward = snapshot.rotation.sandwich(Vector3.NEG_Z) + return (forward dot Vector3.NEG_Y) >= CONTROLLER_ANGLE_THRESHOLD +} + +private fun isHmdLeveled(snapshot: TrackerSnapshot): Boolean { + val up = snapshot.rotation.sandwich(Vector3.POS_Y) + return (up dot Vector3.POS_Y) >= HEAD_ANGLE_THRESHOLD +} + +object CalibrationBehaviour : HeightCalibrationBehaviourType { + override fun reduce(state: HeightCalibrationState, action: HeightCalibrationActions) = when (action) { + is HeightCalibrationActions.Update -> state.copy( + status = action.status, + currentHeight = action.currentHeight, + ) + } +} + +internal suspend fun runCalibrationSession( + context: HeightCalibrationContext, + userConfig: UserConfig, + hmdUpdates: kotlinx.coroutines.flow.Flow, + controllerUpdates: kotlinx.coroutines.flow.Flow, + clock: () -> Long = System::nanoTime, +) { + var currentFloorLevel = Float.MAX_VALUE + var currentHeight = 0f + var floorStableStart: Long? = null + var heightStableStart: Long? = null + + var floorFiltered: Vector3? = null + var floorEnergyEma = 0f + var hmdFiltered: Vector3? = null + var hmdEnergyEma = 0f + + fun dispatch(status: UserHeightCalibrationStatus, height: Float = currentHeight) { + currentHeight = height + context.dispatch(HeightCalibrationActions.Update(status, height)) + } + + dispatch(UserHeightCalibrationStatus.RECORDING_FLOOR) + + withTimeoutOrNull(TIMEOUT_MS) { + // Floor phase: collect controller updates until the floor level is locked in + controllerUpdates + .sample(SAMPLE_INTERVAL_MS) + .takeWhile { context.state.value.status != UserHeightCalibrationStatus.WAITING_FOR_RISE } + .collect { snapshot -> + val now = clock() + + if (snapshot.position.y > MAX_FLOOR_Y) { + floorStableStart = null + floorFiltered = null + floorEnergyEma = 0f + return@collect + } + + if (!isControllerPointingDown(snapshot)) { + dispatch(UserHeightCalibrationStatus.WAITING_FOR_CONTROLLER_PITCH) + floorStableStart = null + floorFiltered = null + floorEnergyEma = 0f + return@collect + } + + val pos = snapshot.position + val prev = floorFiltered ?: pos + val newFiltered = prev * (1f - FLOOR_ALPHA) + pos * FLOOR_ALPHA + floorFiltered = newFiltered + currentFloorLevel = minOf(currentFloorLevel, pos.y) + + val dev = pos - newFiltered + floorEnergyEma = floorEnergyEma * (1f - FLOOR_ALPHA) + (dev dot dev) * FLOOR_ALPHA + + if (sqrt(floorEnergyEma) > CONTROLLER_STABILITY_THRESHOLD) { + floorStableStart = null + floorFiltered = null + floorEnergyEma = 0f + return@collect + } + + val stableStart = floorStableStart ?: now.also { floorStableStart = it } + if (now - stableStart >= CONTROLLER_STABILITY_DURATION) { + dispatch(UserHeightCalibrationStatus.WAITING_FOR_RISE) + } + } + + // Height phase: collect HMD updates until a terminal status is reached + hmdUpdates + .sample(SAMPLE_INTERVAL_MS) + .takeWhile { !context.state.value.status.isTerminal() } + .collect { snapshot -> + val now = clock() + val relativeY = snapshot.position.y - currentFloorLevel + + if (relativeY <= HMD_RISE_THRESHOLD) { + dispatch(UserHeightCalibrationStatus.WAITING_FOR_RISE, relativeY) + heightStableStart = null + hmdFiltered = null + hmdEnergyEma = 0f + return@collect + } + + if (!isHmdLeveled(snapshot)) { + dispatch(UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK, relativeY) + heightStableStart = null + hmdFiltered = null + hmdEnergyEma = 0f + return@collect + } + + dispatch(UserHeightCalibrationStatus.RECORDING_HEIGHT, relativeY) + + val pos = snapshot.position + val prev = hmdFiltered ?: pos + val newFiltered = prev * (1f - HMD_ALPHA) + pos * HMD_ALPHA + hmdFiltered = newFiltered + + val dev = pos - newFiltered + hmdEnergyEma = hmdEnergyEma * (1f - HMD_ALPHA) + (dev dot dev) * HMD_ALPHA + + if (sqrt(hmdEnergyEma) > HMD_STABILITY_THRESHOLD) { + heightStableStart = null + hmdFiltered = null + hmdEnergyEma = 0f + return@collect + } + + val stableStart = heightStableStart ?: now.also { heightStableStart = it } + if (now - stableStart >= HEAD_STABILITY_DURATION) { + val finalStatus = when { + relativeY < HEIGHT_MIN -> UserHeightCalibrationStatus.ERROR_TOO_SMALL + relativeY > HEIGHT_MAX -> UserHeightCalibrationStatus.ERROR_TOO_HIGH + else -> UserHeightCalibrationStatus.DONE + } + dispatch(finalStatus, relativeY) + + if (finalStatus == UserHeightCalibrationStatus.DONE) { + userConfig.context.dispatch( + UserConfigActions.Update { + copy(userHeight = relativeY, proportions = computeDefaultProportionsByBone(relativeY)) + }, + ) + } + } + } + } ?: dispatch(UserHeightCalibrationStatus.ERROR_TIMEOUT) +} diff --git a/server/core/src/main/java/dev/slimevr/heightcalibration/module.kt b/server/core/src/main/java/dev/slimevr/heightcalibration/module.kt new file mode 100644 index 0000000000..51f88a472d --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/heightcalibration/module.kt @@ -0,0 +1,107 @@ +@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + +package dev.slimevr.heightcalibration + +import dev.slimevr.Phase1ContextProvider +import dev.slimevr.VRServer +import dev.slimevr.config.UserConfig +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.rpc.UserHeightCalibrationStatus + +data class TrackerSnapshot(val position: Vector3, val rotation: Quaternion) + +data class HeightCalibrationState( + val status: UserHeightCalibrationStatus, + val currentHeight: Float, +) + +sealed interface HeightCalibrationActions { + data class Update(val status: UserHeightCalibrationStatus, val currentHeight: Float) : HeightCalibrationActions +} + +typealias HeightCalibrationContext = Context +typealias HeightCalibrationBehaviourType = Behaviour + +val INITIAL_HEIGHT_CALIBRATION_STATE = HeightCalibrationState( + status = UserHeightCalibrationStatus.NONE, + currentHeight = 0f, +) + +class HeightCalibrationManager( + val context: HeightCalibrationContext, + val serverContext: VRServer, + private val userConfig: UserConfig, +) { + fun startObserving() = context.observeAll(this) + + private var sessionJob: Job? = null + + // These Flows do nothing until the calibration use collect on it + val hmdUpdates: Flow = serverContext.context.state + .flatMapLatest { state -> + val hmd = state.trackers.values + .find { it.context.state.value.bodyPart == BodyPart.HEAD && it.context.state.value.position != null } + ?: return@flatMapLatest emptyFlow() + hmd.context.state.map { s -> + TrackerSnapshot( + position = s.position ?: error("head (or HMD) will always have a position in this case"), + rotation = s.rawRotation, + ) + } + } + + val controllerUpdates: Flow = serverContext.context.state + .flatMapLatest { state -> + val controllers = state.trackers.values.filter { + val bodyPart = it.context.state.value.bodyPart + (bodyPart == BodyPart.LEFT_HAND || bodyPart == BodyPart.RIGHT_HAND) && it.context.state.value.position != null + } + if (controllers.isEmpty()) return@flatMapLatest emptyFlow() + combine( + controllers.map { controller -> + controller.context.state.map { s -> + val position = s.position ?: error("hands (or Controller) will always have a position in this case") + TrackerSnapshot(position = position, rotation = s.rawRotation) + } + }, + ) { snapshots -> snapshots.minByOrNull { it.position.y }!! } + } + + fun start() { + sessionJob?.cancel() + sessionJob = context.scope.launch { runCalibrationSession(context, userConfig, hmdUpdates, controllerUpdates) } + } + + fun cancel() { + sessionJob?.cancel() + sessionJob = null + context.dispatch(HeightCalibrationActions.Update(UserHeightCalibrationStatus.NONE, 0f)) + } + + companion object { + fun create( + ctx: Phase1ContextProvider, + scope: CoroutineScope, + ): HeightCalibrationManager { + val behaviours = listOf(CalibrationBehaviour) + val context = Context.create( + initialState = INITIAL_HEIGHT_CALIBRATION_STATE, + scope = scope, + behaviours = behaviours, + ) + return HeightCalibrationManager(context = context, serverContext = ctx.server, userConfig = ctx.config.userConfig) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/hid/behaviours.kt b/server/core/src/main/java/dev/slimevr/hid/behaviours.kt new file mode 100644 index 0000000000..eb2d3efcff --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/hid/behaviours.kt @@ -0,0 +1,185 @@ +package dev.slimevr.hid + +import dev.slimevr.AppLogger +import dev.slimevr.VRServerActions +import dev.slimevr.device.Device +import dev.slimevr.device.DeviceActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.tracker.Tracker +import dev.slimevr.tracker.TrackerActions +import solarxr_protocol.datatypes.TrackerStatus + +object HIDRegistrationBehaviour : HIDReceiverBehaviour { + override fun reduce(state: HIDReceiverState, action: HIDReceiverActions) = when (action) { + is HIDReceiverActions.DeviceRegistered -> state.copy( + trackers = state.trackers + + ( + action.hidId to HIDTrackerRecord( + hidId = action.hidId, + address = action.address, + deviceId = action.deviceId, + trackerId = null, + ) + ), + ) + + else -> state + } + + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + val state = receiver.context.state.value + val existing = state.trackers[packet.hidId] + if (existing != null) return@onPacket + + val existingDevice = receiver.appContext.server.context.state.value.devices.values + .find { it.context.state.value.macAddress == packet.address && it.context.state.value.origin == DeviceOrigin.HID } + + if (existingDevice != null) { + receiver.context.dispatch(HIDReceiverActions.DeviceRegistered(packet.hidId, packet.address, existingDevice.context.state.value.id)) + AppLogger.hid.info("Reconnected HID device ${packet.address} (hidId=${packet.hidId})") + return@onPacket + } + + val deviceId = receiver.appContext.server.nextHandle() + val device = Device.create( + scope = receiver.appContext.server.context.scope, + id = deviceId, + address = packet.address, + macAddress = packet.address, + origin = DeviceOrigin.HID, + protocolVersion = 0, + ) + receiver.appContext.server.context.dispatch(VRServerActions.NewDevice(deviceId, device)) + receiver.context.dispatch(HIDReceiverActions.DeviceRegistered(packet.hidId, packet.address, deviceId)) + AppLogger.hid.info("Registered HID device ${packet.address} (hidId=${packet.hidId})") + } + } +} + +object HIDDeviceInfoBehaviour : HIDReceiverBehaviour { + override fun reduce(state: HIDReceiverState, action: HIDReceiverActions): HIDReceiverState = when (action) { + is HIDReceiverActions.TrackerRegistered -> { + val existing = state.trackers[action.hidId] ?: return state + state.copy(trackers = state.trackers + (action.hidId to existing.copy(trackerId = action.trackerId))) + } + + else -> state + } + + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + val device = receiver.getDevice(packet.hidId) ?: return@onPacket + + device.context.dispatch( + DeviceActions.Update { + copy( + boardType = packet.boardType, + mcuType = packet.mcuType, + firmware = packet.firmware, + batteryLevel = packet.batteryLevel, + batteryVoltage = packet.batteryVoltage, + signalStrength = packet.rssi, + ) + }, + ) + + val tracker = receiver.getTracker(packet.hidId) + if (tracker == null) { + val deviceState = device.context.state.value + + val existingTracker = receiver.appContext.server.context.state.value.trackers.values + .find { it.context.state.value.hardwareId == deviceState.address && it.context.state.value.origin == DeviceOrigin.HID } + + if (existingTracker != null) { + receiver.context.dispatch(HIDReceiverActions.TrackerRegistered(packet.hidId, existingTracker.context.state.value.id)) + // HID does not have a rest calibration signal + existingTracker.context.dispatch(TrackerActions.Update { copy(sensorType = packet.imuType, completedRestCalibration = true) }) + } else { + val trackerId = receiver.appContext.server.nextHandle() + val newTracker = Tracker.create( + scope = receiver.appContext.server.context.scope, + id = trackerId, + deviceId = deviceState.id, + sensorType = packet.imuType, + hardwareId = deviceState.address, + origin = DeviceOrigin.HID, + server = receiver.appContext.server, + settings = receiver.appContext.config.settings, + ) + receiver.appContext.server.context.dispatch(VRServerActions.NewTracker(trackerId, newTracker)) + receiver.context.dispatch(HIDReceiverActions.TrackerRegistered(packet.hidId, trackerId)) + } + } else { + // HID does not have a rest calibration signal + tracker.context.dispatch(TrackerActions.Update { copy(sensorType = packet.imuType, completedRestCalibration = true) }) + tracker.context.dispatch(TrackerActions.SetStatus(TrackerStatus.OK)) + } + + + } + } +} + +object HIDRotationBehaviour : HIDReceiverBehaviour { + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + val tracker = receiver.getTracker(packet.hidId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = packet.rotation, acceleration = packet.acceleration)) + } + + receiver.packetEvents.onPacket { packet -> + val tracker = receiver.getTracker(packet.hidId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = packet.rotation, acceleration = packet.acceleration)) + } + + receiver.packetEvents.onPacket { packet -> + val tracker = receiver.getTracker(packet.hidId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = packet.rotation)) + } + + receiver.packetEvents.onPacket { packet -> + val tracker = receiver.getTracker(packet.hidId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = packet.rotation, acceleration = packet.acceleration)) + } + } +} + +object HIDBatteryBehaviour : HIDReceiverBehaviour { + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + receiver.getDevice(packet.hidId)?.context?.dispatch( + DeviceActions.Update { + copy(batteryLevel = packet.batteryLevel, batteryVoltage = packet.batteryVoltage, signalStrength = packet.rssi) + }, + ) + } + + receiver.packetEvents.onPacket { packet -> + receiver.getDevice(packet.hidId)?.context?.dispatch( + DeviceActions.Update { copy(signalStrength = packet.rssi) }, + ) + } + } +} + +object HIDStatusBehaviour : HIDReceiverBehaviour { + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + if (receiver.getTracker(packet.hidId) == null) return@onPacket + receiver.getDevice(packet.hidId)?.context?.dispatch( + DeviceActions.Update { copy(status = packet.status, signalStrength = packet.rssi) }, + ) + } + } +} + +object HIDPacketLossBehaviour : HIDReceiverBehaviour { + override fun observe(receiver: HIDReceiver) { + receiver.packetEvents.onPacket { packet -> + receiver.getDevice(packet.hidId)?.context?.dispatch( + DeviceActions.PacketStats(packetsReceived = packet.packetsReceived.toLong(), packetsLost = packet.packetsLost.toLong()), + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/hid/module.kt b/server/core/src/main/java/dev/slimevr/hid/module.kt new file mode 100644 index 0000000000..228776a2a4 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/hid/module.kt @@ -0,0 +1,87 @@ +package dev.slimevr.hid + +import dev.slimevr.AppContextProvider +import dev.slimevr.EventDispatcher +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.device.Device +import dev.slimevr.tracker.Tracker +import kotlinx.coroutines.CoroutineScope + +data class HIDTrackerRecord( + val hidId: Int, + val address: String, + val deviceId: Int, + val trackerId: Int?, +) + +data class HIDReceiverState( + val serialNumber: String, + val trackers: Map, +) + +sealed interface HIDReceiverActions { + data class DeviceRegistered(val hidId: Int, val address: String, val deviceId: Int) : HIDReceiverActions + data class TrackerRegistered(val hidId: Int, val trackerId: Int) : HIDReceiverActions +} + +typealias HIDReceiverContext = Context +typealias HIDReceiverBehaviour = Behaviour +typealias HIDPacketDispatcher = EventDispatcher + +@Suppress("UNCHECKED_CAST") +inline fun HIDPacketDispatcher.onPacket(crossinline callback: suspend (T) -> Unit) { + register(T::class) { callback(it as T) } +} + +class HIDReceiver( + val context: HIDReceiverContext, + val appContext: AppContextProvider, + val packetEvents: HIDPacketDispatcher, +) { + fun startObserving() = context.observeAll(this) + + fun getDevice(hidId: Int): Device? { + val record = context.state.value.trackers[hidId] ?: return null + return appContext.server.getDevice(record.deviceId) + } + + fun getTracker(hidId: Int): Tracker? { + val record = context.state.value.trackers[hidId] ?: return null + val trackerId = record.trackerId ?: return null + return appContext.server.getTracker(trackerId) + } + + companion object { + fun create( + serialNumber: String, + appContext: AppContextProvider, + scope: CoroutineScope, + ): HIDReceiver { + val behaviours = listOf( + HIDRegistrationBehaviour, + HIDDeviceInfoBehaviour, + HIDRotationBehaviour, + HIDBatteryBehaviour, + HIDStatusBehaviour, + HIDPacketLossBehaviour, + ) + + val context = Context.create( + initialState = HIDReceiverState(serialNumber = serialNumber, trackers = mapOf()), + scope = scope, + behaviours = behaviours, + ) + + val dispatcher = HIDPacketDispatcher() + + val receiver = HIDReceiver( + context = context, + appContext = appContext, + packetEvents = dispatcher, + ) + receiver.startObserving() + return receiver + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/hid/packets.kt b/server/core/src/main/java/dev/slimevr/hid/packets.kt new file mode 100644 index 0000000000..5faa90f387 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/hid/packets.kt @@ -0,0 +1,205 @@ +package dev.slimevr.hid + +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.datatypes.hardware_info.BoardType +import solarxr_protocol.datatypes.hardware_info.ImuType +import solarxr_protocol.datatypes.hardware_info.McuType +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +sealed interface HIDPacket { + val hidId: Int +} + +/** Receiver associates a wireless tracker ID with its 6-byte address (type 255). */ +data class HIDDeviceRegister(override val hidId: Int, val address: String) : HIDPacket + +/** Board/MCU/firmware/battery + IMU type for tracker registration (type 0). */ +data class HIDDeviceInfo( + override val hidId: Int, + val imuType: ImuType, + val boardType: BoardType, + val mcuType: McuType, + val firmware: String, + val batteryLevel: Float, + val batteryVoltage: Float, + val rssi: Int, +) : HIDPacket + +/** Full-precision Q15 quaternion + Q7 acceleration (type 1). */ +data class HIDRotation( + override val hidId: Int, + val rotation: Quaternion, + val acceleration: Vector3, +) : HIDPacket + +/** Compact exp-map quaternion + Q7 acceleration + battery level + rssi (type 2). */ +data class HIDRotationBattery( + override val hidId: Int, + val rotation: Quaternion, + val acceleration: Vector3, + val batteryLevel: Float, + val batteryVoltage: Float, + val rssi: Int, +) : HIDPacket + +/** Tracker status report + rssi (type 3). */ +data class HIDStatus( + override val hidId: Int, + val status: TrackerStatus, + val rssi: Int, + val packetsReceived: Int, + val packetsLost: Int, +) : HIDPacket + +/** Full-precision Q15 quaternion + Q10 magnetometer (type 4). */ +data class HIDRotationMag( + override val hidId: Int, + val rotation: Quaternion, + val magnetometer: Vector3, +) : HIDPacket + +/** Button state + compact exp-map quaternion + Q7 acceleration + rssi (type 7). */ +data class HIDRotationButton( + override val hidId: Int, + val button: Int, + val rotation: Quaternion, + val acceleration: Vector3, + val rssi: Int, +) : HIDPacket + +private const val HID_PACKET_SIZE = 16 + +private val AXES_OFFSET = Quaternion.fromRotationVector(-PI.toFloat() / 2f, 0f, 0f) + +private fun readLE16Signed(data: ByteArray, offset: Int): Int = data[offset + 1].toInt() shl 8 or data[offset].toUByte().toInt() + +private fun decodeQ15Quat(data: ByteArray, offset: Int): Quaternion { + val scale = 1f / 32768f + val x = readLE16Signed(data, offset).toShort().toFloat() * scale + val y = readLE16Signed(data, offset + 2).toShort().toFloat() * scale + val z = readLE16Signed(data, offset + 4).toShort().toFloat() * scale + val w = readLE16Signed(data, offset + 6).toShort().toFloat() * scale + return AXES_OFFSET * Quaternion(w, x, y, z) +} + +private fun decodeExpMapQuat(data: ByteArray, offset: Int): Quaternion { + val buf = ByteBuffer.wrap(data, offset, 4).order(ByteOrder.LITTLE_ENDIAN).int.toUInt() + val vx = ((buf and 1023u).toFloat() / 1024f) * 2f - 1f + val vy = ((buf shr 10 and 2047u).toFloat() / 2048f) * 2f - 1f + val vz = ((buf shr 21 and 2047u).toFloat() / 2048f) * 2f - 1f + val d = vx * vx + vy * vy + vz * vz + val invSqrtD = 1f / sqrt(d + 1e-6f) + val a = (PI.toFloat() / 2f) * d * invSqrtD + val s = sin(a) + val k = s * invSqrtD + return AXES_OFFSET * Quaternion(cos(a), k * vx, k * vy, k * vz) +} + +private fun decodeAccel(data: ByteArray, offset: Int): Vector3 { + val scale = 1f / 128f + return Vector3( + readLE16Signed(data, offset).toShort().toFloat() * scale, + readLE16Signed(data, offset + 2).toShort().toFloat() * scale, + readLE16Signed(data, offset + 4).toShort().toFloat() * scale, + ) +} + +private fun decodeBattery(raw: Int): Float = if (raw == 128) 1f else (raw and 127).toFloat() / 100f + +private fun decodeBatteryVoltage(raw: Int): Float = (raw.toFloat() + 245f) / 100f + +private fun parseSingleHIDPacket(data: ByteArray, i: Int): HIDPacket? { + val packetType = data[i].toUByte().toInt() + val hidId = data[i + 1].toUByte().toInt() + + return when (packetType) { + 255 -> { + val addr = ByteBuffer.wrap(data, i + 2, 8).order(ByteOrder.LITTLE_ENDIAN).long and 0x0000_FFFF_FFFF_FFFFL + HIDDeviceRegister(hidId, "%012X".format(addr)) + } + + 0 -> { + val batt = data[i + 2].toUByte().toInt() + val battV = data[i + 3].toUByte().toInt() + val brdId = data[i + 5].toUByte().toInt() + val mcuId = data[i + 6].toUByte().toInt() + val imuId = data[i + 8].toUByte().toInt() + val fwDate = data[i + 11].toUByte().toInt() shl 8 or data[i + 10].toUByte().toInt() + val fwMajor = data[i + 12].toUByte().toInt() + val fwMinor = data[i + 13].toUByte().toInt() + val fwPatch = data[i + 14].toUByte().toInt() + val rssi = data[i + 15].toUByte().toInt() + val fwYear = 2020 + (fwDate shr 9 and 127) + val fwMonth = fwDate shr 5 and 15 + val fwDay = fwDate and 31 + HIDDeviceInfo( + hidId = hidId, + imuType = ImuType.fromValue(imuId.toUShort()) ?: ImuType.Other, + boardType = BoardType.fromValue(brdId.toUShort()) ?: BoardType.UNKNOWN, + mcuType = McuType.fromValue(mcuId.toUShort()) ?: McuType.Other, + firmware = "%04d-%02d-%02d %d.%d.%d".format(fwYear, fwMonth, fwDay, fwMajor, fwMinor, fwPatch), + batteryLevel = decodeBattery(batt), + batteryVoltage = decodeBatteryVoltage(battV), + rssi = -rssi, + ) + } + + 1 -> HIDRotation( + hidId = hidId, + rotation = decodeQ15Quat(data, i + 2), + acceleration = decodeAccel(data, i + 10), + ) + + 2 -> HIDRotationBattery( + hidId = hidId, + rotation = decodeExpMapQuat(data, i + 5), + acceleration = decodeAccel(data, i + 9), + batteryLevel = decodeBattery(data[i + 2].toUByte().toInt()), + batteryVoltage = decodeBatteryVoltage(data[i + 3].toUByte().toInt()), + rssi = -data[i + 15].toUByte().toInt(), + ) + + 3 -> HIDStatus( + hidId = hidId, + status = TrackerStatus.fromValue((data[i + 2].toUByte().toInt() + 1).toUByte()) ?: TrackerStatus.OK, + rssi = -data[i + 15].toUByte().toInt(), + packetsReceived = data[i + 4].toUByte().toInt(), + packetsLost = data[i + 5].toUByte().toInt(), + ) + + 4 -> { + val scaleMag = 1000f / 1024f + HIDRotationMag( + hidId = hidId, + rotation = decodeQ15Quat(data, i + 2), + magnetometer = Vector3( + readLE16Signed(data, i + 10).toShort().toFloat() * scaleMag, + readLE16Signed(data, i + 12).toShort().toFloat() * scaleMag, + readLE16Signed(data, i + 14).toShort().toFloat() * scaleMag, + ), + ) + } + + 7 -> HIDRotationButton( + hidId = hidId, + button = data[i + 2].toUByte().toInt(), + rotation = decodeExpMapQuat(data, i + 5), + acceleration = decodeAccel(data, i + 9), + rssi = -data[i + 15].toUByte().toInt(), + ) + + else -> null + } +} + +fun parseHIDPackets(data: ByteArray): List { + if (data.size % HID_PACKET_SIZE != 0) return emptyList() + return (0 until data.size / HID_PACKET_SIZE).mapNotNull { parseSingleHIDPacket(data, it * HID_PACKET_SIZE) } +} diff --git a/server/core/src/main/java/dev/slimevr/logger.kt b/server/core/src/main/java/dev/slimevr/logger.kt new file mode 100644 index 0000000000..ead2e5eba5 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/logger.kt @@ -0,0 +1,29 @@ +package dev.slimevr + +import io.klogging.Level +import io.klogging.config.loggingConfiguration +import io.klogging.logger +import io.klogging.rendering.RENDER_SIMPLE +import io.klogging.sending.STDOUT + +object AppLogger { + val tracker = logger("Tracker") + val device = logger("Device") + val udp = logger("UDPConnection") + val solarxr = logger("SolarXR") + val hid = logger("HID") + val serial = logger("Serial") + val firmware = logger("Firmware") + val vrc = logger("VRChat") + + init { + loggingConfiguration { + sink("stdout", RENDER_SIMPLE, STDOUT) + logging { + fromMinLevel(Level.INFO) { + toSink("stdout") + } + } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/math/Angle.kt b/server/core/src/main/java/dev/slimevr/math/Angle.kt deleted file mode 100644 index 7bcad5c658..0000000000 --- a/server/core/src/main/java/dev/slimevr/math/Angle.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.slimevr.math - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/** - * An angle between [-PI, PI). - */ -@JvmInline -value class Angle(private val rad: Float) { - - fun toRad() = rad - - fun toDeg() = rad * FastMath.RAD_TO_DEG - - operator fun unaryPlus() = this - - operator fun unaryMinus() = Angle(normalize(-rad)) - - operator fun plus(other: Angle) = Angle(normalize(rad + other.rad)) - - operator fun minus(other: Angle) = Angle(normalize(rad - other.rad)) - - operator fun times(scale: Float) = Angle(normalize(rad * scale)) - - operator fun div(scale: Float) = Angle(normalize(rad / scale)) - - operator fun compareTo(other: Angle) = rad.compareTo(other.rad) - - override fun toString() = "${toDeg()} deg" - - companion object { - val ZERO = Angle(0.0f) - - fun ofRad(rad: Float) = Angle(normalize(rad)) - - fun ofDeg(deg: Float) = Angle(normalize(deg * FastMath.DEG_TO_RAD)) - - // Angle between two vectors - fun absBetween(a: Vector3, b: Vector3) = Angle(normalize(a.angleTo(b))) - - // Angle between two rotations in rotation space - fun absBetween(a: Quaternion, b: Quaternion) = Angle(normalize(a.angleToR(b))) - - /** - * Normalizes an angle to [-PI, PI) - */ - private fun normalize(rad: Float): Float { - // Normalize to [0, 2*PI) - val r = - if (rad < 0.0f || rad >= FastMath.TWO_PI) { - rad - floor(rad * FastMath.INV_TWO_PI) * FastMath.TWO_PI - } else { - rad - } - - // Normalize to [-PI, PI) - return if (r > FastMath.PI) { - r - FastMath.TWO_PI - } else { - r - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/math/AngleAverage.kt b/server/core/src/main/java/dev/slimevr/math/AngleAverage.kt deleted file mode 100644 index 98fbfccb72..0000000000 --- a/server/core/src/main/java/dev/slimevr/math/AngleAverage.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.slimevr.math - -import kotlin.math.* - -/** - * Averages angles by summing vectors. - * - * See https://www.themathdoctors.org/averaging-angles/ - */ -class AngleAverage { - - private var sumX = 0.0f - private var sumY = 0.0f - - /** - * Adds another angle to the average. - */ - fun add(angle: Angle, weight: Float = 1.0f) { - sumX += cos(angle.toRad()) * weight - sumY += sin(angle.toRad()) * weight - } - - /** - * Gets the average angle. - */ - fun toAngle(): Angle = if (isEmpty()) { - Angle.ZERO - } else { - Angle.ofRad(atan2(sumY, sumX)) - } - - /** - * Whether there are any angles to average. - */ - fun isEmpty() = sumX == 0.0f && sumY == 0.0f -} diff --git a/server/core/src/main/java/dev/slimevr/math/AngleErrors.kt b/server/core/src/main/java/dev/slimevr/math/AngleErrors.kt deleted file mode 100644 index 9d0431ac70..0000000000 --- a/server/core/src/main/java/dev/slimevr/math/AngleErrors.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.slimevr.math - -import kotlin.math.* - -class AngleErrors { - - private var sumSqrErrors = 0.0f - - fun add(error: Angle) { - sumSqrErrors += error.toRad() * error.toRad() - } - - fun toL2Norm() = Angle.ofRad(sqrt(sumSqrErrors)) -} diff --git a/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java b/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java deleted file mode 100644 index fb0f0d2459..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slimevr.osc; - -import com.illposed.osc.transport.OSCPortIn; -import com.illposed.osc.transport.OSCPortOut; - -import java.net.InetAddress; - - -public interface OSCHandler { - - public void refreshSettings(boolean refreshRouterSettings); - - public void updateOscReceiver(int portIn, String[] args); - - public void updateOscSender(int portOut, String address); - - public void update(); - - public OSCPortOut getOscSender(); - - public int getPortOut(); - - public InetAddress getAddress(); - - public OSCPortIn getOscReceiver(); - - public int getPortIn(); - -} diff --git a/server/core/src/main/java/dev/slimevr/osc/OSCRouter.java b/server/core/src/main/java/dev/slimevr/osc/OSCRouter.java deleted file mode 100644 index 0621dfa2b8..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/OSCRouter.java +++ /dev/null @@ -1,179 +0,0 @@ -package dev.slimevr.osc; - -import com.illposed.osc.*; -import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector; -import com.illposed.osc.transport.OSCPortIn; -import com.illposed.osc.transport.OSCPortOut; -import dev.slimevr.config.OSCConfig; -import io.eiren.util.collections.FastList; -import io.eiren.util.logging.LogManager; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; - - -public class OSCRouter { - private OSCPortIn oscReceiver; - private OSCPortOut oscSender; - private final OSCConfig config; - private final FastList oscHandlers; - private int lastPortIn; - private int lastPortOut; - private InetAddress lastAddress; - private long timeAtLastError; - - public OSCRouter( - OSCConfig oscConfig, - FastList oscHandlers - ) { - this.config = oscConfig; - this.oscHandlers = oscHandlers; - - refreshSettings(false); - } - - public void refreshSettings(boolean refreshHandlersSettings) { - if (refreshHandlersSettings) { - for (OSCHandler oscHandler : oscHandlers) { - oscHandler.refreshSettings(false); - } - } - - // Stops listening and closes OSC port - boolean wasListening = oscReceiver != null && oscReceiver.isListening(); - if (wasListening) { - oscReceiver.stopListening(); - } - oscReceiver = null; - boolean wasConnected = oscSender != null && oscSender.isConnected(); - if (wasConnected) { - try { - oscSender.close(); - } catch (IOException e) { - LogManager.severe("[OSCRouter] Error closing the OSC sender: " + e); - } - } - oscSender = null; - - if (config.getEnabled()) { - // Instantiates the OSC receiver - int portIn = config.getPortIn(); - // Check if another OSC receiver with same port exists - for (OSCHandler oscHandler : oscHandlers) { - if (oscHandler.getPortIn() == portIn) { - if (oscHandler.getOscReceiver().isListening()) { - oscReceiver = oscHandler.getOscReceiver(); - LogManager.info("[OSCRouter] Listening to port " + portIn); - } - } - } - // Else, create our own OSC receiver - if (oscReceiver == null) { - try { - oscReceiver = new OSCPortIn(portIn); - if (lastPortIn != portIn || !wasListening) { - LogManager.info("[OSCRouter] Listening to port " + portIn); - } - lastPortIn = portIn; - } catch (IOException e) { - LogManager - .severe( - "[OSCRouter] Error listening to the port " - + config.getPortIn() - + ": " - + e - ); - } - } - - // Instantiate the OSC sender - int portOut = config.getPortOut(); - InetAddress address; - try { - address = InetAddress.getByName(config.getAddress()); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - // Check if another OSC sender with same port and address exists - for (OSCHandler oscHandler : oscHandlers) { - if (oscHandler.getPortOut() == portOut && oscHandler.getAddress() == address) { - if (oscHandler.getOscSender().isConnected()) { - oscSender = oscHandler.getOscSender(); - LogManager - .info( - "[OSCRouter] Sending to port " - + portOut - + " at address " - + address.toString() - ); - } - } - } - // Else, create our own OSC sender - if (oscSender == null) { - try { - oscSender = new OSCPortOut(new InetSocketAddress(address, portOut)); - if ((lastPortOut != portOut && lastAddress != address) || !wasConnected) { - LogManager - .info( - "[OSCRouter] Sending to port " - + portOut - + " at address " - + address.toString() - ); - } - lastPortOut = portOut; - lastAddress = address; - - oscSender.connect(); - } catch (IOException e) { - LogManager - .severe( - "[OSCRouter] Error connecting to port " - + config.getPortOut() - + " at the address " - + config.getAddress() - + ": " - + e - ); - } - } - - // Starts listening to messages - if (oscReceiver != null) { - OSCMessageListener listener = this::handleReceivedMessage; - // Listens for any message ("//" is a wildcard) - MessageSelector selector = new OSCPatternAddressMessageSelector("//"); - oscReceiver.getDispatcher().addListener(selector, listener); - if (!oscReceiver.isListening()) - oscReceiver.startListening(); - } - } - } - - void handleReceivedMessage(OSCMessageEvent event) { - if (oscSender != null && oscSender.isConnected()) { - OSCMessage oscMessage = new OSCMessage( - event.getMessage().getAddress(), - event.getMessage().getArguments() - ); - try { - oscSender.send(oscMessage); - } catch (IOException | OSCSerializeException e) { - // Avoid spamming AsynchronousCloseException too many - // times per second - if (System.currentTimeMillis() - timeAtLastError > 100) { - timeAtLastError = System.currentTimeMillis(); - LogManager - .warning( - "[OSCRouter] Error sending OSC packet: " - + e - ); - } - } - } - } -} - diff --git a/server/core/src/main/java/dev/slimevr/osc/UnityArmature.kt b/server/core/src/main/java/dev/slimevr/osc/UnityArmature.kt deleted file mode 100644 index c355766a81..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/UnityArmature.kt +++ /dev/null @@ -1,332 +0,0 @@ -package dev.slimevr.osc - -import com.jme3.math.FastMath -import dev.slimevr.tracking.processor.TransformNode -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 - -/** - * TODO make this class use Bone.kt - */ -class UnityArmature(localRot: Boolean) { - - // Head - private val headNode = TransformNode(localRotation = localRot) - private val neckTailNode = TransformNode(localRotation = localRot) - private val neckHeadNode = TransformNode(localRotation = localRot) - - // Spine - private val upperChestNode = TransformNode(localRotation = localRot) - private val chestNode = TransformNode(localRotation = localRot) - private val spineTailNode = TransformNode(localRotation = localRot) - private val spineHeadNode = TransformNode(localRotation = localRot) - private val hipsNode = TransformNode(localRotation = localRot) - private val leftHipNode = TransformNode(localRotation = localRot) - private val rightHipNode = TransformNode(localRotation = localRot) - - // Legs - private val leftKneeNode = TransformNode(localRotation = localRot) - private val leftAnkleNode = TransformNode(localRotation = localRot) - private val leftFootNode = TransformNode(localRotation = localRot) - private val rightKneeNode = TransformNode(localRotation = localRot) - private val rightAnkleNode = TransformNode(localRotation = localRot) - private val rightFootNode = TransformNode(localRotation = localRot) - - // Arms - private val leftShoulderHeadNode = TransformNode(localRotation = localRot) - private val rightShoulderHeadNode = TransformNode(localRotation = localRot) - private val leftShoulderTailNode = TransformNode(localRotation = localRot) - private val rightShoulderTailNode = TransformNode(localRotation = localRot) - private val leftElbowNode = TransformNode(localRotation = localRot) - private val rightElbowNode = TransformNode(localRotation = localRot) - private val leftWristNode = TransformNode(localRotation = localRot) - private val rightWristNode = TransformNode(localRotation = localRot) - private val leftHandNode = TransformNode(localRotation = !localRot) - private val rightHandNode = TransformNode(localRotation = !localRot) - - // Fingers - val leftThumbProximalHeadNode = TransformNode(localRotation = localRot) - val leftThumbProximalTailNode = TransformNode(localRotation = localRot) - val leftThumbIntermediateNode = TransformNode(localRotation = localRot) - val leftThumbDistalNode = TransformNode(localRotation = localRot) - val leftIndexProximalHeadNode = TransformNode(localRotation = localRot) - val leftIndexProximalTailNode = TransformNode(localRotation = localRot) - val leftIndexIntermediateNode = TransformNode(localRotation = localRot) - val leftIndexDistalNode = TransformNode(localRotation = localRot) - val leftMiddleProximalHeadNode = TransformNode(localRotation = localRot) - val leftMiddleProximalTailNode = TransformNode(localRotation = localRot) - val leftMiddleIntermediateNode = TransformNode(localRotation = localRot) - val leftMiddleDistalNode = TransformNode(localRotation = localRot) - val leftRingProximalHeadNode = TransformNode(localRotation = localRot) - val leftRingProximalTailNode = TransformNode(localRotation = localRot) - val leftRingIntermediateNode = TransformNode(localRotation = localRot) - val leftRingDistalNode = TransformNode(localRotation = localRot) - val leftLittleProximalHeadNode = TransformNode(localRotation = localRot) - val leftLittleProximalTailNode = TransformNode(localRotation = localRot) - val leftLittleIntermediateNode = TransformNode(localRotation = localRot) - val leftLittleDistalNode = TransformNode(localRotation = localRot) - val rightThumbProximalHeadNode = TransformNode(localRotation = localRot) - val rightThumbProximalTailNode = TransformNode(localRotation = localRot) - val rightThumbIntermediateNode = TransformNode(localRotation = localRot) - val rightThumbDistalNode = TransformNode(localRotation = localRot) - val rightIndexProximalHeadNode = TransformNode(localRotation = localRot) - val rightIndexProximalTailNode = TransformNode(localRotation = localRot) - val rightIndexIntermediateNode = TransformNode(localRotation = localRot) - val rightIndexDistalNode = TransformNode(localRotation = localRot) - val rightMiddleProximalHeadNode = TransformNode(localRotation = localRot) - val rightMiddleProximalTailNode = TransformNode(localRotation = localRot) - val rightMiddleIntermediateNode = TransformNode(localRotation = localRot) - val rightMiddleDistalNode = TransformNode(localRotation = localRot) - val rightRingProximalHeadNode = TransformNode(localRotation = localRot) - val rightRingProximalTailNode = TransformNode(localRotation = localRot) - val rightRingIntermediateNode = TransformNode(localRotation = localRot) - val rightRingDistalNode = TransformNode(localRotation = localRot) - val rightLittleProximalHeadNode = TransformNode(localRotation = localRot) - val rightLittleProximalTailNode = TransformNode(localRotation = localRot) - val rightLittleIntermediateNode = TransformNode(localRotation = localRot) - val rightLittleDistalNode = TransformNode(localRotation = localRot) - - private var rootPosition = Vector3.NULL - private var rootRotation = Quaternion.IDENTITY - - init { - // Attach nodes - // Spine - hipsNode.attachChild(spineHeadNode) - spineHeadNode.attachChild(spineTailNode) - spineTailNode.attachChild(chestNode) - chestNode.attachChild(upperChestNode) - upperChestNode.attachChild(neckHeadNode) - neckHeadNode.attachChild(neckTailNode) - neckTailNode.attachChild(headNode) - - // Legs - hipsNode.attachChild(leftHipNode) - hipsNode.attachChild(rightHipNode) - leftHipNode.attachChild(leftKneeNode) - rightHipNode.attachChild(rightKneeNode) - leftKneeNode.attachChild(leftAnkleNode) - rightKneeNode.attachChild(rightAnkleNode) - leftAnkleNode.attachChild(leftFootNode) - rightAnkleNode.attachChild(rightFootNode) - - // Arms - upperChestNode.attachChild(leftShoulderHeadNode) - upperChestNode.attachChild(rightShoulderHeadNode) - leftShoulderHeadNode.attachChild(leftShoulderTailNode) - rightShoulderHeadNode.attachChild(rightShoulderTailNode) - leftShoulderTailNode.attachChild(leftElbowNode) - rightShoulderTailNode.attachChild(rightElbowNode) - leftElbowNode.attachChild(leftWristNode) - rightElbowNode.attachChild(rightWristNode) - leftWristNode.attachChild(leftHandNode) - rightWristNode.attachChild(rightHandNode) - - // Fingers - leftHandNode.attachChild(leftThumbProximalHeadNode) - leftThumbProximalHeadNode.attachChild(leftThumbProximalTailNode) - leftThumbProximalTailNode.attachChild(leftThumbIntermediateNode) - leftThumbIntermediateNode.attachChild(leftThumbDistalNode) - leftHandNode.attachChild(leftIndexProximalHeadNode) - leftIndexProximalHeadNode.attachChild(leftIndexProximalTailNode) - leftIndexProximalTailNode.attachChild(leftIndexIntermediateNode) - leftIndexIntermediateNode.attachChild(leftIndexDistalNode) - leftHandNode.attachChild(leftMiddleProximalHeadNode) - leftMiddleProximalHeadNode.attachChild(leftMiddleProximalTailNode) - leftMiddleProximalTailNode.attachChild(leftMiddleIntermediateNode) - leftMiddleIntermediateNode.attachChild(leftMiddleDistalNode) - leftHandNode.attachChild(leftRingProximalHeadNode) - leftRingProximalHeadNode.attachChild(leftRingProximalTailNode) - leftRingProximalTailNode.attachChild(leftRingIntermediateNode) - leftRingIntermediateNode.attachChild(leftRingDistalNode) - leftHandNode.attachChild(leftLittleProximalHeadNode) - leftLittleProximalHeadNode.attachChild(leftLittleProximalTailNode) - leftLittleProximalTailNode.attachChild(leftLittleIntermediateNode) - leftLittleIntermediateNode.attachChild(leftLittleDistalNode) - rightHandNode.attachChild(rightThumbProximalHeadNode) - rightThumbProximalHeadNode.attachChild(rightThumbProximalTailNode) - rightThumbProximalTailNode.attachChild(rightThumbIntermediateNode) - rightThumbIntermediateNode.attachChild(rightThumbDistalNode) - rightHandNode.attachChild(rightIndexProximalHeadNode) - rightIndexProximalHeadNode.attachChild(rightIndexProximalTailNode) - rightIndexProximalTailNode.attachChild(rightIndexIntermediateNode) - rightIndexIntermediateNode.attachChild(rightIndexDistalNode) - rightHandNode.attachChild(rightMiddleProximalHeadNode) - rightMiddleProximalHeadNode.attachChild(rightMiddleProximalTailNode) - rightMiddleProximalTailNode.attachChild(rightMiddleIntermediateNode) - rightMiddleIntermediateNode.attachChild(rightMiddleDistalNode) - rightHandNode.attachChild(rightRingProximalHeadNode) - rightRingProximalHeadNode.attachChild(rightRingProximalTailNode) - rightRingProximalTailNode.attachChild(rightRingIntermediateNode) - rightRingIntermediateNode.attachChild(rightRingDistalNode) - rightHandNode.attachChild(rightLittleProximalHeadNode) - rightLittleProximalHeadNode.attachChild(rightLittleProximalTailNode) - rightLittleProximalTailNode.attachChild(rightLittleIntermediateNode) - rightLittleIntermediateNode.attachChild(rightLittleDistalNode) - } - - fun update() { - // Set the upper chest node's rotation to the chest's - upperChestNode.localTransform.rotation = chestNode.localTransform.rotation - // Update the root node - hipsNode.update() - } - - fun setRootPose(globalPos: Vector3, globalRot: Quaternion) { - rootPosition = globalPos - rootRotation = globalRot - } - - fun setGlobalRotationForBone(unityBone: UnityBone, globalRot: Quaternion) { - val node = getHeadNodeOfBone(unityBone) - if (node != null) { - node.localTransform.rotation = if (UnityBone.isLeftArmBone(unityBone)) { - globalRot * LEFT_SHOULDER_OFFSET - } else if (UnityBone.isRightArmBone(unityBone)) { - globalRot * RIGHT_SHOULDER_OFFSET - } else { - globalRot - } - } - } - - fun setLocalRotationForBone(unityBone: UnityBone, localRot: Quaternion) { - val node = getHeadNodeOfBone(unityBone) - if (node != null) { - if (unityBone == UnityBone.HIPS) { - node.worldTransform.rotation = localRot - } else { - node.localTransform.rotation = if (UnityBone.isLeftStartOfArmOrFingerBone(unityBone)) { - localRot * RIGHT_SHOULDER_OFFSET - } else if (UnityBone.isRightStartOfArmOrFingerBone(unityBone)) { - localRot * LEFT_SHOULDER_OFFSET - } else { - localRot - } - } - } - } - - fun getGlobalTranslationForBone(unityBone: UnityBone): Vector3 { - val node = getHeadNodeOfBone(unityBone) - return if (node != null) { - if (unityBone == UnityBone.HIPS) { - val hipsAverage = ( - leftHipNode.worldTransform.translation + - rightHipNode.worldTransform.translation - ) * - 0.5f - node.worldTransform.translation * 2f - hipsAverage + rootPosition - } else { - node.worldTransform.translation + rootPosition - } - } else { - Vector3.NULL - } - } - - fun getLocalTranslationForBone(unityBone: UnityBone): Vector3 { - val node = getHeadNodeOfBone(unityBone) - return if (node != null) { - if (unityBone == UnityBone.HIPS) { - val hipsAverage = ( - leftHipNode.worldTransform.translation + - rightHipNode.worldTransform.translation - ) * - 0.5f - node.worldTransform.translation * 2f - hipsAverage + rootPosition - } else { - node.localTransform.translation - } - } else { - Vector3.NULL - } - } - - fun getGlobalRotationForBone(unityBone: UnityBone?): Quaternion { - val node = getHeadNodeOfBone(unityBone) - return if (node != null) { - node.worldTransform.rotation * rootRotation - } else { - Quaternion.IDENTITY - } - } - - fun getLocalRotationForBone(unityBone: UnityBone): Quaternion { - val node = getHeadNodeOfBone(unityBone) - return if (node != null) { - if (unityBone == UnityBone.HIPS) { - node.worldTransform.rotation * rootRotation - } else { - node.parent!!.worldTransform.rotation.inv() * node.worldTransform.rotation - } - } else { - Quaternion.IDENTITY - } - } - - fun getHeadNodeOfBone(unityBone: UnityBone?): TransformNode? = if (unityBone == null) { - null - } else { - when (unityBone) { - UnityBone.HEAD -> neckTailNode - UnityBone.NECK -> neckHeadNode - UnityBone.UPPER_CHEST -> chestNode - UnityBone.CHEST -> spineTailNode - UnityBone.SPINE -> spineHeadNode - UnityBone.HIPS -> hipsNode - UnityBone.LEFT_UPPER_LEG -> leftHipNode - UnityBone.RIGHT_UPPER_LEG -> rightHipNode - UnityBone.LEFT_LOWER_LEG -> leftKneeNode - UnityBone.RIGHT_LOWER_LEG -> rightKneeNode - UnityBone.LEFT_FOOT -> leftAnkleNode - UnityBone.RIGHT_FOOT -> rightAnkleNode - UnityBone.LEFT_SHOULDER -> leftShoulderHeadNode - UnityBone.RIGHT_SHOULDER -> rightShoulderHeadNode - UnityBone.LEFT_UPPER_ARM -> leftShoulderTailNode - UnityBone.RIGHT_UPPER_ARM -> rightShoulderTailNode - UnityBone.LEFT_LOWER_ARM -> leftElbowNode - UnityBone.RIGHT_LOWER_ARM -> rightElbowNode - UnityBone.LEFT_HAND -> leftWristNode - UnityBone.RIGHT_HAND -> rightWristNode - UnityBone.LEFT_THUMB_PROXIMAL -> leftThumbProximalHeadNode - UnityBone.LEFT_THUMB_INTERMEDIATE -> leftThumbProximalTailNode - UnityBone.LEFT_THUMB_DISTAL -> leftThumbIntermediateNode - UnityBone.LEFT_INDEX_PROXIMAL -> leftIndexProximalHeadNode - UnityBone.LEFT_INDEX_INTERMEDIATE -> leftIndexProximalTailNode - UnityBone.LEFT_INDEX_DISTAL -> leftIndexIntermediateNode - UnityBone.LEFT_MIDDLE_PROXIMAL -> leftMiddleProximalHeadNode - UnityBone.LEFT_MIDDLE_INTERMEDIATE -> leftMiddleProximalTailNode - UnityBone.LEFT_MIDDLE_DISTAL -> leftMiddleIntermediateNode - UnityBone.LEFT_RING_PROXIMAL -> leftRingProximalHeadNode - UnityBone.LEFT_RING_INTERMEDIATE -> leftRingProximalTailNode - UnityBone.LEFT_RING_DISTAL -> leftRingIntermediateNode - UnityBone.LEFT_LITTLE_PROXIMAL -> leftLittleProximalHeadNode - UnityBone.LEFT_LITTLE_INTERMEDIATE -> leftLittleProximalTailNode - UnityBone.LEFT_LITTLE_DISTAL -> leftLittleIntermediateNode - UnityBone.RIGHT_THUMB_PROXIMAL -> rightThumbProximalHeadNode - UnityBone.RIGHT_THUMB_INTERMEDIATE -> rightThumbProximalTailNode - UnityBone.RIGHT_THUMB_DISTAL -> rightThumbIntermediateNode - UnityBone.RIGHT_INDEX_PROXIMAL -> rightIndexProximalHeadNode - UnityBone.RIGHT_INDEX_INTERMEDIATE -> rightIndexProximalTailNode - UnityBone.RIGHT_INDEX_DISTAL -> rightIndexIntermediateNode - UnityBone.RIGHT_MIDDLE_PROXIMAL -> rightMiddleProximalHeadNode - UnityBone.RIGHT_MIDDLE_INTERMEDIATE -> rightMiddleProximalTailNode - UnityBone.RIGHT_MIDDLE_DISTAL -> rightMiddleIntermediateNode - UnityBone.RIGHT_RING_PROXIMAL -> rightRingProximalHeadNode - UnityBone.RIGHT_RING_INTERMEDIATE -> rightRingProximalTailNode - UnityBone.RIGHT_RING_DISTAL -> rightRingIntermediateNode - UnityBone.RIGHT_LITTLE_PROXIMAL -> rightLittleProximalHeadNode - UnityBone.RIGHT_LITTLE_INTERMEDIATE -> rightLittleProximalTailNode - UnityBone.RIGHT_LITTLE_DISTAL -> rightLittleIntermediateNode - else -> null - } - } - - companion object { - private val LEFT_SHOULDER_OFFSET = EulerAngles(EulerOrder.YZX, 0f, 0f, FastMath.HALF_PI).toQuaternion() - private val RIGHT_SHOULDER_OFFSET = EulerAngles(EulerOrder.YZX, 0f, 0f, -FastMath.HALF_PI).toQuaternion() - } -} diff --git a/server/core/src/main/java/dev/slimevr/osc/UnityBone.kt b/server/core/src/main/java/dev/slimevr/osc/UnityBone.kt deleted file mode 100644 index 29d89f8f0e..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/UnityBone.kt +++ /dev/null @@ -1,346 +0,0 @@ -package dev.slimevr.osc - -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.trackers.TrackerPosition -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Unity HumanBodyBones from: - * https://docs.unity3d.com/ScriptReference/HumanBodyBones.html - */ -@Serializable -enum class UnityBone( - val stringVal: String, - val boneType: BoneType?, - val trackerPosition: TrackerPosition?, -) { - @SerialName("hips") - HIPS("Hips", BoneType.HIP, TrackerPosition.HIP), - - @SerialName("leftUpperLeg") - LEFT_UPPER_LEG("LeftUpperLeg", BoneType.LEFT_UPPER_LEG, TrackerPosition.LEFT_UPPER_LEG), - - @SerialName("rightUpperLeg") - RIGHT_UPPER_LEG("RightUpperLeg", BoneType.RIGHT_UPPER_LEG, TrackerPosition.RIGHT_UPPER_LEG), - - @SerialName("leftLowerLeg") - LEFT_LOWER_LEG("LeftLowerLeg", BoneType.LEFT_LOWER_LEG, TrackerPosition.LEFT_LOWER_LEG), - - @SerialName("rightLowerLeg") - RIGHT_LOWER_LEG("RightLowerLeg", BoneType.RIGHT_LOWER_LEG, TrackerPosition.RIGHT_LOWER_LEG), - - @SerialName("leftFoot") - LEFT_FOOT("LeftFoot", BoneType.LEFT_FOOT, TrackerPosition.LEFT_FOOT), - - @SerialName("rightFoot") - RIGHT_FOOT("RightFoot", BoneType.RIGHT_FOOT, TrackerPosition.RIGHT_FOOT), - - @SerialName("spine") - SPINE("Spine", BoneType.WAIST, TrackerPosition.WAIST), - - @SerialName("chest") - CHEST("Chest", BoneType.CHEST, TrackerPosition.CHEST), - - @SerialName("upperChest") - UPPER_CHEST("UpperChest", BoneType.CHEST, TrackerPosition.CHEST), - - @SerialName("neck") - NECK("Neck", BoneType.NECK, TrackerPosition.NECK), - - @SerialName("head") - HEAD("Head", BoneType.HEAD, TrackerPosition.HEAD), - - @SerialName("leftShoulder") - LEFT_SHOULDER("LeftShoulder", BoneType.LEFT_SHOULDER, TrackerPosition.LEFT_SHOULDER), - - @SerialName("rightShoulder") - RIGHT_SHOULDER("RightShoulder", BoneType.RIGHT_SHOULDER, TrackerPosition.RIGHT_SHOULDER), - - @SerialName("leftUpperArm") - LEFT_UPPER_ARM("LeftUpperArm", BoneType.LEFT_UPPER_ARM, TrackerPosition.LEFT_UPPER_ARM), - - @SerialName("rightUpperArm") - RIGHT_UPPER_ARM("RightUpperArm", BoneType.RIGHT_UPPER_ARM, TrackerPosition.RIGHT_UPPER_ARM), - - @SerialName("leftLowerArm") - LEFT_LOWER_ARM("LeftLowerArm", BoneType.LEFT_LOWER_ARM, TrackerPosition.LEFT_LOWER_ARM), - - @SerialName("rightLowerArm") - RIGHT_LOWER_ARM("RightLowerArm", BoneType.RIGHT_LOWER_ARM, TrackerPosition.RIGHT_LOWER_ARM), - - @SerialName("leftHand") - LEFT_HAND("LeftHand", BoneType.LEFT_HAND, TrackerPosition.LEFT_HAND), - - @SerialName("rightHand") - RIGHT_HAND("RightHand", BoneType.RIGHT_HAND, TrackerPosition.RIGHT_HAND), - - @SerialName("leftToes") - LEFT_TOES("LeftToes", null, null), - - @SerialName("rightToes") - RIGHT_TOES("RightToes", null, null), - - @SerialName("leftEye") - LEFT_EYE("LeftEye", null, null), - - @SerialName("rightEye") - RIGHT_EYE("RightEye", null, null), - - @SerialName("jaw") - JAW("Jaw", null, null), - - @SerialName("leftThumbMetacarpal") - LEFT_THUMB_PROXIMAL("LeftThumbProximal", BoneType.LEFT_THUMB_METACARPAL, TrackerPosition.LEFT_THUMB_METACARPAL), - - @SerialName("leftThumbProximal") - LEFT_THUMB_INTERMEDIATE("LeftThumbIntermediate", BoneType.LEFT_THUMB_PROXIMAL, TrackerPosition.LEFT_THUMB_PROXIMAL), - - @SerialName("leftThumbDistal") - LEFT_THUMB_DISTAL("LeftThumbDistal", BoneType.LEFT_THUMB_DISTAL, TrackerPosition.LEFT_THUMB_DISTAL), - - @SerialName("leftIndexProximal") - LEFT_INDEX_PROXIMAL("LeftIndexProximal", BoneType.LEFT_INDEX_PROXIMAL, TrackerPosition.LEFT_INDEX_PROXIMAL), - - @SerialName("leftIndexIntermediate") - LEFT_INDEX_INTERMEDIATE("LeftIndexIntermediate", BoneType.LEFT_INDEX_INTERMEDIATE, TrackerPosition.LEFT_INDEX_INTERMEDIATE), - - @SerialName("leftIndexDistal") - LEFT_INDEX_DISTAL("LeftIndexDistal", BoneType.LEFT_INDEX_DISTAL, TrackerPosition.LEFT_INDEX_DISTAL), - - @SerialName("leftMiddleProximal") - LEFT_MIDDLE_PROXIMAL("LeftMiddleProximal", BoneType.LEFT_MIDDLE_PROXIMAL, TrackerPosition.LEFT_MIDDLE_PROXIMAL), - - @SerialName("leftMiddleIntermediate") - LEFT_MIDDLE_INTERMEDIATE("LeftMiddleIntermediate", BoneType.LEFT_MIDDLE_INTERMEDIATE, TrackerPosition.LEFT_MIDDLE_INTERMEDIATE), - - @SerialName("leftMiddleDistal") - LEFT_MIDDLE_DISTAL("LeftMiddleDistal", BoneType.LEFT_MIDDLE_DISTAL, TrackerPosition.LEFT_MIDDLE_DISTAL), - - @SerialName("leftRingProximal") - LEFT_RING_PROXIMAL("LeftRingProximal", BoneType.LEFT_RING_PROXIMAL, TrackerPosition.LEFT_RING_PROXIMAL), - - @SerialName("leftRingIntermediate") - LEFT_RING_INTERMEDIATE("LeftRingIntermediate", BoneType.LEFT_RING_INTERMEDIATE, TrackerPosition.LEFT_RING_INTERMEDIATE), - - @SerialName("leftRingDistal") - LEFT_RING_DISTAL("LeftRingDistal", BoneType.LEFT_RING_DISTAL, TrackerPosition.LEFT_RING_DISTAL), - - @SerialName("leftLittleProximal") - LEFT_LITTLE_PROXIMAL("LeftLittleProximal", BoneType.LEFT_LITTLE_PROXIMAL, TrackerPosition.LEFT_LITTLE_PROXIMAL), - - @SerialName("leftLittleIntermediate") - LEFT_LITTLE_INTERMEDIATE("LeftLittleIntermediate", BoneType.LEFT_LITTLE_INTERMEDIATE, TrackerPosition.LEFT_LITTLE_INTERMEDIATE), - - @SerialName("leftLittleDistal") - LEFT_LITTLE_DISTAL("LeftLittleDistal", BoneType.LEFT_LITTLE_DISTAL, TrackerPosition.LEFT_LITTLE_DISTAL), - - @SerialName("rightThumbMetacarpal") - RIGHT_THUMB_PROXIMAL("RightThumbProximal", BoneType.RIGHT_THUMB_METACARPAL, TrackerPosition.RIGHT_THUMB_METACARPAL), - - @SerialName("rightThumbProximal") - RIGHT_THUMB_INTERMEDIATE("RightThumbIntermediate", BoneType.RIGHT_THUMB_PROXIMAL, TrackerPosition.RIGHT_THUMB_PROXIMAL), - - @SerialName("rightThumbDistal") - RIGHT_THUMB_DISTAL("RightThumbDistal", BoneType.RIGHT_THUMB_DISTAL, TrackerPosition.RIGHT_THUMB_DISTAL), - - @SerialName("rightIndexProximal") - RIGHT_INDEX_PROXIMAL("RightIndexProximal", BoneType.RIGHT_INDEX_PROXIMAL, TrackerPosition.RIGHT_INDEX_PROXIMAL), - - @SerialName("rightIndexIntermediate") - RIGHT_INDEX_INTERMEDIATE("RightIndexIntermediate", BoneType.RIGHT_INDEX_INTERMEDIATE, TrackerPosition.RIGHT_INDEX_INTERMEDIATE), - - @SerialName("rightIndexDistal") - RIGHT_INDEX_DISTAL("RightIndexDistal", BoneType.RIGHT_INDEX_DISTAL, TrackerPosition.RIGHT_INDEX_DISTAL), - - @SerialName("rightMiddleProximal") - RIGHT_MIDDLE_PROXIMAL("RightMiddleProximal", BoneType.RIGHT_MIDDLE_PROXIMAL, TrackerPosition.RIGHT_MIDDLE_PROXIMAL), - - @SerialName("rightMiddleIntermediate") - RIGHT_MIDDLE_INTERMEDIATE("RightMiddleIntermediate", BoneType.RIGHT_MIDDLE_INTERMEDIATE, TrackerPosition.RIGHT_MIDDLE_INTERMEDIATE), - - @SerialName("rightMiddleDistal") - RIGHT_MIDDLE_DISTAL("RightMiddleDistal", BoneType.RIGHT_MIDDLE_DISTAL, TrackerPosition.RIGHT_MIDDLE_DISTAL), - - @SerialName("rightRingProximal") - RIGHT_RING_PROXIMAL("RightRingProximal", BoneType.RIGHT_RING_PROXIMAL, TrackerPosition.RIGHT_RING_PROXIMAL), - - @SerialName("rightRingIntermediate") - RIGHT_RING_INTERMEDIATE("RightRingIntermediate", BoneType.RIGHT_RING_INTERMEDIATE, TrackerPosition.RIGHT_RING_INTERMEDIATE), - - @SerialName("rightRingDistal") - RIGHT_RING_DISTAL("RightRingDistal", BoneType.RIGHT_RING_DISTAL, TrackerPosition.RIGHT_RING_DISTAL), - - @SerialName("rightLittleProximal") - RIGHT_LITTLE_PROXIMAL("RightLittleProximal", BoneType.RIGHT_LITTLE_PROXIMAL, TrackerPosition.RIGHT_LITTLE_PROXIMAL), - - @SerialName("rightLittleIntermediate") - RIGHT_LITTLE_INTERMEDIATE("RightLittleIntermediate", BoneType.RIGHT_LITTLE_INTERMEDIATE, TrackerPosition.RIGHT_LITTLE_INTERMEDIATE), - - @SerialName("rightLittleDistal") - RIGHT_LITTLE_DISTAL("RightLittleDistal", BoneType.RIGHT_LITTLE_DISTAL, TrackerPosition.RIGHT_LITTLE_DISTAL), - - LAST_BONE("LastBone", null, null), - ; - - companion object { - private val byStringVal: Map = values().associateBy { it.stringVal.lowercase() } - - @JvmStatic - fun getByStringVal(stringVal: String): UnityBone? = byStringVal[stringVal.lowercase()] - - /** - * Returns the bone on the opposite limb, or the original bone if - * it not a limb bone. - */ - fun tryGetOppositeArmBone(bone: UnityBone): UnityBone = when (bone) { - LEFT_SHOULDER -> RIGHT_SHOULDER - LEFT_UPPER_ARM -> RIGHT_UPPER_ARM - LEFT_LOWER_ARM -> RIGHT_LOWER_ARM - LEFT_HAND -> RIGHT_HAND - RIGHT_SHOULDER -> LEFT_SHOULDER - RIGHT_UPPER_ARM -> LEFT_UPPER_ARM - RIGHT_LOWER_ARM -> LEFT_LOWER_ARM - RIGHT_HAND -> LEFT_HAND - LEFT_UPPER_LEG -> RIGHT_UPPER_LEG - LEFT_LOWER_LEG -> RIGHT_LOWER_LEG - LEFT_FOOT -> RIGHT_FOOT - RIGHT_UPPER_LEG -> LEFT_UPPER_LEG - RIGHT_LOWER_LEG -> LEFT_LOWER_LEG - RIGHT_FOOT -> LEFT_FOOT - LEFT_THUMB_PROXIMAL -> RIGHT_THUMB_PROXIMAL - LEFT_THUMB_INTERMEDIATE -> RIGHT_THUMB_INTERMEDIATE - LEFT_THUMB_DISTAL -> RIGHT_THUMB_DISTAL - LEFT_INDEX_PROXIMAL -> RIGHT_INDEX_PROXIMAL - LEFT_INDEX_INTERMEDIATE -> RIGHT_INDEX_INTERMEDIATE - LEFT_INDEX_DISTAL -> RIGHT_INDEX_DISTAL - LEFT_MIDDLE_PROXIMAL -> RIGHT_MIDDLE_PROXIMAL - LEFT_MIDDLE_INTERMEDIATE -> RIGHT_MIDDLE_INTERMEDIATE - LEFT_MIDDLE_DISTAL -> RIGHT_MIDDLE_DISTAL - LEFT_RING_PROXIMAL -> RIGHT_RING_PROXIMAL - LEFT_RING_INTERMEDIATE -> RIGHT_RING_INTERMEDIATE - LEFT_RING_DISTAL -> RIGHT_RING_DISTAL - LEFT_LITTLE_PROXIMAL -> RIGHT_LITTLE_PROXIMAL - LEFT_LITTLE_INTERMEDIATE -> RIGHT_LITTLE_INTERMEDIATE - LEFT_LITTLE_DISTAL -> RIGHT_LITTLE_DISTAL - RIGHT_THUMB_PROXIMAL -> LEFT_THUMB_PROXIMAL - RIGHT_THUMB_INTERMEDIATE -> LEFT_THUMB_INTERMEDIATE - RIGHT_THUMB_DISTAL -> LEFT_THUMB_DISTAL - RIGHT_INDEX_PROXIMAL -> LEFT_INDEX_PROXIMAL - RIGHT_INDEX_INTERMEDIATE -> LEFT_INDEX_INTERMEDIATE - RIGHT_INDEX_DISTAL -> LEFT_INDEX_DISTAL - RIGHT_MIDDLE_PROXIMAL -> LEFT_MIDDLE_PROXIMAL - RIGHT_MIDDLE_INTERMEDIATE -> LEFT_MIDDLE_INTERMEDIATE - RIGHT_MIDDLE_DISTAL -> LEFT_MIDDLE_DISTAL - RIGHT_RING_PROXIMAL -> LEFT_RING_PROXIMAL - RIGHT_RING_INTERMEDIATE -> LEFT_RING_INTERMEDIATE - RIGHT_RING_DISTAL -> LEFT_RING_DISTAL - RIGHT_LITTLE_PROXIMAL -> LEFT_LITTLE_PROXIMAL - RIGHT_LITTLE_INTERMEDIATE -> LEFT_LITTLE_INTERMEDIATE - RIGHT_LITTLE_DISTAL -> LEFT_LITTLE_DISTAL - else -> bone - } - - /** - * Returns true if the bone is part of the left arm (incl. fingers, excl. shoulder) - */ - fun isLeftArmBone(bone: UnityBone): Boolean = bone == LEFT_UPPER_ARM || - bone == LEFT_LOWER_ARM || - bone == LEFT_HAND || - bone == LEFT_THUMB_PROXIMAL || - bone == LEFT_THUMB_INTERMEDIATE || - bone == LEFT_THUMB_DISTAL || - bone == LEFT_INDEX_PROXIMAL || - bone == LEFT_INDEX_INTERMEDIATE || - bone == LEFT_INDEX_DISTAL || - bone == LEFT_MIDDLE_PROXIMAL || - bone == LEFT_MIDDLE_INTERMEDIATE || - bone == LEFT_MIDDLE_DISTAL || - bone == LEFT_RING_PROXIMAL || - bone == LEFT_RING_INTERMEDIATE || - bone == LEFT_RING_DISTAL || - bone == LEFT_LITTLE_PROXIMAL || - bone == LEFT_LITTLE_INTERMEDIATE || - bone == LEFT_LITTLE_DISTAL - - /** - * Returns true if the bone is part of the right arm (incl. fingers, excl. shoulder) - */ - fun isRightArmBone(bone: UnityBone): Boolean = bone == RIGHT_UPPER_ARM || - bone == RIGHT_LOWER_ARM || - bone == RIGHT_HAND || - bone == RIGHT_THUMB_PROXIMAL || - bone == RIGHT_THUMB_INTERMEDIATE || - bone == RIGHT_THUMB_DISTAL || - bone == RIGHT_INDEX_PROXIMAL || - bone == RIGHT_INDEX_INTERMEDIATE || - bone == RIGHT_INDEX_DISTAL || - bone == RIGHT_MIDDLE_PROXIMAL || - bone == RIGHT_MIDDLE_INTERMEDIATE || - bone == RIGHT_MIDDLE_DISTAL || - bone == RIGHT_RING_PROXIMAL || - bone == RIGHT_RING_INTERMEDIATE || - bone == RIGHT_RING_DISTAL || - bone == RIGHT_LITTLE_PROXIMAL || - bone == RIGHT_LITTLE_INTERMEDIATE || - bone == RIGHT_LITTLE_DISTAL - - /** - * Returns true if the bone is the left upper arm or proximal left finger bone - */ - fun isLeftStartOfArmOrFingerBone(bone: UnityBone): Boolean = bone == LEFT_UPPER_ARM || - bone == LEFT_THUMB_PROXIMAL || - bone == LEFT_INDEX_PROXIMAL || - bone == LEFT_MIDDLE_PROXIMAL || - bone == LEFT_RING_PROXIMAL || - bone == LEFT_LITTLE_PROXIMAL - - /** - * Returns true if the bone is the right upper arm or proximal right finger bone - */ - fun isRightStartOfArmOrFingerBone(bone: UnityBone): Boolean = bone == RIGHT_UPPER_ARM || - bone == RIGHT_THUMB_PROXIMAL || - bone == RIGHT_INDEX_PROXIMAL || - bone == RIGHT_MIDDLE_PROXIMAL || - bone == RIGHT_RING_PROXIMAL || - bone == RIGHT_LITTLE_PROXIMAL - - /** - * Returns true if the bone is part of the left fingers - */ - fun isLeftFingerBone(bone: UnityBone): Boolean = bone == LEFT_THUMB_PROXIMAL || - bone == LEFT_THUMB_INTERMEDIATE || - bone == LEFT_THUMB_DISTAL || - bone == LEFT_INDEX_PROXIMAL || - bone == LEFT_INDEX_INTERMEDIATE || - bone == LEFT_INDEX_DISTAL || - bone == LEFT_MIDDLE_PROXIMAL || - bone == LEFT_MIDDLE_INTERMEDIATE || - bone == LEFT_MIDDLE_DISTAL || - bone == LEFT_RING_PROXIMAL || - bone == LEFT_RING_INTERMEDIATE || - bone == LEFT_RING_DISTAL || - bone == LEFT_LITTLE_PROXIMAL || - bone == LEFT_LITTLE_INTERMEDIATE || - bone == LEFT_LITTLE_DISTAL - - /** - * Returns true if the bone part of the right fingers - */ - fun isRightFingerBone(bone: UnityBone): Boolean = bone == RIGHT_THUMB_PROXIMAL || - bone == RIGHT_THUMB_INTERMEDIATE || - bone == RIGHT_THUMB_DISTAL || - bone == RIGHT_INDEX_PROXIMAL || - bone == RIGHT_INDEX_INTERMEDIATE || - bone == RIGHT_INDEX_DISTAL || - bone == RIGHT_MIDDLE_PROXIMAL || - bone == RIGHT_MIDDLE_INTERMEDIATE || - bone == RIGHT_MIDDLE_DISTAL || - bone == RIGHT_RING_PROXIMAL || - bone == RIGHT_RING_INTERMEDIATE || - bone == RIGHT_RING_DISTAL || - bone == RIGHT_LITTLE_PROXIMAL || - bone == RIGHT_LITTLE_INTERMEDIATE || - bone == RIGHT_LITTLE_DISTAL - } -} diff --git a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt deleted file mode 100644 index 52b40325e5..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt +++ /dev/null @@ -1,511 +0,0 @@ -package dev.slimevr.osc - -import com.illposed.osc.OSCBundle -import com.illposed.osc.OSCMessage -import com.illposed.osc.OSCMessageEvent -import com.illposed.osc.OSCMessageListener -import com.illposed.osc.OSCSerializeException -import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector -import com.illposed.osc.transport.OSCPortIn -import com.illposed.osc.transport.OSCPortOut -import dev.slimevr.VRServer -import dev.slimevr.VRServer.Companion.currentLocalTrackerId -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.config.VMCConfig -import dev.slimevr.osc.UnityBone.Companion.getByStringVal -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY -import io.github.axisangles.ktmath.Vector3 -import io.github.axisangles.ktmath.Vector3.Companion.NULL -import io.github.axisangles.ktmath.Vector3.Companion.POS_Y -import java.io.IOException -import java.net.InetAddress -import java.net.InetSocketAddress - -/** - * VMC documentation: https://protocol.vmc.info/english - * - * - * Notes: VMC uses local rotation from hip (unlike SlimeVR, which uses rotations - * from head). VMC works with Unity's coordinate system, which means - * Quaternions' z and w components and Vectors' z components need to be inverse - */ -class VMCHandler( - private val server: VRServer, - private val humanPoseManager: HumanPoseManager, - private val config: VMCConfig, -) : OSCHandler { - private var oscReceiver: OSCPortIn? = null - private var oscSender: OSCPortOut? = null - private val computedTrackers: MutableList = FastList() - private val oscArgs = FastList() - private val startTime = System.currentTimeMillis() - private val byTrackerNameTracker: MutableMap = HashMap() - private var yawOffset = IDENTITY - private var inputUnityArmature: UnityArmature? = null - private var outputUnityArmature: UnityArmature? = null - private var vrmHeight = 0f - private var trackerDevice: Device? = null - private var timeAtLastError: Long = 0 - private var timeAtLastSend: Long = 0 - private var anchorHip = false - private var mirrorTracking = false - private var lastPortIn = 0 - private var lastPortOut = 0 - private var lastAddress: InetAddress? = null - - init { - refreshSettings(false) - } - - override fun refreshSettings(refreshRouterSettings: Boolean) { - anchorHip = config.anchorHip - mirrorTracking = config.mirrorTracking - - updateOscReceiver( - config.portIn, - arrayOf( - "/VMC/Ext/Bone/Pos", - "/VMC/Ext/Hmd/Pos", - "/VMC/Ext/Con/Pos", - "/VMC/Ext/Tra/Pos", - "/VMC/Ext/Root/Pos", - ), - ) - updateOscSender(config.portOut, config.address) - - if (config.enabled) { - // Load VRM data - if (outputUnityArmature != null && config.vrmJson != null) { - val vrmReader = VRMReader(config.vrmJson!!) - for (unityBone in UnityBone.entries) { - val node = outputUnityArmature!!.getHeadNodeOfBone(unityBone) - if (node != null) { - val offset = if (unityBone == UnityBone.HIPS) { - // For the hip bone, add average of upper leg offsets (which are negative). The hip bone's offset is global. - vrmReader.getOffsetForBone(UnityBone.HIPS) + - ((vrmReader.getOffsetForBone(UnityBone.LEFT_UPPER_LEG) + vrmReader.getOffsetForBone(UnityBone.RIGHT_UPPER_LEG)) / 2f) - } else { - vrmReader.getOffsetForBone(unityBone) - } - - node.localTransform.translation = offset - } - } - // Make sure to account for the upper legs because of the hip. - vrmHeight = ( - vrmReader.getOffsetForBone(UnityBone.HIPS) + - ((vrmReader.getOffsetForBone(UnityBone.LEFT_UPPER_LEG) + vrmReader.getOffsetForBone(UnityBone.RIGHT_UPPER_LEG))) + - vrmReader.getOffsetForBone(UnityBone.SPINE) + - vrmReader.getOffsetForBone(UnityBone.CHEST) + - vrmReader.getOffsetForBone(UnityBone.UPPER_CHEST) + - vrmReader.getOffsetForBone(UnityBone.NECK) + - vrmReader.getOffsetForBone(UnityBone.HEAD) - ).y - } - } - - if (refreshRouterSettings) server.oSCRouter.refreshSettings(false) - } - - override fun updateOscReceiver(portIn: Int, args: Array) { - // Stops listening and closes OSC port - val wasListening = oscReceiver != null && oscReceiver!!.isListening - if (wasListening) { - oscReceiver!!.stopListening() - } - - if (config.enabled) { - // Instantiates the OSC receiver - try { - oscReceiver = OSCPortIn(portIn) - if (lastPortIn != portIn || !wasListening) { - LogManager.info("[VMCHandler] Listening to port $portIn") - } - lastPortIn = portIn - } catch (e: IOException) { - LogManager - .severe( - "[VMCHandler] Error listening to the port $portIn: $e", - ) - } - - // Starts listening for VMC messages - if (oscReceiver != null) { - val listener = OSCMessageListener { event: OSCMessageEvent -> this.handleReceivedMessage(event) } - - for (address in args) { - oscReceiver!! - .dispatcher - .addListener(OSCPatternAddressMessageSelector(address), listener) - } - - oscReceiver!!.startListening() - } - } - } - - override fun updateOscSender(portOut: Int, ip: String) { - // Stop sending - val wasConnected = oscSender != null && oscSender!!.isConnected - if (wasConnected) { - try { - oscSender!!.close() - } catch (e: IOException) { - LogManager.severe("[VMCHandler] Error closing the OSC sender: $e") - } - } - - if (config.enabled) { - // Instantiate the OSC sender - try { - val addr = InetAddress.getByName(ip) - oscSender = OSCPortOut(InetSocketAddress(addr, portOut)) - if ((lastPortOut != portOut && lastAddress != addr) || !wasConnected) { - LogManager - .info( - "[VMCHandler] Sending to port $portOut at address $ip", - ) - } - lastPortOut = portOut - lastAddress = addr - - oscSender!!.connect() - outputUnityArmature = UnityArmature(false) - } catch (e: IOException) { - LogManager - .severe( - "[VMCHandler] Error connecting to port $portOut at the address $ip: $e", - ) - } - } - } - - private fun handleReceivedMessage(event: OSCMessageEvent) { - when (event.message.address) { - // Is bone (rotation) - "/VMC/Ext/Bone/Pos" -> { - var trackerPosition: TrackerPosition? = null - val bone = getByStringVal(event.message.arguments[0].toString()) - if (bone != null) trackerPosition = bone.trackerPosition - - // If received bone is part of SlimeVR's skeleton - if (trackerPosition != null) { - handleReceivedTracker( - "VMC-Bone-" + event.message.arguments[0], - trackerPosition, - null, - Quaternion( - -(event.message.arguments[7] as Float), - event.message.arguments[4] as Float, - event.message.arguments[5] as Float, - -(event.message.arguments[6] as Float), - ), - true, - getByStringVal( - event.message.arguments[0].toString(), - ), - ) - } - } - - // Is tracker (position + rotation) - "/VMC/Ext/Hmd/Pos", "/VMC/Ext/Con/Pos", "/VMC/Ext/Tra/Pos" -> - handleReceivedTracker( - "VMC-Tracker-" + event.message.arguments[0], - null, - Vector3( - event.message.arguments[1] as Float, - event.message.arguments[2] as Float, - -(event.message.arguments[3] as Float), - ), - Quaternion( - -(event.message.arguments[7] as Float), - event.message.arguments[4] as Float, - event.message.arguments[5] as Float, - -(event.message.arguments[6] as Float), - ), - false, - null, - ) - - // Is VMC tracking root (offsets all rotations) - "/VMC/Ext/Root/Pos" -> { - if (inputUnityArmature != null) { - inputUnityArmature!! - .setRootPose( - Vector3( - event.message.arguments[1] as Float, - event.message.arguments[2] as Float, - -(event.message.arguments[3] as Float), - ), - Quaternion( - -(event.message.arguments[7] as Float), - event.message.arguments[4] as Float, - event.message.arguments[5] as Float, - -(event.message.arguments[6] as Float), - ), - ) - } - } - } - } - - private fun handleReceivedTracker( - name: String, - trackerPosition: TrackerPosition?, - position: Vector3?, - rotation: Quaternion, - localRotation: Boolean, - unityBone: UnityBone?, - ) { - // Create device if it doesn't exist - var rot = rotation - if (trackerDevice == null) { - trackerDevice = server.deviceManager.createDevice("VMC receiver", "1.0", "VMC") - server.deviceManager.addDevice(trackerDevice!!) - } - - // Try to get tracker - var tracker = byTrackerNameTracker[name] - - // Create tracker if trying to get it returned null - if (tracker == null) { - tracker = Tracker( - trackerDevice, - getNextLocalTrackerId(), - name, - "VMC Tracker #$currentLocalTrackerId", - trackerPosition, - hasPosition = position != null, - hasRotation = true, - userEditable = true, - isComputed = position != null, - usesTimeout = true, - allowReset = position != null, - ) - trackerDevice!!.trackers[trackerDevice!!.trackers.size] = tracker - byTrackerNameTracker[name] = tracker - server.registerTracker(tracker) - } - tracker.status = TrackerStatus.OK - - // Set position - if (position != null) { - tracker.position = position - } - - // Set rotation - if (localRotation) { - // Instantiate unityHierarchy if not done - if (inputUnityArmature == null) inputUnityArmature = UnityArmature(true) - inputUnityArmature!!.setLocalRotationForBone(unityBone!!, rot) - rot = inputUnityArmature!!.getGlobalRotationForBone(unityBone) - rot = yawOffset.times(rot) - } - tracker.setRotation(rot) - - tracker.dataTick() - } - - override fun update() { - // Update unity hierarchy - if (inputUnityArmature != null) inputUnityArmature!!.update() - - val currentTime = System.currentTimeMillis() - if (currentTime - timeAtLastSend > 3) { // 200hz to not crash VSF - timeAtLastSend = currentTime - // Send OSC data - if (oscSender != null && oscSender!!.isConnected) { - // Create new OSC Bundle - val oscBundle = OSCBundle() - - // Add our relative time - oscArgs.clear() - oscArgs.add((System.currentTimeMillis() - startTime) / 1000f) - oscBundle.addPacket(OSCMessage("/VMC/Ext/T", oscArgs.clone())) - - if (humanPoseManager.isSkeletonPresent) { - // Indicate tracking is available - oscArgs.clear() - oscArgs.add(1) - oscBundle.addPacket(OSCMessage("/VMC/Ext/OK", oscArgs.clone())) - - oscArgs.clear() - oscArgs.add("root") - addTransformToArgs(NULL, IDENTITY) - oscBundle.addPacket(OSCMessage("/VMC/Ext/Root/Pos", oscArgs.clone())) - - for (unityBone in UnityBone.entries) { - // Get opposite bone if tracking must be mirrored - val boneType = (if (mirrorTracking) UnityBone.tryGetOppositeArmBone(unityBone) else unityBone).boneType - - if (boneType == null) continue - - // Get SlimeVR bone - val bone = humanPoseManager.getBone(boneType) - - // Update unity hierarchy from bone's global rotation - val boneRotation = if (mirrorTracking) { - // Mirror tracking horizontally - val rotBuf = bone.getGlobalRotation() * bone.rotationOffset.inv() - Quaternion(rotBuf.w, rotBuf.x, -rotBuf.y, -rotBuf.z) - } else { - bone.getGlobalRotation() * bone.rotationOffset.inv() - } - outputUnityArmature?.setGlobalRotationForBone(unityBone, boneRotation) - } - - if (!anchorHip) { - // Anchor from head - outputUnityArmature?.let { unityArmature -> - // Scale the SlimeVR neck position with the VRM model - // We're only getting the height up to the neck because we don't want to factor the neck's length into the scaling - val slimevrScaledRootPos = humanPoseManager.getBone(BoneType.NECK).getTailPosition() * - (vrmHeight / humanPoseManager.userNeckHeightFromConfig) - - // Get the VRM head and hip positions - val vrmHeadPos = unityArmature.getHeadNodeOfBone(UnityBone.HEAD)!!.parent!!.worldTransform.translation - val vrmHipPos = unityArmature.getHeadNodeOfBone(UnityBone.HIPS)!!.worldTransform.translation - - // Calculate the new VRM hip position by subtracting the difference head-hip distance from the SlimeVR head - val calculatedVrmHipPos = slimevrScaledRootPos - (vrmHeadPos - vrmHipPos) - - // Set the VRM's hip position - unityArmature.getHeadNodeOfBone(UnityBone.HIPS)?.localTransform?.translation = calculatedVrmHipPos - } - } - - // Update Unity skeleton - outputUnityArmature?.update() - - // Add Unity humanoid bones transforms - for (unityBone in UnityBone.entries) { - // Don't send bones for which we don't have an equivalent - // Don't send fingers if we don't have any tracker for them - // Don't send arm bones if we're tracking from the controller - if (unityBone.boneType != null && - (!UnityBone.isLeftFingerBone(unityBone) || humanPoseManager.skeleton.hasLeftFingerTracker || (mirrorTracking && humanPoseManager.skeleton.hasRightFingerTracker)) && - (!UnityBone.isRightFingerBone(unityBone) || humanPoseManager.skeleton.hasRightFingerTracker || (mirrorTracking && humanPoseManager.skeleton.hasLeftFingerTracker)) && - !(humanPoseManager.isTrackingLeftArmFromController && (UnityBone.isLeftArmBone(unityBone) || unityBone == UnityBone.LEFT_SHOULDER)) && - !(humanPoseManager.isTrackingRightArmFromController && (UnityBone.isRightArmBone(unityBone) || unityBone == UnityBone.RIGHT_SHOULDER)) - ) { - oscArgs.clear() - oscArgs.add(unityBone.stringVal) - outputUnityArmature?.let { - addTransformToArgs( - it.getLocalTranslationForBone(unityBone), - it.getLocalRotationForBone(unityBone), - ) - } - - oscBundle.addPacket(OSCMessage("/VMC/Ext/Bone/Pos", oscArgs.clone())) - } - } - } - - for (tracker in computedTrackers) { - if (!tracker.status.reset) { - oscArgs.clear() - - val name = tracker.name - oscArgs.add(name) - - addTransformToArgs( - tracker.position, - tracker.getRotation(), - ) - - var address: String - val role = tracker.trackerPosition - address = if (role == TrackerPosition.HEAD) { - "/VMC/Ext/Hmd/Pos" - } else if (role == TrackerPosition.LEFT_HAND || role == TrackerPosition.RIGHT_HAND) { - "/VMC/Ext/Con/Pos" - } else { - "/VMC/Ext/Tra/Pos" - } - oscBundle - .addPacket( - OSCMessage( - address, - oscArgs.clone(), - ), - ) - } - } - - // Send OSC packets as bundle - try { - oscSender!!.send(oscBundle) - } catch (e: IOException) { - // Avoid spamming AsynchronousCloseException too many - // times per second - if (System.currentTimeMillis() - timeAtLastError > 100) { - timeAtLastError = System.currentTimeMillis() - LogManager - .warning( - "[VMCHandler] Error sending OSC packets: " + - e, - ) - } - } catch (e: OSCSerializeException) { - if (System.currentTimeMillis() - timeAtLastError > 100) { - timeAtLastError = System.currentTimeMillis() - LogManager - .warning( - "[VMCHandler] Error sending OSC packets: " + - e, - ) - } - } - } - } - } - - /** - * Set the Quaternion to shift the received VMC tracking rotations' yaw - * - * @param reference the head's rotation - */ - fun alignVMCTracking(reference: Quaternion) { - yawOffset = reference.project(POS_Y).unit() - } - - /** - * Add a computed tracker to the list of trackers to send. - * - * @param computedTracker the computed tracker - */ - fun addComputedTracker(computedTracker: Tracker) { - computedTrackers.add(computedTracker) - } - - private fun addTransformToArgs(pos: Vector3, rot: Quaternion) { - oscArgs.add(pos.x) - oscArgs.add(pos.y) - oscArgs.add(-pos.z) - oscArgs.add(rot.x) - oscArgs.add(rot.y) - oscArgs.add(-rot.z) - oscArgs.add(-rot.w) - } - - override fun getOscSender(): OSCPortOut = oscSender!! - - override fun getPortOut(): Int = lastPortOut - - override fun getAddress(): InetAddress = lastAddress!! - - override fun getOscReceiver(): OSCPortIn = oscReceiver!! - - override fun getPortIn(): Int = lastPortIn -} diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt deleted file mode 100644 index 1690a8b593..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt +++ /dev/null @@ -1,560 +0,0 @@ -package dev.slimevr.osc - -import com.illposed.osc.OSCBundle -import com.illposed.osc.OSCMessage -import com.illposed.osc.OSCMessageEvent -import com.illposed.osc.OSCMessageListener -import com.illposed.osc.OSCSerializeException -import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector -import com.illposed.osc.transport.OSCPortIn -import com.illposed.osc.transport.OSCPortOut -import com.jme3.math.FastMath -import com.jme3.system.NanoTimer -import dev.slimevr.VRServer -import dev.slimevr.config.VRCOSCConfig -import dev.slimevr.protocol.rpc.setup.RPCUtil -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import java.io.IOException -import java.net.InetAddress -import java.net.InetSocketAddress - -private const val OFFSET_SLERP_FACTOR = 0.5f // Guessed from eyeing VRChat - -/** - * VRChat OSCTracker documentation: https://docs.vrchat.com/docs/osc-trackers - */ -class VRCOSCHandler( - private val server: VRServer, - private val config: VRCOSCConfig, - private val computedTrackers: List, -) : OSCHandler { - private val localIp = RPCUtil.getLocalIp() - private val loopbackIp = InetAddress.getLoopbackAddress().hostAddress - private val vrsystemTrackersAddresses = arrayOf( - "/tracking/vrsystem/head/pose", - "/tracking/vrsystem/leftwrist/pose", - "/tracking/vrsystem/rightwrist/pose", - ) - private val oscTrackersAddresses = arrayOf( - "/tracking/trackers/*/position", - "/tracking/trackers/*/rotation", - ) - private var oscReceiver: OSCPortIn? = null - private var oscSender: OSCPortOut? = null - private var oscQuerySender: OSCPortOut? = null - private var oscMessage: OSCMessage? = null - private var headTracker: Tracker? = null - private var oscTrackersDevice: Device? = null - private var vrsystemTrackersDevice: Device? = null - private val oscArgs = FastList(3) - private val trackersEnabled: BooleanArray = BooleanArray(computedTrackers.size) - private var oscPortIn = 0 - private var oscPortOut = 0 - private var oscIp: InetAddress? = null - private var oscQuerySenderState = false - private var oscQueryPortOut = 0 - private var oscQueryIp: String? = null - private var timeAtLastError: Long = 0 - private var receivingPositionOffset = Vector3.NULL - private var postReceivingPositionOffset = Vector3.NULL - private var receivingRotationOffset = Quaternion.IDENTITY - private var receivingRotationOffsetGoal = Quaternion.IDENTITY - private val postReceivingOffset = EulerAngles(EulerOrder.YXZ, 0f, FastMath.PI, 0f).toQuaternion() - private var timeAtLastReceivedRotationOffset = System.currentTimeMillis() - private var fpsTimer: NanoTimer? = null - private var vrcOscQueryHandler: VRCOSCQueryHandler? = null - - init { - refreshSettings(false) - } - - override fun refreshSettings(refreshRouterSettings: Boolean) { - // Sets which trackers are enabled and force head and hands to false - for (i in computedTrackers.indices) { - if (computedTrackers[i].trackerPosition != TrackerPosition.HEAD || computedTrackers[i].trackerPosition != TrackerPosition.LEFT_HAND || computedTrackers[i].trackerPosition != TrackerPosition.RIGHT_HAND) { - trackersEnabled[i] = config - .getOSCTrackerRole( - computedTrackers[i].trackerPosition!!.trackerRole!!, - false, - ) - } else { - trackersEnabled[i] = false - } - } - - updateOscReceiver(config.portIn, vrsystemTrackersAddresses + oscTrackersAddresses) - updateOscSender(config.portOut, config.address) - - if (vrcOscQueryHandler == null && config.enabled && config.oscqueryEnabled) { - try { - vrcOscQueryHandler = VRCOSCQueryHandler(this) - } catch (e: Throwable) { - LogManager.severe("Unable to initialize OSCQuery: $e", e) - } - } else if (vrcOscQueryHandler != null && (!config.enabled || !config.oscqueryEnabled)) { - vrcOscQueryHandler?.close() - vrcOscQueryHandler = null - } - - if (refreshRouterSettings) { - server.oSCRouter.refreshSettings(false) - } - } - - /** - * Adds an OSC Sender from OSCQuery - */ - fun addOSCQuerySender(oscPortOut: Int, oscIP: String) { - val addr = InetAddress.getByName(oscIP) - oscQuerySenderState = true - oscQueryIp = oscIP - oscQueryPortOut = oscPortOut - if (oscPortOut != portOut || (oscIP != address.hostName && !(oscIP == localIp && address.hostName == loopbackIp))) { - try { - oscQuerySender = OSCPortOut(InetSocketAddress(addr, oscPortOut)) - oscQuerySender?.connect() - LogManager.info("[VRCOSCHandler] OSCQuery sender sending to port $oscPortOut at address $oscIP") - } catch (e: IOException) { - LogManager.severe("[VRCOSCHandler] Error connecting to port $oscPortOut at the address $oscIP: $e") - } - } - } - - /** - * Close/remove the osc query sender - */ - fun closeOscQuerySender(newState: Boolean) { - oscQuerySender?.let { - try { - it.close() - oscQuerySender = null - oscQuerySenderState = newState - } catch (e: IOException) { - LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e") - } - } - } - - override fun updateOscReceiver(portIn: Int, args: Array) { - // Stop listening - val wasListening = oscReceiver != null && oscReceiver!!.isListening - if (wasListening) { - oscReceiver!!.stopListening() - } - - if (config.enabled) { - // Instantiates the OSC receiver - try { - oscReceiver = OSCPortIn(portIn) - if (oscPortIn != portIn || !wasListening) { - LogManager.info("[VRCOSCHandler] Listening to port $portIn") - } - oscPortIn = portIn - vrcOscQueryHandler?.updateOSCQuery(portIn.toUShort()) - } catch (e: IOException) { - LogManager - .severe( - "[VRCOSCHandler] Error listening to the port $portIn: $e", - ) - } - - // Starts listening for VRC or OSCTrackers messages - oscReceiver?.let { - val listener = OSCMessageListener { event: OSCMessageEvent -> - handleReceivedMessage(event) - } - for (address in args) { - it.dispatcher.addListener( - OSCPatternAddressMessageSelector(address), - listener, - ) - } - it.startListening() - } - } - } - - override fun updateOscSender(portOut: Int, ip: String) { - // Stop sending - val wasConnected = oscSender != null && oscSender!!.isConnected - if (wasConnected) { - try { - oscSender!!.close() - } catch (e: IOException) { - LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e") - } - } - - if (config.enabled) { - // Instantiate the OSC sender - try { - val addr = InetAddress.getByName(ip) - oscSender = OSCPortOut(InetSocketAddress(addr, portOut)) - if ((oscPortOut != portOut && oscIp != addr) || !wasConnected) { - LogManager.info("[VRCOSCHandler] Sending to port $portOut at address $ip") - } - oscPortOut = portOut - oscIp = addr - oscSender?.connect() - } catch (e: IOException) { - LogManager - .severe( - "[VRCOSCHandler] Error connecting to port $portOut at the address $ip: $e", - ) - return - } - - if (oscQueryPortOut == portOut && (oscQueryIp == ip || (oscQueryIp == localIp && ip == loopbackIp))) { - if (oscQuerySender != null) { - // Close the oscQuerySender if it has the same port/ip - closeOscQuerySender(true) - } - } else if (oscQuerySender == null && oscQuerySenderState) { - // Instantiate the oscQuerySender if it could not be instantiated. - addOSCQuerySender(oscQueryPortOut, oscQueryIp!!) - } - } - } - - private fun handleReceivedMessage(event: OSCMessageEvent) { - if (vrsystemTrackersAddresses.contains(event.message.address)) { - // Receiving Head and Wrist pose data thanks to OSCQuery - // Create device if it doesn't exist - if (vrsystemTrackersDevice == null) { - // Instantiate OSC Trackers device - vrsystemTrackersDevice = server.deviceManager.createDevice("VRC VRSystem", null, "VRChat") - server.deviceManager.addDevice(vrsystemTrackersDevice!!) - } - - // Look at xxx in "/tracking/vrsystem/xxx/pose" to know TrackerPosition - var name = "VRChat " - val trackerPosition = when (event.message.address.split('/')[3]) { - "head" -> { - name += "head" - TrackerPosition.HEAD - } - - "leftwrist" -> { - name += "left hand" - TrackerPosition.LEFT_HAND - } - - "rightwrist" -> { - name += "right hand" - TrackerPosition.RIGHT_HAND - } - - else -> { - LogManager.warning("[VRCOSCHandler] Received invalid body part in message \"${event.message.address}\"") - return - } - } - - // Try to get the tracker - var tracker = vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] - - // Build the tracker if it doesn't exist - if (tracker == null) { - tracker = Tracker( - device = vrsystemTrackersDevice, - id = VRServer.getNextLocalTrackerId(), - name = name, - displayName = name, - trackerNum = trackerPosition.ordinal, - trackerPosition = trackerPosition, - hasRotation = true, - hasPosition = true, - userEditable = true, - isComputed = true, - allowReset = trackerPosition != TrackerPosition.HEAD, - usesTimeout = true, - ) - vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker - server.registerTracker(tracker) - } - - // Sets the tracker status to OK - tracker.status = TrackerStatus.OK - - // Update tracker position - tracker.position = Vector3( - event.message.arguments[0] as Float, - event.message.arguments[1] as Float, - -(event.message.arguments[2] as Float), - ) - - // Update tracker rotation - val (w, x, y, z) = EulerAngles( - EulerOrder.YXZ, - event.message.arguments[3] as Float * FastMath.DEG_TO_RAD, - event.message.arguments[4] as Float * FastMath.DEG_TO_RAD, - event.message.arguments[5] as Float * FastMath.DEG_TO_RAD, - ).toQuaternion() - val rot = Quaternion(w, -x, -y, z) - tracker.setRotation(rot) - - tracker.dataTick() - } else { - // Receiving OSC Trackers data. This is not from VRChat. - if (oscTrackersDevice == null) { - // Instantiate OSC Trackers device - oscTrackersDevice = server.deviceManager.createDevice("OSC Tracker", null, "OSC Trackers") - server.deviceManager.addDevice(oscTrackersDevice!!) - } - - // Extract the xxx in "/tracking/trackers/xxx/..." - val splitAddress = event.message.address.split('/') - val trackerStringValue = splitAddress[3] - val dataType = event.message.address.split('/')[4] - if (trackerStringValue == "head") { - // Head data - if (dataType == "position") { - // Position offset - receivingPositionOffset = Vector3( - event.message.arguments[0] as Float, - event.message.arguments[1] as Float, - -(event.message.arguments[2] as Float), - ) - - headTracker?.let { - if (it.hasPosition) { - postReceivingPositionOffset = it.position - } - } - } else { - // Rotation offset - val (w, x, y, z) = EulerAngles(EulerOrder.YXZ, event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, event.message.arguments[2] as Float * FastMath.DEG_TO_RAD).toQuaternion() - receivingRotationOffsetGoal = Quaternion(w, -x, -y, z).inv() - - headTracker.let { - receivingRotationOffsetGoal = if (it != null && it.hasRotation) { - it.getRotation().project(Vector3.POS_Y).unit() * receivingRotationOffsetGoal - } else { - receivingRotationOffsetGoal - } - } - - // If greater than 300ms, snap to rotation - if (System.currentTimeMillis() - timeAtLastReceivedRotationOffset > 300) { - receivingRotationOffset = receivingRotationOffsetGoal - } - - // Update time variable - timeAtLastReceivedRotationOffset = System.currentTimeMillis() - } - } else { - // Trackers data (1-8) - val trackerId = trackerStringValue.toInt() - var tracker = oscTrackersDevice!!.trackers[trackerId] - - if (tracker == null) { - tracker = Tracker( - device = oscTrackersDevice, - id = VRServer.getNextLocalTrackerId(), - name = "OSC Tracker #$trackerId", - displayName = "OSC Tracker #$trackerId", - trackerNum = trackerId, - trackerPosition = null, - hasRotation = true, - hasPosition = true, - userEditable = true, - isComputed = true, - allowReset = true, - usesTimeout = true, - ) - oscTrackersDevice!!.trackers[trackerId] = tracker - server.registerTracker(tracker) - } - - // Sets the tracker status to OK - tracker.status = TrackerStatus.OK - - if (dataType == "position") { - // Update tracker position - tracker.position = receivingRotationOffset.sandwich( - Vector3( - event.message.arguments[0] as Float, - event.message.arguments[1] as Float, - -(event.message.arguments[2] as Float), - ) - - receivingPositionOffset, - ) + - postReceivingPositionOffset - } else { - // Update tracker rotation - val (w, x, y, z) = EulerAngles( - EulerOrder.YXZ, - event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, - event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, - event.message.arguments[2] as Float * FastMath.DEG_TO_RAD, - ).toQuaternion() - val rot = Quaternion(w, -x, -y, z) - tracker.setRotation(receivingRotationOffset * rot * postReceivingOffset) - } - - tracker.dataTick() - } - } - } - - override fun update() { - // Gets timer from vrServer - if (fpsTimer == null) { - fpsTimer = VRServer.instance.fpsTimer - } - // Update received trackers' offset rotation slerp - if (receivingRotationOffset != receivingRotationOffsetGoal) { - receivingRotationOffset = receivingRotationOffset.interpR(receivingRotationOffsetGoal, OFFSET_SLERP_FACTOR * (fpsTimer?.timePerFrame ?: 1f)) - } - - // Update current time - val currentTime = System.currentTimeMillis().toFloat() - - // Send OSC data - if (oscSender != null && oscSender!!.isConnected) { - // Create new bundle - val bundle = OSCBundle() - - for (i in computedTrackers.indices) { - if (trackersEnabled[i]) { - // Send regular trackers' positions - val (x, y, z) = computedTrackers[i].position - oscArgs.clear() - oscArgs.add(x) - oscArgs.add(y) - oscArgs.add(-z) - bundle.addPacket( - OSCMessage( - "/tracking/trackers/${getVRCOSCTrackersId(computedTrackers[i].trackerPosition)}/position", - oscArgs.clone(), - ), - ) - - // Send regular trackers' rotations - val (w, x1, y1, z1) = computedTrackers[i].getRotation() - // We flip the X and Y components of the quaternion because - // we flip the z direction when communicating from - // our right-handed API to VRChat's left-handed API. - // X quaternion represents a rotation from y to z - // Y quaternion represents a rotation from z to x - // When we negate the z direction, X and Y quaternion - // components must be negated. - val (_, x2, y2, z2) = Quaternion( - w, - -x1, - -y1, - z1, - ).toEulerAngles(EulerOrder.YXZ) - oscArgs.clear() - oscArgs.add(x2 * FastMath.RAD_TO_DEG) - oscArgs.add(y2 * FastMath.RAD_TO_DEG) - oscArgs.add(z2 * FastMath.RAD_TO_DEG) - bundle.addPacket( - OSCMessage( - "/tracking/trackers/${getVRCOSCTrackersId(computedTrackers[i].trackerPosition)}/rotation", - oscArgs.clone(), - ), - ) - } - if (computedTrackers[i].trackerPosition == TrackerPosition.HEAD) { - // Send HMD position - val (x, y, z) = computedTrackers[i].position - oscArgs.clear() - oscArgs.add(x) - oscArgs.add(y) - oscArgs.add(-z) - bundle.addPacket( - OSCMessage( - "/tracking/trackers/head/position", - oscArgs.clone(), - ), - ) - } - } - - try { - oscSender?.send(bundle) - oscQuerySender?.send(bundle) - } catch (e: IOException) { - // Avoid spamming AsynchronousCloseException too many - // times per second - if (currentTime - timeAtLastError > 100) { - timeAtLastError = System.currentTimeMillis() - LogManager.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e") - } - } catch (e: OSCSerializeException) { - if (currentTime - timeAtLastError > 100) { - timeAtLastError = System.currentTimeMillis() - LogManager.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e") - } - } - } - } - - private fun getVRCOSCTrackersId(trackerPosition: TrackerPosition?): Int { - // Needs to range from 1-8. - // Don't change as third party applications may rely - // on this for mapping trackers to body parts. - return when (trackerPosition) { - TrackerPosition.HIP -> 1 - TrackerPosition.LEFT_FOOT -> 2 - TrackerPosition.RIGHT_FOOT -> 3 - TrackerPosition.LEFT_UPPER_LEG -> 4 - TrackerPosition.RIGHT_UPPER_LEG -> 5 - TrackerPosition.UPPER_CHEST -> 6 - TrackerPosition.LEFT_UPPER_ARM -> 7 - TrackerPosition.RIGHT_UPPER_ARM -> 8 - else -> -1 - } - } - - fun setHeadTracker(headTracker: Tracker?) { - this.headTracker = headTracker - } - - /** - * Sends the expected HMD rotation upon reset to align the trackers in VRC - */ - fun yawAlign(headRot: Quaternion) { - if (oscSender != null && oscSender!!.isConnected) { - val (_, _, y, _) = headRot.toEulerAngles(EulerOrder.YXZ) - oscArgs.clear() - oscArgs.add(0f) - oscArgs.add(-y * FastMath.RAD_TO_DEG) - oscArgs.add(0f) - oscMessage = OSCMessage( - "/tracking/trackers/head/rotation", - oscArgs, - ) - try { - oscSender?.send(oscMessage) - oscQuerySender?.send(oscMessage) - } catch (e: IOException) { - LogManager - .warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e") - } catch (e: OSCSerializeException) { - LogManager - .warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e") - } - } - } - - override fun getOscSender(): OSCPortOut = oscSender!! - - override fun getPortOut(): Int = oscPortOut - - override fun getAddress(): InetAddress = oscIp!! - - override fun getOscReceiver(): OSCPortIn = oscReceiver!! - - override fun getPortIn(): Int = oscPortIn -} diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt deleted file mode 100644 index 827d597a3e..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt +++ /dev/null @@ -1,86 +0,0 @@ -package dev.slimevr.osc - -import OSCQueryNode -import OSCQueryServer -import ServiceInfo -import dev.slimevr.protocol.rpc.setup.RPCUtil -import io.eiren.util.logging.LogManager -import randomFreePort -import java.io.IOException -import kotlin.concurrent.thread - -private const val serviceStartsWith = "VRChat-Client" -private const val queryPath = "/tracking/vrsystem" - -/** - * Handler for OSCQuery for VRChat using our library - * https://github.com/SlimeVR/oscquery-kt - */ -class VRCOSCQueryHandler( - private val vrcOscHandler: VRCOSCHandler, -) { - private val oscQueryServer: OSCQueryServer - - init { - // Request data - val localIp = RPCUtil.getLocalIp() ?: throw IllegalStateException("No local IP address found for OSCQuery to bind to") - val httpPort = randomFreePort() - oscQueryServer = OSCQueryServer( - "SlimeVR-Server-$httpPort", - OscTransport.UDP, - localIp, - vrcOscHandler.portIn.toUShort(), - httpPort, - ) - oscQueryServer.rootNode.addNode(OSCQueryNode(queryPath)) - oscQueryServer.init() - LogManager.info("[VRCOSCQueryHandler] SlimeVR OSCQueryServer started at http://$localIp:$httpPort") - - try { - // Add service listener - LogManager.info("[VRCOSCQueryHandler] Listening for VRChat OSCQuery") - oscQueryServer.service.addServiceListener( - "_osc._udp.local.", - onServiceAdded = ::serviceAdded, - ) - } catch (e: IOException) { - LogManager.warning("[VRCOSCQueryHandler] " + e.message) - } - } - - /** - * Updates the OSC service's port - */ - fun updateOSCQuery(port: UShort) { - if (oscQueryServer.oscPort != port) { - thread(start = true) { - oscQueryServer.updateOscService(port) - } - } - } - - /** - * Called when a service is added - */ - private fun serviceAdded(info: ServiceInfo) { - // Check the service name - if (!info.name.startsWith(serviceStartsWith)) return - - // Get url from ServiceInfo - val ip = info.inetAddresses[0].hostAddress - val port = info.port - - // create a new OSCHandler for this service - vrcOscHandler.addOSCQuerySender(port, ip) - } - - /** - * Closes the OSCQueryServer and the associated OSC sender. - */ - fun close() { - vrcOscHandler.closeOscQuerySender(false) - thread(start = true) { - oscQueryServer.close() - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/osc/VRMReader.kt b/server/core/src/main/java/dev/slimevr/osc/VRMReader.kt deleted file mode 100644 index a567eedb7d..0000000000 --- a/server/core/src/main/java/dev/slimevr/osc/VRMReader.kt +++ /dev/null @@ -1,103 +0,0 @@ -package dev.slimevr.osc - -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Vector3 -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import java.util.* - -private val jsonIgnoreKeys = Json { ignoreUnknownKeys = true } -class VRMReader(vrmJson: String) { - - private val data: GLTF = jsonIgnoreKeys.decodeFromString(vrmJson) - - fun getOffsetForBone(unityBone: UnityBone): Vector3 { - val node = try { - if (data.extensions.vrmV1 != null) { - if (data.extensions.vrmV1.specVersion != "1.0") { - LogManager.warning("[VRMReader] VRM version is not 1.0") - } - data.extensions.vrmV1.humanoid.humanBones.getValue(unityBone).node - } else { - data.extensions.vrmV0?.humanoid?.humanBones?.first { - it.bone.equals(unityBone.stringVal, ignoreCase = true) - }?.node - } - } catch (_: NoSuchElementException) { - LogManager.warning("[VRMReader] Bone ${unityBone.stringVal} not found in JSON") - null - } ?: return Vector3.NULL - - val translationNode = data.nodes[node].translation ?: return Vector3.NULL - - return Vector3(translationNode[0].toFloat(), translationNode[1].toFloat(), translationNode[2].toFloat()) - } -} - -@Serializable -data class GLTF( - val extensions: Extensions, - val extensionsUsed: List, - val nodes: List, -) - -@Serializable -data class Extensions( - @SerialName("VRM") - val vrmV0: VRMV0? = null, - @SerialName("VRMC_vrm") - val vrmV1: VRMV1? = null, -) - -@Serializable -data class VRMV1( - val specVersion: String, - val humanoid: HumanoidV1, -) - -@Serializable -data class HumanoidV1( - val humanBones: Map, -) - -@Serializable -data class HumanBoneV1( - val node: Int, -) - -@Serializable -data class VRMV0( - val humanoid: HumanoidV0, -) - -@Serializable -data class HumanoidV0( - val humanBones: List, - val armStretch: Double, - val legStretch: Double, - val upperArmTwist: Double, - val lowerArmTwist: Double, - val upperLegTwist: Double, - val lowerLegTwist: Double, - val feetSpacing: Double, - val hasTranslationDoF: Boolean, -) - -@Serializable -data class HumanBoneV0( - val bone: String, - val node: Int, - val useDefaultValues: Boolean, -) - -@Serializable -data class Node( - val translation: List? = null, - val rotation: List? = null, - val scale: List? = null, - // GLTF says that there can be a matrix instead of translation, - // rotation and scale, so we need to support that too in the future. - // val matrix: List>, - val children: List = emptyList(), -) diff --git a/server/core/src/main/java/dev/slimevr/platform.kt b/server/core/src/main/java/dev/slimevr/platform.kt new file mode 100644 index 0000000000..ffcd82f89a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/platform.kt @@ -0,0 +1,106 @@ +package dev.slimevr + +import java.io.File +import java.nio.file.Path +import java.util.* +import kotlin.io.path.Path +import kotlin.io.path.exists + +const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR" + +enum class Platform { + LINUX, + WINDOWS, + OSX, + UNKNOWN, +} + +val CURRENT_PLATFORM: Platform = detectPlatform() + +private fun detectPlatform(): Platform { + val os = System.getProperty("os.name").lowercase(Locale.getDefault()) + if (os.contains("win")) return Platform.WINDOWS + if (os.contains("mac") || os.contains("darwin")) return Platform.OSX + if (os.contains("linux") || os.contains("unix")) return Platform.LINUX + return Platform.UNKNOWN +} + +fun getJavaExecutable(forceConsole: Boolean): String { + val bin = System.getProperty("java.home") + File.separator + "bin" + File.separator + + if (CURRENT_PLATFORM == Platform.WINDOWS && !forceConsole) { + val javaw = bin + "javaw.exe" + if (File(javaw).isFile) return javaw + } + + if (CURRENT_PLATFORM == Platform.WINDOWS) return bin + "java.exe" + return bin + "java" +} + +fun getSocketDirectory(): String { + val envDir = System.getenv("SLIMEVR_SOCKET_DIR") + if (envDir != null) return envDir + + if (CURRENT_PLATFORM == Platform.LINUX) { + val xdg = System.getenv("XDG_RUNTIME_DIR") + if (xdg != null) return xdg + } + + return System.getProperty("java.io.tmpdir") +} + +fun resolveConfigDirectory(): Path? { + if (Path("config/").exists()) { // this is only for dev + return Path("config/") + } + + val home = System.getenv("HOME") + + return when (CURRENT_PLATFORM) { + Platform.WINDOWS -> { + val appData = System.getenv("AppData") + if (appData != null) Path(appData, SLIMEVR_IDENTIFIER) else null + } + + Platform.LINUX -> { + val xdg = System.getenv("XDG_CONFIG_HOME") + if (xdg != null) { + Path(xdg, SLIMEVR_IDENTIFIER) + } else if (home != null) { + Path(home, ".config", SLIMEVR_IDENTIFIER) + } else { + null + } + } + + Platform.OSX -> { + if (home != null) Path(home, "Library", "Application Support", SLIMEVR_IDENTIFIER) else null + } + + else -> null + } +} + +fun resolveLogDirectory(): Path? { + val home = System.getenv("HOME") + val appData = System.getenv("AppData") + + return when (CURRENT_PLATFORM) { + Platform.WINDOWS -> if (appData != null) Path(appData, SLIMEVR_IDENTIFIER, "logs") else null + + Platform.OSX -> if (home != null) Path(home, "Library", "Logs", SLIMEVR_IDENTIFIER) else null + + Platform.LINUX -> { + val xdg = System.getenv("XDG_DATA_HOME") + if (xdg != null) { + Path(xdg, SLIMEVR_IDENTIFIER, "logs") + } else if (home != null) { + Path(home, ".local", "share", SLIMEVR_IDENTIFIER, "logs") + } else { + null + } + } + + else -> null + } +} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/PfrIO.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/PfrIO.kt deleted file mode 100644 index bfeb2908ac..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/PfrIO.kt +++ /dev/null @@ -1,189 +0,0 @@ -package dev.slimevr.poseframeformat - -import dev.slimevr.poseframeformat.trackerdata.TrackerFrame -import dev.slimevr.poseframeformat.trackerdata.TrackerFrameData -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByDesignation -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.io.DataInputStream -import java.io.DataOutputStream -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException - -object PfrIO { - @Throws(IOException::class) - private fun writeVector3f(outputStream: DataOutputStream, vector: Vector3) { - outputStream.writeFloat(vector.x) - outputStream.writeFloat(vector.y) - outputStream.writeFloat(vector.z) - } - - @Throws(IOException::class) - private fun writeQuaternion(outputStream: DataOutputStream, quaternion: Quaternion) { - outputStream.writeFloat(quaternion.x) - outputStream.writeFloat(quaternion.y) - outputStream.writeFloat(quaternion.z) - outputStream.writeFloat(quaternion.w) - } - - fun writeFrame(outputStream: DataOutputStream, trackerFrame: TrackerFrame?) { - if (trackerFrame == null) { - outputStream.writeInt(0) - return - } - - var dataFlags = trackerFrame.dataFlags - - // Don't write destination strings anymore, replace with - // the enum - if (trackerFrame.hasData(TrackerFrameData.DESIGNATION_STRING)) { - dataFlags = TrackerFrameData.TRACKER_POSITION_ENUM - .add(TrackerFrameData.DESIGNATION_STRING.remove(dataFlags)) - } - outputStream.writeInt(dataFlags) - if (trackerFrame.hasData(TrackerFrameData.ROTATION)) { - writeQuaternion(outputStream, trackerFrame.rotation!!) - } - if (trackerFrame.hasData(TrackerFrameData.POSITION)) { - writeVector3f(outputStream, trackerFrame.position!!) - } - if (TrackerFrameData.TRACKER_POSITION_ENUM.check(dataFlags)) { - // ID is offset by 1 for historical reasons - outputStream.writeInt(trackerFrame.trackerPosition!!.id - 1) - } - if (trackerFrame.hasData(TrackerFrameData.ACCELERATION)) { - writeVector3f(outputStream, trackerFrame.acceleration!!) - } - if (trackerFrame.hasData(TrackerFrameData.RAW_ROTATION)) { - writeQuaternion(outputStream, trackerFrame.rawRotation!!) - } - } - - fun writeFrames(outputStream: DataOutputStream, frames: PoseFrames) { - outputStream.writeInt(frames.frameHolders.size) - for (tracker in frames.frameHolders) { - outputStream.writeUTF(tracker.name) - outputStream.writeInt(tracker.frames.size) - for (i in 0 until tracker.frames.size) { - writeFrame(outputStream, tracker.tryGetFrame(i)) - } - } - } - - fun tryWriteFrames(outputStream: DataOutputStream, frames: PoseFrames): Boolean = try { - writeFrames(outputStream, frames) - true - } catch (e: Exception) { - LogManager.severe("Error writing frame to stream.", e) - false - } - - fun writeToFile(file: File, frames: PoseFrames) { - DataOutputStream( - BufferedOutputStream(FileOutputStream(file)), - ).use { writeFrames(it, frames) } - } - - fun tryWriteToFile(file: File, frames: PoseFrames): Boolean = try { - writeToFile(file, frames) - true - } catch (e: Exception) { - LogManager.severe("Error writing frames to file.", e) - false - } - - @Throws(IOException::class) - private fun readVector3f(inputStream: DataInputStream): Vector3 = Vector3( - inputStream.readFloat(), - inputStream.readFloat(), - inputStream.readFloat(), - ) - - @Throws(IOException::class) - private fun readQuaternion(inputStream: DataInputStream): Quaternion { - val x = inputStream.readFloat() - val y = inputStream.readFloat() - val z = inputStream.readFloat() - val w = inputStream.readFloat() - - return Quaternion(w, x, y, z) - } - - fun readFrame(inputStream: DataInputStream): TrackerFrame { - val dataFlags = inputStream.readInt() - - var designation: TrackerPosition? = null - if (TrackerFrameData.DESIGNATION_STRING.check(dataFlags)) { - designation = getByDesignation(inputStream.readUTF()) - } - var rotation: Quaternion? = null - if (TrackerFrameData.ROTATION.check(dataFlags)) { - rotation = readQuaternion(inputStream) - } - var position: Vector3? = null - if (TrackerFrameData.POSITION.check(dataFlags)) { - position = readVector3f(inputStream) - } - if (TrackerFrameData.TRACKER_POSITION_ENUM.check(dataFlags)) { - // ID is offset by 1 for historical reasons - designation = TrackerPosition.getById(inputStream.readInt() + 1) - } - var acceleration: Vector3? = null - if (TrackerFrameData.ACCELERATION.check(dataFlags)) { - acceleration = readVector3f(inputStream) - } - var rawRotation: Quaternion? = null - if (TrackerFrameData.RAW_ROTATION.check(dataFlags)) { - rawRotation = readQuaternion(inputStream) - } - - return TrackerFrame( - designation, - rotation, - position, - acceleration, - rawRotation, - ) - } - - fun readFrames(inputStream: DataInputStream): PoseFrames { - val trackerCount = inputStream.readInt() - val trackers = FastList(trackerCount) - for (i in 0 until trackerCount) { - val name = inputStream.readUTF() - val trackerFrameCount = inputStream.readInt() - val trackerFrames = FastList( - trackerFrameCount, - ) - for (j in 0 until trackerFrameCount) { - trackerFrames.add(readFrame(inputStream)) - } - trackers.add(TrackerFrames(name, trackerFrames)) - } - return PoseFrames(trackers) - } - - fun tryReadFrames(inputStream: DataInputStream): PoseFrames? = try { - readFrames(inputStream) - } catch (e: Exception) { - LogManager.severe("Error reading frames from stream.", e) - null - } - - fun readFromFile(file: File): PoseFrames = DataInputStream(BufferedInputStream(FileInputStream(file))).use { readFrames(it) } - - fun tryReadFromFile(file: File): PoseFrames? = try { - readFromFile(file) - } catch (e: Exception) { - LogManager.severe("Error reading frames from file.", e) - null - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/PfsIO.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/PfsIO.kt deleted file mode 100644 index d8b3d13e33..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/PfsIO.kt +++ /dev/null @@ -1,197 +0,0 @@ -package dev.slimevr.poseframeformat - -import dev.slimevr.config.SkeletonConfig -import dev.slimevr.poseframeformat.trackerdata.TrackerFrame -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import io.eiren.util.logging.LogManager -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.io.DataInputStream -import java.io.DataOutputStream -import java.io.EOFException -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException - -/** - * PoseFrameStream File IO, designed to handle the internal PoseFrames format with - * the new file format for streaming and storing additional debugging info - */ -object PfsIO { - private fun writeRecordingDef(stream: DataOutputStream, frameInterval: Float) { - stream.writeByte(PfsPackets.RECORDING_DEFINITION.id) - stream.writeFloat(frameInterval) - } - - private fun writeTrackerDef(stream: DataOutputStream, id: Int, name: String) { - stream.writeByte(PfsPackets.TRACKER_DEFINITION.id) - stream.writeByte(id) - stream.writeUTF(name) - } - - private fun writeTrackerFrame(stream: DataOutputStream, id: Int, frameIndex: Int, frame: TrackerFrame) { - stream.writeByte(PfsPackets.TRACKER_FRAME.id) - stream.writeByte(id) - stream.writeInt(frameIndex) - // Write frame data (same format as PFR) - PfrIO.writeFrame(stream, frame) - } - - private fun writeBodyProportions(stream: DataOutputStream, skeletonConfig: SkeletonConfig) { - stream.writeByte(PfsPackets.PROPORTIONS_CONFIG.id) - // HMD height - stream.writeFloat(skeletonConfig.hmdHeight) - // Floor height - stream.writeFloat(skeletonConfig.floorHeight) - // Write config map - stream.writeShort(skeletonConfig.offsets.size) - for ((key, value) in skeletonConfig.offsets) { - stream.writeUTF(key) - stream.writeFloat(value) - } - } - - fun writeFrames(stream: DataOutputStream, frames: PoseFrames) { - // Give trackers IDs (max 255) - val trackers = frames.frameHolders.mapIndexed { i, t -> i to t } - - // Write recording definition - writeRecordingDef(stream, frames.frameInterval) - - // Write tracker definitions - for (tracker in trackers) { - writeTrackerDef(stream, tracker.first, tracker.second.name) - } - - // Write tracker frames - for (i in 0 until frames.maxFrameCount) { - for (tracker in trackers) { - // If the tracker has a frame at the index - val frame = tracker.second.tryGetFrame(i) - if (frame != null) { - writeTrackerFrame(stream, tracker.first, i, frame) - } - } - } - } - - fun tryWriteFrames(stream: DataOutputStream, frames: PoseFrames): Boolean = try { - writeFrames(stream, frames) - true - } catch (e: Exception) { - LogManager.severe("[PfsIO] Error writing frame to stream.", e) - false - } - - fun writeToFile(file: File, frames: PoseFrames) { - DataOutputStream( - BufferedOutputStream(FileOutputStream(file)), - ).use { writeFrames(it, frames) } - } - - fun tryWriteToFile(file: File, frames: PoseFrames): Boolean = try { - writeToFile(file, frames) - true - } catch (e: Exception) { - LogManager.severe("[PfsIO] Error writing frames to file.", e) - false - } - - fun readFrame(stream: DataInputStream, poseFrames: PoseFrames, trackers: MutableMap) { - val packetId = stream.readUnsignedByte() - val packetType = PfsPackets.byId[packetId] - - when (packetType) { - null -> { - throw IOException("Encountered unknown packet ID ($packetId) while deserializing PFS stream.") - } - - PfsPackets.RECORDING_DEFINITION -> { - // Unused, useful for debugging - val frameInterval = stream.readFloat() - poseFrames.frameInterval = frameInterval - LogManager.debug("[PfsIO] Frame interval: $frameInterval s") - } - - PfsPackets.TRACKER_DEFINITION -> { - val trackerId = stream.readUnsignedByte() - val name = stream.readUTF() - - // Get or make tracker and set its name - trackers.getOrPut(trackerId) { - TrackerFrames(name) - }.name = name - } - - PfsPackets.TRACKER_FRAME -> { - val trackerId = stream.readUnsignedByte() - val tracker = trackers.getOrPut(trackerId) { - // If tracker doesn't exist yet, make one - TrackerFrames() - } - val frameNum = stream.readInt() - val frame = PfrIO.readFrame(stream) - - tracker.frames.add(frameNum, frame) - } - - PfsPackets.PROPORTIONS_CONFIG -> { - // Unused, useful for debugging - - val hmdHeight = stream.readFloat() - val floorHeight = stream.readFloat() - LogManager.debug("[PfsIO] HMD height: $hmdHeight, Floor height: $floorHeight") - - // Currently just prints JSON format config to console - val configCount = stream.readUnsignedShort() - val sb = StringBuilder("[PfsIO] Body proportion configs ($configCount): {") - for (i in 0 until configCount) { - if (i > 0) { - sb.append(", ") - } - sb.append(stream.readUTF()) - sb.append(": ") - sb.append(stream.readFloat()) - } - sb.append('}') - - LogManager.debug(sb.toString()) - } - } - } - - fun readFrames(stream: DataInputStream): PoseFrames { - val poseFrames = PoseFrames() - val trackers = mutableMapOf() - - while (true) { - try { - readFrame(stream, poseFrames, trackers) - } catch (_: EOFException) { - // Reached end of stream, stop reading and return the recording - // LogManager.debug("[PfsIO] Reached end of PFS stream.", e) - break - } - } - - poseFrames.frameHolders.addAll(trackers.values) - return poseFrames - } - - fun tryReadFrames(stream: DataInputStream): PoseFrames? = try { - readFrames(stream) - } catch (e: Exception) { - LogManager.severe("[PfsIO] Error reading frames from stream.", e) - null - } - - fun readFromFile(file: File): PoseFrames = DataInputStream(BufferedInputStream(FileInputStream(file))).use { readFrames(it) } - - fun tryReadFromFile(file: File): PoseFrames? = try { - readFromFile(file) - } catch (e: Exception) { - LogManager.severe("[PfsIO] Error reading frames from file.", e) - null - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/PfsPackets.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/PfsPackets.kt deleted file mode 100644 index f0c6d0da76..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/PfsPackets.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.slimevr.poseframeformat - -/** - * Packet ID ([UByte]), - * Packet data (see [PfsPackets], implemented in [PfsIO]) - */ -enum class PfsPackets(val id: Int) { - /** - * Frame interval ([Float] seconds) - */ - RECORDING_DEFINITION(0), - - /** - * Tracker ID ([UByte]), - * Tracker name (UTF-8 [String]) - */ - TRACKER_DEFINITION(1), - - /** - * Tracker ID ([UByte]), - * Frame number ([UInt]), - * PFR frame data (see [PfrIO.writeFrame] & [PfrIO.readFrame]) - */ - TRACKER_FRAME(2), - - /** - * Hmd height ([Float]), - * Floor height ([Float]), - * Body proportion configs (Count ([UShort]) x (Key (UTF-8 [String]), Value ([Float])) - */ - PROPORTIONS_CONFIG(3), - ; - - val byteId = id.toUByte() - - companion object { - val byId = entries.associateBy { it.id } - val byByteId = entries.associateBy { it.byteId } - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/PoseFrames.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/PoseFrames.kt deleted file mode 100644 index 40842e2691..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/PoseFrames.kt +++ /dev/null @@ -1,164 +0,0 @@ -package dev.slimevr.poseframeformat - -import dev.slimevr.poseframeformat.trackerdata.TrackerFrame -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.trackers.TrackerPosition -import io.eiren.util.collections.FastList - -class PoseFrames : Iterable> { - val frameHolders: FastList - - /** - * Frame interval in seconds - */ - var frameInterval: Float = 0.02f - - /** - * Creates a [PoseFrames] object with the provided list of - * [TrackerFrames]s as the internal [TrackerFrames] list. - * - * @see [FastList] - * @see [TrackerFrames] - */ - constructor(frameHolders: FastList) { - this.frameHolders = frameHolders - } - - /** - * Creates a [PoseFrames] object with the specified initial tracker - * capacity. - * - * @see [PoseFrames] - */ - constructor(initialCapacity: Int = 5) { - frameHolders = FastList(initialCapacity) - } - - /** - * @return The [TrackerFrames] associated with [position] at frame - * index [index]. - */ - fun getTrackerForPosition(position: TrackerPosition, index: Int = 0): TrackerFrames? { - for (tracker in frameHolders) { - if (tracker.tryGetFrame(index)?.trackerPosition == position) return tracker - } - return null - } - - // region Data Utilities - - /** - * @return The maximum Y value of the [TrackerFrames] associated with the - * [TrackerPosition.HEAD] tracker position on the first frame, otherwise `0f` if - * no [TrackerFrames] is associated with [TrackerPosition.HEAD] or if there are no - * valid positions. - * @see [getMaxHeight] - */ - val maxHmdHeight: Float - get() { - return getMaxHeight( - getTrackerForPosition(TrackerPosition.HEAD) - ?: return 0f, - ) - } - - /** - * @return The maximum Y value of the [trackerFrames], otherwise `0f` if - * there are no valid positions. - * @see [TrackerPosition] - */ - fun getMaxHeight(trackerFrames: TrackerFrames): Float { - var maxHeight = 0f - for (frame in trackerFrames.frames) { - val framePosition = frame?.tryGetPosition() ?: continue - - if (framePosition.y > maxHeight) { - maxHeight = framePosition.y - } - } - - return maxHeight - } - // endregion - - /** - * @return The maximum number of [TrackerFrame]s contained within each - * [TrackerFrames] in the internal [TrackerFrames] list. - * @see [TrackerFrames.frames] - * @see [List.size] - */ - val maxFrameCount: Int - get() { - return frameHolders.maxOfOrNull { tracker -> tracker.frames.size } ?: 0 - } - - /** - * Using the provided array buffer, get the [TrackerFrame]s contained - * within each [TrackerFrames] in the internal - * [TrackerFrames] list at the specified index. - * - * @return The number of frames written to the buffer. - * @see [TrackerFrames.tryGetFrame] - */ - fun getFrames(frameIndex: Int, buffer: Array): Int { - var frameCount = 0 - for (tracker in frameHolders) { - if (tracker == null) { - continue - } - val frame = tracker.tryGetFrame(frameIndex) ?: continue - buffer[frameCount++] = frame - } - return frameCount - } - - /** - * Using the provided [List] buffer, get the [TrackerFrame]s - * contained within each [TrackerFrames] in the internal - * [TrackerFrames] list at the specified index. - * - * @return The number of frames written to the buffer. - * @see [TrackerFrames.tryGetFrame] - */ - fun getFrames(frameIndex: Int, buffer: MutableList): Int { - var frameCount = 0 - for (tracker in frameHolders) { - if (tracker == null) { - continue - } - val frame = tracker.tryGetFrame(frameIndex) ?: continue - buffer[frameCount++] = frame - } - return frameCount - } - - /** - * @return The [TrackerFrame]s contained within each - * [TrackerFrames] in the internal [TrackerFrames] list at - * the specified index. - * @see [TrackerFrames.tryGetFrame] - */ - fun getFrames(frameIndex: Int): Array { - val trackerFrames = arrayOfNulls(frameHolders.size) - getFrames(frameIndex, trackerFrames) - return trackerFrames - } - - override fun iterator(): Iterator> = PoseFrameIterator(this) - - inner class PoseFrameIterator(private val poseFrame: PoseFrames) : Iterator> { - private val trackerFrameBuffer: Array = arrayOfNulls(poseFrame.frameHolders.size) - private val maxCursor = poseFrame.maxFrameCount - private var cursor = 0 - - override fun hasNext(): Boolean = frameHolders.isNotEmpty() && cursor < maxCursor - - override fun next(): Array { - if (!hasNext()) { - throw NoSuchElementException() - } - poseFrame.getFrames(cursor++, trackerFrameBuffer) - return trackerFrameBuffer - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/PoseRecorder.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/PoseRecorder.kt deleted file mode 100644 index a2d1ee8e83..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/PoseRecorder.kt +++ /dev/null @@ -1,162 +0,0 @@ -package dev.slimevr.poseframeformat - -import dev.slimevr.VRServer -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.util.TickReducer -import dev.slimevr.util.ann.VRServerThread -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import org.apache.commons.lang3.tuple.Pair -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException -import java.util.concurrent.Future -import java.util.function.Consumer - -class PoseRecorder(private val server: VRServer) { - inner class RecordingProgress(val frame: Int, val totalFrames: Int) - - private var poseFrame: PoseFrames? = null - private var numFrames = -1 - private var frameCursor = 0 - - // Default 50 TPS - private val ticker = TickReducer({ onTick() }, 0.02f) - - private var recordingFuture: CompletableFuture? = null - private var frameCallback: Consumer? = null - var trackers = FastList>() - - init { - server.addOnTick { - if (numFrames > 0) { - ticker.tick(server.fpsTimer.timePerFrame) - } - } - } - - // Make sure it's synchronized since this is the server thread interacting with - // an unknown outside thread controlling this class - @Synchronized - @VRServerThread - fun onTick() { - if (frameCursor >= numFrames) { - // If done and hasn't yet, send finished recording - stopFrameRecording() - return - } - - // A stopped recording will be accounted for by an empty "trackers" list - val cursor = frameCursor++ - for (tracker in trackers) { - // Add a frame for each tracker - tracker.right.addFrameFromTracker(cursor, tracker.left) - } - - // Send the number of finished frames - frameCallback?.accept(RecordingProgress(frameCursor, numFrames)) - // If done, send finished recording - if (frameCursor >= numFrames) { - stopFrameRecording() - } - } - - @Synchronized - fun startFrameRecording( - numFrames: Int, - interval: Float, - trackers: List = server.allTrackers, - frameCallback: Consumer? = null, - ): Future { - require(numFrames >= 1) { "numFrames must at least have a value of 1." } - require(interval > 0) { "interval must be greater than 0." } - require(trackers.isNotEmpty()) { "trackers must have at least one entry." } - - cancelFrameRecording() - val poseFrame = PoseFrames(trackers.size) - poseFrame.frameInterval = interval - - // Update tracker list - this.trackers.ensureCapacity(trackers.size) - for (tracker in trackers) { - // Ignore null and internal trackers - if (tracker == null || tracker.isInternal) { - continue - } - - // Create a tracker recording - val trackerFrames = TrackerFrames(tracker, numFrames) - poseFrame.frameHolders.add(trackerFrames) - - // Pair tracker with recording - this.trackers.add(Pair.of(tracker, trackerFrames)) - } - require(this.trackers.isNotEmpty()) { "trackers must have at least one valid tracker." } - - // Ticking setup - ticker.interval = interval - ticker.reset() - - val recordingFuture = CompletableFuture() - this.recordingFuture = recordingFuture - this.frameCallback = frameCallback - - // Recording setup - this.poseFrame = poseFrame - frameCursor = 0 - this.numFrames = numFrames - - LogManager.info( - "[PoseRecorder] Recording $numFrames samples at a $interval s frame interval", - ) - - return recordingFuture - } - - @Synchronized - private fun internalStopFrameRecording(cancel: Boolean) { - val currentRecording = recordingFuture - if (currentRecording != null && !currentRecording.isDone) { - val currentFrames = poseFrame - if (cancel || currentFrames == null) { - // If it's supposed to be cancelled or there's actually no recording, - // then cancel the recording and return nothing - currentRecording.cancel(true) - } else { - // Stop the recording, returning the frames recorded - currentRecording.complete(currentFrames) - } - } - numFrames = -1 - frameCursor = 0 - trackers.clear() - poseFrame = null - } - - @Synchronized - fun stopFrameRecording() { - internalStopFrameRecording(false) - } - - @Synchronized - fun cancelFrameRecording() { - internalStopFrameRecording(true) - } - - val isReadyToRecord: Boolean - get() = server.trackersCount > 0 - - val isRecording: Boolean - get() = numFrames > frameCursor - - fun hasRecording(): Boolean = recordingFuture != null - - val framesAsync: Future? - get() = recordingFuture - - @get:Throws(ExecutionException::class, InterruptedException::class) - val frames: PoseFrames? - get() { - return recordingFuture?.get() - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/player/PlayerTracker.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/player/PlayerTracker.kt deleted file mode 100644 index 0421613820..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/player/PlayerTracker.kt +++ /dev/null @@ -1,72 +0,0 @@ -package dev.slimevr.poseframeformat.player - -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.trackers.Tracker - -class PlayerTracker(val trackerFrames: TrackerFrames, val tracker: Tracker, private var internalCursor: Int = 0, private var internalScale: Float = 1f) { - - var cursor: Int - get() = internalCursor - set(value) { - val limitedCursor = limitCursor(value) - internalCursor = limitedCursor - setTrackerStateFromIndex(limitedCursor) - } - - var scale: Float - get() = internalScale - set(value) { - internalScale = value - setTrackerStateFromIndex() - } - - init { - setTrackerStateFromIndex(limitCursor()) - } - - fun limitCursor(cursor: Int): Int { - return if (cursor < 0 || trackerFrames.frames.isEmpty()) { - return 0 - } else if (cursor >= trackerFrames.frames.size) { - return trackerFrames.frames.size - 1 - } else { - cursor - } - } - - fun limitCursor(): Int { - val limitedCursor = limitCursor(internalCursor) - internalCursor = limitedCursor - return limitedCursor - } - - private fun setTrackerStateFromIndex(index: Int = internalCursor) { - val frame = trackerFrames.tryGetFrame(index) ?: return - - /* - * TODO: No way to set adjusted rotation manually? That might be nice to have... - * for now we'll stick with just setting the final rotation as raw and not - * enabling any adjustments - */ - - val trackerPosition = frame.tryGetTrackerPosition() - if (trackerPosition != null) { - tracker.trackerPosition = trackerPosition - } - - val rotation = frame.tryGetRotation() - if (rotation != null) { - tracker.setRotation(rotation) - } - - val position = frame.tryGetPosition() - if (position != null) { - tracker.position = position * internalScale - } - - val acceleration = frame.tryGetAcceleration() - if (acceleration != null) { - tracker.setAcceleration(acceleration * internalScale) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/player/TrackerFramesPlayer.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/player/TrackerFramesPlayer.kt deleted file mode 100644 index d65b1b7250..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/player/TrackerFramesPlayer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.slimevr.poseframeformat.player - -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.poseframeformat.trackerdata.TrackerFrame -import dev.slimevr.poseframeformat.trackerdata.TrackerFrames -import dev.slimevr.tracking.trackers.Tracker - -class TrackerFramesPlayer(vararg val frameHolders: TrackerFrames) { - - val playerTrackers: Array = frameHolders.map { trackerFrames -> - PlayerTracker( - trackerFrames, - trackerFrames.toTracker(), - ) - }.toTypedArray() - - val trackers: Array = - playerTrackers.map { playerTracker -> playerTracker.tracker }.toTypedArray() - - /** - * @return The maximum number of [TrackerFrame]s contained within each - * [TrackerFrames] in the internal [TrackerFrames] array. - * @see [TrackerFrames.frames] - * @see [List.size] - */ - val maxFrameCount: Int - get() { - return frameHolders.maxOfOrNull { tracker -> tracker.frames.size } ?: 0 - } - - constructor(poseFrames: PoseFrames) : this(frameHolders = poseFrames.frameHolders.toTypedArray()) - - fun setCursors(index: Int) { - for (playerTracker in playerTrackers) { - playerTracker.cursor = index - } - } - - fun setScales(scale: Float) { - for (playerTracker in playerTrackers) { - playerTracker.scale = scale - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrame.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrame.kt deleted file mode 100644 index 33747f98b1..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrame.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.slimevr.poseframeformat.trackerdata - -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 - -data class TrackerFrame( - val trackerPosition: TrackerPosition? = null, - val rotation: Quaternion? = null, - val position: Vector3? = null, - val acceleration: Vector3? = null, - val rawRotation: Quaternion? = null, -) { - val dataFlags: Int - - val name: String - get() = "TrackerFrame:/${trackerPosition?.designation ?: "null"}" - - init { - var initDataFlags = 0 - - if (trackerPosition != null) { - initDataFlags = TrackerFrameData.TRACKER_POSITION_ENUM.add(initDataFlags) - } - if (rotation != null) { - initDataFlags = TrackerFrameData.ROTATION.add(initDataFlags) - } - if (position != null) { - initDataFlags = TrackerFrameData.POSITION.add(initDataFlags) - } - if (acceleration != null) { - initDataFlags = TrackerFrameData.ACCELERATION.add(initDataFlags) - } - if (rawRotation != null) { - initDataFlags = TrackerFrameData.RAW_ROTATION.add(initDataFlags) - } - - dataFlags = initDataFlags - } - - fun hasData(flag: TrackerFrameData): Boolean = flag.check(dataFlags) - - // region Tracker Try Getters - fun tryGetTrackerPosition(): TrackerPosition? = if (hasData(TrackerFrameData.TRACKER_POSITION_ENUM) || hasData(TrackerFrameData.DESIGNATION_STRING)) { - trackerPosition - } else { - null - } - - fun tryGetRotation(): Quaternion? = if (hasData(TrackerFrameData.ROTATION)) { - rotation - } else { - null - } - - fun tryGetRawRotation(): Quaternion? = if (hasData(TrackerFrameData.RAW_ROTATION)) { - rawRotation - } else { - null - } - - fun tryGetPosition(): Vector3? = if (hasData(TrackerFrameData.POSITION)) { - position - } else { - null - } - - fun tryGetAcceleration(): Vector3? = if (hasData(TrackerFrameData.ACCELERATION)) { - acceleration - } else { - null - } - - fun hasRotation(): Boolean = hasData(TrackerFrameData.ROTATION) - - fun hasPosition(): Boolean = hasData(TrackerFrameData.POSITION) - - fun hasAcceleration(): Boolean = hasData(TrackerFrameData.ACCELERATION) - // endregion - - companion object { - val empty = TrackerFrame() - - fun fromTracker(tracker: Tracker): TrackerFrame? { - // If the tracker is not ready - if (tracker.status != TrackerStatus.OK && tracker.status != TrackerStatus.BUSY && tracker.status != TrackerStatus.OCCLUDED) { - return null - } - - val trackerPosition = tracker.trackerPosition - - // If tracker has no data at all, there's no point in writing a frame - // Note: This includes rawRotation because of `!tracker.hasRotation` - if (trackerPosition == null && !tracker.hasRotation && !tracker.hasPosition && !tracker.hasAcceleration) { - return null - } - - val rotation: Quaternion? = if (tracker.hasRotation) tracker.getRotation() else null - val position: Vector3? = if (tracker.hasPosition) tracker.position else null - val acceleration: Vector3? = if (tracker.hasAcceleration) tracker.getAcceleration() else null - - var rawRotation: Quaternion? = if (tracker.hasAdjustedRotation) tracker.getRawRotation() else null - // If the rawRotation is the same as rotation, there's no point in saving it, set it back to null - if (rawRotation == rotation) rawRotation = null - - return TrackerFrame( - trackerPosition, - rotation, - position, - acceleration, - rawRotation, - ) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrameData.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrameData.kt deleted file mode 100644 index f6ea7e2a54..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrameData.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.slimevr.poseframeformat.trackerdata - -enum class TrackerFrameData(val id: Int) { - DESIGNATION_STRING(0), - ROTATION(1), - POSITION(2), - TRACKER_POSITION_ENUM(3), - ACCELERATION(4), - RAW_ROTATION(5), - ; - - val flag: Int = 1 shl id - - /* - * Inline is fine for these, there's no negative to inlining them as they'll never - * change, so any warning about it can be safely ignored - */ - inline fun check(dataFlags: Int): Boolean = dataFlags and flag != 0 - - inline fun add(dataFlags: Int): Int = dataFlags or flag - - inline fun remove(dataFlags: Int): Int = dataFlags xor flag -} diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt deleted file mode 100644 index 49021ea593..0000000000 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.slimevr.poseframeformat.trackerdata - -import dev.slimevr.VRServer -import dev.slimevr.poseframeformat.trackerdata.TrackerFrame.Companion.fromTracker -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import io.eiren.util.collections.FastList - -data class TrackerFrames(var name: String = "", val frames: FastList) { - - constructor(name: String = "", initialCapacity: Int = 5) : this(name, FastList(initialCapacity)) - constructor(baseTracker: Tracker, frames: FastList) : this(baseTracker.name, frames) - constructor(baseTracker: Tracker, initialCapacity: Int = 5) : this(baseTracker, FastList(initialCapacity)) - - fun addFrameFromTracker(index: Int, tracker: Tracker): TrackerFrame? { - val trackerFrame = fromTracker(tracker) - frames.add(index, trackerFrame) - return trackerFrame - } - - fun addFrameFromTracker(tracker: Tracker): TrackerFrame? { - val trackerFrame = fromTracker(tracker) - frames.add(trackerFrame) - return trackerFrame - } - - fun tryGetFrame(index: Int): TrackerFrame? = if (index < 0 || index >= frames.size) null else frames[index] - - fun tryGetFirstNotNullFrame(): TrackerFrame? = frames.firstOrNull { frame -> frame != null } - - fun toTracker(): Tracker { - val firstFrame = tryGetFirstNotNullFrame() ?: TrackerFrame.empty - val tracker = Tracker( - device = null, - id = VRServer.getNextLocalTrackerId(), - name = name, - trackerPosition = firstFrame.tryGetTrackerPosition(), - hasPosition = firstFrame.hasPosition(), - hasRotation = firstFrame.hasRotation(), - hasAcceleration = firstFrame.hasAcceleration(), - // Make sure this is false!! Otherwise HumanSkeleton ignores it - isInternal = false, - isComputed = true, - trackRotDirection = false, - ) - - tracker.status = TrackerStatus.OK - - return tracker - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/BVHFileStream.kt b/server/core/src/main/java/dev/slimevr/posestreamer/BVHFileStream.kt deleted file mode 100644 index 3712ef12f9..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/BVHFileStream.kt +++ /dev/null @@ -1,213 +0,0 @@ -package dev.slimevr.posestreamer - -import com.jme3.math.FastMath -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import org.apache.commons.lang3.StringUtils -import java.io.BufferedWriter -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStream -import java.io.OutputStreamWriter - -class BVHFileStream : PoseDataStream { - var bvhSettings: BVHSettings = BVHSettings.BLENDER - - private val writer: BufferedWriter - private var frameCount: Long = 0 - private var frameCountOffset: Long = 0 - - constructor(outputStream: OutputStream, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(outputStream) { - this.bvhSettings = bvhSettings - writer = BufferedWriter(OutputStreamWriter(outputStream), 4096) - } - - constructor(file: File, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(file) { - this.bvhSettings = bvhSettings - writer = BufferedWriter(OutputStreamWriter(outputStream), 4096) - } - - constructor(file: String, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(file) { - this.bvhSettings = bvhSettings - writer = BufferedWriter(OutputStreamWriter(outputStream), 4096) - } - - private fun getBufferedFrameCount(frameCount: Long): String { - val frameString = frameCount.toString() - val bufferCount = LONG_MAX_VALUE_DIGITS - frameString.length - - return if (bufferCount > 0) frameString + StringUtils.repeat(' ', bufferCount) else frameString - } - - private fun internalNavigateSkeleton( - bone: Bone, - header: (bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) -> Unit, - footer: (distance: Int) -> Unit, - lastBone: Bone? = null, - invertParentRot: Quaternion = Quaternion.IDENTITY, - distance: Int = 0, - isParent: Boolean = false, - ) { - val parent = bone.parent - // If we're visiting the parents or at root, continue to the next parent - val visitParent = (isParent || lastBone == null) && parent != null - - val children = bone.children - val childCount = children.size - (if (isParent) 1 else 0) - - val hasBranch = visitParent || childCount > 0 - - header(bone, lastBone, invertParentRot, distance, hasBranch, isParent) - - if (hasBranch) { - // Cache this inverted rotation to reduce computation for each branch - val thisInvertRot = bone.getGlobalRotation().inv() - - if (visitParent) { - internalNavigateSkeleton(parent, header, footer, bone, thisInvertRot, distance + 1, true) - } - - for (child in children) { - // If we're a parent, ignore the child - if (isParent && child == lastBone) continue - internalNavigateSkeleton(child, header, footer, bone, thisInvertRot, distance + 1, false) - } - } - - footer(distance) - } - - private fun navigateSkeleton( - root: Bone, - header: (bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) -> Unit, - footer: (distance: Int) -> Unit = {}, - ) { - internalNavigateSkeleton(root, header, footer) - } - - private fun writeBoneDefHeader(bone: Bone?, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) { - val indentLevel = StringUtils.repeat("\t", distance) - val nextIndentLevel = indentLevel + "\t" - - // Handle ends - if (bone == null) { - writer.write("${indentLevel}End Site\n") - } else { - writer - .write("${indentLevel}${if (distance > 0) "JOINT" else "ROOT"} ${bone.boneType}\n") - } - writer.write("$indentLevel{\n") - - // Ignore the root and endpoint offsets - if (bone != null && lastBone != null) { - writer.write( - "${nextIndentLevel}OFFSET 0.0 ${(if (isParent) lastBone.length else -lastBone.length) * bvhSettings.offsetScale} 0.0\n", - ) - } else { - writer.write("${nextIndentLevel}OFFSET 0.0 0.0 0.0\n") - } - - // Define channels - if (bone != null) { - // Only give position for root - if (lastBone != null) { - writer.write("${nextIndentLevel}CHANNELS 3 Zrotation Xrotation Yrotation\n") - } else { - writer.write( - "${nextIndentLevel}CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n", - ) - } - - // Write an empty end bone if there are no branches - // We use null for convenience and treat it as an end node (no bone) - if (!hasBranch) { - val endDistance = distance + 1 - writeBoneDefHeader(null, bone, Quaternion.IDENTITY, endDistance, false, false) - writeBoneDefFooter(endDistance) - } - } - } - - private fun writeBoneDefFooter(level: Int) { - // Closing bracket - writer.write("${StringUtils.repeat("\t", level)}}\n") - } - - private fun writeSkeletonDef(rootBone: Bone) { - navigateSkeleton(rootBone, ::writeBoneDefHeader, ::writeBoneDefFooter) - } - - @Throws(IOException::class) - override fun writeHeader(skeleton: HumanSkeleton, streamer: PoseStreamer) { - writer.write("HIERARCHY\n") - writeSkeletonDef(skeleton.getBone(bvhSettings.rootBone)) - - writer.write("MOTION\n") - writer.write("Frames: ") - - // Get frame offset for finishing writing the file - if (outputStream is FileOutputStream) { - // Flush buffer to get proper offset - writer.flush() - frameCountOffset = outputStream.channel.position() - } - - writer.write(getBufferedFrameCount(frameCount) + "\n") - - // Frame time in seconds - writer.write("Frame Time: ${streamer.frameInterval}\n") - } - - private fun writeBoneRot(bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) { - val rot = invertParentRot * bone.getGlobalRotation() - val angles = rot.toEulerAngles(EulerOrder.ZXY) - - // Output in order of roll (Z), pitch (X), yaw (Y) (extrinsic) - // Assume spacing is needed at the start (we start with position with no following space) - writer - .write(" ${angles.z * FastMath.RAD_TO_DEG} ${angles.x * FastMath.RAD_TO_DEG} ${angles.y * FastMath.RAD_TO_DEG}") - } - - @Throws(IOException::class) - override fun writeFrame(skeleton: HumanSkeleton) { - val rootBone = skeleton.getBone(bvhSettings.rootBone) - - val rootPos = rootBone.getPosition() - - // Write root position - val positionScale = bvhSettings.positionScale - writer - .write("${rootPos.x * positionScale} ${rootPos.y * positionScale} ${rootPos.z * positionScale}") - - navigateSkeleton(rootBone, ::writeBoneRot) - writer.newLine() - - frameCount++ - } - - @Throws(IOException::class) - override fun writeFooter(skeleton: HumanSkeleton) { - // Write the final frame count for files - if (outputStream is FileOutputStream) { - // Flush before anything else - writer.flush() - // Seek to the count offset - outputStream.channel.position(frameCountOffset) - // Overwrite the count with a new value - writer.write(frameCount.toString()) - } - } - - @Throws(IOException::class) - override fun close() { - writer.close() - super.close() - } - - companion object { - private const val LONG_MAX_VALUE_DIGITS = Long.MAX_VALUE.toString().length - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt b/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt deleted file mode 100644 index 8fe09c92be..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.VRServer -import io.eiren.util.logging.LogManager -import java.io.File -import java.io.IOException -import java.nio.file.Path - -class BVHRecorder(server: VRServer) { - private val poseStreamer: ServerPoseStreamer = ServerPoseStreamer(server) - private var poseDataStream: PoseDataStream? = null - - val isRecording: Boolean - get() = poseDataStream != null - - fun startRecording(path: Path) { - val filePath = path.toFile() - - val file = if (filePath.isDirectory()) { - getBvhFile(filePath) ?: return - } else { - filePath - } - - try { - val stream = BVHFileStream(file) - poseDataStream = stream - // 100 FPS - poseStreamer.setOutput(stream, 1f / 100f) - } catch (_: IOException) { - LogManager.severe("[BVH] Failed to create the recording file \"${file.path}\".") - } - } - - fun endRecording() { - try { - val stream = poseDataStream - if (stream != null) { - poseStreamer.closeOutput(stream) - } - } catch (e1: Exception) { - LogManager.severe("[BVH] Exception while closing poseDataStream", e1) - } finally { - poseDataStream = null - } - } - - private fun getBvhFile(bvhSaveDir: File): File? { - if (bvhSaveDir.isDirectory() || bvhSaveDir.mkdirs()) { - var saveRecording: File? - var recordingIndex = 1 - do { - saveRecording = - File(bvhSaveDir, "BVH-Recording${recordingIndex++}.bvh") - } while (saveRecording.exists()) - - return saveRecording - } else { - LogManager - .severe( - "[BVH] Failed to create the recording directory \"${bvhSaveDir.path}\".", - ) - } - - return null - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/BVHSettings.kt b/server/core/src/main/java/dev/slimevr/posestreamer/BVHSettings.kt deleted file mode 100644 index 62e1e8b7a6..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/BVHSettings.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.tracking.processor.BoneType - -class BVHSettings { - var offsetScale: Float = 100f - private set - var positionScale: Float = 100f - private set - var rootBone: BoneType = BoneType.HIP - private set - - constructor() - - constructor(source: BVHSettings) { - this.offsetScale = source.offsetScale - this.positionScale = source.positionScale - } - - fun setOffsetScale(offsetScale: Float): BVHSettings { - this.offsetScale = offsetScale - return this - } - - fun setPositionScale(positionScale: Float): BVHSettings { - this.positionScale = positionScale - return this - } - - fun setRootBone(rootBone: BoneType): BVHSettings { - this.rootBone = rootBone - return this - } - - companion object { - val DEFAULT: BVHSettings = BVHSettings() - val BLENDER: BVHSettings = BVHSettings(DEFAULT) - .setOffsetScale(1f) - .setPositionScale(1f) - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/PoseDataStream.kt b/server/core/src/main/java/dev/slimevr/posestreamer/PoseDataStream.kt deleted file mode 100644 index 406615e656..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/PoseDataStream.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStream -import java.lang.AutoCloseable - -abstract class PoseDataStream protected constructor(protected val outputStream: OutputStream) : AutoCloseable { - var isClosed: Boolean = false - protected set - - protected constructor(file: File) : this(FileOutputStream(file)) - protected constructor(file: String) : this(FileOutputStream(file)) - - @Throws(IOException::class) - open fun writeHeader(skeleton: HumanSkeleton, streamer: PoseStreamer) { - } - - @Throws(IOException::class) - abstract fun writeFrame(skeleton: HumanSkeleton) - - @Throws(IOException::class) - open fun writeFooter(skeleton: HumanSkeleton) { - } - - @Throws(IOException::class) - override fun close() { - outputStream.close() - this.isClosed = true - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/PoseFrameStreamer.kt b/server/core/src/main/java/dev/slimevr/posestreamer/PoseFrameStreamer.kt deleted file mode 100644 index 1bf6040554..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/PoseFrameStreamer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.poseframeformat.PfrIO.readFromFile -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.poseframeformat.player.TrackerFramesPlayer -import dev.slimevr.tracking.processor.HumanPoseManager -import java.io.File - -class PoseFrameStreamer : PoseStreamer { - - val player: TrackerFramesPlayer - val hpm: HumanPoseManager - - private constructor( - player: TrackerFramesPlayer, - hpm: HumanPoseManager, - ) : super(hpm.skeleton) { - this.player = player - this.hpm = hpm - } - - constructor(player: TrackerFramesPlayer) : this(player, HumanPoseManager(player.trackers.toList())) - constructor(poseFrames: PoseFrames) : this(TrackerFramesPlayer(poseFrames)) - constructor(file: File) : this(readFromFile(file)) - constructor(path: String) : this(File(path)) - - @Synchronized - fun streamAllFrames() { - for (i in 0 until player.maxFrameCount) { - player.setCursors(i) - hpm.update() - captureFrame() - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/PoseStreamer.kt b/server/core/src/main/java/dev/slimevr/posestreamer/PoseStreamer.kt deleted file mode 100644 index f1630d5c55..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/PoseStreamer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import io.eiren.util.logging.LogManager -import java.io.IOException - -open class PoseStreamer(skeleton: HumanSkeleton) { - // 60 FPS - private var intervalInternal: Float = 1f / 60f - private var stream: PoseDataStream? = null - - var skeleton: HumanSkeleton = skeleton - protected set - - @Synchronized - fun captureFrame() { - // Make sure the stream is open before trying to write - val stream = stream - if (stream == null || stream.isClosed) { - return - } - - try { - stream.writeFrame(skeleton) - } catch (e: Exception) { - // Handle any exceptions without crashing the program - LogManager.severe("[PoseStreamer] Exception while saving frame", e) - } - } - - open var frameInterval: Float - get() = intervalInternal - set(interval) { - require(interval > 0f) { "interval must be a value greater than 0" } - this.intervalInternal = interval - } - - @Synchronized - @Throws(IOException::class) - fun setOutput(poseFileStream: PoseDataStream, interval: Float) { - this.frameInterval = interval - this.output = poseFileStream - } - - @set:Throws(IOException::class) - @set:Synchronized - open var output: PoseDataStream? - get() = stream - set(stream) { - requireNotNull(stream) { "stream must not be null" } - stream.writeHeader(skeleton, this) - this.stream = stream - } - - val hasOutput - get() = output?.isClosed == false - - @Synchronized - @Throws(IOException::class) - fun closeOutput() { - val stream = this.stream - if (stream != null) { - closeOutput(stream) - this.stream = null - } - } - - @Synchronized - @Throws(IOException::class) - fun closeOutput(stream: PoseDataStream) { - stream.writeFooter(skeleton) - stream.close() - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/ServerPoseStreamer.kt b/server/core/src/main/java/dev/slimevr/posestreamer/ServerPoseStreamer.kt deleted file mode 100644 index e6566709ee..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/ServerPoseStreamer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.VRServer -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.util.ann.VRServerThread - -class ServerPoseStreamer(val server: VRServer) : TickPoseStreamer(server.humanPoseManager.skeleton) { - - init { - // Register callbacks/events - server.addSkeletonUpdatedCallback { skeleton: HumanSkeleton? -> - this.onSkeletonUpdated( - skeleton, - ) - } - server.addOnTick { this.tick() } - } - - @VRServerThread - fun onSkeletonUpdated(skeleton: HumanSkeleton?) { - if (skeleton != null) { - this.skeleton = skeleton - } - } - - @VRServerThread - fun tick() { - super.tick(server.fpsTimer.timePerFrame) - } -} diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/TickPoseStreamer.kt b/server/core/src/main/java/dev/slimevr/posestreamer/TickPoseStreamer.kt deleted file mode 100644 index 327986f796..0000000000 --- a/server/core/src/main/java/dev/slimevr/posestreamer/TickPoseStreamer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.slimevr.posestreamer - -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.util.TickReducer -import java.io.IOException - -open class TickPoseStreamer(skeleton: HumanSkeleton) : PoseStreamer(skeleton) { - private val ticker = TickReducer({ captureFrame() }, frameInterval) - - @set:Throws(IOException::class) - @set:Synchronized - override var output: PoseDataStream? - get() = super.output - set(value) { - super.output = value - ticker.reset() - } - - @set:Synchronized - override var frameInterval: Float - get() = super.frameInterval - set(value) { - super.frameInterval = value - ticker.interval = value - } - - fun tick(tickDelta: Float) { - // Only tick if there is an output - if (hasOutput) { - ticker.tick(tickDelta) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt b/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt deleted file mode 100644 index ea72472db7..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.slimevr.protocol - -class ConnectionContext { - val dataFeedList: MutableList = mutableListOf() - val subscribedTopics: MutableList = mutableListOf() - - var useSerial: Boolean = false - var useProvisioning: Boolean = false - var useAutoBone: Boolean = false -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/DataFeed.kt b/server/core/src/main/java/dev/slimevr/protocol/DataFeed.kt deleted file mode 100644 index 8e72f31ab4..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/DataFeed.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slimevr.protocol - -import solarxr_protocol.data_feed.DataFeedConfigT - -class DataFeed(val config: DataFeedConfigT) { - var timeLastSent: Long = System.currentTimeMillis() -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/GenericConnection.kt b/server/core/src/main/java/dev/slimevr/protocol/GenericConnection.kt deleted file mode 100644 index c857157d9f..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/GenericConnection.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.slimevr.protocol - -import java.nio.ByteBuffer -import java.util.UUID - -interface GenericConnection { - val connectionId: UUID - - val context: ConnectionContext - - fun send(bytes: ByteBuffer) -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/ProtocolAPI.kt b/server/core/src/main/java/dev/slimevr/protocol/ProtocolAPI.kt deleted file mode 100644 index 485fd4d5b7..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/ProtocolAPI.kt +++ /dev/null @@ -1,47 +0,0 @@ -package dev.slimevr.protocol - -import dev.slimevr.VRServer -import dev.slimevr.protocol.datafeed.DataFeedHandler -import dev.slimevr.protocol.pubsub.PubSubHandler -import dev.slimevr.protocol.rpc.RPCHandler -import solarxr_protocol.MessageBundle -import java.nio.ByteBuffer - -class ProtocolAPI(val server: VRServer) { - val apiServers: MutableList = ArrayList() - val dataFeedHandler: DataFeedHandler = DataFeedHandler(this) - val pubSubHandler: PubSubHandler = PubSubHandler(this) - val rpcHandler: RPCHandler = RPCHandler(this) - - fun onMessage(conn: GenericConnection, message: ByteBuffer) { - val messageBundle = MessageBundle.getRootAsMessageBundle(message) - - try { - for (index in 0.. -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/ProtocolHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/ProtocolHandler.kt deleted file mode 100644 index 96ff4e14c7..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/ProtocolHandler.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.slimevr.protocol - -import java.util.function.BiConsumer - -abstract class ProtocolHandler { - @JvmField - val handlers: Array?> - - init { - this.handlers = arrayOfNulls(this.messagesCount()) - } - - abstract fun onMessage(conn: GenericConnection, message: H) - - abstract fun messagesCount(): Int - - fun registerPacketListener(packetType: Byte, consumer: BiConsumer) { - val packetInt = packetType.toInt() - if (handlers[packetInt] != null) { - handlers[packetInt] = handlers[packetInt]!!.andThen(consumer) - } else { - handlers[packetInt] = consumer - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.kt b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.kt deleted file mode 100644 index 8d1f2cb67d..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.kt +++ /dev/null @@ -1,506 +0,0 @@ -package dev.slimevr.protocol.datafeed - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.guards.ServerGuards -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.processor.stayaligned.trackers.RestDetector -import dev.slimevr.tracking.processor.stayaligned.trackers.StayAlignedTrackerState -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.udp.MagnetometerStatus -import dev.slimevr.tracking.trackers.udp.UDPDevice -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import solarxr_protocol.data_feed.DataFeedUpdate -import solarxr_protocol.data_feed.device_data.DeviceData -import solarxr_protocol.data_feed.device_data.DeviceDataMaskT -import solarxr_protocol.data_feed.stay_aligned.StayAlignedPose -import solarxr_protocol.data_feed.stay_aligned.StayAlignedTracker -import solarxr_protocol.data_feed.tracker.TrackerData -import solarxr_protocol.data_feed.tracker.TrackerDataMaskT -import solarxr_protocol.data_feed.tracker.TrackerInfo -import solarxr_protocol.datatypes.DeviceId -import solarxr_protocol.datatypes.Ipv4Address -import solarxr_protocol.datatypes.Temperature -import solarxr_protocol.datatypes.TrackerId -import solarxr_protocol.datatypes.hardware_info.HardwareInfo -import solarxr_protocol.datatypes.hardware_info.HardwareStatus -import solarxr_protocol.datatypes.math.Quat -import solarxr_protocol.datatypes.math.Vec3f -import java.nio.ByteBuffer -import java.util.function.Consumer - -fun createHardwareInfo(fbb: FlatBufferBuilder, device: Device): Int { - val nameOffset = if (device.firmwareVersion != null) { - fbb.createString(device.firmwareVersion) - } else { - 0 - } - - val manufacturerOffset = if (device.manufacturer != null) { - fbb.createString(device.manufacturer) - } else { - 0 - } - - val firmwareDateOffset = if (device.firmwareDate != null) { - fbb.createString(device.firmwareDate) - } else { - 0 - } - - val hardwareIdentifierOffset = fbb.createString(device.hardwareIdentifier) - - HardwareInfo.startHardwareInfo(fbb) - HardwareInfo.addFirmwareVersion(fbb, nameOffset) - HardwareInfo.addFirmwareDate(fbb, firmwareDateOffset) - HardwareInfo.addManufacturer(fbb, manufacturerOffset) - HardwareInfo.addHardwareIdentifier(fbb, hardwareIdentifierOffset) - - if (device is UDPDevice) { - val address = device.ipAddress.address - HardwareInfo - .addIpAddress( - fbb, - Ipv4Address - .createIpv4Address( - fbb, - ByteBuffer.wrap(address).getInt().toLong(), - ), - ) - - HardwareInfo.addNetworkProtocolVersion(fbb, device.protocolVersion) - } - - // BRUH MOMENT - // TODO need support: HardwareInfo.addHardwareRevision(fbb, - // hardwareRevisionOffset); - // TODO need support: HardwareInfo.addDisplayName(fbb, de); - HardwareInfo.addMcuId(fbb, device.mcuType.getSolarType()) - HardwareInfo.addOfficialBoardType(fbb, device.boardType.getSolarType()) - return HardwareInfo.endHardwareInfo(fbb) -} - -fun createTrackerId(fbb: FlatBufferBuilder, tracker: Tracker): Int { - TrackerId.startTrackerId(fbb) - - TrackerId.addTrackerNum(fbb, tracker.trackerNum) - if (tracker.device != null) { - TrackerId.addDeviceId( - fbb, - DeviceId.createDeviceId(fbb, tracker.device.id), - ) - } - - return TrackerId.endTrackerId(fbb) -} - -fun createVec3(fbb: FlatBufferBuilder, vec: Vector3): Int = Vec3f - .createVec3f( - fbb, - vec.x, - vec.y, - vec.z, - ) - -fun createQuat(fbb: FlatBufferBuilder, quaternion: Quaternion): Int = Quat - .createQuat( - fbb, - quaternion.x, - quaternion.y, - quaternion.z, - quaternion.w, - ) - -fun createTrackerInfos( - fbb: FlatBufferBuilder, - infoMask: Boolean, - tracker: Tracker, -): Int { - if (!infoMask) return 0 - - val displayNameOffset = fbb.createString(tracker.displayName) - val customNameOffset = if (tracker.customName != null) { - fbb.createString(tracker.customName) - } else { - 0 - } - - TrackerInfo.startTrackerInfo(fbb) - if (tracker.trackerPosition != null) { - TrackerInfo.addBodyPart( - fbb, - tracker.trackerPosition!!.bodyPart, - ) - } - TrackerInfo.addEditable(fbb, tracker.userEditable) - TrackerInfo.addIsComputed(fbb, tracker.isComputed) - TrackerInfo.addDisplayName(fbb, displayNameOffset) - TrackerInfo.addCustomName(fbb, customNameOffset) - if (tracker.imuType != null) { - TrackerInfo.addImuType(fbb, tracker.imuType.getSolarType()) - } - - // TODO need support: TrackerInfo.addPollRate(fbb, tracker.); - if (tracker.isImu()) { - TrackerInfo.addIsImu(fbb, true) - TrackerInfo - .addAllowDriftCompensation( - fbb, - tracker.resetsHandler.allowDriftCompensation, - ) - } else { - TrackerInfo.addIsImu(fbb, false) - TrackerInfo.addAllowDriftCompensation(fbb, false) - } - - if (tracker.allowMounting) { - val quaternion = tracker.resetsHandler.mountingOrientation - val mountResetFix = tracker.resetsHandler.mountRotFix - TrackerInfo.addMountingOrientation(fbb, createQuat(fbb, quaternion)) - TrackerInfo.addMountingResetOrientation(fbb, createQuat(fbb, mountResetFix)) - } - - TrackerInfo.addMagnetometer(fbb, tracker.magStatus.getSolarType()) - TrackerInfo.addIsHmd(fbb, tracker.isHmd) - - TrackerInfo.addDataSupport(fbb, tracker.trackerDataType.getSolarType()) - - return TrackerInfo.endTrackerInfo(fbb) -} - -fun createTrackerPosition(fbb: FlatBufferBuilder, tracker: Tracker): Int = createVec3(fbb, tracker.position) - -fun createTrackerRotation(fbb: FlatBufferBuilder, tracker: Tracker): Int = createQuat(fbb, tracker.getRawRotation()) - -fun createTrackerAcceleration(fbb: FlatBufferBuilder, tracker: Tracker): Int = createVec3(fbb, tracker.getAcceleration()) - -fun createTrackerMagneticVector(fbb: FlatBufferBuilder, tracker: Tracker): Int = createVec3(fbb, tracker.getMagVector()) - -fun createTrackerTemperature(fbb: FlatBufferBuilder, tracker: Tracker): Int { - if (tracker.temperature == null) return 0 - return Temperature.createTemperature(fbb, tracker.temperature!!) -} - -fun createTrackerData( - fbb: FlatBufferBuilder, - mask: TrackerDataMaskT, - tracker: Tracker, -): Int { - val trackerInfosOffset = createTrackerInfos(fbb, mask.info, tracker) - val trackerIdOffset = createTrackerId(fbb, tracker) - - var stayAlignedOffset = 0 - if (mask.stayAligned) { - stayAlignedOffset = - createTrackerStayAlignedTracker(fbb, tracker.stayAligned) - } - - TrackerData.startTrackerData(fbb) - - TrackerData.addTrackerId(fbb, trackerIdOffset) - - if (trackerInfosOffset != 0) TrackerData.addInfo(fbb, trackerInfosOffset) - if (mask.status) TrackerData.addStatus(fbb, tracker.status.id + 1) - if (mask.position && tracker.hasPosition) { - TrackerData.addPosition( - fbb, - createTrackerPosition(fbb, tracker), - ) - } - if (mask.rotation && tracker.hasRotation) { - TrackerData.addRotation( - fbb, - createTrackerRotation(fbb, tracker), - ) - } - if (mask.linearAcceleration && tracker.hasAcceleration) { - TrackerData - .addLinearAcceleration( - fbb, - createTrackerAcceleration(fbb, tracker), - ) - } - if (mask.temp) { - val trackerTemperatureOffset = createTrackerTemperature(fbb, tracker) - if (trackerTemperatureOffset != 0) { - TrackerData.addTemp( - fbb, - trackerTemperatureOffset, - ) - } - } - if (tracker.allowMounting && tracker.hasRotation) { - if (mask.rotationReferenceAdjusted) { - TrackerData - .addRotationReferenceAdjusted( - fbb, - createQuat(fbb, tracker.getRotation()), - ) - } - if (mask.rotationIdentityAdjusted) { - TrackerData - .addRotationIdentityAdjusted( - fbb, - createQuat(fbb, tracker.getIdentityAdjustedRotation()), - ) - } - } else if (tracker.allowReset && tracker.hasRotation) { - if (mask.rotationReferenceAdjusted) { - TrackerData - .addRotationReferenceAdjusted( - fbb, - createQuat(fbb, tracker.getRotation()), - ) - } - if (mask.rotationIdentityAdjusted) { - TrackerData - .addRotationIdentityAdjusted( - fbb, - createQuat(fbb, tracker.getRawRotation()), - ) - } - } - if (mask.tps) { - TrackerData.addTps(fbb, tracker.tps.toInt()) - } - if (mask.rawMagneticVector && tracker.magStatus == MagnetometerStatus.ENABLED) { - TrackerData.addRawMagneticVector( - fbb, - createTrackerMagneticVector(fbb, tracker), - ) - } - if (mask.stayAligned) { - TrackerData.addStayAligned(fbb, stayAlignedOffset) - } - - return TrackerData.endTrackerData(fbb) -} - -fun createTrackersData( - fbb: FlatBufferBuilder, - mask: DeviceDataMaskT, - device: Device, -): Int { - if (mask.trackerData == null) return 0 - - val trackersOffsets: MutableList = ArrayList() - - device - .trackers - .forEach { (_: Int, value: Tracker) -> - trackersOffsets - .add(createTrackerData(fbb, mask.trackerData, value)) - } - - DeviceData.startTrackersVector(fbb, trackersOffsets.size) - trackersOffsets.forEach( - Consumer { offset: Int -> - DeviceData.addTrackers( - fbb, - offset, - ) - }, - ) - return fbb.endVector() -} - -fun createDeviceData( - fbb: FlatBufferBuilder, - id: Int, - mask: DeviceDataMaskT, - device: Device, -): Int { - if (!mask.deviceData) return 0 - - if (device.trackers.isEmpty()) return 0 - - var firstTracker = device.trackers[0] - if (firstTracker == null) { - // Not actually the "first" tracker, but do we care? - firstTracker = device.trackers.entries.iterator().next().value - } - - val tracker: Tracker = firstTracker - - HardwareStatus.startHardwareStatus(fbb) - HardwareStatus.addErrorStatus(fbb, tracker.status.id) - - if (tracker.batteryVoltage != null) { - HardwareStatus.addBatteryVoltage(fbb, tracker.batteryVoltage!!) - } - if (tracker.batteryLevel != null) { - HardwareStatus.addBatteryPctEstimate(fbb, tracker.batteryLevel!!.toInt()) - } - if (tracker.ping != null) { - HardwareStatus.addPing(fbb, tracker.ping!!) - } - if (tracker.signalStrength != null) { - HardwareStatus.addRssi(fbb, tracker.signalStrength!!.toShort()) - } - if (tracker.packetLoss != null) { - HardwareStatus.addPacketLoss(fbb, tracker.packetLoss!!) - } - if (tracker.packetsLost != null) { - HardwareStatus.addPacketsLost(fbb, tracker.packetsLost!!) - } - if (tracker.packetsReceived != null) { - HardwareStatus.addPacketsReceived(fbb, tracker.packetsReceived!!) - } - if (tracker.batteryRemainingRuntime != null) { - HardwareStatus.addBatteryRuntimeEstimate(fbb, tracker.batteryRemainingRuntime!!) - } - - val hardwareDataOffset = HardwareStatus.endHardwareStatus(fbb) - val hardwareInfoOffset = createHardwareInfo(fbb, device) - val trackersOffset = createTrackersData(fbb, mask, device) - - val nameOffset = if (device.name != null) { - fbb.createString(device.name) - } else { - 0 - } - - DeviceData.startDeviceData(fbb) - DeviceData.addCustomName(fbb, nameOffset) - DeviceData.addId(fbb, DeviceId.createDeviceId(fbb, id)) - DeviceData.addHardwareStatus(fbb, hardwareDataOffset) - DeviceData.addHardwareInfo(fbb, hardwareInfoOffset) - DeviceData.addTrackers(fbb, trackersOffset) - - return DeviceData.endDeviceData(fbb) -} - -fun createSyntheticTrackersData( - fbb: FlatBufferBuilder, - trackerDataMaskT: TrackerDataMaskT?, - trackers: MutableList, -): Int { - if (trackerDataMaskT == null) return 0 - - val trackerOffsets: MutableList = ArrayList() - - trackers - .forEach( - Consumer { tracker: Tracker -> - trackerOffsets - .add(createTrackerData(fbb, trackerDataMaskT, tracker)) - }, - ) - - DataFeedUpdate.startSyntheticTrackersVector(fbb, trackerOffsets.size) - trackerOffsets.forEach( - ( - Consumer { tracker: Int -> - DataFeedUpdate.addSyntheticTrackers( - fbb, - tracker, - ) - } - ), - ) - return fbb.endVector() -} - -fun createDevicesData( - fbb: FlatBufferBuilder, - deviceDataMaskT: DeviceDataMaskT?, - devices: MutableList, -): Int { - if (deviceDataMaskT == null) return 0 - - val devicesDataOffsets = IntArray(devices.size) - for (i in devices.indices) { - val device = devices[i] - devicesDataOffsets[i] = - createDeviceData(fbb, device.id, deviceDataMaskT, device) - } - - return DataFeedUpdate.createDevicesVector(fbb, devicesDataOffsets) -} - -fun createBonesData( - fbb: FlatBufferBuilder, - shouldSend: Boolean, - bones: MutableList, -): Int { - if (!shouldSend) { - return 0 - } - - val boneOffsets = IntArray(bones.size) - for (i in bones.indices) { - val bi = bones[i] - - val headPosG = - bi.getPosition() - val rotG = - bi.getGlobalRotation() - val length = bi.length - - solarxr_protocol.data_feed.Bone.startBone(fbb) - - val rotGOffset = createQuat(fbb, rotG) - solarxr_protocol.data_feed.Bone.addRotationG(fbb, rotGOffset) - val headPosGOffset = Vec3f - .createVec3f(fbb, headPosG.x, headPosG.y, headPosG.z) - solarxr_protocol.data_feed.Bone.addHeadPositionG(fbb, headPosGOffset) - solarxr_protocol.data_feed.Bone.addBodyPart(fbb, bi.boneType.bodyPart) - solarxr_protocol.data_feed.Bone.addBoneLength(fbb, length) - - boneOffsets[i] = solarxr_protocol.data_feed.Bone.endBone(fbb) - } - - return DataFeedUpdate.createBonesVector(fbb, boneOffsets) -} - -fun createStayAlignedPose( - fbb: FlatBufferBuilder, - humanSkeleton: HumanSkeleton, -): Int { - val relaxedPose = RelaxedPose.fromTrackers(humanSkeleton) - - StayAlignedPose.startStayAlignedPose(fbb) - - StayAlignedPose.addUpperLegAngleInDeg(fbb, relaxedPose.upperLeg.toDeg()) - StayAlignedPose.addLowerLegAngleInDeg(fbb, relaxedPose.lowerLeg.toDeg()) - StayAlignedPose.addFootAngleInDeg(fbb, relaxedPose.foot.toDeg()) - - return StayAlignedPose.endStayAlignedPose(fbb) -} - -fun createTrackerStayAlignedTracker( - fbb: FlatBufferBuilder, - state: StayAlignedTrackerState, -): Int { - StayAlignedTracker.startStayAlignedTracker(fbb) - - StayAlignedTracker.addYawCorrectionInDeg(fbb, state.yawCorrection.toDeg()) - StayAlignedTracker.addLockedErrorInDeg( - fbb, - state.yawErrors.lockedError.toL2Norm().toDeg(), - ) - StayAlignedTracker.addCenterErrorInDeg( - fbb, - state.yawErrors.centerError.toL2Norm().toDeg(), - ) - StayAlignedTracker.addNeighborErrorInDeg( - fbb, - state.yawErrors.neighborError.toL2Norm().toDeg(), - ) - StayAlignedTracker.addLocked( - fbb, - state.restDetector.state == RestDetector.State.AT_REST, - ) - - return StayAlignedTracker.endStayAlignedTracker(fbb) -} - -fun createServerGuard(fbb: FlatBufferBuilder, serverGuards: ServerGuards): Int = solarxr_protocol.data_feed.server.ServerGuards.createServerGuards( - fbb, - serverGuards.canDoMounting, - serverGuards.canDoYawReset, - serverGuards.canDoUserHeightCalibration, -) diff --git a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt deleted file mode 100644 index dfb3d5dd6f..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt +++ /dev/null @@ -1,177 +0,0 @@ -package dev.slimevr.protocol.datafeed - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.DataFeed -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolAPIServer -import dev.slimevr.protocol.ProtocolHandler -import dev.slimevr.tracking.trackers.Tracker -import io.eiren.util.logging.LogManager -import solarxr_protocol.MessageBundle -import solarxr_protocol.data_feed.DataFeedConfigT -import solarxr_protocol.data_feed.DataFeedMessage -import solarxr_protocol.data_feed.DataFeedMessageHeader -import solarxr_protocol.data_feed.DataFeedUpdate -import solarxr_protocol.data_feed.PollDataFeed -import solarxr_protocol.data_feed.StartDataFeed -import java.util.function.Consumer -import java.util.stream.Collectors - -class DataFeedHandler(private val api: ProtocolAPI) : ProtocolHandler() { - init { - registerPacketListener(DataFeedMessage.StartDataFeed, ::onStartDataFeed) - registerPacketListener(DataFeedMessage.PollDataFeed, ::onPollDataFeedRequest) - this.api.server.addOnTick { this.sendDataFeedUpdate() } - } - - private fun onStartDataFeed(conn: GenericConnection, header: DataFeedMessageHeader) { - val req = header.message(StartDataFeed()) as StartDataFeed? ?: return - val dataFeeds = req.dataFeedsLength() - - val feedList = conn.context.dataFeedList - synchronized(feedList) { - feedList.clear() - for (i in 0.. - server.apiConnections.forEach { conn: GenericConnection -> - var fbb: FlatBufferBuilder? = null - val feedList = conn.context.dataFeedList - synchronized(feedList) { - val configsCount = feedList.size - val data = IntArray(configsCount) - for (index in 0.. configT.minimumTimeSinceLast) { - if (fbb == null) { - // That way we create a buffer only when needed - fbb = FlatBufferBuilder(300) - } - - val messageOffset = this.buildDatafeed(fbb, configT, index) - - DataFeedMessageHeader.startDataFeedMessageHeader(fbb) - DataFeedMessageHeader.addMessage(fbb, messageOffset) - DataFeedMessageHeader.addMessageType(fbb, DataFeedMessage.DataFeedUpdate) - data[index] = DataFeedMessageHeader.endDataFeedMessageHeader(fbb) - - feed.timeLastSent = currTime - val messages = MessageBundle.createDataFeedMsgsVector(fbb, data) - val packet = createMessage(fbb, messages) - fbb.finish(packet) - conn.send(fbb.dataBuffer()) - } - } - } - } - }, - ) - } - - override fun onMessage(conn: GenericConnection, message: DataFeedMessageHeader) { - val consumer = this.handlers[message.messageType().toInt()] - if (consumer != null) { - consumer.accept(conn, message) - } else { - LogManager - .info( - "[ProtocolAPI] Unhandled Datafeed packet received id: " + message.messageType(), - ) - } - } - - override fun messagesCount(): Int = DataFeedMessage.names.size - - fun createMessage(fbb: FlatBufferBuilder, datafeedMessagesOffset: Int): Int { - MessageBundle.startMessageBundle(fbb) - if (datafeedMessagesOffset > -1) MessageBundle.addDataFeedMsgs(fbb, datafeedMessagesOffset) - return MessageBundle.endMessageBundle(fbb) - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/pubsub/HashedTopicId.kt b/server/core/src/main/java/dev/slimevr/protocol/pubsub/HashedTopicId.kt deleted file mode 100644 index 44fccae153..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/pubsub/HashedTopicId.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slimevr.protocol.pubsub - -import solarxr_protocol.pub_sub.TopicIdT - -// This class is so the HashMap referencing the TopicId as key works -// it needs a unique hashcode based on the topicId and also an equals function -// because equals hashcode does not mean equals strings -class HashedTopicId(val inner: TopicIdT) { - private val hashcode: Int = ( - ( - inner.appName + - "." + - inner.organization + - "." + - inner.topic - ) - ).hashCode() - - override fun hashCode(): Int = hashcode - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val that = other as HashedTopicId - return inner.organization == that.inner.organization && - inner.appName == that.inner.appName && - inner.topic == that.inner.topic - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/pubsub/PubSubHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/pubsub/PubSubHandler.kt deleted file mode 100644 index fe9d24f8d0..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/pubsub/PubSubHandler.kt +++ /dev/null @@ -1,170 +0,0 @@ -package dev.slimevr.protocol.pubsub - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolAPIServer -import dev.slimevr.protocol.ProtocolHandler -import io.eiren.util.logging.LogManager -import solarxr_protocol.MessageBundle -import solarxr_protocol.pub_sub.Message -import solarxr_protocol.pub_sub.PubSubHeader -import solarxr_protocol.pub_sub.PubSubUnion -import solarxr_protocol.pub_sub.SubscriptionRequest -import solarxr_protocol.pub_sub.Topic -import solarxr_protocol.pub_sub.TopicHandle -import solarxr_protocol.pub_sub.TopicHandleRequest -import solarxr_protocol.pub_sub.TopicId -import solarxr_protocol.pub_sub.TopicIdT -import solarxr_protocol.pub_sub.TopicMapping -import java.util.concurrent.atomic.AtomicInteger -import java.util.function.Consumer - -class PubSubHandler(private val api: ProtocolAPI) : ProtocolHandler() { - // Two ways maps for faster reading when handling lots of packets - var topicsHandle: HashMap = HashMap() - var handleTopics: HashMap = HashMap() - - var nextLocalHandle: AtomicInteger = AtomicInteger() - - init { - registerPacketListener(PubSubUnion.SubscriptionRequest, ::onSubscriptionRequest) - registerPacketListener(PubSubUnion.TopicHandleRequest, ::onTopicHandleRequest) - registerPacketListener(PubSubUnion.Message, ::onTopicMessage) - } - - private fun getTopicHandle(topicIdT: TopicIdT): Int { - val hashedTopicId = HashedTopicId(topicIdT) - var handleT = topicsHandle.get(hashedTopicId) - // if no handle exists for this topic id we create one and return it - // anyway - if (handleT == null) { - handleT = nextLocalHandle.incrementAndGet() - topicsHandle[hashedTopicId] = handleT - handleTopics[handleT] = hashedTopicId - } - - return handleT - } - - fun onSubscriptionRequest(conn: GenericConnection, messageHeader: PubSubHeader) { - val req = - messageHeader.u(SubscriptionRequest()) as SubscriptionRequest? ?: return - - var subHandle = -1 - if (req.topicType() == Topic.TopicHandle) { - val handle = req.topic(TopicHandle()) as TopicHandle? - if (handle != null && handleTopics.containsKey(handle.id())) subHandle = handle.id() - } else if (req.topicType() == Topic.TopicId) { - val topicId = req.topic(TopicId()) as TopicId? - if (topicId != null) subHandle = getTopicHandle(topicId.unpack()) - } - - assert(subHandle != -1) - - val finalSubHandle = subHandle - val first = conn - .context - .subscribedTopics - .stream() - .filter { handle: Int -> handle == finalSubHandle } - .findFirst() - if (!first.isPresent) { - conn.context.subscribedTopics.add(finalSubHandle) - } - - val fbb = FlatBufferBuilder(32) - val topicIdOffset = TopicId.pack(fbb, handleTopics.get(finalSubHandle)!!.inner) - val topicHandleOffset = TopicHandle.createTopicHandle(fbb, finalSubHandle) - - val outbound = createMessage( - fbb, - PubSubUnion.TopicMapping, - TopicMapping.createTopicMapping(fbb, topicIdOffset, topicHandleOffset), - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onTopicHandleRequest(conn: GenericConnection, messageHeader: PubSubHeader) { - val req = messageHeader.u(TopicHandleRequest()) as TopicHandleRequest? ?: return - - val topicRequest = req.unpack() - val handle = getTopicHandle(topicRequest.id) - - val fbb = FlatBufferBuilder(32) - val topicIdOffset = TopicId.pack(fbb, topicRequest.id) - val topicHandleOffset = TopicHandle.createTopicHandle(fbb, handle) - - val outbound = createMessage( - fbb, - PubSubUnion.TopicMapping, - TopicMapping.createTopicMapping(fbb, topicIdOffset, topicHandleOffset), - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onTopicMessage(c: GenericConnection, messageHeader: PubSubHeader) { - val req = messageHeader.u(Message()) as Message? ?: return - - val messageT = req.unpack() - - var subHandle = 1 - if (messageT.topic.type == Topic.TopicHandle) { - subHandle = messageT.topic.asTopicHandle().id - } else if (messageT.topic.type == Topic.TopicId) { - subHandle = getTopicHandle(messageT.topic.asTopicId()) - } - - assert(subHandle != -1) - - val finalSubHandle = subHandle - - this.api.apiServers.forEach( - Consumer { server: ProtocolAPIServer -> - server.apiConnections.forEach { conn: GenericConnection -> - // Make sure that we are not sending a message to ourselves - // And check that the receiver has subscribed to the topic - if (conn.connectionId != c.connectionId && - conn.context.subscribedTopics - .contains(finalSubHandle) - ) { - val fbb = FlatBufferBuilder(32) - val outbound = createMessage( - fbb, - PubSubUnion.Message, - Message.pack(fbb, messageT), - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - } - }, - ) - } - - override fun onMessage(conn: GenericConnection, message: PubSubHeader) { - val consumer = this.handlers[message.uType().toInt()] - if (consumer != null) { - consumer.accept(conn, message) - } else { - LogManager - .info("[ProtocolAPI] Unhandled PubSub packet received id: " + message.uType()) - } - } - - override fun messagesCount(): Int = PubSubUnion.names.size - - fun createMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int): Int { - val data = IntArray(1) - - data[0] = PubSubHeader.createPubSubHeader(fbb, messageType, messageOffset) - - val messages = MessageBundle.createPubSubMsgsVector(fbb, data) - - MessageBundle.startMessageBundle(fbb) - MessageBundle.addPubSubMsgs(fbb, messages) - return MessageBundle.endMessageBundle(fbb) - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCBuilder.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCBuilder.kt deleted file mode 100644 index 89bb3be931..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCBuilder.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.slimevr.protocol.rpc - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import solarxr_protocol.rpc.SkeletonConfigResponse -import solarxr_protocol.rpc.SkeletonPart - -fun createSkeletonConfig( - fbb: FlatBufferBuilder, - humanPoseManager: HumanPoseManager, -): Int { - val partsOffsets = IntArray(SkeletonConfigOffsets.entries.size) - - for (index in SkeletonConfigOffsets.entries.toTypedArray().indices) { - val `val` = SkeletonConfigOffsets.values[index] - val part = SkeletonPart - .createSkeletonPart(fbb, `val`.id, humanPoseManager.getOffset(`val`)) - partsOffsets[index] = part - } - - val parts = SkeletonConfigResponse.createSkeletonPartsVector(fbb, partsOffsets) - val userHeight = humanPoseManager.userHeightFromConfig - return SkeletonConfigResponse.createSkeletonConfigResponse(fbb, parts, userHeight) -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt deleted file mode 100644 index d1c63d76f0..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ /dev/null @@ -1,616 +0,0 @@ -package dev.slimevr.protocol.rpc - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.config.MountingMethods -import dev.slimevr.config.config -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolHandler -import dev.slimevr.protocol.datafeed.createTrackerId -import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler -import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler -import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler -import dev.slimevr.protocol.rpc.reset.RPCResetHandler -import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler -import dev.slimevr.protocol.rpc.serial.RPCSerialHandler -import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler -import dev.slimevr.protocol.rpc.settings.createSettingsResponse -import dev.slimevr.protocol.rpc.setup.RPCHandshakeHandler -import dev.slimevr.protocol.rpc.setup.RPCTapSetupHandler -import dev.slimevr.protocol.rpc.setup.RPCUtil.getLocalIp -import dev.slimevr.protocol.rpc.status.RPCStatusHandler -import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler -import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByBodyPart -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import kotlinx.coroutines.* -import solarxr_protocol.MessageBundle -import solarxr_protocol.datatypes.TransactionId -import solarxr_protocol.rpc.* -import kotlin.io.path.Path - -class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler() { - private val mainScope = CoroutineScope(SupervisorJob()) - - init { - RPCResetHandler(this, api) - RPCSerialHandler(this, api) - RPCProvisioningHandler(this, api) - RPCSettingsHandler(this, api) - RPCTapSetupHandler(this, api) - RPCStatusHandler(this, api) - RPCAutoBoneHandler(this, api) - RPCHandshakeHandler(this, api) - RPCTrackingPause(this, api) - RPCFirmwareUpdateHandler(this, api) - RPCVRChatHandler(this, api) - RPCTrackingChecklistHandler(this, api) - RPCUserHeightCalibration(this, api) - - registerPacketListener( - RpcMessage.AssignTrackerRequest, - ::onAssignTrackerRequest, - ) - - registerPacketListener( - RpcMessage.ClearDriftCompensationRequest, - ::onClearDriftCompensationRequest, - ) - - registerPacketListener( - RpcMessage.RecordBVHRequest, - ::onRecordBVHRequest, - ) - - registerPacketListener( - RpcMessage.RecordBVHStatusRequest, - ::onBVHStatusRequest, - ) - - registerPacketListener( - RpcMessage.SkeletonResetAllRequest, - ::onSkeletonResetAllRequest, - ) - registerPacketListener( - RpcMessage.SkeletonConfigRequest, - ::onSkeletonConfigRequest, - ) - registerPacketListener( - RpcMessage.ChangeSkeletonConfigRequest, - ::onChangeSkeletonConfigRequest, - ) - - registerPacketListener( - RpcMessage.OverlayDisplayModeChangeRequest, - ::onOverlayDisplayModeChangeRequest, - ) - registerPacketListener( - RpcMessage.OverlayDisplayModeRequest, - ::onOverlayDisplayModeRequest, - ) - - registerPacketListener( - RpcMessage.ServerInfosRequest, - ::onServerInfosRequest, - ) - - registerPacketListener( - RpcMessage.LegTweaksTmpChange, - ::onLegTweaksTmpChange, - ) - - registerPacketListener( - RpcMessage.LegTweaksTmpClear, - ::onLegTweaksTmpClear, - ) - - registerPacketListener( - RpcMessage.StatusSystemRequest, - ::onStatusSystemRequest, - ) - - registerPacketListener( - RpcMessage.SetPauseTrackingRequest, - ::onSetPauseTrackingRequest, - ) - - registerPacketListener( - RpcMessage.HeightRequest, - ::onHeightRequest, - ) - - registerPacketListener( - RpcMessage.MagToggleRequest, - ::onMagToggleRequest, - ) - - registerPacketListener( - RpcMessage.ChangeMagToggleRequest, - ::onChangeMagToggleRequest, - ) - - registerPacketListener( - RpcMessage.EnableStayAlignedRequest, - ::onEnableStayAlignedRequest, - ) - - registerPacketListener( - RpcMessage.DetectStayAlignedRelaxedPoseRequest, - ::onDetectStayAlignedRelaxedPoseRequest, - ) - - registerPacketListener( - RpcMessage.ResetStayAlignedRelaxedPoseRequest, - ::onResetStayAlignedRelaxedPoseRequest, - ) - } - - private fun onServerInfosRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val fbb = FlatBufferBuilder(32) - - val localIp = getLocalIp() ?: return - val response = ServerInfosResponse - .createServerInfosResponse(fbb, fbb.createString(localIp)) - val outbound = this.createRPCMessage(fbb, RpcMessage.ServerInfosResponse, response, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - private fun onOverlayDisplayModeRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val fbb = FlatBufferBuilder(32) - val config = api.server.configManager.vrConfig.overlay - val response = OverlayDisplayModeResponse - .createOverlayDisplayModeResponse(fbb, config.isVisible, config.isMirrored) - val outbound = this.createRPCMessage(fbb, RpcMessage.OverlayDisplayModeResponse, response, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - private fun onOverlayDisplayModeChangeRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(OverlayDisplayModeChangeRequest()) as? OverlayDisplayModeChangeRequest ?: return - val config = api.server.configManager.vrConfig.overlay - config.isMirrored = req.isMirrored - config.isVisible = req.isVisible - - api.server.configManager.saveConfig() - } - - fun onSkeletonResetAllRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - if (messageHeader - .message(SkeletonResetAllRequest()) !is SkeletonResetAllRequest - ) { - return - } - - api.server.queueTask { - api.server.humanPoseManager.resetOffsets() - api.server.humanPoseManager.saveConfig() - api.server.configManager.saveConfig() - - api.server.trackingChecklistManager.resetMountingCompleted = false - api.server.trackingChecklistManager.feetResetMountingCompleted = false - - // might not be a good idea maybe let the client ask again - val fbb = FlatBufferBuilder(300) - val config = createSkeletonConfig(fbb, api.server.humanPoseManager) - val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - } - - fun onSkeletonConfigRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - if (messageHeader - .message(SkeletonConfigRequest()) !is SkeletonConfigRequest - ) { - return - } - - val fbb = FlatBufferBuilder(300) - val config = createSkeletonConfig(fbb, api.server.humanPoseManager) - val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onChangeSkeletonConfigRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(ChangeSkeletonConfigRequest()) as? ChangeSkeletonConfigRequest ?: return - - val joint = SkeletonConfigOffsets.getById(req.bone()) - - api.server.humanPoseManager.setOffset(joint, req.value()) - api.server.humanPoseManager.saveConfig() - api.server.configManager.saveConfig() - } - - fun onRecordBVHRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(RecordBVHRequest()) as? RecordBVHRequest ?: return - - if (req.stop()) { - if (api.server.bvhRecorder.isRecording) { - api.server.bvhRecorder.endRecording() - } - } else { - if (!api.server.bvhRecorder.isRecording) { - api.server.bvhRecorder.startRecording(Path(req.path())) - } - } - - val fbb = FlatBufferBuilder(40) - val status = RecordBVHStatus - .createRecordBVHStatus(fbb, api.server.bvhRecorder.isRecording) - val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onBVHStatusRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - if (messageHeader.message(RecordBVHStatusRequest()) !is RecordBVHStatusRequest) return - - val fbb = FlatBufferBuilder(40) - val status = RecordBVHStatus - .createRecordBVHStatus(fbb, api.server.bvhRecorder.isRecording) - val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onAssignTrackerRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(AssignTrackerRequest()) as? AssignTrackerRequest ?: return - - val tracker = api.server.getTrackerById(req.trackerId().unpack()) ?: return - - val pos = getByBodyPart(req.bodyPosition()) - val previousTracker = if (pos != null) { - getTrackerForSkeleton(api.server.allTrackers, pos) - } else { - null - } - if (previousTracker != null) { - previousTracker.trackerPosition = null - api.server.trackerUpdated(previousTracker) - } - tracker.trackerPosition = pos - - if (req.mountingOrientation() != null) { - if (tracker.allowMounting) { - tracker - .resetsHandler - .mountingOrientation = Quaternion( - req.mountingOrientation().w(), - req.mountingOrientation().x(), - req.mountingOrientation().y(), - req.mountingOrientation().z(), - ) - api.server.configManager.vrConfig.resetsConfig.lastMountingMethod = - MountingMethods.MANUAL - } - } - - if (req.displayName() != null) { - tracker.customName = req.displayName() - } - - if (tracker.isImu()) { - tracker.resetsHandler.allowDriftCompensation = req.allowDriftCompensation() - } - - api.server.trackerUpdated(tracker) - } - - fun onClearDriftCompensationRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - if (messageHeader - .message(ClearDriftCompensationRequest()) !is ClearDriftCompensationRequest - ) { - return - } - - api.server.clearTrackersDriftCompensation() - } - - fun onLegTweaksTmpChange( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(LegTweaksTmpChange()) as? LegTweaksTmpChange ?: return - - api.server.humanPoseManager - .setLegTweaksStateTemp( - req.skatingCorrection(), - req.floorClip(), - req.toeSnap(), - req.footPlant(), - ) - } - - fun onLegTweaksTmpClear( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(LegTweaksTmpClear()) as? LegTweaksTmpClear ?: return - - api.server.humanPoseManager - .clearLegTweaksStateTemp( - req.skatingCorrection(), - req.floorClip(), - req.toeSnap(), - req.footPlant(), - ) - } - - override fun onMessage(conn: GenericConnection, message: RpcMessageHeader) { - val consumer = handlers[message.messageType().toInt()] - if (consumer != null) { - consumer.accept(conn, message) - } else { - LogManager - .info("[ProtocolAPI] Unhandled RPC packet received id: ${message.messageType()}") - } - } - - @JvmOverloads - fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int, respondTo: RpcMessageHeader? = null): Int { - val data = IntArray(1) - - RpcMessageHeader.startRpcMessageHeader(fbb) - RpcMessageHeader.addMessage(fbb, messageOffset) - RpcMessageHeader.addMessageType(fbb, messageType) - respondTo?.txId()?.let { txId -> - RpcMessageHeader.addTxId(fbb, TransactionId.createTransactionId(fbb, txId.id())) - } - data[0] = RpcMessageHeader.endRpcMessageHeader(fbb) - - val messages = MessageBundle.createRpcMsgsVector(fbb, data) - - MessageBundle.startMessageBundle(fbb) - MessageBundle.addRpcMsgs(fbb, messages) - return MessageBundle.endMessageBundle(fbb) - } - - override fun messagesCount(): Int = RpcMessage.names.size - - fun onStatusSystemRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(StatusSystemRequest()) as? StatusSystemRequest ?: return - - val statuses = api.server.statusSystem.getStatuses() - - val fbb = FlatBufferBuilder( - statuses.size * RPCStatusHandler.STATUS_EXPECTED_SIZE, - ) - val response = StatusSystemResponseT() - response.currentStatuses = statuses - val offset = StatusSystemResponse.pack(fbb, response) - val outbound = this.createRPCMessage(fbb, RpcMessage.StatusSystemResponse, offset, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onSetPauseTrackingRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(SetPauseTrackingRequest()) as? SetPauseTrackingRequest ?: return - - api.server.setPauseTracking(req.pauseTracking(), RPCResetHandler.RESET_SOURCE_NAME) - } - - fun onHeightRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) { - val fbb = FlatBufferBuilder(32) - - val posTrackers = api.server.allTrackers.filter { !it.isInternal && it.status == TrackerStatus.OK && it.hasPosition && it.trackerPosition != null } - val response = if (posTrackers.isNotEmpty()) { - HeightResponse - .createHeightResponse( - fbb, - posTrackers.minOf { it.position.y }, - posTrackers.find { it.trackerPosition == TrackerPosition.HEAD }?.position?.y - ?: posTrackers.maxOf { it.position.y }, - ) - } else { - HeightResponse - .createHeightResponse( - fbb, - 0f, - 0f, - ) - } - fbb.finish(createRPCMessage(fbb, RpcMessage.HeightResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - } - - fun onMagToggleRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(MagToggleRequest()) as? MagToggleRequest ?: return - val fbb = FlatBufferBuilder(32) - - if (req.trackerId() == null) { - val response = MagToggleResponse.createMagToggleResponse( - fbb, - 0, - api.server.configManager.vrConfig.server.useMagnetometerOnAllTrackers, - ) - fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - return - } - - val tracker = api.server.getTrackerById(req.trackerId().unpack()) ?: return - val trackerId = createTrackerId(fbb, tracker) - val response = MagToggleResponse.createMagToggleResponse( - fbb, - trackerId, - tracker.config.shouldHaveMagEnabled == true, - ) - fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - } - - fun onChangeMagToggleRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(ChangeMagToggleRequest()) as? ChangeMagToggleRequest ?: return - - if (req.trackerId() == null) { - mainScope.launch { - withTimeoutOrNull(MAG_TIMEOUT) { - api.server.configManager.vrConfig.server.defineMagOnAllTrackers(req.enable()) - } - - val fbb = FlatBufferBuilder(32) - val response = MagToggleResponse.createMagToggleResponse( - fbb, - 0, - api.server.configManager.vrConfig.server.useMagnetometerOnAllTrackers, - ) - fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - } - return - } - - val tracker = api.server.getTrackerById(req.trackerId().unpack()) ?: return - if (tracker.device == null || tracker.config.shouldHaveMagEnabled == req.enable()) return - val state = req.enable() - tracker.config.shouldHaveMagEnabled = state - // Don't apply magnetometer setting if use magnetometer global setting is not enabled - if (!api.server.configManager.vrConfig.server.useMagnetometerOnAllTrackers) { - val fbb = FlatBufferBuilder(32) - val trackerId = createTrackerId(fbb, tracker) - val response = MagToggleResponse.createMagToggleResponse( - fbb, - trackerId, - state, - ) - fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - return - } - - mainScope.launch { - withTimeoutOrNull(MAG_TIMEOUT) { - tracker.device.setMag(state, tracker.trackerNum) - } - - val fbb = FlatBufferBuilder(32) - val trackerId = createTrackerId(fbb, tracker) - val response = MagToggleResponse.createMagToggleResponse( - fbb, - trackerId, - state, - ) - fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader)) - conn.send(fbb.dataBuffer()) - } - } - - private fun onEnableStayAlignedRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val request = - messageHeader.message(EnableStayAlignedRequest()) as? EnableStayAlignedRequest - ?: return - - val configManager = api.server.configManager - - val config = configManager.vrConfig.stayAlignedConfig - config.enabled = request.enable() - if (request.enable()) { - config.setupComplete = true - } - - configManager.saveConfig() - - sendSettingsChangedResponse(conn, messageHeader) - } - - private fun onDetectStayAlignedRelaxedPoseRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val request = - messageHeader.message(ResetStayAlignedRelaxedPoseRequest()) as? ResetStayAlignedRelaxedPoseRequest - ?: return - - val configManager = api.server.configManager - val config = configManager.vrConfig.stayAlignedConfig - - val pose = request.pose() - - val poseConfig = - when (pose) { - StayAlignedRelaxedPose.STANDING -> config.standingRelaxedPose - StayAlignedRelaxedPose.SITTING -> config.sittingRelaxedPose - StayAlignedRelaxedPose.FLAT -> config.flatRelaxedPose - else -> return - } - - val relaxedPose = RelaxedPose.fromTrackers(api.server.humanPoseManager.skeleton) - - poseConfig.enabled = true - poseConfig.upperLegAngleInDeg = relaxedPose.upperLeg.toDeg() - poseConfig.lowerLegAngleInDeg = relaxedPose.lowerLeg.toDeg() - poseConfig.footAngleInDeg = relaxedPose.foot.toDeg() - - configManager.saveConfig() - - LogManager.info("[detectStayAlignedRelaxedPose] pose=$pose $relaxedPose") - - sendSettingsChangedResponse(conn, messageHeader) - } - - private fun onResetStayAlignedRelaxedPoseRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val request = - messageHeader.message(ResetStayAlignedRelaxedPoseRequest()) as? ResetStayAlignedRelaxedPoseRequest - ?: return - - val configManager = api.server.configManager - val config = configManager.vrConfig.stayAlignedConfig - - val pose = request.pose() - - val poseConfig = - when (pose) { - StayAlignedRelaxedPose.STANDING -> config.standingRelaxedPose - StayAlignedRelaxedPose.SITTING -> config.sittingRelaxedPose - StayAlignedRelaxedPose.FLAT -> config.flatRelaxedPose - else -> return - } - - poseConfig.enabled = false - poseConfig.upperLegAngleInDeg = 0.0f - poseConfig.lowerLegAngleInDeg = 0.0f - poseConfig.footAngleInDeg = 0.0f - - LogManager.info("[resetStayAlignedRelaxedPose] pose=$pose") - - sendSettingsChangedResponse(conn, messageHeader) - } - - fun sendSettingsChangedResponse(conn: GenericConnection, messageHeader: RpcMessageHeader?) { - val fbb = FlatBufferBuilder(32) - val settings = createSettingsResponse(fbb, api.server) - val outbound = createRPCMessage(fbb, RpcMessage.SettingsResponse, settings, messageHeader) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } -} -const val MAG_TIMEOUT: Long = 10_000L diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCUserHeightCalibration.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCUserHeightCalibration.kt deleted file mode 100644 index 4f100c5dbd..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCUserHeightCalibration.kt +++ /dev/null @@ -1,65 +0,0 @@ -package dev.slimevr.protocol.rpc - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.tracking.processor.skeleton.UserHeightCalibrationListener -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.UserHeightRecordingStatusResponse -import solarxr_protocol.rpc.UserHeightRecordingStatusResponseT - -class RPCUserHeightCalibration(var rpcHandler: RPCHandler, var api: ProtocolAPI) : UserHeightCalibrationListener { - val userHeightCal = this.api.server.humanPoseManager.skeleton.userHeightCalibration - - init { - userHeightCal?.addListener( - this, - ) ?: error( - "unavailable", - ) - - rpcHandler.registerPacketListener( - RpcMessage.StartUserHeightCalibration, - ::onStartUserHeightCalibration, - ) - rpcHandler.registerPacketListener( - RpcMessage.CancelUserHeightCalibration, - ::onCancelUserHeightCalibration, - ) - } - - fun onCancelUserHeightCalibration( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - userHeightCal?.clear() - } - - fun onStartUserHeightCalibration( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - userHeightCal?.start() - } - - override fun onStatusChange(status: UserHeightRecordingStatusResponseT) { - val fbb = FlatBufferBuilder(32) - - val res = UserHeightRecordingStatusResponse.pack(fbb, status) - - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.UserHeightRecordingStatusResponse, - res, - ) - fbb.finish(outbound) - - api - .apiServers.forEach { server -> - server.apiConnections.forEach { conn -> - conn.send(fbb.dataBuffer()) - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/autobone/RPCAutoBoneHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/autobone/RPCAutoBoneHandler.kt deleted file mode 100644 index c00b9851ef..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/autobone/RPCAutoBoneHandler.kt +++ /dev/null @@ -1,181 +0,0 @@ -package dev.slimevr.protocol.rpc.autobone - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.autobone.AutoBone.Epoch -import dev.slimevr.autobone.AutoBoneListener -import dev.slimevr.autobone.AutoBoneProcessType -import dev.slimevr.autobone.AutoBoneProcessType.Companion.getById -import dev.slimevr.poseframeformat.PoseFrames -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.protocol.rpc.createSkeletonConfig -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import solarxr_protocol.rpc.AutoBoneEpochResponse -import solarxr_protocol.rpc.AutoBoneProcessRequest -import solarxr_protocol.rpc.AutoBoneProcessStatusResponse -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.SkeletonPart -import java.util.* - -class RPCAutoBoneHandler( - private val rpcHandler: RPCHandler, - val api: ProtocolAPI, -) : AutoBoneListener { - init { - rpcHandler.registerPacketListener( - RpcMessage.AutoBoneProcessRequest, - ::onAutoBoneProcessRequest, - ) - rpcHandler.registerPacketListener( - RpcMessage.AutoBoneApplyRequest, - ::onAutoBoneApplyRequest, - ) - rpcHandler.registerPacketListener( - RpcMessage.AutoBoneStopRecordingRequest, - ::onAutoBoneStopRecordingRequest, - ) - rpcHandler.registerPacketListener( - RpcMessage.AutoBoneCancelRecordingRequest, - ::onAutoBoneCancelRecordingRequest, - ) - - this.api.server.autoBoneHandler.addListener(this) - } - - fun onAutoBoneProcessRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(AutoBoneProcessRequest()) as AutoBoneProcessRequest - if (conn.context.useAutoBone) return - conn.context.useAutoBone = true - api.server - .autoBoneHandler - .startProcessByType(getById(req.processType())) - } - - override fun onAutoBoneProcessStatus( - processType: AutoBoneProcessType, - message: String?, - current: Long, - total: Long, - eta: Float, - completed: Boolean, - success: Boolean, - ) { - forAllListeners { conn -> - if (!conn.context.useAutoBone) { - return@forAllListeners - } - - val fbb = FlatBufferBuilder(32) - - AutoBoneProcessStatusResponse.startAutoBoneProcessStatusResponse(fbb) - AutoBoneProcessStatusResponse.addProcessType( - fbb, - processType.id, - ) - - AutoBoneProcessStatusResponse.addCurrent(fbb, current) - AutoBoneProcessStatusResponse.addTotal(fbb, total) - AutoBoneProcessStatusResponse.addEta(fbb, eta) - AutoBoneProcessStatusResponse.addCompleted(fbb, completed) - AutoBoneProcessStatusResponse.addSuccess(fbb, success) - - val update = AutoBoneProcessStatusResponse - .endAutoBoneProcessStatusResponse(fbb) - val outbound: Int = rpcHandler.createRPCMessage( - fbb, - RpcMessage.AutoBoneProcessStatusResponse, - update, - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - if (completed) { - conn.context.useAutoBone = false - } - } - } - - override fun onAutoBoneRecordingEnd(recording: PoseFrames) { - // Do nothing, this is broadcasted by "onAutoBoneProcessStatus" uwu - } - - override fun onAutoBoneEpoch(epoch: Epoch) { - forAllListeners { conn -> - if (!conn.context.useAutoBone) { - return@forAllListeners - } - - val fbb = FlatBufferBuilder(32) - - val skeletonPartsOffset = AutoBoneEpochResponse - .createAdjustedSkeletonPartsVector( - fbb, - epoch.configValues.map { (key, value) -> - SkeletonPart.createSkeletonPart(fbb, key.id, value) - }.toIntArray(), - ) - val update = AutoBoneEpochResponse - .createAutoBoneEpochResponse( - fbb, - epoch.epoch.toLong(), - epoch.totalEpochs.toLong(), - epoch.epochError.mean, - skeletonPartsOffset, - ) - val outbound: Int = rpcHandler.createRPCMessage( - fbb, - RpcMessage.AutoBoneEpochResponse, - update, - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - } - - override fun onAutoBoneEnd(configValues: EnumMap) { - // Do nothing, the last epoch from "onAutoBoneEpoch" should be all - // that's needed - } - - private fun onAutoBoneApplyRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - api.server.autoBoneHandler.applyValues() - - // Send the new body proportions, this is to update the listener's state - val fbb = FlatBufferBuilder(300) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.SkeletonConfigResponse, - createSkeletonConfig(fbb, api.server.humanPoseManager), - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - private fun onAutoBoneStopRecordingRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - api.server.autoBoneHandler.stopRecording() - } - - private fun onAutoBoneCancelRecordingRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - api.server.autoBoneHandler.cancelRecording() - } - - private fun forAllListeners(action: (GenericConnection) -> Unit) { - api.apiServers.forEach { - it.apiConnections.forEach(action) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/firmware/RPCFirmwareUpdateHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/firmware/RPCFirmwareUpdateHandler.kt deleted file mode 100644 index b8cf4168ad..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/firmware/RPCFirmwareUpdateHandler.kt +++ /dev/null @@ -1,133 +0,0 @@ -package dev.slimevr.protocol.rpc.firmware - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.firmware.FirmwareUpdateListener -import dev.slimevr.firmware.FirmwareUpdateMethod -import dev.slimevr.firmware.UpdateDeviceId -import dev.slimevr.firmware.UpdateStatusEvent -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import solarxr_protocol.datatypes.DeviceIdT -import solarxr_protocol.datatypes.DeviceIdTableT -import solarxr_protocol.rpc.FirmwareUpdateDeviceId -import solarxr_protocol.rpc.FirmwareUpdateDeviceIdUnion -import solarxr_protocol.rpc.FirmwareUpdateRequest -import solarxr_protocol.rpc.FirmwareUpdateRequestT -import solarxr_protocol.rpc.FirmwareUpdateStatusResponse -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.SerialDevicePortT - -class RPCFirmwareUpdateHandler( - private val rpcHandler: RPCHandler, - var api: ProtocolAPI, -) : FirmwareUpdateListener { - - init { - api.server.firmwareUpdateHandler.addListener(this) - rpcHandler.registerPacketListener( - RpcMessage.FirmwareUpdateRequest, - this::onFirmwareUpdateRequest, - ) - rpcHandler.registerPacketListener( - RpcMessage.FirmwareUpdateStopQueuesRequest, - this::onFirmwareUpdateStopQueuesRequest, - ) - } - - private fun onFirmwareUpdateStopQueuesRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - api.server.firmwareUpdateHandler.cancelUpdates() - } - - private fun onFirmwareUpdateRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = - (messageHeader.message(FirmwareUpdateRequest()) as FirmwareUpdateRequest).unpack() - val updateDeviceId = buildUpdateDeviceId(req) ?: return - - api.server.firmwareUpdateHandler.queueFirmwareUpdate( - req, - updateDeviceId, - ) - } - - override fun onUpdateStatusChange(event: UpdateStatusEvent<*>) { - val fbb = FlatBufferBuilder(32) - - val dataUnion = FirmwareUpdateDeviceIdUnion() - dataUnion.type = event.deviceId.type.id - dataUnion.value = createUpdateDeviceId(event.deviceId) - - val deviceIdOffset = FirmwareUpdateDeviceIdUnion.pack(fbb, dataUnion) - - FirmwareUpdateStatusResponse.startFirmwareUpdateStatusResponse(fbb) - FirmwareUpdateStatusResponse.addStatus(fbb, event.status.id) - FirmwareUpdateStatusResponse.addDeviceIdType(fbb, dataUnion.type) - FirmwareUpdateStatusResponse.addDeviceId(fbb, deviceIdOffset) - FirmwareUpdateStatusResponse.addProgress(fbb, event.progress.toByte()) - - val update = FirmwareUpdateStatusResponse.endFirmwareUpdateStatusResponse(fbb) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.FirmwareUpdateStatusResponse, - update, - ) - fbb.finish(outbound) - - api - .apiServers.forEach { server -> - server.apiConnections.forEach { conn -> - conn.send(fbb.dataBuffer()) - } - } - } - - private fun buildUpdateDeviceId(req: FirmwareUpdateRequestT): UpdateDeviceId? { - when (req.method.type) { - FirmwareUpdateDeviceId.solarxr_protocol_datatypes_DeviceIdTable -> { - return UpdateDeviceId( - FirmwareUpdateMethod.OTA, - req.method.asOTAFirmwareUpdate().deviceId.id, - ) - } - - FirmwareUpdateDeviceId.SerialDevicePort -> { - return UpdateDeviceId( - FirmwareUpdateMethod.SERIAL, - req.method.asSerialFirmwareUpdate().deviceId.port, - ) - } - } - return null - } - - private fun createUpdateDeviceId(data: UpdateDeviceId<*>): Any = when (data.type) { - FirmwareUpdateMethod.NONE -> error("Unsupported method") - - FirmwareUpdateMethod.OTA -> { - if (data.id !is Int) { - error("Invalid state, the id type should be Int") - } - DeviceIdTableT().apply { - id = DeviceIdT().apply { - id = data.id - } - } - } - - FirmwareUpdateMethod.SERIAL -> { - if (data.id !is String) { - error("Invalid state, the id type should be String") - } - SerialDevicePortT().apply { - port = data.id - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRCBuilder.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRCBuilder.kt deleted file mode 100644 index 62b2e3744d..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRCBuilder.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.slimevr.protocol.rpc.games.vrchat - -import com.google.flatbuffers.FlatBufferBuilder -import solarxr_protocol.rpc.* - -fun buildVRCConfigValues(fbb: FlatBufferBuilder, values: dev.slimevr.games.vrchat.VRCConfigValues): Int { - VRCConfigValues.startVRCConfigValues(fbb) - VRCConfigValues.addCalibrationRange(fbb, values.calibrationRange.toFloat()) - VRCConfigValues.addCalibrationVisuals(fbb, values.calibrationVisuals) - VRCConfigValues.addSpineMode(fbb, values.spineMode.id) - VRCConfigValues.addLegacyMode(fbb, values.legacyMode) - VRCConfigValues.addShoulderTrackingDisabled(fbb, values.shoulderTrackingDisabled) - VRCConfigValues.addTrackerModel(fbb, values.trackerModel.id) - VRCConfigValues.addAvatarMeasurementType(fbb, values.avatarMeasurementType.id) - VRCConfigValues.addUserHeight(fbb, values.userHeight.toFloat()) - VRCConfigValues.addShoulderWidthCompensation(fbb, values.shoulderWidthCompensation) - return VRCConfigValues.endVRCConfigValues(fbb) -} - -fun buildVRCConfigValidity(fbb: FlatBufferBuilder, validity: dev.slimevr.games.vrchat.VRCConfigValidity): Int { - VRCConfigValidity.startVRCConfigValidity(fbb) - VRCConfigValidity.addCalibrationRangeOk(fbb, validity.calibrationRangeOk) - VRCConfigValidity.addCalibrationVisualsOk(fbb, validity.calibrationVisualsOk) - VRCConfigValidity.addSpineModeOk(fbb, validity.spineModeOk) - VRCConfigValidity.addLegacyModeOk(fbb, validity.legacyModeOk) - VRCConfigValidity.addShoulderTrackingOk(fbb, validity.shoulderTrackingOk) - VRCConfigValidity.addTrackerModelOk(fbb, validity.trackerModelOk) - VRCConfigValidity.addUserHeightOk(fbb, validity.userHeightOk) - VRCConfigValidity.addAvatarMeasurementTypeOk(fbb, validity.avatarMeasurementTypeOk) - VRCConfigValidity.addShoulderWidthCompensationOk(fbb, validity.shoulderWidthCompensationOk) - return VRCConfigValidity.endVRCConfigValidity(fbb) -} - -fun buildVRCConfigRecommendedValues(fbb: FlatBufferBuilder, values: dev.slimevr.games.vrchat.VRCConfigRecommendedValues): Int { - val spineModeOffset = VRCConfigRecommendedValues - .createSpineModeVector( - fbb, - values.spineMode.map { it.id.toByte() }.toByteArray(), - ) - - VRCConfigRecommendedValues.startVRCConfigRecommendedValues(fbb) - VRCConfigRecommendedValues.addCalibrationRange(fbb, values.calibrationRange.toFloat()) - VRCConfigRecommendedValues.addCalibrationVisuals(fbb, values.calibrationVisuals) - VRCConfigRecommendedValues.addSpineMode(fbb, spineModeOffset) - VRCConfigRecommendedValues.addLegacyMode(fbb, values.legacyMode) - VRCConfigRecommendedValues.addShoulderTrackingDisabled(fbb, values.shoulderTrackingDisabled) - VRCConfigRecommendedValues.addTrackerModel(fbb, values.trackerModel.id) - VRCConfigRecommendedValues.addAvatarMeasurementType(fbb, values.avatarMeasurementType.id) - VRCConfigRecommendedValues.addUserHeight(fbb, values.userHeight.toFloat()) - VRCConfigRecommendedValues.addShoulderWidthCompensation(fbb, values.shoulderWidthCompensation) - return VRCConfigRecommendedValues.endVRCConfigRecommendedValues(fbb) -} - -fun buildVRCConfigStateResponse( - fbb: FlatBufferBuilder, - isSupported: Boolean, - validity: dev.slimevr.games.vrchat.VRCConfigValidity?, - values: dev.slimevr.games.vrchat.VRCConfigValues?, - recommended: dev.slimevr.games.vrchat.VRCConfigRecommendedValues?, - muted: List, -): Int { - if (!isSupported) { - VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb) - VRCConfigStateChangeResponse.addIsSupported(fbb, false) - return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb) - } - - if (validity == null || values == null || recommended == null) { - error("invalid state - all should be set") - } - - val validityOffset = buildVRCConfigValidity(fbb, validity) - val valuesOffset = buildVRCConfigValues(fbb, values) - val recommendedOffset = buildVRCConfigRecommendedValues(fbb, recommended) - val mutedOffset = VRCConfigStateChangeResponse.createMutedVector(fbb, muted.map { fbb.createString(it) }.toIntArray()) - - VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb) - VRCConfigStateChangeResponse.addIsSupported(fbb, true) - VRCConfigStateChangeResponse.addValidity(fbb, validityOffset) - VRCConfigStateChangeResponse.addState(fbb, valuesOffset) - VRCConfigStateChangeResponse.addRecommended(fbb, recommendedOffset) - VRCConfigStateChangeResponse.addMuted(fbb, mutedOffset) - return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb) -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt deleted file mode 100644 index 481e918d35..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt +++ /dev/null @@ -1,81 +0,0 @@ -package dev.slimevr.protocol.rpc.games.vrchat - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.games.vrchat.VRCConfigListener -import dev.slimevr.games.vrchat.VRCConfigRecommendedValues -import dev.slimevr.games.vrchat.VRCConfigValidity -import dev.slimevr.games.vrchat.VRCConfigValues -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import solarxr_protocol.rpc.* - -class RPCVRChatHandler( - private val rpcHandler: RPCHandler, - var api: ProtocolAPI, -) : VRCConfigListener { - - init { - api.server.vrcConfigManager.addListener(this) - - rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest, ::onConfigStateRequest) - rpcHandler.registerPacketListener(RpcMessage.VRCConfigSettingToggleMute, ::onToggleMuteRequest) - } - - private fun onConfigStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val fbb = FlatBufferBuilder(32) - - val configManager = api.server.vrcConfigManager - val values = configManager.currentValues - val recommended = configManager.recommendedValues() - // FUCKING KOTLIN BRING ME BACK MY FUCKING TERNARY OPERATORS!!!!!!!!!!!!!!!!! - With love <3 Futura - val validity = if (values !== null) configManager.checkValidity(values, recommended) else null - - val response = buildVRCConfigStateResponse( - fbb, - isSupported = api.server.vrcConfigManager.isSupported, - validity = validity, - values = values, - recommended = api.server.vrcConfigManager.recommendedValues(), - muted = api.server.configManager.vrConfig.vrcConfig.mutedWarnings, - ) - - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.VRCConfigStateChangeResponse, - response, - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - private fun onToggleMuteRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(VRCConfigSettingToggleMute()) as VRCConfigSettingToggleMute? - ?: return - api.server.vrcConfigManager.toggleMuteWarning(req.key()) - } - - override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) { - val fbb = FlatBufferBuilder(32) - - val response = buildVRCConfigStateResponse( - fbb, - isSupported = api.server.vrcConfigManager.isSupported, - validity = validity, - values = values, - recommended = recommended, - muted, - ) - - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.VRCConfigStateChangeResponse, - response, - ) - fbb.finish(outbound) - - this.api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/reset/RPCResetHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/reset/RPCResetHandler.kt deleted file mode 100644 index 2ae1b934d7..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/reset/RPCResetHandler.kt +++ /dev/null @@ -1,139 +0,0 @@ -package dev.slimevr.protocol.rpc.reset - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolAPIServer -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.reset.ResetListener -import solarxr_protocol.rpc.ClearMountingResetRequest -import solarxr_protocol.rpc.ResetRequest -import solarxr_protocol.rpc.ResetResponse -import solarxr_protocol.rpc.ResetStatus -import solarxr_protocol.rpc.ResetType -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import java.util.function.Consumer - -class RPCResetHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : ResetListener { - val resetsConfig = api.server.configManager.vrConfig.resetsConfig - - init { - this.api.server.resetHandler.addListener(this) - - rpcHandler.registerPacketListener(RpcMessage.ResetRequest, ::onResetRequest) - rpcHandler.registerPacketListener(RpcMessage.ClearMountingResetRequest, ::onClearMountingResetRequest) - } - - fun onResetRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(ResetRequest()) as? ResetRequest ?: return - - // Get the list of bodyparts we want to reset - // If empty, check in HumanSkeleton will reset all - val bodyParts = mutableListOf() - if (req.bodyPartsLength() > 0) { - val buffer = req.bodyPartsAsByteBuffer() - while (buffer.hasRemaining()) { - bodyParts.add(buffer.get().toInt()) - } - } - - if (req.resetType() == ResetType.Yaw) { - val delay = if (req.hasDelay()) { - req.delay() - } else { - resetsConfig.yawResetDelay - } - if (bodyParts.isEmpty()) { - api.server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (delay * 1000).toLong()) - } else { - api.server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (delay * 1000).toLong(), bodyParts.toList()) - } - } - if (req.resetType() == ResetType.Full) { - val delay = if (req.hasDelay()) { - req.delay() - } else { - resetsConfig.fullResetDelay - } - if (bodyParts.isEmpty()) { - api.server.scheduleResetTrackersFull(RESET_SOURCE_NAME, (delay * 1000).toLong()) - } else { - api.server.scheduleResetTrackersFull(RESET_SOURCE_NAME, (delay * 1000).toLong(), bodyParts.toList()) - } - } - if (req.resetType() == ResetType.Mounting) { - val delay = if (req.hasDelay()) { - req.delay() - } else { - resetsConfig.mountingResetDelay - } - if (bodyParts.isEmpty()) { - api.server.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (delay * 1000).toLong()) - } else { - api.server.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (delay * 1000).toLong(), bodyParts.toList()) - } - } - } - - fun sendResetStatusResponse(resetType: Int, status: Int, bodyParts: List? = null, progress: Int = 0, duration: Int = 0) { - val fbb = FlatBufferBuilder(32) - - val bodyPartsOffset = if (bodyParts != null) ResetResponse.createBodyPartsVector(fbb, bodyParts.map { it.toByte() }.toByteArray()) else 0 - - ResetResponse.startResetResponse(fbb) - ResetResponse.addResetType(fbb, resetType) - ResetResponse.addStatus(fbb, status) - if (bodyPartsOffset >= 0) { - ResetResponse.addBodyParts(fbb, bodyPartsOffset) - } - ResetResponse.addProgress(fbb, progress) - ResetResponse.addDuration(fbb, duration) - val update = ResetResponse.endResetResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.ResetResponse, update) - fbb.finish(outbound) - - this.forAllListeners( - Consumer { conn: GenericConnection -> - conn.send(fbb.dataBuffer()) - }, - ) - } - - fun onClearMountingResetRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - if (messageHeader - .message(ClearMountingResetRequest()) !is ClearMountingResetRequest - ) { - return - } - - api.server.clearTrackersMounting(RESET_SOURCE_NAME) - } - - override fun onStarted(resetType: Int, bodyParts: List?, progress: Int, duration: Int) { - sendResetStatusResponse(resetType, ResetStatus.STARTED, bodyParts, progress, duration) - } - - override fun onFinished(resetType: Int, bodyParts: List?, duration: Int) { - sendResetStatusResponse(resetType, ResetStatus.FINISHED, bodyParts, duration, duration) - } - - fun forAllListeners(action: Consumer?) { - this.api - .apiServers - .forEach( - Consumer { server: ProtocolAPIServer -> - server - .apiConnections - .forEach(action) - }, - ) - } - - companion object { - const val RESET_SOURCE_NAME = "WebSocketAPI" - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt deleted file mode 100644 index b1cd0387f2..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt +++ /dev/null @@ -1,68 +0,0 @@ -package dev.slimevr.protocol.rpc.serial - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolAPIServer -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.serial.ProvisioningListener -import dev.slimevr.serial.ProvisioningStatus -import dev.slimevr.serial.SerialPort -import solarxr_protocol.rpc.* -import java.util.function.Consumer - -class RPCProvisioningHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : ProvisioningListener { - init { - rpcHandler.registerPacketListener(RpcMessage.StartWifiProvisioningRequest, ::onStartWifiProvisioningRequest) - rpcHandler.registerPacketListener(RpcMessage.StopWifiProvisioningRequest, ::onStopWifiProvisioningRequest) - this.api.server.provisioningHandler.addListener(this) - } - - fun onStartWifiProvisioningRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(StartWifiProvisioningRequest()) as StartWifiProvisioningRequest? - if (req == null) return - this.api.server.provisioningHandler.start(req.ssid(), req.password(), req.port()) - conn.context.useProvisioning = true - } - - fun onStopWifiProvisioningRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(StopWifiProvisioningRequest()) as StopWifiProvisioningRequest? - if (req == null) return - conn.context.useProvisioning = false - this.api.server.provisioningHandler.stop() - } - - override fun onProvisioningStatusChange(status: ProvisioningStatus, port: SerialPort?) { - val fbb = FlatBufferBuilder(32) - - WifiProvisioningStatusResponse.startWifiProvisioningStatusResponse(fbb) - WifiProvisioningStatusResponse.addStatus(fbb, status.id) - val update = WifiProvisioningStatusResponse.endWifiProvisioningStatusResponse(fbb) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.WifiProvisioningStatusResponse, update) - fbb.finish(outbound) - - this.forAllListeners(Consumer { conn: GenericConnection -> conn.send(fbb.dataBuffer()) }) - } - - private fun forAllListeners(action: Consumer?) { - this.api - .apiServers - .forEach( - Consumer { server: ProtocolAPIServer -> - server - .apiConnections - .filter { conn: GenericConnection -> conn.context.useProvisioning } - .forEach(action) - }, - ) - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt deleted file mode 100644 index e621390af3..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt +++ /dev/null @@ -1,263 +0,0 @@ -package dev.slimevr.protocol.rpc.serial - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.ProtocolAPIServer -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.serial.SerialListener -import dev.slimevr.serial.SerialPort -import io.eiren.util.logging.LogManager -import solarxr_protocol.rpc.* -import java.util.* -import java.util.function.Consumer - -class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : SerialListener { - init { - rpcHandler.registerPacketListener(RpcMessage.SerialTrackerRebootRequest, ::onSerialTrackerRebootRequest) - rpcHandler.registerPacketListener(RpcMessage.SerialTrackerGetInfoRequest, ::onSerialTrackerGetInfoRequest) - rpcHandler.registerPacketListener(RpcMessage.SerialTrackerFactoryResetRequest, ::onSerialTrackerFactoryResetRequest) - rpcHandler.registerPacketListener(RpcMessage.SerialTrackerGetWifiScanRequest, ::onSerialTrackerGetWifiScanRequest) - rpcHandler.registerPacketListener(RpcMessage.SerialTrackerCustomCommandRequest, ::onSerialTrackerCustomCommandRequest) - rpcHandler.registerPacketListener(RpcMessage.SetWifiRequest, ::onSetWifiRequest) - rpcHandler.registerPacketListener(RpcMessage.OpenSerialRequest, ::onOpenSerialRequest) - rpcHandler.registerPacketListener(RpcMessage.CloseSerialRequest, ::onCloseSerialRequest) - rpcHandler.registerPacketListener(RpcMessage.SerialDevicesRequest, ::onRequestSerialDevices) - this.api.server.serialHandler.addListener(this) - } - - override fun onSerialDisconnected() { - val fbb = FlatBufferBuilder(32) - - SerialUpdateResponse.startSerialUpdateResponse(fbb) - SerialUpdateResponse.addClosed(fbb, true) - val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) - fbb.finish(outbound) - - this.forAllListeners( - Consumer { conn: GenericConnection -> - conn.send(fbb.dataBuffer()) - conn.context.useSerial = false - }, - ) - } - - override fun onSerialLog(str: String, server: Boolean) { - val fbb = FlatBufferBuilder(32) - - val logOffset = fbb.createString(str) - - SerialUpdateResponse.startSerialUpdateResponse(fbb) - SerialUpdateResponse.addLog(fbb, logOffset) - val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) - fbb.finish(outbound) - - this.forAllListeners( - Consumer { conn: GenericConnection -> - conn.send(fbb.dataBuffer()) - }, - ) - } - - override fun onNewSerialDevice(port: SerialPort) { - val fbb = FlatBufferBuilder(32) - - val portOffset = fbb.createString(port.portLocation) - val nameOffset = fbb.createString(port.descriptivePortName) - val deviceOffset = SerialDevice.createSerialDevice(fbb, portOffset, nameOffset) - val newSerialOffset = NewSerialDeviceResponse - .createNewSerialDeviceResponse(fbb, deviceOffset) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.NewSerialDeviceResponse, newSerialOffset) - fbb.finish(outbound) - - this.api - .apiServers - .forEach( - Consumer { server: ProtocolAPIServer -> - server - .apiConnections - .forEach { conn: GenericConnection -> - conn.send(fbb.dataBuffer()) - } - }, - ) - } - - override fun onSerialConnected(port: SerialPort) { - val fbb = FlatBufferBuilder(32) - - SerialUpdateResponse.startSerialUpdateResponse(fbb) - SerialUpdateResponse.addClosed(fbb, false) - val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) - fbb.finish(outbound) - - this.forAllListeners( - Consumer { conn: GenericConnection -> - conn.send(fbb.dataBuffer()) - }, - ) - } - - fun onSerialTrackerRebootRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(SerialTrackerRebootRequest()) as SerialTrackerRebootRequest? - if (req == null) return - - this.api.server.serialHandler.rebootRequest() - } - - fun onSerialTrackerGetInfoRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(SerialTrackerGetInfoRequest()) as SerialTrackerGetInfoRequest? - if (req == null) return - - this.api.server.serialHandler.infoRequest() - } - - fun onSerialTrackerFactoryResetRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(SerialTrackerFactoryResetRequest()) as SerialTrackerFactoryResetRequest? - if (req == null) return - - this.api.server.serialHandler.factoryResetRequest() - } - - fun onSerialTrackerGetWifiScanRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(SerialTrackerGetWifiScanRequest()) as SerialTrackerGetWifiScanRequest? - if (req == null) return - - this.api.server.serialHandler.wifiScanRequest() - } - - fun onSerialTrackerCustomCommandRequest( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader - .message(SerialTrackerCustomCommandRequest()) as SerialTrackerCustomCommandRequest? - - if (req == null || req.command() == null) return - - this.api.server.serialHandler.customCommandRequest(Objects.requireNonNull(req.command())) - } - - private fun onRequestSerialDevices(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(SerialDevicesRequest()) as SerialDevicesRequest? - if (req == null) return - - val fbb = FlatBufferBuilder(32) - - val devicesOffsets: MutableList = ArrayList() - - try { - this.api.server.serialHandler.knownPorts.forEach { port: SerialPort -> - val portOffset = fbb.createString(port.portLocation) - val nameOffset = fbb.createString(port.descriptivePortName) - devicesOffsets.add(SerialDevice.createSerialDevice(fbb, portOffset, nameOffset)) - } - } catch (e: Throwable) { - LogManager.severe("Using serial ports is not supported on this platform", e) - } - - SerialDevicesResponse.startDevicesVector(fbb, devicesOffsets.size) - devicesOffsets.forEach(Consumer { offset: Int -> SerialDevicesResponse.addDevices(fbb, offset) }) - val devices = fbb.endVector() - val serialDeviceOffsets = SerialDevicesResponse.createSerialDevicesResponse(fbb, devices) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.SerialDevicesResponse, serialDeviceOffsets) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun onSetWifiRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(SetWifiRequest()) as SetWifiRequest? ?: return - - if (req.password() == null || req.ssid() == null || !this.api.server.serialHandler.isConnected) { - return - } - this.api.server.serialHandler.setWifi(req.ssid(), req.password()) - } - - fun onOpenSerialRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = - messageHeader.message(OpenSerialRequest()) as OpenSerialRequest? ?: return - - conn.context.useSerial = true - - this.api.server.queueTask { - try { - this.api.server.serialHandler.openSerial(req.port(), req.auto()) - } catch (e: Exception) { - LogManager.severe("Unable to open serial port", e) - } catch (e: Throwable) { - LogManager.severe( - "Using serial ports is not supported on this platform", - e, - ) - } - val fbb = FlatBufferBuilder(32) - SerialUpdateResponse.startSerialUpdateResponse(fbb) - SerialUpdateResponse.addClosed( - fbb, - !this.api.server.serialHandler.isConnected, - ) - val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - } - - fun onCloseSerialRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(CloseSerialRequest()) as CloseSerialRequest? - if (req == null) return - - conn.context.useSerial = false - - this.api.server.serialHandler.closeSerial() - - val fbb = FlatBufferBuilder(32) - SerialUpdateResponse.startSerialUpdateResponse(fbb) - SerialUpdateResponse.addClosed(fbb, !this.api.server.serialHandler.isConnected) - val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - fun forAllListeners(action: Consumer?) { - this.api - .apiServers - .forEach( - Consumer { server: ProtocolAPIServer -> - server - .apiConnections - .filter { conn: GenericConnection -> conn.context.useSerial } - .forEach(action) - }, - ) - } - - override fun onSerialDeviceDeleted(port: SerialPort) { - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt deleted file mode 100644 index a933bf1936..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.kt +++ /dev/null @@ -1,458 +0,0 @@ -package dev.slimevr.protocol.rpc.settings - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.VRServer -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.config.AutoBoneConfig -import dev.slimevr.config.DriftCompensationConfig -import dev.slimevr.config.FiltersConfig -import dev.slimevr.config.HIDConfig -import dev.slimevr.config.LegTweaksConfig -import dev.slimevr.config.OSCConfig -import dev.slimevr.config.ResetsConfig -import dev.slimevr.config.SkeletonConfig -import dev.slimevr.config.StayAlignedConfig -import dev.slimevr.config.TapDetectionConfig -import dev.slimevr.config.VMCConfig -import dev.slimevr.config.VRCOSCConfig -import dev.slimevr.filtering.TrackerFilters.Companion.getByConfigkey -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.processor.config.SkeletonConfigValues -import dev.slimevr.tracking.trackers.TrackerRole -import solarxr_protocol.rpc.AutoBoneSettings -import solarxr_protocol.rpc.DriftCompensationSettings -import solarxr_protocol.rpc.FilteringSettings -import solarxr_protocol.rpc.HIDSettings -import solarxr_protocol.rpc.OSCRouterSettings -import solarxr_protocol.rpc.OSCSettings -import solarxr_protocol.rpc.OSCTrackersSetting -import solarxr_protocol.rpc.ResetsSettings -import solarxr_protocol.rpc.SettingsResponse -import solarxr_protocol.rpc.StayAlignedSettings -import solarxr_protocol.rpc.SteamVRTrackersSetting -import solarxr_protocol.rpc.TapDetectionSettings -import solarxr_protocol.rpc.VMCOSCSettings -import solarxr_protocol.rpc.VRCOSCSettings -import solarxr_protocol.rpc.settings.LegTweaksSettings -import solarxr_protocol.rpc.settings.ModelRatios -import solarxr_protocol.rpc.settings.ModelSettings -import solarxr_protocol.rpc.settings.ModelToggles -import solarxr_protocol.rpc.settings.SkeletonHeight - -fun createOSCRouterSettings( - fbb: FlatBufferBuilder, - config: OSCConfig, -): Int { - val addressStringOffset = fbb.createString(config.address) - - val oscSettingOffset = OSCSettings - .createOSCSettings( - fbb, - config.enabled, - config.portIn, - config.portOut, - addressStringOffset, - ) - - OSCRouterSettings.startOSCRouterSettings(fbb) - OSCRouterSettings.addOscSettings(fbb, oscSettingOffset) - - return OSCRouterSettings.endOSCRouterSettings(fbb) -} - -fun createVRCOSCSettings( - fbb: FlatBufferBuilder, - config: VRCOSCConfig, -): Int { - val addressStringOffset = fbb.createString(config.address) - val generalSettingOffset = OSCSettings - .createOSCSettings( - fbb, - config.enabled, - config.portIn, - config.portOut, - addressStringOffset, - ) - val oscSettingOffset = OSCTrackersSetting - .createOSCTrackersSetting( - fbb, - config.getOSCTrackerRole(TrackerRole.HEAD, false), - config.getOSCTrackerRole(TrackerRole.CHEST, false), - config.getOSCTrackerRole(TrackerRole.WAIST, false), - config.getOSCTrackerRole(TrackerRole.LEFT_KNEE, false) && - config.getOSCTrackerRole(TrackerRole.RIGHT_KNEE, false), - config.getOSCTrackerRole(TrackerRole.LEFT_FOOT, false) && - config.getOSCTrackerRole(TrackerRole.RIGHT_FOOT, false), - config.getOSCTrackerRole(TrackerRole.LEFT_ELBOW, false) && - config.getOSCTrackerRole(TrackerRole.RIGHT_ELBOW, false), - config.getOSCTrackerRole(TrackerRole.LEFT_HAND, false) && - config.getOSCTrackerRole(TrackerRole.RIGHT_HAND, false), - ) - VRCOSCSettings.startVRCOSCSettings(fbb) - VRCOSCSettings.addOscSettings(fbb, generalSettingOffset) - VRCOSCSettings.addTrackers(fbb, oscSettingOffset) - VRCOSCSettings.addOscqueryEnabled(fbb, config.oscqueryEnabled) - - return VRCOSCSettings.endVRCOSCSettings(fbb) -} - -fun createVMCOSCSettings( - fbb: FlatBufferBuilder, - config: VMCConfig, -): Int { - val addressStringOffset = fbb.createString(config.address) - val generalSettingOffset = OSCSettings - .createOSCSettings( - fbb, - config.enabled, - config.portIn, - config.portOut, - addressStringOffset, - ) - - val vrmJson = config.vrmJson - var vrmJsonOffset = 0 - if (vrmJson != null) vrmJsonOffset = fbb.createString(vrmJson) - - VMCOSCSettings.startVMCOSCSettings(fbb) - VMCOSCSettings.addOscSettings(fbb, generalSettingOffset) - if (vrmJson != null) VMCOSCSettings.addVrmJson(fbb, vrmJsonOffset) - VMCOSCSettings.addAnchorHip(fbb, config.anchorHip) - VMCOSCSettings.addMirrorTracking(fbb, config.mirrorTracking) - - return VMCOSCSettings.endVMCOSCSettings(fbb) -} - -fun createFilterSettings( - fbb: FlatBufferBuilder, - filtersConfig: FiltersConfig, -): Int = FilteringSettings - .createFilteringSettings( - fbb, - getByConfigkey(filtersConfig.type)!!.id, - filtersConfig.amount, - ) - -fun createDriftCompensationSettings( - fbb: FlatBufferBuilder, - driftCompensationConfig: DriftCompensationConfig, -): Int = DriftCompensationSettings - .createDriftCompensationSettings( - fbb, - driftCompensationConfig.enabled, - driftCompensationConfig.prediction, - driftCompensationConfig.amount, - driftCompensationConfig.maxResets, - ) - -fun createTapDetectionSettings( - fbb: FlatBufferBuilder, - tapDetectionConfig: TapDetectionConfig, -): Int = TapDetectionSettings - .createTapDetectionSettings( - fbb, - tapDetectionConfig.fullResetDelay, - tapDetectionConfig.fullResetEnabled, - tapDetectionConfig.fullResetTaps, - tapDetectionConfig.yawResetDelay, - tapDetectionConfig.yawResetEnabled, - tapDetectionConfig.yawResetTaps, - tapDetectionConfig.mountingResetDelay, - tapDetectionConfig.mountingResetEnabled, - tapDetectionConfig.mountingResetTaps, - tapDetectionConfig.setupMode, - tapDetectionConfig.numberTrackersOverThreshold, - ) - -fun createSteamVRSettings(fbb: FlatBufferBuilder, bridge: ISteamVRBridge?): Int { - var steamvrTrackerSettings = 0 - if (bridge != null) { - steamvrTrackerSettings = SteamVRTrackersSetting - .createSteamVRTrackersSetting( - fbb, - bridge.getShareSetting(TrackerRole.WAIST), - bridge.getShareSetting(TrackerRole.CHEST), - bridge.getAutomaticSharedTrackers(), - - bridge.getShareSetting(TrackerRole.LEFT_FOOT), - bridge.getShareSetting(TrackerRole.RIGHT_FOOT), - bridge.getShareSetting(TrackerRole.LEFT_KNEE), - bridge.getShareSetting(TrackerRole.RIGHT_KNEE), - bridge.getShareSetting(TrackerRole.LEFT_ELBOW), - bridge.getShareSetting(TrackerRole.RIGHT_ELBOW), - bridge.getShareSetting(TrackerRole.LEFT_HAND), - bridge.getShareSetting(TrackerRole.RIGHT_HAND), - ) - } - return steamvrTrackerSettings -} - -fun createModelSettings( - fbb: FlatBufferBuilder, - humanPoseManager: HumanPoseManager, - legTweaksConfig: LegTweaksConfig, - skeletonConfig: SkeletonConfig, -): Int { - val togglesOffset = ModelToggles - .createModelToggles( - fbb, - humanPoseManager.getToggle(SkeletonConfigToggles.EXTENDED_SPINE_MODEL), - humanPoseManager.getToggle(SkeletonConfigToggles.EXTENDED_PELVIS_MODEL), - humanPoseManager.getToggle(SkeletonConfigToggles.EXTENDED_KNEE_MODEL), - humanPoseManager.getToggle(SkeletonConfigToggles.FORCE_ARMS_FROM_HMD), - humanPoseManager.getToggle(SkeletonConfigToggles.FLOOR_CLIP), - humanPoseManager.getToggle(SkeletonConfigToggles.SKATING_CORRECTION), - humanPoseManager.getToggle(SkeletonConfigToggles.TOE_SNAP), - humanPoseManager.getToggle(SkeletonConfigToggles.FOOT_PLANT), - humanPoseManager.getToggle(SkeletonConfigToggles.SELF_LOCALIZATION), - humanPoseManager.getToggle(SkeletonConfigToggles.USE_POSITION), - humanPoseManager.getToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS), - humanPoseManager.getToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS), - ) - val ratiosOffset = ModelRatios - .createModelRatios( - fbb, - humanPoseManager.getValue(SkeletonConfigValues.WAIST_FROM_CHEST_HIP_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.WAIST_FROM_CHEST_LEGS_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.HIP_FROM_CHEST_LEGS_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.HIP_FROM_WAIST_LEGS_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.HIP_LEGS_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.KNEE_TRACKER_ANKLE_AVERAGING), - humanPoseManager.getValue(SkeletonConfigValues.KNEE_ANKLE_AVERAGING), - ) - val legTweaksOffset = LegTweaksSettings - .createLegTweaksSettings( - fbb, - legTweaksConfig.correctionStrength, - ) - val skeletonConfigOffset = SkeletonHeight - .createSkeletonHeight( - fbb, - skeletonConfig.hmdHeight, - skeletonConfig.floorHeight, - ) - return ModelSettings - .createModelSettings( - fbb, - togglesOffset, - ratiosOffset, - legTweaksOffset, - skeletonConfigOffset, - ) -} - -fun createAutoBoneSettings( - fbb: FlatBufferBuilder, - autoBoneConfig: AutoBoneConfig, -): Int = AutoBoneSettings - .createAutoBoneSettings( - fbb, - autoBoneConfig.cursorIncrement, - autoBoneConfig.minDataDistance, - autoBoneConfig.maxDataDistance, - autoBoneConfig.numEpochs, - autoBoneConfig.printEveryNumEpochs, - autoBoneConfig.initialAdjustRate, - autoBoneConfig.adjustRateDecay, - autoBoneConfig.slideErrorFactor, - autoBoneConfig.offsetSlideErrorFactor, - autoBoneConfig.footHeightOffsetErrorFactor, - autoBoneConfig.bodyProportionErrorFactor, - autoBoneConfig.heightErrorFactor, - autoBoneConfig.positionErrorFactor, - autoBoneConfig.positionOffsetErrorFactor, - autoBoneConfig.calcInitError, - autoBoneConfig.randomizeFrameOrder, - autoBoneConfig.scaleEachStep, - autoBoneConfig.sampleCount, - autoBoneConfig.sampleRateMs, - autoBoneConfig.saveRecordings, - autoBoneConfig.useSkeletonHeight, - autoBoneConfig.randSeed, - ) - -/** - * Writes values from AutoBoneSettings to an AutoBoneConfig. - * - * @param autoBoneSettings The settings to read from. - * @param autoBoneConfig The config to write to. - * @return The autoBoneConfig parameter. - */ -fun readAutoBoneSettings( - autoBoneSettings: AutoBoneSettings, - autoBoneConfig: AutoBoneConfig, -): AutoBoneConfig { - if (autoBoneSettings.hasCursorIncrement()) { - autoBoneConfig.cursorIncrement = autoBoneSettings.cursorIncrement() - } - if (autoBoneSettings.hasMinDataDistance()) { - autoBoneConfig.minDataDistance = autoBoneSettings.minDataDistance() - } - if (autoBoneSettings.hasMaxDataDistance()) { - autoBoneConfig.maxDataDistance = autoBoneSettings.maxDataDistance() - } - if (autoBoneSettings.hasNumEpochs()) { - autoBoneConfig.numEpochs = autoBoneSettings.numEpochs() - } - if (autoBoneSettings.hasPrintEveryNumEpochs()) { - autoBoneConfig.printEveryNumEpochs = autoBoneSettings.printEveryNumEpochs() - } - if (autoBoneSettings.hasInitialAdjustRate()) { - autoBoneConfig.initialAdjustRate = autoBoneSettings.initialAdjustRate() - } - if (autoBoneSettings.hasAdjustRateDecay()) { - autoBoneConfig.adjustRateDecay = autoBoneSettings.adjustRateDecay() - } - if (autoBoneSettings.hasSlideErrorFactor()) { - autoBoneConfig.slideErrorFactor = autoBoneSettings.slideErrorFactor() - } - if (autoBoneSettings.hasOffsetSlideErrorFactor()) { - autoBoneConfig.offsetSlideErrorFactor = - autoBoneSettings.offsetSlideErrorFactor() - } - if (autoBoneSettings.hasFootHeightOffsetErrorFactor()) { - autoBoneConfig - .footHeightOffsetErrorFactor = - autoBoneSettings.footHeightOffsetErrorFactor() - } - if (autoBoneSettings.hasBodyProportionErrorFactor()) { - autoBoneConfig - .bodyProportionErrorFactor = autoBoneSettings.bodyProportionErrorFactor() - } - if (autoBoneSettings.hasHeightErrorFactor()) { - autoBoneConfig.heightErrorFactor = autoBoneSettings.heightErrorFactor() - } - if (autoBoneSettings.hasPositionErrorFactor()) { - autoBoneConfig.positionErrorFactor = autoBoneSettings.positionErrorFactor() - } - if (autoBoneSettings.hasPositionOffsetErrorFactor()) { - autoBoneConfig - .positionOffsetErrorFactor = autoBoneSettings.positionOffsetErrorFactor() - } - if (autoBoneSettings.hasCalcInitError()) { - autoBoneConfig.calcInitError = autoBoneSettings.calcInitError() - } - if (autoBoneSettings.hasRandomizeFrameOrder()) { - autoBoneConfig.randomizeFrameOrder = autoBoneSettings.randomizeFrameOrder() - } - if (autoBoneSettings.hasScaleEachStep()) { - autoBoneConfig.scaleEachStep = autoBoneSettings.scaleEachStep() - } - if (autoBoneSettings.hasSampleCount()) { - autoBoneConfig.sampleCount = autoBoneSettings.sampleCount() - } - if (autoBoneSettings.hasSampleRateMs()) { - autoBoneConfig.sampleRateMs = autoBoneSettings.sampleRateMs() - } - if (autoBoneSettings.hasSaveRecordings()) { - autoBoneConfig.saveRecordings = autoBoneSettings.saveRecordings() - } - if (autoBoneSettings.hasUseSkeletonHeight()) { - autoBoneConfig.useSkeletonHeight = autoBoneSettings.useSkeletonHeight() - } - if (autoBoneSettings.hasRandSeed()) { - autoBoneConfig.randSeed = autoBoneSettings.randSeed() - } - - return autoBoneConfig -} - -fun createArmsResetModeSettings( - fbb: FlatBufferBuilder, - resetsConfig: ResetsConfig, -): Int = ResetsSettings - .createResetsSettings( - fbb, - resetsConfig.resetMountingFeet, - resetsConfig.mode.id, - resetsConfig.yawResetSmoothTime, - resetsConfig.saveMountingReset, - resetsConfig.resetHmdPitch, - ) - -fun createSettingsResponse(fbb: FlatBufferBuilder, server: VRServer): Int { - val bridge = server.getVRBridge(ISteamVRBridge::class.java) - - return SettingsResponse - .createSettingsResponse( - fbb, - createSteamVRSettings(fbb, bridge), - createFilterSettings( - fbb, - server.configManager.vrConfig.filters, - ), - createDriftCompensationSettings( - fbb, - server.configManager.vrConfig.driftCompensation, - ), - createOSCRouterSettings( - fbb, - server.configManager.vrConfig.oscRouter, - ), - createVRCOSCSettings( - fbb, - server.configManager.vrConfig.vrcOSC, - ), - createVMCOSCSettings( - fbb, - server.configManager.vrConfig.vmc, - ), - createModelSettings( - fbb, - server.humanPoseManager, - server.configManager.vrConfig.legTweaks, - server.configManager.vrConfig.skeleton, - ), - createTapDetectionSettings( - fbb, - server.configManager.vrConfig.tapDetection, - ), - createAutoBoneSettings( - fbb, - server.configManager.vrConfig.autoBone, - ), - createArmsResetModeSettings( - fbb, - server.configManager.vrConfig.resetsConfig, - ), - createStayAlignedSettings( - fbb, - server.configManager.vrConfig.stayAlignedConfig, - ), - createHIDSettings(fbb, server.configManager.vrConfig.hidConfig), - ) -} - -fun createStayAlignedSettings( - fbb: FlatBufferBuilder, - config: StayAlignedConfig, -): Int = StayAlignedSettings - .createStayAlignedSettings( - fbb, - config.enabled, - false, // deprecated - config.hideYawCorrection, - config.standingRelaxedPose.enabled, - config.standingRelaxedPose.upperLegAngleInDeg, - config.standingRelaxedPose.lowerLegAngleInDeg, - config.standingRelaxedPose.footAngleInDeg, - config.sittingRelaxedPose.enabled, - config.sittingRelaxedPose.upperLegAngleInDeg, - config.sittingRelaxedPose.lowerLegAngleInDeg, - config.sittingRelaxedPose.footAngleInDeg, - config.flatRelaxedPose.enabled, - config.flatRelaxedPose.upperLegAngleInDeg, - config.flatRelaxedPose.lowerLegAngleInDeg, - config.flatRelaxedPose.footAngleInDeg, - config.setupComplete, - ) - -fun createHIDSettings( - fbb: FlatBufferBuilder, - config: HIDConfig, -): Int = HIDSettings - .createHIDSettings( - fbb, - config.trackersOverHID, - ) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt deleted file mode 100644 index 5f0453107a..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ /dev/null @@ -1,389 +0,0 @@ -package dev.slimevr.protocol.rpc.settings - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.config.ArmsResetModes -import dev.slimevr.filtering.TrackerFilters -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.processor.config.SkeletonConfigValues -import dev.slimevr.tracking.trackers.TrackerRole -import solarxr_protocol.rpc.ChangeSettingsRequest -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.SettingsResponse -import kotlin.math.* - -class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { - init { - rpcHandler.registerPacketListener(RpcMessage.SettingsRequest, ::onSettingsRequest) - rpcHandler.registerPacketListener(RpcMessage.ChangeSettingsRequest, ::onChangeSettingsRequest) - rpcHandler.registerPacketListener(RpcMessage.SettingsResetRequest, ::onSettingsResetRequest) - } - - fun onSettingsRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) { - rpcHandler.sendSettingsChangedResponse(conn, messageHeader) - } - - fun onChangeSettingsRequest(conn: GenericConnection?, messageHeader: RpcMessageHeader) { - val req = messageHeader - .message(ChangeSettingsRequest()) as? ChangeSettingsRequest ?: return - - if (req.steamVrTrackers() != null) { - val bridge = api.server - .getVRBridge(ISteamVRBridge::class.java) - - if (bridge != null) { - bridge.changeShareSettings(TrackerRole.WAIST, req.steamVrTrackers().waist()) - bridge.changeShareSettings(TrackerRole.CHEST, req.steamVrTrackers().chest()) - bridge.changeShareSettings(TrackerRole.LEFT_FOOT, req.steamVrTrackers().leftFoot()) - bridge.changeShareSettings(TrackerRole.RIGHT_FOOT, req.steamVrTrackers().rightFoot()) - bridge.changeShareSettings(TrackerRole.LEFT_KNEE, req.steamVrTrackers().leftKnee()) - bridge.changeShareSettings(TrackerRole.RIGHT_KNEE, req.steamVrTrackers().rightKnee()) - bridge.changeShareSettings(TrackerRole.LEFT_ELBOW, req.steamVrTrackers().leftElbow()) - bridge.changeShareSettings(TrackerRole.RIGHT_ELBOW, req.steamVrTrackers().rightElbow()) - bridge.changeShareSettings(TrackerRole.LEFT_HAND, req.steamVrTrackers().leftHand()) - bridge.changeShareSettings(TrackerRole.RIGHT_HAND, req.steamVrTrackers().rightHand()) - bridge.setAutomaticSharedTrackers(req.steamVrTrackers().automaticTrackerToggle()) - } - } - - if (req.filtering() != null) { - val type = TrackerFilters.fromId(req.filtering().type()) - if (type != null) { - val filtersConfig = api.server.configManager - .vrConfig - .filters - filtersConfig.type = type.configKey - filtersConfig.amount = req.filtering().amount() - filtersConfig.updateTrackersFilters() - } - } - - if (req.driftCompensation() != null) { - val driftCompensationConfig = api.server.configManager - .vrConfig - .driftCompensation - driftCompensationConfig.enabled = req.driftCompensation().enabled() - driftCompensationConfig.prediction = req.driftCompensation().prediction() - driftCompensationConfig.amount = req.driftCompensation().amount() - driftCompensationConfig.maxResets = req.driftCompensation().maxResets() - driftCompensationConfig.updateTrackersDriftCompensation() - } - - if (req.oscRouter() != null) { - val oscRouterConfig = api.server.configManager - .vrConfig - .oscRouter - val oscRouter = api.server.oSCRouter - val osc = req.oscRouter().oscSettings() - if (osc != null) { - oscRouterConfig.enabled = osc.enabled() - oscRouterConfig.portIn = osc.portIn() - oscRouterConfig.portOut = osc.portOut() - oscRouterConfig.address = osc.address() - } - - oscRouter.refreshSettings(true) - } - - if (req.vrcOsc() != null) { - val vrcOSCConfig = api.server.configManager - .vrConfig - .vrcOSC - val vrcOscHandler = api.server.vrcOSCHandler - val osc = req.vrcOsc().oscSettings() - val trackers = req.vrcOsc().trackers() - - if (osc != null) { - vrcOSCConfig.enabled = osc.enabled() - vrcOSCConfig.portIn = osc.portIn() - vrcOSCConfig.portOut = osc.portOut() - vrcOSCConfig.address = osc.address() - } - if (trackers != null) { - vrcOSCConfig.setOSCTrackerRole(TrackerRole.HEAD, trackers.head()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.CHEST, trackers.chest()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.WAIST, trackers.waist()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.LEFT_KNEE, trackers.knees()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.RIGHT_KNEE, trackers.knees()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.LEFT_FOOT, trackers.feet()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.RIGHT_FOOT, trackers.feet()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.LEFT_ELBOW, trackers.elbows()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.RIGHT_ELBOW, trackers.elbows()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.LEFT_HAND, trackers.hands()) - vrcOSCConfig.setOSCTrackerRole(TrackerRole.RIGHT_HAND, trackers.hands()) - } - vrcOSCConfig.oscqueryEnabled = req.vrcOsc().oscqueryEnabled() - - vrcOscHandler.refreshSettings(true) - } - - if (req.vmcOsc() != null) { - val vmcConfig = api.server.configManager - .vrConfig - .vmc - val vmcHandler = api.server.vMCHandler - val osc = req.vmcOsc().oscSettings() - - if (osc != null) { - vmcConfig.enabled = osc.enabled() - vmcConfig.portIn = osc.portIn() - vmcConfig.portOut = osc.portOut() - vmcConfig.address = osc.address() - } - if (req.vmcOsc().vrmJson() != null) { - vmcConfig.vrmJson = req.vmcOsc().vrmJson().ifEmpty { null } - } - vmcConfig.anchorHip = req.vmcOsc().anchorHip() - vmcConfig.mirrorTracking = req.vmcOsc().mirrorTracking() - - vmcHandler.refreshSettings(true) - } - - if (req.tapDetectionSettings() != null) { - val tapDetectionConfig = api.server.configManager - .vrConfig - .tapDetection - val tapDetectionSettings = req.tapDetectionSettings() - - if (tapDetectionSettings != null) { - // enable/disable tap detection - tapDetectionConfig.yawResetEnabled = tapDetectionSettings.yawResetEnabled() - tapDetectionConfig.fullResetEnabled = tapDetectionSettings.fullResetEnabled() - tapDetectionConfig - .mountingResetEnabled = tapDetectionSettings.mountingResetEnabled() - tapDetectionConfig.setupMode = tapDetectionSettings.setupMode() - - // set number of trackers that can have high accel before taps - // are rejected - if (tapDetectionSettings.hasNumberTrackersOverThreshold()) { - tapDetectionConfig - .numberTrackersOverThreshold = tapDetectionSettings.numberTrackersOverThreshold() - } - - // set tap detection delays - if (tapDetectionSettings.hasYawResetDelay()) { - tapDetectionConfig.yawResetDelay = tapDetectionSettings.yawResetDelay() - } - if (tapDetectionSettings.hasFullResetDelay()) { - tapDetectionConfig.fullResetDelay = tapDetectionSettings.fullResetDelay() - } - if (tapDetectionSettings.hasMountingResetDelay()) { - tapDetectionConfig - .mountingResetDelay = tapDetectionSettings.mountingResetDelay() - } - - // set the number of taps required for each action - if (tapDetectionSettings.hasYawResetTaps()) { - tapDetectionConfig - .yawResetTaps = tapDetectionSettings.yawResetTaps() - } - if (tapDetectionSettings.hasFullResetTaps()) { - tapDetectionConfig - .fullResetTaps = tapDetectionSettings.fullResetTaps() - } - if (tapDetectionSettings.hasMountingResetTaps()) { - tapDetectionConfig - .mountingResetTaps = tapDetectionSettings.mountingResetTaps() - } - - api.server.humanPoseManager.updateTapDetectionConfig() - } - } - - val modelSettings = req.modelSettings() - if (modelSettings != null) { - val hpm = api.server.humanPoseManager - val legTweaksConfig = api.server.configManager.vrConfig.legTweaks - val toggles = modelSettings.toggles() - val ratios = modelSettings.ratios() - val legTweaks = modelSettings.legTweaks() - - if (toggles != null) { - // Note: toggles.has____ returns the same as toggles._____ this - // seems like a bug - hpm.setToggle(SkeletonConfigToggles.EXTENDED_SPINE_MODEL, toggles.extendedSpine()) - hpm - .setToggle( - SkeletonConfigToggles.EXTENDED_PELVIS_MODEL, - toggles.extendedPelvis(), - ) - hpm.setToggle(SkeletonConfigToggles.EXTENDED_KNEE_MODEL, toggles.extendedKnee()) - hpm - .setToggle( - SkeletonConfigToggles.FORCE_ARMS_FROM_HMD, - toggles.forceArmsFromHmd(), - ) - hpm.setToggle(SkeletonConfigToggles.EXTENDED_SPINE_MODEL, toggles.extendedSpine()) - hpm - .setToggle( - SkeletonConfigToggles.EXTENDED_PELVIS_MODEL, - toggles.extendedPelvis(), - ) - hpm.setToggle(SkeletonConfigToggles.EXTENDED_KNEE_MODEL, toggles.extendedKnee()) - hpm - .setToggle( - SkeletonConfigToggles.FORCE_ARMS_FROM_HMD, - toggles.forceArmsFromHmd(), - ) - hpm.setToggle(SkeletonConfigToggles.FLOOR_CLIP, toggles.floorClip()) - hpm - .setToggle( - SkeletonConfigToggles.SKATING_CORRECTION, - toggles.skatingCorrection(), - ) - hpm.setToggle(SkeletonConfigToggles.TOE_SNAP, toggles.toeSnap()) - hpm.setToggle(SkeletonConfigToggles.FOOT_PLANT, toggles.footPlant()) - hpm.setToggle(SkeletonConfigToggles.SELF_LOCALIZATION, toggles.selfLocalization()) - hpm.setToggle(SkeletonConfigToggles.USE_POSITION, toggles.usePosition()) - hpm.setToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS, toggles.enforceConstraints()) - hpm.setToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS, toggles.correctConstraints()) - } - - if (ratios != null) { - if (ratios.hasImputeWaistFromChestHip()) { - hpm - .setValue( - SkeletonConfigValues.WAIST_FROM_CHEST_HIP_AVERAGING, - max(0f, ratios.imputeWaistFromChestHip()), - ) - } - if (ratios.hasImputeWaistFromChestLegs()) { - hpm - .setValue( - SkeletonConfigValues.WAIST_FROM_CHEST_LEGS_AVERAGING, - max(0f, ratios.imputeWaistFromChestLegs()), - ) - } - if (ratios.hasImputeHipFromChestLegs()) { - hpm - .setValue( - SkeletonConfigValues.HIP_FROM_CHEST_LEGS_AVERAGING, - max(0f, ratios.imputeHipFromChestLegs()), - ) - } - if (ratios.hasImputeHipFromWaistLegs()) { - hpm - .setValue( - SkeletonConfigValues.HIP_FROM_WAIST_LEGS_AVERAGING, - max(0f, ratios.imputeHipFromWaistLegs()), - ) - } - if (ratios.hasInterpHipLegs()) { - hpm - .setValue( - SkeletonConfigValues.HIP_LEGS_AVERAGING, - max(0f, ratios.interpHipLegs()), - ) - } - if (ratios.hasInterpKneeTrackerAnkle()) { - hpm - .setValue( - SkeletonConfigValues.KNEE_TRACKER_ANKLE_AVERAGING, - max(0f, ratios.interpKneeTrackerAnkle()), - ) - } - if (ratios.hasInterpKneeAnkle()) { - hpm - .setValue( - SkeletonConfigValues.KNEE_ANKLE_AVERAGING, - max(0f, ratios.interpKneeAnkle()), - ) - } - } - - if (legTweaks != null) { - if (legTweaks.hasCorrectionStrength()) { - legTweaksConfig.correctionStrength = legTweaks.correctionStrength() - } - api.server.humanPoseManager.updateLegTweaksConfig() - } - - modelSettings.skeletonHeight()?.let { - api.server.configManager.vrConfig.skeleton.hmdHeight = it.hmdHeight() - api.server.configManager.vrConfig.skeleton.floorHeight = it.floorHeight() - } - - hpm.saveConfig() - } - - val autoBoneSettings = req.autoBoneSettings() - if (autoBoneSettings != null) { - val autoBoneConfig = api.server.configManager - .vrConfig - .autoBone - - readAutoBoneSettings(autoBoneSettings, autoBoneConfig) - } - - if (req.resetsSettings() != null) { - val resetsConfig = api.server.configManager - .vrConfig - .resetsConfig - val mode = ArmsResetModes - .fromId(max(req.resetsSettings().armsMountingResetMode(), 0)) - if (mode != null) { - resetsConfig.mode = mode - } - resetsConfig.resetMountingFeet = req.resetsSettings().resetMountingFeet() - resetsConfig.saveMountingReset = req.resetsSettings().saveMountingReset() - resetsConfig.yawResetSmoothTime = req.resetsSettings().yawResetSmoothTime() - resetsConfig.resetHmdPitch = req.resetsSettings().resetHmdPitch() - resetsConfig.updateTrackersResetsSettings() - } - - if (req.stayAligned() != null) { - val config = api.server.configManager.vrConfig.stayAlignedConfig - val requestConfig = req.stayAligned() - config.enabled = requestConfig.enabled() - config.hideYawCorrection = requestConfig.hideYawCorrection() - config.standingRelaxedPose.enabled = requestConfig.standingEnabled() - config.standingRelaxedPose.upperLegAngleInDeg = requestConfig.standingUpperLegAngle() - config.standingRelaxedPose.lowerLegAngleInDeg = requestConfig.standingLowerLegAngle() - config.standingRelaxedPose.footAngleInDeg = requestConfig.standingFootAngle() - config.sittingRelaxedPose.enabled = requestConfig.sittingEnabled() - config.sittingRelaxedPose.upperLegAngleInDeg = requestConfig.sittingUpperLegAngle() - config.sittingRelaxedPose.lowerLegAngleInDeg = requestConfig.sittingLowerLegAngle() - config.sittingRelaxedPose.footAngleInDeg = requestConfig.sittingFootAngle() - config.flatRelaxedPose.enabled = requestConfig.flatEnabled() - config.flatRelaxedPose.upperLegAngleInDeg = requestConfig.flatUpperLegAngle() - config.flatRelaxedPose.lowerLegAngleInDeg = requestConfig.flatLowerLegAngle() - config.flatRelaxedPose.footAngleInDeg = requestConfig.flatFootAngle() - } - - if (req.hidSettings() != null) { - val config = api.server.configManager.vrConfig.hidConfig - val requestConfig = req.hidSettings() - config.trackersOverHID = requestConfig.trackersOverHid() - } - - api.server.configManager.saveConfig() - } - - fun onSettingsResetRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) { - api.server.configManager.resetConfig() - } - - companion object { - fun sendSteamVRUpdatedSettings(api: ProtocolAPI, rpcHandler: RPCHandler) { - val fbb = FlatBufferBuilder(32) - val bridge: ISteamVRBridge = - api.server.getVRBridge(ISteamVRBridge::class.java) ?: return - - val settings = SettingsResponse - .createSettingsResponse( - fbb, - createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ) - val outbound = - rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings) - fbb.finish(outbound) - api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt deleted file mode 100644 index 8cfb4f7786..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt +++ /dev/null @@ -1,86 +0,0 @@ -package dev.slimevr.protocol.rpc.setup - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.setup.HandshakeListener -import dev.slimevr.tracking.trackers.udp.UDPDevice -import solarxr_protocol.rpc.AddUnknownDeviceRequest -import solarxr_protocol.rpc.ForgetDeviceRequest -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.UnknownDeviceHandshakeNotification - -class RPCHandshakeHandler( - private val rpcHandler: RPCHandler, - val api: ProtocolAPI, -) : HandshakeListener { - init { - rpcHandler.registerPacketListener( - RpcMessage.AddUnknownDeviceRequest, - ::onAddUnknownDevice, - ) - - rpcHandler.registerPacketListener( - RpcMessage.ForgetDeviceRequest, - ::onForgetDevice, - ) - - this.api.server.handshakeHandler.addListener(this) - } - - override fun onUnknownHandshake(macAddress: String) { - val fbb = FlatBufferBuilder(32) - val string = fbb.createString(macAddress) - val update = - UnknownDeviceHandshakeNotification.createUnknownDeviceHandshakeNotification( - fbb, - string, - ) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.UnknownDeviceHandshakeNotification, - update, - ) - fbb.finish(outbound) - - api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { - it.send(fbb.dataBuffer()) - } - } - } - - fun onAddUnknownDevice( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = - messageHeader.message(AddUnknownDeviceRequest()) as AddUnknownDeviceRequest? - ?: return - - this.api.server.configManager.vrConfig.addKnownDevice( - req.macAddress() ?: return, - ) - this.api.server.configManager.saveConfig() - } - - fun onForgetDevice( - conn: GenericConnection, - messageHeader: RpcMessageHeader, - ) { - val req = messageHeader.message(ForgetDeviceRequest()) as ForgetDeviceRequest? - ?: return - - this.api.server.configManager.vrConfig.forgetKnownDevice( - req.macAddress() ?: return, - ) - val device = - this.api.server.deviceManager.devices.find { it.hardwareIdentifier == req.macAddress() } - if (device != null && device is UDPDevice) { - this.api.server.trackersServer.disconnectDevice(device) - } - this.api.server.configManager.saveConfig() - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt deleted file mode 100644 index ea5ac3adc5..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.slimevr.protocol.rpc.setup - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.datafeed.createTrackerId -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.setup.TapSetupListener -import dev.slimevr.tracking.trackers.Tracker -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.TapDetectionSetupNotification - -class RPCTapSetupHandler( - private val rpcHandler: RPCHandler, - val api: ProtocolAPI, -) : TapSetupListener { - init { - this.api.server.tapSetupHandler.addListener(this) - } - - override fun onStarted(tracker: Tracker) { - val fbb = FlatBufferBuilder(32) - val idOffset = createTrackerId(fbb, tracker) - val update = TapDetectionSetupNotification.createTapDetectionSetupNotification(fbb, idOffset) - val outbound = - rpcHandler.createRPCMessage(fbb, RpcMessage.TapDetectionSetupNotification, update) - fbb.finish(outbound) - - forAllListeners { - it.send( - fbb.dataBuffer(), - ) - } - } - - private fun forAllListeners(action: (GenericConnection) -> Unit) { - api - .apiServers - .forEach { - it - .apiConnections - .forEach(action) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCUtil.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCUtil.kt deleted file mode 100644 index ef46f2a6c3..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.slimevr.protocol.rpc.setup - -import java.net.NetworkInterface - -object RPCUtil { - @JvmStatic - fun getLocalIp(): String? { - for (netInt in NetworkInterface.getNetworkInterfaces()) { - if (netInt.isUp && !netInt.isLoopback && !netInt.isVirtual) { - for (netAddr in netInt.interfaceAddresses) { - if (netAddr.address.isSiteLocalAddress && netAddr.broadcast != null) { - return netAddr.address.hostAddress - } - } - } - } - return null - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt deleted file mode 100644 index f9df6f8341..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt +++ /dev/null @@ -1,76 +0,0 @@ -package dev.slimevr.protocol.rpc.status - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.status.StatusListener -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.StatusDataUnion -import solarxr_protocol.rpc.StatusMessage -import solarxr_protocol.rpc.StatusSystemFixed -import solarxr_protocol.rpc.StatusSystemUpdate - -class RPCStatusHandler( - private val rpcHandler: RPCHandler, - val api: ProtocolAPI, -) : StatusListener { - - init { - api.server.statusSystem.addListener(this) - } - - override fun onStatusChanged( - id: UInt, - message: StatusDataUnion, - prioritized: Boolean, - ) { - val fbb = FlatBufferBuilder(STATUS_EXPECTED_SIZE) - - val messageOffset = StatusDataUnion.pack(fbb, message) - - StatusMessage.startStatusMessage(fbb) - StatusMessage.addData(fbb, messageOffset) - StatusMessage.addId(fbb, id.toLong()) - StatusMessage.addDataType(fbb, message.type) - StatusMessage.addPrioritized(fbb, prioritized) - val statusOffset = StatusMessage.endStatusMessage(fbb) - - StatusSystemUpdate.startStatusSystemUpdate(fbb) - StatusSystemUpdate.addNewStatus(fbb, statusOffset) - val update = StatusSystemUpdate.endStatusSystemUpdate(fbb) - - val outbound = this.rpcHandler.createRPCMessage( - fbb, - RpcMessage.StatusSystemUpdate, - update, - ) - fbb.finish(outbound) - - this.api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } - - override fun onStatusRemoved(id: UInt) { - val fbb = FlatBufferBuilder(4) - - StatusSystemFixed.startStatusSystemFixed(fbb) - StatusSystemFixed.addFixedStatusId(fbb, id.toLong()) - val update = StatusSystemFixed.endStatusSystemFixed(fbb) - - val outbound = this.rpcHandler.createRPCMessage( - fbb, - RpcMessage.StatusSystemFixed, - update, - ) - fbb.finish(outbound) - - this.api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } - - companion object { - const val STATUS_EXPECTED_SIZE = 32 - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt deleted file mode 100644 index ca6f6b34e7..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.slimevr.protocol.rpc.trackingchecklist - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.trackingchecklist.TrackingChecklistListener -import solarxr_protocol.rpc.* - -class RPCTrackingChecklistHandler( - private val rpcHandler: RPCHandler, - var api: ProtocolAPI, -) : TrackingChecklistListener { - - init { - api.server.trackingChecklistManager.addListener(this) - - rpcHandler.registerPacketListener(RpcMessage.TrackingChecklistRequest, ::onTrackingChecklistRequest) - rpcHandler.registerPacketListener(RpcMessage.IgnoreTrackingChecklistStepRequest, ::onToggleTrackingChecklistRequest) - } - - fun buildTrackingChecklistResponse(fbb: FlatBufferBuilder): Int = TrackingChecklistResponse.pack( - fbb, - TrackingChecklistResponseT().apply { - steps = api.server.trackingChecklistManager.steps.toTypedArray() - ignoredSteps = api.server.configManager.vrConfig.trackingChecklist.ignoredStepsIds.toIntArray() - }, - ) - - private fun onTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val fbb = FlatBufferBuilder(32) - val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.TrackingChecklistResponse, - response, - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - private fun onToggleTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - val req = messageHeader.message(IgnoreTrackingChecklistStepRequest()) as IgnoreTrackingChecklistStepRequest? - ?: return - val step = api.server.trackingChecklistManager.steps.find { it.id == req.stepId() } ?: error("invalid step id requested") - - api.server.trackingChecklistManager.ignoreStep(step, req.ignore()) - - val fbb = FlatBufferBuilder(32) - val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.TrackingChecklistResponse, - response, - ) - fbb.finish(outbound) - conn.send(fbb.dataBuffer()) - } - - override fun onStepsUpdate() { - val fbb = FlatBufferBuilder(32) - val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( - fbb, - RpcMessage.TrackingChecklistResponse, - response, - ) - fbb.finish(outbound) - this.api.apiServers.forEach { apiServer -> - apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt deleted file mode 100644 index 8ff19b2458..0000000000 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.slimevr.protocol.rpc.trackingpause - -import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.protocol.GenericConnection -import dev.slimevr.protocol.ProtocolAPI -import dev.slimevr.protocol.rpc.RPCHandler -import dev.slimevr.trackingpause.TrackingPauseListener -import solarxr_protocol.rpc.RpcMessage -import solarxr_protocol.rpc.RpcMessageHeader -import solarxr_protocol.rpc.TrackingPauseStateResponse -import java.nio.ByteBuffer - -class RPCTrackingPause(private val rpcHandler: RPCHandler, private val api: ProtocolAPI) : TrackingPauseListener { - - private val currentPauseState - get() = api.server.humanPoseManager.skeleton.getPauseTracking() - - init { - rpcHandler.registerPacketListener( - RpcMessage.TrackingPauseStateRequest, - ::onTrackingPauseStateRequest, - ) - - // HumanPoseManager might not be immediately available, so queue the server - // to register the listener once it's fully initialized - api.server.queueTask { - api.server.humanPoseManager.trackingPauseHandler.addListener(this) - } - } - - private fun getPauseStateResponse(trackingPaused: Boolean, messageHeader: RpcMessageHeader? = null): ByteBuffer { - val fbb = FlatBufferBuilder(32) - val state = TrackingPauseStateResponse.createTrackingPauseStateResponse(fbb, trackingPaused) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.TrackingPauseStateResponse, state, messageHeader) - fbb.finish(outbound) - return fbb.dataBuffer() - } - - private fun onTrackingPauseStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { - conn.send(getPauseStateResponse(currentPauseState, messageHeader)) - } - - override fun onTrackingPause(trackingPaused: Boolean) { - val pauseState = getPauseStateResponse(trackingPaused) - forAllListeners { it.send(pauseState) } - } - - private fun forAllListeners(action: (GenericConnection) -> Unit) { - api.apiServers.forEach { - it.apiConnections.forEach(action) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/provisioning/behaviours.kt b/server/core/src/main/java/dev/slimevr/provisioning/behaviours.kt new file mode 100644 index 0000000000..fcf2fee737 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/provisioning/behaviours.kt @@ -0,0 +1,22 @@ +package dev.slimevr.provisioning + +import dev.slimevr.provisioning.ProvisioningManager.Companion.INITIAL_STATE + +object ProvisioningManagerBaseBehaviour : ProvisioningManagerBehaviour { + override fun reduce(state: ProvisioningManagerState, action: ProvisioningActions) = when (action) { + is ProvisioningActions.PortSelected -> state.copy( + portLocation = action.portLocation, + macAddress = null, + ) + + is ProvisioningActions.StatusChanged -> state.copy( + status = action.status, + ) + + is ProvisioningActions.MacAddressObtained -> state.copy( + macAddress = action.mac, + ) + + is ProvisioningActions.Clear -> INITIAL_STATE + } +} diff --git a/server/core/src/main/java/dev/slimevr/provisioning/module.kt b/server/core/src/main/java/dev/slimevr/provisioning/module.kt new file mode 100644 index 0000000000..5bbfc4c28a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/provisioning/module.kt @@ -0,0 +1,105 @@ +package dev.slimevr.provisioning + +import dev.slimevr.Phase1ContextProvider +import dev.slimevr.VRServer +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.serial.SerialConnection +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import solarxr_protocol.rpc.WifiProvisioningStatus + +data class ProvisioningManagerState( + val status: WifiProvisioningStatus, + val portLocation: String?, + val macAddress: String?, +) + +sealed interface ProvisioningActions { + data class PortSelected(val portLocation: String) : ProvisioningActions + data class StatusChanged(val status: WifiProvisioningStatus) : ProvisioningActions + data class MacAddressObtained(val mac: String) : ProvisioningActions + data object Clear : ProvisioningActions +} + +typealias ProvisioningManagerContext = Context +typealias ProvisioningManagerBehaviour = Behaviour + +data class ProvisioningManager( + val context: ProvisioningManagerContext, + private val serialServer: SerialServer, + private val scope: CoroutineScope, +) { + fun startObserving() = context.observeAll(this) + + // Jobs cannot be held into a state / mutable flow + // as we cannot guarantee immutability + private var provisioningJob: Job? = null + + suspend fun stopProvisioning() { + provisioningJob?.cancelAndJoin() + context.dispatch(ProvisioningActions.Clear) + } + + suspend fun startProvisioning( + server: VRServer, + ssid: String, + password: String?, + port: String?, + ) { + val currentJob = provisioningJob + if (currentJob != null) { + currentJob.cancelAndJoin() + context.dispatch(ProvisioningActions.Clear) + } + provisioningJob = scope.launch { + // TODO handle port field, currently ignored + while (isActive) { + if (!selectAndOpenPort(context, serialServer)) continue + val portLocation = context.state.value.portLocation ?: continue + + val serialConn = + serialServer.context.state.value.connections[portLocation] as? SerialConnection.Console + + if (serialConn == null) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND)) + } else { + provisionPort(context, server, serialConn, ssid, password) + } + + // Any outcome (success or failure) waits for the port to disconnect + // before looking for the next tracker + serialServer.context.state.filter { portLocation !in it.availablePorts }.first() + context.dispatch(ProvisioningActions.Clear) + } + } + } + + companion object { + val INITIAL_STATE = ProvisioningManagerState( + status = WifiProvisioningStatus.NONE, + portLocation = null, + macAddress = null, + ) + + fun create(ctx: Phase1ContextProvider, scope: CoroutineScope): ProvisioningManager { + val behaviours = listOf(ProvisioningManagerBaseBehaviour) + val context = Context.create( + initialState = INITIAL_STATE, + scope = scope, + behaviours = behaviours, + ) + return ProvisioningManager( + context = context, + serialServer = ctx.serialServer, + scope = scope, + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/provisioning/phases.kt b/server/core/src/main/java/dev/slimevr/provisioning/phases.kt new file mode 100644 index 0000000000..6344ee5260 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/provisioning/phases.kt @@ -0,0 +1,218 @@ +package dev.slimevr.provisioning + +import dev.slimevr.VRServer +import dev.slimevr.firmware.waitForConnected +import dev.slimevr.serial.MAC_REGEX +import dev.slimevr.serial.SerialConnection +import dev.slimevr.serial.SerialConnectionActions +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withTimeoutOrNull +import solarxr_protocol.rpc.WifiProvisioningStatus + +internal const val MAX_CONNECTION_RETRIES = 3 + +// Waits for an available port, opens a serial connection, and dispatches PortSelected. +// Returns false if no port found within timeout. +internal suspend fun selectAndOpenPort( + context: ProvisioningManagerContext, + serialServer: SerialServer, +): Boolean { + val portEntry = withTimeoutOrNull(15_000) { + serialServer.context.state + .mapNotNull { state -> state.availablePorts.entries.firstOrNull() } + .first() + } + + if (portEntry == null) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND)) + delay(2_000) + return false + } + + context.dispatchAll( + listOf( + ProvisioningActions.PortSelected(portEntry.key), + ProvisioningActions.StatusChanged(WifiProvisioningStatus.SERIAL_INIT), + ), + ) + serialServer.openConnection(portEntry.key) + return true +} + +// Reboots the tracker and waits for a MAC address in the serial logs. +// Handles NO_SERIAL_LOGS_ERROR by blocking until logs appear (not counted as a retry). +// Dispatches MacAddressObtained on success. Returns false on failure. +internal suspend fun obtainMacAddress( + context: ProvisioningManagerContext, + serialConn: SerialConnection.Console, +): Boolean { + // Reboot and clear logs before MAC acquisition + serialConn.context.dispatch(SerialConnectionActions.ClearLogs) + serialConn.handle.writeCommand("REBOOT") + delay(2_000) + + while (currentCoroutineContext().isActive) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.OBTAINING_MAC_ADDRESS)) + serialConn.handle.writeCommand("GET INFO") + + val mac = withTimeoutOrNull(5_000) { + serialConn.context.state.map { it.logLines } + .mapNotNull { lines -> + lines.firstNotNullOfOrNull { MAC_REGEX.find(it)?.groupValues?.get(1)?.uppercase() } + } + .first() + } + + if (mac != null) { + context.dispatch(ProvisioningActions.MacAddressObtained(mac)) + return true + } + + // If no logs arrived at all, the tracker is connected but silent. + // Show the error and block until logs appear, this is not a retry. + if (serialConn.context.state.value.logLines.isEmpty()) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR)) + serialConn.context.state.filter { it.logLines.isNotEmpty() }.first() + + // The GET INFO response may have arrived while we were in the error state. + val existingMac = serialConn.context.state.value.logLines + .firstNotNullOfOrNull { MAC_REGEX.find(it)?.groupValues?.get(1)?.uppercase() } + if (existingMac != null) { + context.dispatch(ProvisioningActions.MacAddressObtained(existingMac)) + return true + } + + // No MAC yet, retry GET INFO without rebooting + continue + } + + // Got logs but no MAC after timeout, genuine error + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTION_ERROR)) + delay(3_000) + return false + } + return false +} + +// Sends Wi-Fi credentials and waits for acknowledgement. +// Returns false on timeout. +internal suspend fun sendCredentials( + context: ProvisioningManagerContext, + serialConn: SerialConnection.Console, + ssid: String, + password: String?, +): Boolean { + serialConn.context.dispatch(SerialConnectionActions.ClearLogs) + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.PROVISIONING)) + serialConn.handle.writeCommand("SET WIFI \"$ssid\" \"${password ?: ""}\"\n") + + val acked = withTimeoutOrNull(5_000) { + serialConn.context.state.map { it.logLines } + .filter { lines -> lines.any { "new wifi credentials set" in it.lowercase() } } + .first() + } + + if (acked == null) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTION_ERROR)) + delay(3_000) + return false + } + return true +} + +// Waits for the tracker to reach "looking for server", retrying on "can't connect" up to MAX_CONNECTION_RETRIES. +// Returns false on timeout or exhausted retries. +internal suspend fun waitForWifiConnect( + context: ProvisioningManagerContext, + serialConn: SerialConnection.Console, +): Boolean { + var connectRetries = 0 + + while (currentCoroutineContext().isActive) { + serialConn.context.dispatch(SerialConnectionActions.ClearLogs) + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTING)) + // null = timeout, true = looking for server, false = can't connect + val connectResult = withTimeoutOrNull(15_000) { + serialConn.context.state.map { it.logLines } + .mapNotNull { lines -> + when { + lines.any { + "looking for the server" in it.lowercase() || + "searching for the server" in it.lowercase() + } -> true + + lines.any { "can't connect from any credentials" in it.lowercase() } -> false + + else -> null + } + } + .first() + } + + when (connectResult) { + true -> return true + + false -> if (connectRetries < MAX_CONNECTION_RETRIES) { + connectRetries++ + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTION_ERROR)) + delay(3_000) + serialConn.handle.writeCommand("REBOOT") + } else { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTION_ERROR)) + delay(3_000) + return false + } + + else -> { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.CONNECTION_ERROR)) + delay(3_000) + return false + } + } + } + return false +} + +// Runs all provisioning phases for an already-opened serial connection. +// Dispatches DONE on success. Each phase dispatches its own error status on failure. +internal suspend fun provisionPort( + context: ProvisioningManagerContext, + server: VRServer, + serialConn: SerialConnection.Console, + ssid: String, + password: String?, +) { + if (!obtainMacAddress(context, serialConn)) return + val macAddress = context.state.value.macAddress ?: return + + if (!sendCredentials(context, serialConn, ssid, password)) return + if (!waitForWifiConnect(context, serialConn)) return + if (!waitForServerConnect(context, server, macAddress)) return + + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.DONE)) +} + +// Waits for the tracker to connect to the server via UDP. +// Returns false on timeout. +internal suspend fun waitForServerConnect( + context: ProvisioningManagerContext, + server: VRServer, + macAddress: String, +): Boolean { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.LOOKING_FOR_SERVER)) + val connected = waitForConnected(server, macAddress) + + if (connected == null) { + context.dispatch(ProvisioningActions.StatusChanged(WifiProvisioningStatus.COULD_NOT_FIND_SERVER)) + delay(3_000) + return false + } + return true +} diff --git a/server/core/src/main/java/dev/slimevr/reset/ResetHandler.kt b/server/core/src/main/java/dev/slimevr/reset/ResetHandler.kt deleted file mode 100644 index 69a68c6cb7..0000000000 --- a/server/core/src/main/java/dev/slimevr/reset/ResetHandler.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.reset - -import java.util.concurrent.CopyOnWriteArrayList -import java.util.function.Consumer - -class ResetHandler { - private val listeners: MutableList = CopyOnWriteArrayList() - - fun sendStarted(resetType: Int, bodyParts: List? = null, progress: Int = 0, duration: Int = 0) { - this.listeners.forEach { listener: ResetListener -> listener.onStarted(resetType, bodyParts, progress, duration) } - } - - fun sendFinished(resetType: Int, bodyParts: List? = null, duration: Int) { - this.listeners.forEach { listener: ResetListener -> listener.onFinished(resetType, bodyParts, duration) } - } - - fun addListener(listener: ResetListener) { - this.listeners.add(listener) - } - - fun removeListener(l: ResetListener) { - listeners.removeIf { listener: ResetListener -> l === listener } - } -} diff --git a/server/core/src/main/java/dev/slimevr/reset/ResetListener.kt b/server/core/src/main/java/dev/slimevr/reset/ResetListener.kt deleted file mode 100644 index b50fd3a91d..0000000000 --- a/server/core/src/main/java/dev/slimevr/reset/ResetListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slimevr.reset - -interface ResetListener { - fun onStarted(resetType: Int, bodyParts: List? = null, progress: Int, duration: Int) - - fun onFinished(resetType: Int, bodyParts: List? = null, duration: Int) -} diff --git a/server/core/src/main/java/dev/slimevr/reset/ResetTimer.kt b/server/core/src/main/java/dev/slimevr/reset/ResetTimer.kt deleted file mode 100644 index 2734e2af33..0000000000 --- a/server/core/src/main/java/dev/slimevr/reset/ResetTimer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.slimevr.reset - -import java.util.Timer -import java.util.TimerTask -import kotlin.concurrent.schedule -import kotlin.math.floor -import kotlin.math.min - -class ResetTimerManager { - val timer: Timer = Timer() - val timers: ArrayList = arrayListOf() - - fun cancelTimers() { - timers.forEach { it.cancel() } - } -} - -fun resetTimer(resetTimerManager: ResetTimerManager, delay: Long, onTick: (progress: Int) -> Unit, onComplete: () -> Unit) { - resetTimerManager.cancelTimers() - - if (delay == 0L) { - onComplete() - return - } - - val ticks: Int = floor(delay / 1000f).toInt() - for (tick in 0..ticks) { - if (tick * 1000L == delay) continue - resetTimerManager.timers.add( - resetTimerManager.timer.schedule(tick * 1000L) { - onTick(tick * 1000) - }, - ) - } - resetTimerManager.timers.add( - resetTimerManager.timer.schedule(delay) { - onComplete() - }, - ) -} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java b/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java deleted file mode 100644 index a17b17dda4..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java +++ /dev/null @@ -1,250 +0,0 @@ -package dev.slimevr.serial; - -import dev.slimevr.VRServer; -import io.eiren.util.logging.LogManager; -import kotlin.text.Regex; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CopyOnWriteArrayList; - - -public class ProvisioningHandler implements SerialListener { - - private ProvisioningStatus provisioningStatus = ProvisioningStatus.NONE; - - private boolean isRunning = false; - private final List listeners = new CopyOnWriteArrayList<>(); - - private String ssid; - private String password; - - private String preferredPort; - - private final Timer provisioningTickTimer = new Timer("ProvisioningTickTimer"); - private long lastStatusChange = -1; - private byte connectRetries = 0; - private boolean hasLogs = false; - private final byte MAX_CONNECTION_RETRIES = 1; - private final VRServer vrServer; - - public ProvisioningHandler(VRServer vrServer) { - this.vrServer = vrServer; - vrServer.serialHandler.addListener(this); - this.provisioningTickTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - if (!isRunning || provisioningStatus == ProvisioningStatus.DONE) - return; - provisioningTick(); - } - }, 0, 1000); - } - - - public void start(String ssid, String password, String port) { - this.isRunning = true; - this.hasLogs = false; - this.ssid = ssid; - this.password = password; - this.preferredPort = port; - this.provisioningStatus = ProvisioningStatus.NONE; - this.connectRetries = 0; - } - - public void stop() { - this.isRunning = false; - this.hasLogs = false; - this.ssid = null; - this.password = null; - this.connectRetries = 0; - this.changeStatus(ProvisioningStatus.NONE); - this.vrServer.serialHandler.closeSerial(); - } - - public void initSerial(String port) { - this.provisioningStatus = ProvisioningStatus.SERIAL_INIT; - this.hasLogs = false; - - try { - boolean openResult = false; - if (port != null) - openResult = vrServer.serialHandler.openSerial(port, false); - else - openResult = vrServer.serialHandler.openSerial(null, true); - if (!openResult) - LogManager.info("[SerialHandler] Serial port wasn't open..."); - } catch (Exception e) { - LogManager.severe("[SerialHandler] Unable to open serial port", e); - } catch (Throwable e) { - LogManager - .severe("[SerialHandler] Using serial ports is not supported on this platform", e); - } - - } - - public void tryObtainMacAddress() { - this.changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS); - vrServer.serialHandler.infoRequest(); - } - - public void tryProvisioning() { - this.changeStatus(ProvisioningStatus.PROVISIONING); - vrServer.serialHandler.setWifi(this.ssid, this.password); - } - - - public void provisioningTick() { - if (this.provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS) - this.tryObtainMacAddress(); - - if ( - !hasLogs - && this.provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS - && System.currentTimeMillis() - this.lastStatusChange > 1_000 - ) { - this.changeStatus(ProvisioningStatus.NO_SERIAL_LOGS_ERROR); - return; - } - - if ( - this.provisioningStatus == ProvisioningStatus.SERIAL_INIT - && vrServer.serialHandler.getKnownPorts().findAny().isEmpty() - && System.currentTimeMillis() - this.lastStatusChange > 15_000 - ) { - this.changeStatus(ProvisioningStatus.NO_SERIAL_DEVICE_FOUND); - return; - } - - if ( - System.currentTimeMillis() - this.lastStatusChange - > this.provisioningStatus.getTimeout() - ) { - if ( - this.provisioningStatus == ProvisioningStatus.NONE - || this.provisioningStatus == ProvisioningStatus.SERIAL_INIT - ) - this.initSerial(this.preferredPort); - else if (this.provisioningStatus == ProvisioningStatus.CONNECTING) - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); - else if (this.provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER) - this.changeStatus(ProvisioningStatus.COULD_NOT_FIND_SERVER); - else if (!this.provisioningStatus.isError()) { - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); // TIMEOUT - } - } - } - - - @Override - public void onSerialConnected(@NotNull SerialPort port) { - if (!isRunning) - return; - this.tryObtainMacAddress(); - } - - @Override - public void onSerialDisconnected() { - if (!isRunning) - return; - this.changeStatus(ProvisioningStatus.NONE); - this.connectRetries = 0; - } - - @Override - public void onSerialLog(@NotNull String str, boolean server) { - if (!isRunning) - return; - if (!server) { - this.hasLogs = true; - if (provisioningStatus == ProvisioningStatus.NO_SERIAL_LOGS_ERROR) { - // Recover the onboarding process if the user turned on the - // tracker afterward - this.changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS); - } - } - - if ( - provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS && str.contains("mac:") - ) { - var match = new Regex("mac: (?([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})), ") - .find(str, str.indexOf("mac:")); - - if (match != null) { - var b = match.getGroups().get(1); - if (b != null) { - vrServer.configManager.getVrConfig().addKnownDevice(b.getValue()); - vrServer.configManager.saveConfig(); - this.tryProvisioning(); - } - } - - } - - if ( - provisioningStatus == ProvisioningStatus.PROVISIONING - && str.contains("New wifi credentials set") - ) { - this.changeStatus(ProvisioningStatus.CONNECTING); - } - - if ( - provisioningStatus == ProvisioningStatus.CONNECTING - && (str.contains("Looking for the server") - || str.contains("Searching for the server")) - ) { - this.changeStatus(ProvisioningStatus.LOOKING_FOR_SERVER); - } - - if ( - provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER - && str.contains("Handshake successful") - ) { - this.changeStatus(ProvisioningStatus.DONE); - } - - if ( - provisioningStatus == ProvisioningStatus.CONNECTING - && str.contains("Can't connect from any credentials") - ) { - if (++connectRetries >= MAX_CONNECTION_RETRIES) { - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); - } else { - this.vrServer.serialHandler.rebootRequest(); - } - } - } - - public void changeStatus(ProvisioningStatus status) { - if (this.provisioningStatus != status) { - this.lastStatusChange = System.currentTimeMillis(); - this.listeners - .forEach( - (l) -> l - .onProvisioningStatusChange(status, vrServer.serialHandler.getCurrentPort()) - ); - this.provisioningStatus = status; - } - } - - @Override - public void onNewSerialDevice(SerialPort port) { - if (!isRunning) - return; - this.initSerial(this.preferredPort); - } - - public void addListener(ProvisioningListener channel) { - this.listeners.add(channel); - } - - public void removeListener(ProvisioningListener l) { - listeners.removeIf(listener -> l == listener); - } - - @Override - public void onSerialDeviceDeleted(@NotNull SerialPort port) { - } -} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java b/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java deleted file mode 100644 index a1cf17a03d..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.serial; - -public interface ProvisioningListener { - - void onProvisioningStatusChange(ProvisioningStatus status, SerialPort port); -} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningStatus.kt b/server/core/src/main/java/dev/slimevr/serial/ProvisioningStatus.kt deleted file mode 100644 index 59ba210be2..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/ProvisioningStatus.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.slimevr.serial - -import solarxr_protocol.rpc.WifiProvisioningStatus - -enum class ProvisioningStatus(@JvmField val id: Int, val isError: Boolean, val timeout: Int = 10_000) { - NONE(WifiProvisioningStatus.NONE, false, 3_000), - SERIAL_INIT(WifiProvisioningStatus.SERIAL_INIT, false, 3_000), - PROVISIONING(WifiProvisioningStatus.PROVISIONING, false), - OBTAINING_MAC_ADDRESS(WifiProvisioningStatus.OBTAINING_MAC_ADDRESS, false), - CONNECTING(WifiProvisioningStatus.CONNECTING, false, 30_000), - CONNECTION_ERROR(WifiProvisioningStatus.CONNECTION_ERROR, true), - LOOKING_FOR_SERVER(WifiProvisioningStatus.LOOKING_FOR_SERVER, false), - COULD_NOT_FIND_SERVER(WifiProvisioningStatus.COULD_NOT_FIND_SERVER, true), - NO_SERIAL_LOGS_ERROR(WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR, true), - NO_SERIAL_DEVICE_FOUND(WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND, true), - DONE(WifiProvisioningStatus.DONE, false), -} diff --git a/server/core/src/main/java/dev/slimevr/serial/SerialHandler.kt b/server/core/src/main/java/dev/slimevr/serial/SerialHandler.kt deleted file mode 100644 index b14c22831a..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/SerialHandler.kt +++ /dev/null @@ -1,83 +0,0 @@ -package dev.slimevr.serial - -import java.util.stream.Stream - -abstract class SerialHandler { - abstract val isConnected: Boolean - abstract val knownPorts: Stream - - abstract fun addListener(channel: SerialListener) - abstract fun removeListener(channel: SerialListener) - - abstract fun openSerial(portLocation: String?, auto: Boolean): Boolean - abstract fun rebootRequest() - abstract fun factoryResetRequest() - abstract fun infoRequest() - abstract fun wifiScanRequest() - abstract fun customCommandRequest(command: String) - abstract fun closeSerial() - abstract fun write(buff: ByteArray) - abstract fun setWifi(ssid: String, passwd: String) - abstract fun getCurrentPort(): SerialPort? - - companion object { - // Please also update the udev rules when updating these in: - // gui/src-tauri/69-slimevr-devices.rules - val supportedSerial: Set> = setOf( - // / QinHeng - // CH340 - Pair(0x1A86, 0x7522), - Pair(0x1A86, 0x7523), - // CH341 - Pair(0x1A86, 0x5523), - // CH343 - Pair(0x1A86, 0x55D3), - // CH9102x - Pair(0x1A86, 0x55D4), - // / Silabs - // CP210x - Pair(0x10C4, 0xEA60), - // / Espressif - // ESP32-S3 / ESP32-C3 / ESP32-C5 / ESP32-C6 / ESP32-C61 / ESP32-H2 / ESP32-P4 - Pair(0x303A, 0x1001), - // ESP32-S2 - Pair(0x303A, 0x0002), - // / FTDI - // FT232BM/L/Q, FT245BM/L/Q - // FT232RL/Q, FT245RL/Q - // VNC1L with VDPS Firmware - // VNC2 with FT232Slave - Pair(0x0403, 0x6001), - ) - fun isKnownBoard(port: SerialPort): Boolean = supportedSerial.contains(Pair(port.vendorId, port.productId)) - } -} - -class SerialHandlerStub : SerialHandler() { - override val isConnected: Boolean = false - override val knownPorts: Stream = Stream.empty() - - override fun addListener(channel: SerialListener) {} - - override fun removeListener(channel: SerialListener) {} - - override fun openSerial(portLocation: String?, auto: Boolean): Boolean = false - - override fun rebootRequest() {} - - override fun factoryResetRequest() {} - - override fun infoRequest() {} - - override fun wifiScanRequest() {} - - override fun customCommandRequest(command: String) {} - - override fun closeSerial() {} - - override fun write(buff: ByteArray) {} - - override fun setWifi(ssid: String, passwd: String) {} - - override fun getCurrentPort(): SerialPort? = null -} diff --git a/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt b/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt deleted file mode 100644 index fb60905341..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.slimevr.serial - -import java.util.* - -abstract class SerialPort { - abstract val portLocation: String - abstract val descriptivePortName: String - abstract val vendorId: Int? - abstract val productId: Int? - - override fun equals(other: Any?): Boolean { - val other: SerialPort = other as? SerialPort ?: return super.equals(other) - - return this.portLocation == other.portLocation && - this.descriptivePortName == other.descriptivePortName && - this.vendorId == other.vendorId && - this.productId == other.productId - } - - override fun hashCode(): Int = Objects.hash(portLocation, descriptivePortName, vendorId, productId) -} - -interface SerialListener { - fun onSerialConnected(port: SerialPort) - fun onSerialDisconnected() - - // var server indicates if the log is injected by the server (not an actual serial log) - fun onSerialLog(str: String, server: Boolean) - fun onNewSerialDevice(port: SerialPort) - - // This is called when the serial diver does not see the device anymore - fun onSerialDeviceDeleted(port: SerialPort) -} diff --git a/server/core/src/main/java/dev/slimevr/serial/behaviours.kt b/server/core/src/main/java/dev/slimevr/serial/behaviours.kt new file mode 100644 index 0000000000..15c6082739 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/serial/behaviours.kt @@ -0,0 +1,32 @@ +package dev.slimevr.serial + +internal const val MAX_LOG_LINES = 500 + +object SerialServerBaseBehaviour : SerialServerBehaviour { + override fun reduce(state: SerialServerState, action: SerialServerActions) = when (action) { + is SerialServerActions.PortDetected -> + state.copy(availablePorts = state.availablePorts + (action.info.portLocation to action.info)) + + is SerialServerActions.PortLost -> + state.copy(availablePorts = state.availablePorts - action.portLocation) + + is SerialServerActions.RegisterConnection -> + state.copy(connections = state.connections + (action.portLocation to action.connection)) + + is SerialServerActions.RemoveConnection -> + state.copy(connections = state.connections - action.portLocation) + } +} + +object SerialLogBehaviour : SerialConnectionBehaviour { + override fun reduce(state: SerialConnectionState, action: SerialConnectionActions) = when (action) { + is SerialConnectionActions.LogLine -> { + val lines = if (state.logLines.size >= MAX_LOG_LINES) state.logLines.drop(1) else state.logLines + state.copy(logLines = lines + action.line) + } + + is SerialConnectionActions.ClearLogs -> state.copy(logLines = listOf()) + + is SerialConnectionActions.Disconnected -> state.copy(connected = false) + } +} diff --git a/server/core/src/main/java/dev/slimevr/serial/connection.kt b/server/core/src/main/java/dev/slimevr/serial/connection.kt new file mode 100644 index 0000000000..225e706ca7 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/serial/connection.kt @@ -0,0 +1,58 @@ +package dev.slimevr.serial + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope + +val MAC_REGEX = Regex("mac: (([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2})", RegexOption.IGNORE_CASE) + +data class SerialPortHandle( + val portLocation: String, + val descriptivePortName: String, + val writeCommand: (String) -> Unit, + val close: () -> Unit, +) + +data class SerialConnectionState( + val portLocation: String, + val descriptivePortName: String, + val connected: Boolean, + val logLines: List, +) + +sealed interface SerialConnectionActions { + data class LogLine(val line: String) : SerialConnectionActions + data object ClearLogs : SerialConnectionActions + data object Disconnected : SerialConnectionActions +} + +typealias SerialConnectionContext = Context +typealias SerialConnectionBehaviour = Behaviour + +sealed interface SerialConnection { + class Console( + val context: SerialConnectionContext, + val handle: SerialPortHandle, + ) : SerialConnection { + companion object { + fun create(handle: SerialPortHandle, scope: CoroutineScope): Console { + val behaviours = listOf(SerialLogBehaviour) + val context = Context.create( + initialState = SerialConnectionState( + portLocation = handle.portLocation, + descriptivePortName = handle.descriptivePortName, + connected = true, + logLines = listOf(), + ), + scope = scope, + behaviours = behaviours, + ) + val conn = Console(context = context, handle = handle) + behaviours.forEach { it.observe(conn) } + return conn + } + } + } + + data object Flashing : SerialConnection +} diff --git a/server/core/src/main/java/dev/slimevr/serial/module.kt b/server/core/src/main/java/dev/slimevr/serial/module.kt new file mode 100644 index 0000000000..d7a3beef0a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/serial/module.kt @@ -0,0 +1,128 @@ +package dev.slimevr.serial + +import dev.llelievr.espflashkotlin.FlasherSerialInterface +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import solarxr_protocol.rpc.SerialDevice + +typealias FlashingHandler = FlasherSerialInterface + +data class SerialPortInfo( + val portLocation: String, + val descriptivePortName: String, + val vendorId: Int, + val productId: Int, +) { + fun toSerialDevice() = SerialDevice( + port = portLocation, + name = descriptivePortName, + ) +} + +data class SerialServerState( + val availablePorts: Map, + val connections: Map, +) + +sealed interface SerialServerActions { + data class PortDetected(val info: SerialPortInfo) : SerialServerActions + data class PortLost(val portLocation: String) : SerialServerActions + data class RegisterConnection(val portLocation: String, val connection: SerialConnection) : SerialServerActions + data class RemoveConnection(val portLocation: String) : SerialServerActions +} + +typealias SerialServerContext = Context +typealias SerialServerBehaviour = Behaviour + +class SerialServer( + val context: SerialServerContext, + private val openPortFactory: ( + portLocation: String, + onDataReceived: (portLocation: String, line: String) -> Unit, + onPortDisconnected: (portLocation: String) -> Unit, + ) -> SerialPortHandle?, + private val openFlashingPortFactory: () -> FlashingHandler, +) { + fun onPortDetected(info: SerialPortInfo) { + context.dispatch(SerialServerActions.PortDetected(info)) + } + + fun onPortLost(portLocation: String) { + val conn = context.state.value.connections[portLocation] + if (conn is SerialConnection.Console) { + conn.handle.close() + } + context.dispatchAll( + listOf( + SerialServerActions.RemoveConnection(portLocation), + SerialServerActions.PortLost(portLocation), + ), + ) + } + + fun onDataReceived(portLocation: String, line: String) { + val conn = context.state.value.connections[portLocation] + if (conn is SerialConnection.Console) conn.context.dispatch(SerialConnectionActions.LogLine(line)) + } + + fun onPortDisconnected(portLocation: String) { + val conn = context.state.value.connections[portLocation] + if (conn !is SerialConnection.Console) return + conn.context.dispatch(SerialConnectionActions.Disconnected) + conn.handle.close() + context.dispatch(SerialServerActions.RemoveConnection(portLocation)) + } + + fun openConnection(portLocation: String) { + val state = context.state.value + if (!state.availablePorts.containsKey(portLocation) || state.connections.containsKey(portLocation)) return + val handle = openPortFactory(portLocation, ::onDataReceived, ::onPortDisconnected) ?: return + context.dispatch(SerialServerActions.RegisterConnection(portLocation, SerialConnection.Console.create(handle, context.scope))) + } + + fun closeConnection(portLocation: String) { + val conn = context.state.value.connections[portLocation] + if (conn !is SerialConnection.Console) return + conn.context.dispatch(SerialConnectionActions.Disconnected) + conn.handle.close() + context.dispatch(SerialServerActions.RemoveConnection(portLocation)) + } + + fun openForFlashing(portLocation: String): FlashingHandler? { + val state = context.state.value + if (!state.availablePorts.containsKey(portLocation) || state.connections.containsKey(portLocation)) return null + closeConnection(portLocation) + val handler = openFlashingPortFactory() + context.dispatch(SerialServerActions.RegisterConnection(portLocation, SerialConnection.Flashing)) + return object : FlashingHandler by handler { + override fun closeSerial() { + handler.closeSerial() + context.scope.launch { context.dispatch(SerialServerActions.RemoveConnection(portLocation)) } + } + } + } + + companion object { + fun create( + openPort: (portLocation: String, onDataReceived: (String, String) -> Unit, onPortDisconnected: (String) -> Unit) -> SerialPortHandle?, + openFlashingPort: () -> FlashingHandler, + scope: CoroutineScope, + ): SerialServer { + val behaviours = listOf(SerialServerBaseBehaviour) + val context = Context.create( + initialState = SerialServerState(availablePorts = mapOf(), connections = mapOf()), + scope = scope, + behaviours = behaviours, + ) + val server = SerialServer( + context = context, + openPortFactory = openPort, + openFlashingPortFactory = openFlashingPort, + ) + behaviours.forEach { it.observe(context) } + return server + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/setup/HandshakeHandler.kt b/server/core/src/main/java/dev/slimevr/setup/HandshakeHandler.kt deleted file mode 100644 index 31b9288e0c..0000000000 --- a/server/core/src/main/java/dev/slimevr/setup/HandshakeHandler.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.slimevr.setup - -import java.util.concurrent.CopyOnWriteArrayList - -class HandshakeHandler { - private val listeners: MutableList = CopyOnWriteArrayList() - - fun addListener(listener: HandshakeListener) { - listeners.add(listener) - } - - fun removeListener(listener: HandshakeListener) { - listeners.remove(listener) - } - - fun sendUnknownHandshake(macAddress: String) { - listeners.forEach { it.onUnknownHandshake(macAddress) } - } -} - -interface HandshakeListener { - fun onUnknownHandshake(macAddress: String) -} diff --git a/server/core/src/main/java/dev/slimevr/setup/TapSetupHandler.kt b/server/core/src/main/java/dev/slimevr/setup/TapSetupHandler.kt deleted file mode 100644 index 841dd54822..0000000000 --- a/server/core/src/main/java/dev/slimevr/setup/TapSetupHandler.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.setup - -import dev.slimevr.tracking.trackers.Tracker -import java.util.concurrent.CopyOnWriteArrayList - -class TapSetupHandler { - private val listeners: MutableList = CopyOnWriteArrayList() - - fun addListener(listener: TapSetupListener) { - listeners.add(listener) - } - - fun removeListener(listener: TapSetupListener) { - listeners.remove(listener) - } - - fun sendTap(tracker: Tracker) { - listeners.forEach { it.onStarted(tracker) } - } -} - -interface TapSetupListener { - fun onStarted(tracker: Tracker) -} diff --git a/server/core/src/main/java/dev/slimevr/skeleton/behaviours.kt b/server/core/src/main/java/dev/slimevr/skeleton/behaviours.kt new file mode 100644 index 0000000000..6a166e60b5 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/behaviours.kt @@ -0,0 +1,111 @@ +package dev.slimevr.skeleton + +import dev.slimevr.config.UserConfig +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import solarxr_protocol.datatypes.BodyPart +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random + +class ProportionsBehaviour : SkeletonBehaviour { + override fun reduce(state: SkeletonState, action: SkeletonActions): SkeletonState = when (action) { + is SkeletonActions.SetProportions -> { + val newBones = state.bones.mapValues { (bodyPart, bone) -> + bone.copy(length = action.lengths[bodyPart] ?: bone.length) + } + state.copy(bones = newBones, userHeight = computeUserHeight(newBones.mapValues { (_, bone) -> bone.length })) + } + + else -> state + } +} + +class ScaledProportionsBehaviour(private val userConfig: UserConfig) : SkeletonBehaviour { + override fun observe(receiver: Skeleton) { + userConfig.context.state + .map { state -> state.data.proportions } + .distinctUntilChanged() + .onEach { proportions -> + if (proportions.isNotEmpty()) { + receiver.context.dispatch(SkeletonActions.SetProportions(expandProportions(proportions))) + } + } + .launchIn(receiver.context.scope) + } +} + +class HeightLogBehaviour : SkeletonBehaviour { + override fun observe(receiver: Skeleton) { + receiver.context.scope.launch { + receiver.context.state + .map { state -> state.userHeight } + .distinctUntilChanged() + .collect { height -> println("User height changed: ${"%.2f".format(height)}m") } + } + } +} + +class YouSpinMeRightRoundBehaviour(val inputHz: Float = 1f) : SkeletonBehaviour { + + override fun reduce(state: SkeletonState, action: SkeletonActions): SkeletonState { + val bones = state.bones.toMutableMap() + return when (action) { + is SkeletonActions.SetBoneRotation -> { + val bone = bones[action.bodyPart] ?: return state + bones[action.bodyPart] = bone.copy(rotation = action.rotation) + state.copy(bones = bones) + } + + else -> state + } + } + + override fun observe(receiver: Skeleton) { + receiver.context.scope.launch { + val intervalMs = (1000f / inputHz).toLong() + val startTime = System.currentTimeMillis() + while (true) { + delay(intervalMs) + val elapsed = (System.currentTimeMillis() - startTime) / 1000f + receiver.context.dispatch( + SkeletonActions.SetBoneRotation( + BodyPart.CHEST, + Quaternion.fromRotationVector(Vector3(cos(elapsed), sin(elapsed), 0f)), + ), + ) + receiver.context.dispatch( + SkeletonActions.SetBoneRotation( + BodyPart.LEFT_LOWER_LEG, + Quaternion.fromRotationVector(Vector3(cos(elapsed + 1000), sin(elapsed + 1000), 0f)), + ), + ) + } + } + } +} + +class ComputedSkeletonBehaviour( + val hz: Float = 100f, + val processors: List = emptyList(), +) : SkeletonBehaviour { + override fun observe(receiver: Skeleton) { + val intervalMs = (1000f / hz).toLong() + receiver.context.scope.launch { + while (true) { + delay(intervalMs) + val targetState = receiver.context.state.value + val processed = processors + .filter { processor -> processor.enabled } + .fold(targetState) { state, processor -> processor.process(state) } + receiver.computed.value = buildBones(processed) + } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/skeleton/bodypartstructure.kt b/server/core/src/main/java/dev/slimevr/skeleton/bodypartstructure.kt new file mode 100644 index 0000000000..efbfb8a986 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/bodypartstructure.kt @@ -0,0 +1,75 @@ +package dev.slimevr.skeleton + +import solarxr_protocol.datatypes.BodyPart + +val BODY_PART_HIERARCHY_MAP = mapOf( + BodyPart.HEAD to arrayOf(BodyPart.NECK), + BodyPart.NECK to arrayOf(BodyPart.UPPER_CHEST, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER), + + BodyPart.LEFT_SHOULDER to arrayOf(BodyPart.LEFT_UPPER_ARM), + BodyPart.LEFT_UPPER_ARM to arrayOf(BodyPart.LEFT_LOWER_ARM), + BodyPart.LEFT_LOWER_ARM to arrayOf(BodyPart.LEFT_HAND), + + BodyPart.LEFT_HAND to arrayOf( + BodyPart.LEFT_THUMB_METACARPAL, + BodyPart.LEFT_INDEX_PROXIMAL, + BodyPart.LEFT_MIDDLE_PROXIMAL, + BodyPart.LEFT_RING_PROXIMAL, + BodyPart.LEFT_LITTLE_PROXIMAL, + ), + BodyPart.LEFT_THUMB_METACARPAL to arrayOf(BodyPart.LEFT_THUMB_PROXIMAL), + BodyPart.LEFT_THUMB_PROXIMAL to arrayOf(BodyPart.LEFT_THUMB_DISTAL), + BodyPart.LEFT_INDEX_PROXIMAL to arrayOf(BodyPart.LEFT_INDEX_INTERMEDIATE), + BodyPart.LEFT_INDEX_INTERMEDIATE to arrayOf(BodyPart.LEFT_INDEX_DISTAL), + BodyPart.LEFT_MIDDLE_PROXIMAL to arrayOf(BodyPart.LEFT_MIDDLE_INTERMEDIATE), + BodyPart.LEFT_MIDDLE_INTERMEDIATE to arrayOf(BodyPart.LEFT_MIDDLE_DISTAL), + BodyPart.LEFT_RING_PROXIMAL to arrayOf(BodyPart.LEFT_RING_INTERMEDIATE), + BodyPart.LEFT_RING_INTERMEDIATE to arrayOf(BodyPart.LEFT_RING_DISTAL), + BodyPart.LEFT_LITTLE_PROXIMAL to arrayOf(BodyPart.LEFT_LITTLE_INTERMEDIATE), + BodyPart.LEFT_LITTLE_INTERMEDIATE to arrayOf(BodyPart.LEFT_LITTLE_DISTAL), + + BodyPart.RIGHT_SHOULDER to arrayOf(BodyPart.RIGHT_UPPER_ARM), + BodyPart.RIGHT_UPPER_ARM to arrayOf(BodyPart.RIGHT_LOWER_ARM), + BodyPart.RIGHT_LOWER_ARM to arrayOf(BodyPart.RIGHT_HAND), + + BodyPart.RIGHT_HAND to arrayOf( + BodyPart.RIGHT_THUMB_METACARPAL, + BodyPart.RIGHT_INDEX_PROXIMAL, + BodyPart.RIGHT_MIDDLE_PROXIMAL, + BodyPart.RIGHT_RING_PROXIMAL, + BodyPart.RIGHT_LITTLE_PROXIMAL, + ), + BodyPart.RIGHT_THUMB_METACARPAL to arrayOf(BodyPart.RIGHT_THUMB_PROXIMAL), + BodyPart.RIGHT_THUMB_PROXIMAL to arrayOf(BodyPart.RIGHT_THUMB_DISTAL), + BodyPart.RIGHT_INDEX_PROXIMAL to arrayOf(BodyPart.RIGHT_INDEX_INTERMEDIATE), + BodyPart.RIGHT_INDEX_INTERMEDIATE to arrayOf(BodyPart.RIGHT_INDEX_DISTAL), + BodyPart.RIGHT_MIDDLE_PROXIMAL to arrayOf(BodyPart.RIGHT_MIDDLE_INTERMEDIATE), + BodyPart.RIGHT_MIDDLE_INTERMEDIATE to arrayOf(BodyPart.RIGHT_MIDDLE_DISTAL), + BodyPart.RIGHT_RING_PROXIMAL to arrayOf(BodyPart.RIGHT_RING_INTERMEDIATE), + BodyPart.RIGHT_RING_INTERMEDIATE to arrayOf(BodyPart.RIGHT_RING_DISTAL), + BodyPart.RIGHT_LITTLE_PROXIMAL to arrayOf(BodyPart.RIGHT_LITTLE_INTERMEDIATE), + BodyPart.RIGHT_LITTLE_INTERMEDIATE to arrayOf(BodyPart.RIGHT_LITTLE_DISTAL), + + BodyPart.UPPER_CHEST to arrayOf(BodyPart.CHEST), + BodyPart.CHEST to arrayOf(BodyPart.WAIST), + BodyPart.WAIST to arrayOf(BodyPart.HIP), + BodyPart.HIP to arrayOf(BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP), + + BodyPart.LEFT_HIP to arrayOf(BodyPart.LEFT_UPPER_LEG), + BodyPart.LEFT_UPPER_LEG to arrayOf(BodyPart.LEFT_LOWER_LEG), + BodyPart.LEFT_LOWER_LEG to arrayOf(BodyPart.LEFT_FOOT), + + BodyPart.RIGHT_HIP to arrayOf(BodyPart.RIGHT_UPPER_LEG), + BodyPart.RIGHT_UPPER_LEG to arrayOf(BodyPart.RIGHT_LOWER_LEG), + BodyPart.RIGHT_LOWER_LEG to arrayOf(BodyPart.RIGHT_FOOT), +) + +private suspend fun SequenceScope>.visitBodyPart(parentBone: BodyPart?, bone: BodyPart) { + yield(Pair(parentBone, bone)) + val children = BODY_PART_HIERARCHY_MAP[bone] ?: return + for (child in children) visitBodyPart(bone, child) +} + +fun iterateBodyPartHierarchy() = sequence { + visitBodyPart(null, BodyPart.HEAD) +} diff --git a/server/core/src/main/java/dev/slimevr/skeleton/bonegeometry.kt b/server/core/src/main/java/dev/slimevr/skeleton/bonegeometry.kt new file mode 100644 index 0000000000..d8b1eea01a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/bonegeometry.kt @@ -0,0 +1,70 @@ +package dev.slimevr.skeleton + +import com.jme3.math.FastMath +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import solarxr_protocol.datatypes.BodyPart + +internal val BONE_TAIL_OFFSETS: Map = run { + val offsets = mutableMapOf() + iterateBodyPartHierarchy().forEach { (_, child) -> + val length = when (child) { + BodyPart.HEAD, BodyPart.NECK -> 0.1f + + BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER -> 0.175f + + BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, + BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM, + -> 0.26f + + BodyPart.LEFT_HAND, BodyPart.RIGHT_HAND -> 0.13f + + BodyPart.LEFT_THUMB_METACARPAL, BodyPart.LEFT_THUMB_PROXIMAL, BodyPart.LEFT_THUMB_DISTAL, + BodyPart.LEFT_INDEX_DISTAL, BodyPart.LEFT_INDEX_INTERMEDIATE, BodyPart.LEFT_INDEX_PROXIMAL, + BodyPart.LEFT_MIDDLE_DISTAL, BodyPart.LEFT_MIDDLE_INTERMEDIATE, BodyPart.LEFT_MIDDLE_PROXIMAL, + BodyPart.LEFT_RING_DISTAL, BodyPart.LEFT_RING_INTERMEDIATE, BodyPart.LEFT_RING_PROXIMAL, + BodyPart.LEFT_LITTLE_DISTAL, BodyPart.LEFT_LITTLE_INTERMEDIATE, BodyPart.LEFT_LITTLE_PROXIMAL, + + BodyPart.RIGHT_THUMB_METACARPAL, BodyPart.RIGHT_THUMB_PROXIMAL, BodyPart.RIGHT_THUMB_DISTAL, + BodyPart.RIGHT_INDEX_PROXIMAL, BodyPart.RIGHT_INDEX_INTERMEDIATE, BodyPart.RIGHT_INDEX_DISTAL, + BodyPart.RIGHT_MIDDLE_PROXIMAL, BodyPart.RIGHT_MIDDLE_INTERMEDIATE, BodyPart.RIGHT_MIDDLE_DISTAL, + BodyPart.RIGHT_RING_PROXIMAL, BodyPart.RIGHT_RING_INTERMEDIATE, BodyPart.RIGHT_RING_DISTAL, + BodyPart.RIGHT_LITTLE_PROXIMAL, BodyPart.RIGHT_LITTLE_INTERMEDIATE, BodyPart.RIGHT_LITTLE_DISTAL, + -> 0.025f + + BodyPart.UPPER_CHEST, BodyPart.CHEST -> 0.16f + + BodyPart.WAIST -> 0.2f + + BodyPart.HIP -> 0.04f + + BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP -> 0.13f + + BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG -> 0.42f + + BodyPart.LEFT_LOWER_LEG, BodyPart.RIGHT_LOWER_LEG -> 0.5f + + BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT -> 0.05f + + else -> 0.1f + } + val restRotation = when (child) { + BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT -> Quaternion.rotationAroundXAxis(FastMath.HALF_PI) + else -> Quaternion.IDENTITY + } + val offset = when (child) { + BodyPart.HEAD -> Vector3(0f, 0f, length) + BodyPart.LEFT_HAND, BodyPart.RIGHT_HAND -> Vector3(0f, 0f, -length) + BodyPart.LEFT_SHOULDER -> Vector3(-length, -0.08f, 0f) + BodyPart.RIGHT_SHOULDER -> Vector3(length, -0.08f, 0f) + BodyPart.LEFT_HIP -> Vector3(-length, 0f, 0f) + BodyPart.RIGHT_HIP -> Vector3(length, 0f, 0f) + else -> Vector3(0f, -length, 0f) + } + offsets[child] = restRotation.inv().sandwich(offset) + } + offsets +} + +internal val BONE_TAIL_DIRECTIONS: Map = + BONE_TAIL_OFFSETS.mapValues { (_, offset) -> offset.unit() } diff --git a/server/core/src/main/java/dev/slimevr/skeleton/module.kt b/server/core/src/main/java/dev/slimevr/skeleton/module.kt new file mode 100644 index 0000000000..6ffd744490 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/module.kt @@ -0,0 +1,96 @@ +package dev.slimevr.skeleton + +import com.jme3.math.FastMath +import dev.slimevr.Phase1ContextProvider +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import solarxr_protocol.datatypes.BodyPart + +data class BoneState( + val bodyPart: BodyPart, + val length: Float, + val rotation: Quaternion = Quaternion.IDENTITY, + val headPosition: Vector3 = Vector3.NULL, + val tailPosition: Vector3 = Vector3.NULL, + val parentBone: BoneState? = null, +) { + val localRotation: Quaternion + get() = parentBone?.let { it.rotation.inv() * rotation } ?: rotation + val localHeadPosition: Vector3 + get() = parentBone?.let { headPosition - it.tailPosition } ?: headPosition + val localTailPosition: Vector3 + get() = tailPosition - headPosition +} + +data class SkeletonState(val bones: Map, val userHeight: Double) + +val DEFAULT_SKELETON_STATE: SkeletonState = run { + val bones = BONE_TAIL_OFFSETS.entries.associate { (bodyPart, tailOffset) -> + val restRotation = when (bodyPart) { + BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT -> Quaternion.rotationAroundXAxis(FastMath.HALF_PI) + else -> Quaternion.IDENTITY + } + bodyPart to BoneState(bodyPart = bodyPart, length = tailOffset.len(), rotation = restRotation) + } + SkeletonState(bones = bones, userHeight = computeUserHeight(bones.mapValues { (_, bone) -> bone.length })) +} + +fun buildBones(state: SkeletonState, rootHead: Vector3 = Vector3.NULL): Map { + val result = mutableMapOf() + iterateBodyPartHierarchy().forEach { (parentPart, childPart) -> + val bone = state.bones[childPart] ?: return@forEach + val tailDirection = BONE_TAIL_DIRECTIONS[childPart] ?: return@forEach + val parentBone = parentPart?.let { result[it] } + val head = parentBone?.tailPosition ?: rootHead + result[childPart] = bone.copy( + headPosition = head, + tailPosition = head + bone.rotation.sandwich(tailDirection * bone.length), + parentBone = parentBone, + ) + } + return result +} + +sealed interface SkeletonActions { + data class SetBoneRotation(val bodyPart: BodyPart, val rotation: Quaternion) : SkeletonActions + data class SetProportions(val lengths: Map) : SkeletonActions +} + +typealias SkeletonContext = Context +typealias SkeletonBehaviour = Behaviour + +class Skeleton( + val context: SkeletonContext, + val computed: MutableStateFlow>, +) { + fun startObserving() = context.observeAll(this) + + companion object { + fun create(scope: CoroutineScope, ctx: Phase1ContextProvider): Skeleton { + val behaviours = listOf( + ProportionsBehaviour(), + ScaledProportionsBehaviour(ctx.config.userConfig), + HeightLogBehaviour(), + YouSpinMeRightRoundBehaviour(inputHz = 10f), + ComputedSkeletonBehaviour( + processors = listOf( +// PredictionProcessor(predictionAmount = 0.3f), + SmoothingProcessor(smoothing = 0.3f), + ), + ), + ) + + val context = Context.create( + initialState = DEFAULT_SKELETON_STATE, + scope = scope, + behaviours = behaviours, + ) + + return Skeleton(context, MutableStateFlow(buildBones(context.state.value))) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/skeleton/processors.kt b/server/core/src/main/java/dev/slimevr/skeleton/processors.kt new file mode 100644 index 0000000000..4ba99df0f1 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/processors.kt @@ -0,0 +1,76 @@ +package dev.slimevr.skeleton + +import io.github.axisangles.ktmath.Quaternion +import solarxr_protocol.datatypes.BodyPart + +interface SkeletonProcessor { + var enabled: Boolean + fun process(state: SkeletonState): SkeletonState +} + +class SmoothingProcessor(var smoothing: Float) : SkeletonProcessor { + override var enabled: Boolean = true + private var smoothedRotations: Map = emptyMap() + private var smoothedLengths: Map = emptyMap() + + override fun process(state: SkeletonState): SkeletonState { + val alpha = 1f - smoothing.coerceAtMost(0.99f) + smoothedRotations = state.bones.mapValues { (bodyPart, bone) -> + (smoothedRotations[bodyPart] ?: bone.rotation).lerpR(bone.rotation, alpha).unit() + } + smoothedLengths = state.bones.mapValues { (bodyPart, bone) -> + val prev = smoothedLengths[bodyPart] ?: bone.length + prev + (bone.length - prev) * alpha + } + return state.copy( + bones = state.bones.mapValues { (bodyPart, bone) -> + bone.copy( + rotation = smoothedRotations[bodyPart] ?: bone.rotation, + length = smoothedLengths[bodyPart] ?: bone.length, + ) + }, + ) + } +} + +class PredictionProcessor(var predictionAmount: Float) : SkeletonProcessor { + override var enabled: Boolean = true + + private data class BoneVelocity( + val lastRotation: Quaternion, + val rotationDelta: Quaternion, + val lastLength: Float, + val lengthDelta: Float, + ) + + private var velocities: Map = emptyMap() + + override fun process(state: SkeletonState): SkeletonState { + val newVelocities = mutableMapOf() + val newBones = state.bones.mapValues { (bodyPart, bone) -> + val prev = velocities[bodyPart] + if (prev == null) { + newVelocities[bodyPart] = BoneVelocity(bone.rotation, Quaternion.IDENTITY, bone.length, 0f) + return@mapValues bone + } + val rotationDelta = if (bone.rotation !== prev.lastRotation) { + bone.rotation * prev.lastRotation.inv() + } else { + prev.rotationDelta + } + val lengthDelta = if (bone.length != prev.lastLength) { + bone.length - prev.lastLength + } else { + prev.lengthDelta + } + newVelocities[bodyPart] = BoneVelocity(bone.rotation, rotationDelta, bone.length, lengthDelta) + val scaledDelta = Quaternion.IDENTITY.lerpR(rotationDelta, predictionAmount).unit() + bone.copy( + rotation = (scaledDelta * bone.rotation).unit(), + length = bone.length + lengthDelta * predictionAmount, + ) + } + velocities = newVelocities + return state.copy(bones = newBones) + } +} diff --git a/server/core/src/main/java/dev/slimevr/skeleton/proportions.kt b/server/core/src/main/java/dev/slimevr/skeleton/proportions.kt new file mode 100644 index 0000000000..5625bf732f --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/skeleton/proportions.kt @@ -0,0 +1,76 @@ +package dev.slimevr.skeleton + +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.rpc.SkeletonBone + +// Sum of default bone lengths for height-contributing bones (from Drillis and Contini 1966). +// Used to normalize HEIGHT_SCALED_BONE_RATIOS. +private const val DEFAULT_HEIGHT = 0.1f + 0.16f + 0.16f + 0.20f + 0.04f + 0.42f + 0.50f // = 1.58f + +// Maps each solarxr SkeletonBone to the BodyPart(s) it controls in the skeleton. +// Symmetric bones (legs, arms) map to both left and right sides. +val SKELETON_BONE_TO_BODY_PARTS: Map> = mapOf( + SkeletonBone.HEAD to listOf(BodyPart.HEAD), + SkeletonBone.NECK to listOf(BodyPart.NECK), + SkeletonBone.UPPER_CHEST to listOf(BodyPart.UPPER_CHEST), + SkeletonBone.CHEST to listOf(BodyPart.CHEST), + SkeletonBone.WAIST to listOf(BodyPart.WAIST), + SkeletonBone.HIP to listOf(BodyPart.HIP), + SkeletonBone.UPPER_LEG to listOf(BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG), + SkeletonBone.LOWER_LEG to listOf(BodyPart.LEFT_LOWER_LEG, BodyPart.RIGHT_LOWER_LEG), + SkeletonBone.FOOT_LENGTH to listOf(BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT), + SkeletonBone.UPPER_ARM to listOf(BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM), + SkeletonBone.LOWER_ARM to listOf(BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM), +) + +// Per-bone fraction of total standing height, derived from Drillis and Contini (1966). +// Includes spine, legs, and arms, all bones whose length scales with user height. +// Non-height bones (HEAD, FOOT_LENGTH) are absent; they keep fixed defaults from DEFAULT_SKELETON_STATE. +private val HEIGHT_SCALED_BONE_RATIOS: Map = mapOf( + SkeletonBone.NECK to 0.1f / DEFAULT_HEIGHT, + SkeletonBone.UPPER_CHEST to 0.16f / DEFAULT_HEIGHT, + SkeletonBone.CHEST to 0.16f / DEFAULT_HEIGHT, + SkeletonBone.WAIST to 0.20f / DEFAULT_HEIGHT, + SkeletonBone.HIP to 0.04f / DEFAULT_HEIGHT, + SkeletonBone.UPPER_LEG to 0.42f / DEFAULT_HEIGHT, + SkeletonBone.LOWER_LEG to 0.50f / DEFAULT_HEIGHT, + SkeletonBone.UPPER_ARM to 0.26f / DEFAULT_HEIGHT, + SkeletonBone.LOWER_ARM to 0.26f / DEFAULT_HEIGHT, +) + +// Subset of HEIGHT_SCALED_BONE_RATIOS whose lengths actually sum to standing height (spine + legs). +// Arms are excluded: they scale with height but are not part of the height measurement. +// For symmetric bones (UPPER_LEG, LOWER_LEG) computeUserHeight takes the larger side. +private val HEIGHT_CONTRIBUTING_BONES: Set = + HEIGHT_SCALED_BONE_RATIOS.keys - setOf(SkeletonBone.UPPER_ARM, SkeletonBone.LOWER_ARM) + +// Sums the HEIGHT_CONTRIBUTING_BONES lengths to derive standing height. +// For symmetric bones (legs) the larger side is used. +fun computeUserHeight(lengths: Map): Double = HEIGHT_CONTRIBUTING_BONES.sumOf { bone -> + val bodyParts = SKELETON_BONE_TO_BODY_PARTS[bone] ?: return@sumOf 0.0 + bodyParts.maxOfOrNull { lengths[it] ?: 0f }?.toDouble() ?: 0.0 +} + +// Returns proportions keyed by SkeletonBone.name for config storage. +// Only height-scaled bones are included. +fun computeDefaultProportionsByBone(height: Float): Map = HEIGHT_SCALED_BONE_RATIOS.mapKeys { (bone, _) -> bone.name } + .mapValues { (_, ratio) -> height * ratio } + +// Returns proportions for all tracked bones: height-scaled + default lengths for the rest. +fun computeAllDefaultProportionsByBone(height: Float): Map { + val heightScaled = computeDefaultProportionsByBone(height) + val nonScaled = SKELETON_BONE_TO_BODY_PARTS + .filter { (bone, _) -> bone !in HEIGHT_SCALED_BONE_RATIOS } + .mapNotNull { (bone, bodyParts) -> + val length = bodyParts + .mapNotNull { DEFAULT_SKELETON_STATE.bones[it]?.length } + .average().takeIf { !it.isNaN() }?.toFloat() ?: return@mapNotNull null + bone.name to length + }.toMap() + return heightScaled + nonScaled +} + +fun expandProportions(proportions: Map): Map = proportions.entries.flatMap { (boneName, length) -> + val bone = SkeletonBone.entries.firstOrNull { it.name == boneName } ?: return@flatMap emptyList() + (SKELETON_BONE_TO_BODY_PARTS[bone] ?: return@flatMap emptyList()).map { bodyPart -> bodyPart to length } +}.toMap() diff --git a/server/core/src/main/java/dev/slimevr/solarxr/datafeed.kt b/server/core/src/main/java/dev/slimevr/solarxr/datafeed.kt new file mode 100644 index 0000000000..0b867e79c1 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/datafeed.kt @@ -0,0 +1,159 @@ +package dev.slimevr.solarxr + +import dev.slimevr.VRServer +import dev.slimevr.device.DeviceState +import dev.slimevr.skeleton.BoneState +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.tracker.TrackerState +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import solarxr_protocol.data_feed.DataFeedConfig +import solarxr_protocol.data_feed.DataFeedMessageHeader +import solarxr_protocol.data_feed.DataFeedUpdate +import solarxr_protocol.data_feed.PollDataFeed +import solarxr_protocol.data_feed.StartDataFeed +import solarxr_protocol.data_feed.device_data.DeviceData +import solarxr_protocol.data_feed.tracker.TrackerData +import solarxr_protocol.data_feed.tracker.TrackerDataMask +import solarxr_protocol.data_feed.tracker.TrackerInfo +import solarxr_protocol.datatypes.DeviceId +import solarxr_protocol.datatypes.Ipv4Address +import solarxr_protocol.datatypes.Temperature +import solarxr_protocol.datatypes.TrackerId +import solarxr_protocol.datatypes.hardware_info.HardwareInfo +import solarxr_protocol.datatypes.hardware_info.HardwareStatus +import solarxr_protocol.datatypes.math.Quat +import solarxr_protocol.datatypes.math.Vec3f +import java.nio.ByteBuffer + +private fun createTracker(device: DeviceState, tracker: TrackerState, trackerMask: TrackerDataMask, datafeedConfig: DataFeedConfig): TrackerData = TrackerData( + trackerId = TrackerId( + trackerNum = tracker.id.toUByte(), + deviceId = DeviceId(device.id.toUByte()), + ), + status = if (trackerMask.status) tracker.status else null, + rotation = if (trackerMask.rotation) tracker.rawRotation.let { Quat(it.x, it.y, it.z, it.w) } else null, + position = if (trackerMask.position && tracker.position != null) tracker.position.let { Vec3f(it.x, it.y, it.z) } else null, + info = if (trackerMask.info) { + TrackerInfo( + imuType = tracker.sensorType, + bodyPart = tracker.bodyPart, + displayName = tracker.name, + customName = tracker.customName, + mountingOrientation = tracker.mountingOrientation?.let { Quat(it.x, it.y, it.z, it.w) }, + isImu = tracker.sensorType != null, + magnetometer = tracker.magStatus, + ) + } else { + null + }, + tps = if (trackerMask.tps) tracker.tps else null, + temp = if (trackerMask.temp && tracker.imuTemp != null) Temperature(temp = tracker.imuTemp) else null, + rawAcceleration = if (trackerMask.rawAcceleration) tracker.acceleration.let { Vec3f(it.x, it.y, it.z) } else null, + linearAcceleration = if (trackerMask.linearAcceleration) tracker.acceleration.let { Vec3f(it.x, it.y, it.z) } else null, // FIXME: temp value +) + +private fun createDevice( + device: DeviceState, + trackers: List, + datafeedConfig: DataFeedConfig, +): DeviceData { + val trackerMask = datafeedConfig.dataMask?.trackerData + + return DeviceData( + id = DeviceId(device.id.toUByte()), + hardwareStatus = HardwareStatus( + batteryVoltage = device.batteryVoltage, + batteryPctEstimate = (device.batteryLevel * 100).toUInt().toUByte(), + ping = device.ping?.toUShort(), + rssi = device.signalStrength?.toShort(), + packetsReceived = device.packetsReceived.toInt(), + packetsLost = device.packetsLost.toInt(), + packetLoss = if (device.packetsReceived > 0) device.packetsLost.toFloat() / device.packetsReceived.toFloat() else null, + ), + hardwareInfo = HardwareInfo( + mcuId = device.mcuType, + manufacturer = "SlimeVR", + boardType = device.boardType.toString(), + officialBoardType = device.boardType, + model = device.mcuType.toString(), + firmwareVersion = device.firmware, + ipAddress = Ipv4Address(ByteBuffer.wrap(device.address.toByteArray()).getLong().toUInt()), + ), + trackers = if (trackerMask != null) { + trackers.filter { it.deviceId == device.id } + .map { tracker -> createTracker(device, tracker, trackerMask, datafeedConfig) } + } else { + null + }, + ) +} + +private fun createBone(bone: BoneState): solarxr_protocol.data_feed.Bone = solarxr_protocol.data_feed.Bone( + bodyPart = bone.bodyPart, + rotationG = bone.rotation.let { Quat(it.x, it.y, it.z, it.w) }, + boneLength = bone.length, + headPositionG = bone.headPosition.let { Vec3f(it.x, it.y, it.z) }, +) + +fun createDatafeedFrame( + server: VRServer, + datafeedConfig: DataFeedConfig, + skeleton: Skeleton, + index: Int = 0, +): DataFeedMessageHeader { + val serverState = server.context.state.value + val trackers = serverState.trackers.values.map { it.context.state.value } + val devices = serverState.devices.values.map { it.context.state.value } + .map { device -> createDevice(device, trackers, datafeedConfig) } + val bones = if (datafeedConfig.boneMask) { + skeleton.computed.value.values.map { createBone(it) } + } else { + null + } + return DataFeedMessageHeader( + message = DataFeedUpdate( + devices = if (datafeedConfig.dataMask?.deviceData != null) devices else null, + bones = bones, + index = index.toUByte(), + ), + ) +} + +class DataFeedInitBehaviour(val server: VRServer, val skeleton: Skeleton) : SolarXRBridgeBehaviour { + override fun reduce(state: SolarXRBridgeState, action: SolarXRBridgeActions) = when (action) { + is SolarXRBridgeActions.SetConfig -> state.copy( + dataFeedConfigs = action.configs, + datafeedTimers = action.timers, + ) + } + + override fun observe(receiver: SolarXRBridge) { + receiver.dataFeedDispatcher.on { event -> + val datafeeds = event.dataFeeds ?: return@on + + receiver.context.state.value.datafeedTimers.forEach { it.cancelAndJoin() } + + val timers = datafeeds.mapIndexed { index, config -> + receiver.context.scope.launch { + val minTime = config.minimumTimeSinceLast.toLong() + while (isActive) { + receiver.sendDataFeed(createDatafeedFrame(server = server, skeleton = skeleton, datafeedConfig = config, index = index)) + delay(minTime) + } + } + } + + receiver.context.dispatch( + SolarXRBridgeActions.SetConfig(datafeeds, timers = timers), + ) + } + + receiver.dataFeedDispatcher.on { event -> + val config = event.config ?: return@on + receiver.sendDataFeed(createDatafeedFrame(server = server, datafeedConfig = config, skeleton = skeleton)) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/firmware.kt b/server/core/src/main/java/dev/slimevr/solarxr/firmware.kt new file mode 100644 index 0000000000..12b1c4a2c9 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/firmware.kt @@ -0,0 +1,73 @@ +package dev.slimevr.solarxr + +import dev.slimevr.VRServer +import dev.slimevr.firmware.FirmwareJobStatus +import dev.slimevr.firmware.FirmwareManager +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.datatypes.DeviceIdTable +import solarxr_protocol.rpc.FirmwareUpdateRequest +import solarxr_protocol.rpc.FirmwareUpdateStatusResponse +import solarxr_protocol.rpc.FirmwareUpdateStopQueuesRequest +import solarxr_protocol.rpc.OTAFirmwareUpdate +import solarxr_protocol.rpc.SerialFirmwareUpdate + +class FirmwareBehaviour(private val server: VRServer, private val firmwareManager: FirmwareManager) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + val scope = receiver.context.scope + + var prevJobs: Map = firmwareManager.context.state.value.jobs + + firmwareManager.context.state + .map { it.jobs } + .distinctUntilChanged() + .onEach { jobs -> + jobs.forEach { (portLocation, jobStatus) -> + if (prevJobs[portLocation] != jobStatus) { + receiver.sendRpc( + FirmwareUpdateStatusResponse( + deviceId = jobStatus.firmwareDeviceId, + status = jobStatus.status, + progress = jobStatus.progress.toByte(), + ), + ) + } + } + prevJobs = jobs + } + .launchIn(scope) + + receiver.rpcDispatcher.on { req -> + when (val method = req.method) { + is SerialFirmwareUpdate -> { + val portLocation = method.deviceId?.port ?: return@on + val parts = method.firmwarePart ?: return@on + firmwareManager.flash( + portLocation, + parts, + method.needmanualreboot, + method.ssid, + method.password, + server, + ) + } + + is OTAFirmwareUpdate -> { + val deviceId = method.deviceId ?: return@on + val part = method.firmwarePart ?: return@on + val device = server.getDevice(deviceId.id.toInt()) ?: return@on + val deviceIp = device.context.state.value.address + firmwareManager.otaFlash(deviceIp, DeviceIdTable(id = deviceId), part, server) + } + + else -> return@on + } + } + + receiver.rpcDispatcher.on { + firmwareManager.cancelAll() + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/heightcalibration.kt b/server/core/src/main/java/dev/slimevr/solarxr/heightcalibration.kt new file mode 100644 index 0000000000..d44433b910 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/heightcalibration.kt @@ -0,0 +1,32 @@ +package dev.slimevr.solarxr + +import dev.slimevr.heightcalibration.HeightCalibrationManager +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.rpc.CancelUserHeightCalibration +import solarxr_protocol.rpc.StartUserHeightCalibration +import solarxr_protocol.rpc.UserHeightRecordingStatusResponse + +class HeightCalibrationBehaviour( + private val heightCalibrationManager: HeightCalibrationManager, +) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + receiver.rpcDispatcher.on { + heightCalibrationManager.start() + } + + receiver.rpcDispatcher.on { + heightCalibrationManager.cancel() + } + + heightCalibrationManager.context.state.drop(1).onEach { state -> + receiver.sendRpc( + UserHeightRecordingStatusResponse( + status = state.status, + hmdheight = state.currentHeight, + ), + ) + }.launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/ipc.kt b/server/core/src/main/java/dev/slimevr/solarxr/ipc.kt new file mode 100644 index 0000000000..c15fd1d29f --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/ipc.kt @@ -0,0 +1,35 @@ +package dev.slimevr.solarxr + +import com.google.flatbuffers.FlatBufferBuilder +import dev.slimevr.AppContextProvider +import io.ktor.util.moveToByteArray +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import solarxr_protocol.MessageBundle +import java.nio.ByteBuffer + +suspend fun handleSolarXRBridge( + appContext: AppContextProvider, + messages: Flow, + send: suspend (ByteArray) -> Unit, +) = coroutineScope { + val bridge = SolarXRBridge.create( + id = appContext.server.nextHandle(), + appContext = appContext, + scope = this, + ) + + bridge.outbound.on { bundle -> + val fbb = FlatBufferBuilder(256) + fbb.finish(bundle.encode(fbb)) + send(fbb.dataBuffer().moveToByteArray()) + } + + try { + messages.collect { bytes -> + onSolarXRMessage(ByteBuffer.wrap(bytes), bridge) + } + } finally { + bridge.disconnect() + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/mag.kt b/server/core/src/main/java/dev/slimevr/solarxr/mag.kt new file mode 100644 index 0000000000..e5dae7200c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/mag.kt @@ -0,0 +1,98 @@ +package dev.slimevr.solarxr + +import dev.slimevr.AppContextProvider +import dev.slimevr.config.SettingsActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.udp.SensorConfigFlags +import dev.slimevr.udp.UDPConnectionActions +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import solarxr_protocol.datatypes.MagnetometerStatus +import solarxr_protocol.rpc.ChangeMagToggleRequest +import solarxr_protocol.rpc.MagToggleRequest +import solarxr_protocol.rpc.MagToggleResponse + +class MagBehaviour( + private val appContext: AppContextProvider, +) : SolarXRBridgeBehaviour { + private fun setUDPTrackerMag(trackerId: Int, deviceId: Int, enable: Boolean): Boolean { + val connection = appContext.udpServer.findConnectionForDevice(deviceId) ?: return false + val sensorId = connection.context.state.value.trackerIds.find { it.id == trackerId }?.trackerNum ?: return false + connection.context.dispatch( + UDPConnectionActions.SetSensorConfig( + sensorId = sensorId, + flags = SensorConfigFlags(magStatus = if (enable) MagnetometerStatus.ENABLED else MagnetometerStatus.DISABLED), + ), + ) + return true + } + + override fun observe(receiver: SolarXRBridge) { + receiver.rpcDispatcher.on { req -> + val trackerId = req.trackerId + + if (trackerId == null) { + receiver.appContext.config.settings.context.dispatch(SettingsActions.Update { copy(globalMagEnabled = req.enable) }) + appContext.server.context.state.value.trackers.values.forEach { tracker -> + val state = tracker.context.state.value + if (state.magStatus == MagnetometerStatus.NOT_SUPPORTED) return@forEach + when (state.origin) { + DeviceOrigin.UDP -> setUDPTrackerMag(state.id, state.deviceId, req.enable) + DeviceOrigin.HID -> { /* TODO: implement HID mag toggle */ } + else -> Unit + } + } + receiver.sendRpc(MagToggleResponse(trackerId = null, enable = req.enable)) + return@on + } + + val tracker = appContext.server.getTracker(trackerId.trackerNum.toInt()) ?: return@on + val trackerState = tracker.context.state.value + if (trackerState.magStatus == MagnetometerStatus.NOT_SUPPORTED) return@on + + when (trackerState.origin) { + DeviceOrigin.UDP -> { + if (!setUDPTrackerMag(trackerState.id, trackerState.deviceId, req.enable)) return@on + tracker.context.scope.launch { + withTimeout(10_000) { + tracker.context.state + .distinctUntilChangedBy { it.magStatus } + .first { it.magStatus == if (req.enable) MagnetometerStatus.ENABLED else MagnetometerStatus.DISABLED } + receiver.sendRpc(MagToggleResponse(trackerId = trackerId, enable = req.enable)) + } + } + } + + DeviceOrigin.HID -> { + // TODO: implement HID mag toggle + } + + else -> return@on + } + } + + receiver.rpcDispatcher.on { req -> + val trackerId = req.trackerId + + if (trackerId == null) { + receiver.sendRpc( + MagToggleResponse( + trackerId = null, + enable = receiver.appContext.config.settings.context.state.value.data.globalMagEnabled, + ), + ) + return@on + } + + val trackerState = appContext.server.getTracker(trackerId.trackerNum.toInt())?.context?.state?.value ?: return@on + receiver.sendRpc( + MagToggleResponse( + trackerId = trackerId, + enable = trackerState.magStatus == MagnetometerStatus.ENABLED, + ), + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/module.kt b/server/core/src/main/java/dev/slimevr/solarxr/module.kt new file mode 100644 index 0000000000..2f45cbf821 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/module.kt @@ -0,0 +1,100 @@ +package dev.slimevr.solarxr + +import dev.slimevr.AppContextProvider +import dev.slimevr.EventDispatcher +import dev.slimevr.VRServerActions +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import solarxr_protocol.MessageBundle +import solarxr_protocol.data_feed.DataFeedConfig +import solarxr_protocol.data_feed.DataFeedMessage +import solarxr_protocol.data_feed.DataFeedMessageHeader +import solarxr_protocol.rpc.RpcMessage +import solarxr_protocol.rpc.RpcMessageHeader +import java.nio.ByteBuffer + +data class SolarXRBridgeState( + val dataFeedConfigs: List, + val datafeedTimers: List, +) + +sealed interface SolarXRBridgeActions { + data class SetConfig(val configs: List, val timers: List) : SolarXRBridgeActions +} + +typealias SolarXRBridgeContext = Context +typealias SolarXRBridgeBehaviour = Behaviour + +suspend fun onSolarXRMessage(message: ByteBuffer, context: SolarXRBridge) { + val messageBundle = MessageBundle.fromByteBuffer(message) + + messageBundle.dataFeedMsgs?.forEach { + val msg = it.message ?: return + context.dataFeedDispatcher.emit(msg) + } + + messageBundle.rpcMsgs?.forEach { + val msg = it.message ?: return + context.rpcDispatcher.emit(msg) + } +} + +class SolarXRBridge( + val id: Int, + val context: SolarXRBridgeContext, + val appContext: AppContextProvider, + val dataFeedDispatcher: EventDispatcher, + val rpcDispatcher: EventDispatcher, + val outbound: EventDispatcher = EventDispatcher(), +) { + suspend fun sendRpc(message: RpcMessage) = outbound.emit(MessageBundle(rpcMsgs = listOf(RpcMessageHeader(message = message)))) + + suspend fun sendDataFeed(frame: DataFeedMessageHeader) = outbound.emit(MessageBundle(dataFeedMsgs = listOf(frame))) + + fun disconnect() { + appContext.server.context.dispatch(VRServerActions.SolarXRDisconnected(id)) + } + + fun startObserving() = context.observeAll(this) + + companion object { + fun buildBehaviours(appContext: AppContextProvider): List = buildList { + add(DataFeedInitBehaviour(appContext.server, appContext.skeleton)) + add(SerialBehaviour(appContext.serialServer)) + add(FirmwareBehaviour(appContext.server, appContext.firmwareManager)) + appContext.vrcConfigManager?.let { vrc -> + add(VrcBehaviour(vrc, appContext.server, userHeight = { appContext.skeleton.context.state.value.userHeight })) + } + add(HeightCalibrationBehaviour(appContext.heightCalibrationManager)) + add(ProvisioningBehaviour(appContext.server, appContext.provisioningManager)) + add(SkeletonBehaviour(appContext.config.userConfig, appContext.skeleton)) + add(TrackingChecklistBehaviour(appContext.trackingChecklist, appContext.config.settings)) + add(AssignTrackerBehaviour(appContext.server)) + add(MagBehaviour(appContext)) + } + + fun create( + id: Int, + appContext: AppContextProvider, + scope: CoroutineScope, + ): SolarXRBridge { + val context = Context.create( + initialState = SolarXRBridgeState(dataFeedConfigs = listOf(), datafeedTimers = listOf()), + scope = scope, + behaviours = buildBehaviours(appContext), + ) + + val bridge = SolarXRBridge( + id = id, + context = context, + appContext = appContext, + dataFeedDispatcher = EventDispatcher(), + rpcDispatcher = EventDispatcher(), + ) + bridge.startObserving() + return bridge + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/provisioning.kt b/server/core/src/main/java/dev/slimevr/solarxr/provisioning.kt new file mode 100644 index 0000000000..5d60ecc79b --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/provisioning.kt @@ -0,0 +1,40 @@ +package dev.slimevr.solarxr + +import dev.slimevr.VRServer +import dev.slimevr.provisioning.ProvisioningManager +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.rpc.StartWifiProvisioningRequest +import solarxr_protocol.rpc.StopWifiProvisioningRequest +import solarxr_protocol.rpc.WifiProvisioningStatusResponse + +class ProvisioningBehaviour( + private val server: VRServer, + private val provisioningManager: ProvisioningManager, +) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + receiver.rpcDispatcher.on { event -> + val ssid = event.ssid ?: return@on + + provisioningManager.startProvisioning( + server, + ssid, + event.password, + event.port, + ) + } + + receiver.rpcDispatcher.on { + provisioningManager.stopProvisioning() + } + + provisioningManager.context.state.drop(1).onEach { state -> + receiver.sendRpc( + WifiProvisioningStatusResponse( + status = state.status, + ), + ) + }.launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/serial.kt b/server/core/src/main/java/dev/slimevr/solarxr/serial.kt new file mode 100644 index 0000000000..68812a19c9 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/serial.kt @@ -0,0 +1,132 @@ +package dev.slimevr.solarxr + +import dev.slimevr.serial.SerialConnection +import dev.slimevr.serial.SerialPortInfo +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import solarxr_protocol.rpc.CloseSerialRequest +import solarxr_protocol.rpc.NewSerialDeviceResponse +import solarxr_protocol.rpc.OpenSerialRequest +import solarxr_protocol.rpc.SerialDevice +import solarxr_protocol.rpc.SerialDevicesRequest +import solarxr_protocol.rpc.SerialDevicesResponse +import solarxr_protocol.rpc.SerialTrackerCustomCommandRequest +import solarxr_protocol.rpc.SerialTrackerFactoryResetRequest +import solarxr_protocol.rpc.SerialTrackerGetInfoRequest +import solarxr_protocol.rpc.SerialTrackerGetWifiScanRequest +import solarxr_protocol.rpc.SerialTrackerRebootRequest +import solarxr_protocol.rpc.SerialUpdateResponse + +class SerialBehaviour(private val serialServer: SerialServer) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + val scope = receiver.context.scope + + // We assume that you can only subscribe to one serial console at a time + var logSubscription: Job? = null + var activePortLocation: String? = null + + // Notify client of new serial devices as they are detected. + // Existing devices at connection time are not sent here. the client + // should send SerialDevicesRequest to get the current list. + var prevPortKeys = serialServer.context.state.value.availablePorts.keys.toSet() + serialServer.context.state + .map { it.availablePorts } + .distinctUntilChanged() + .onEach { ports -> + (ports.keys - prevPortKeys).forEach { key -> + receiver.sendRpc(NewSerialDeviceResponse(device = ports[key]!!.toSerialDevice())) + } + prevPortKeys = ports.keys.toSet() + } + .launchIn(scope) + + receiver.rpcDispatcher.on { + receiver.sendRpc( + SerialDevicesResponse( + devices = serialServer.context.state.value.availablePorts.values + .map { it.toSerialDevice() }, + ), + ) + } + + receiver.rpcDispatcher.on { req -> + val portLocation = if (req.auto) { + serialServer.context.state.value.availablePorts.keys.firstOrNull() + } else { + req.port + } ?: return@on + + logSubscription?.cancel() + logSubscription = null + activePortLocation = null + + serialServer.openConnection(portLocation) + + val connection = serialServer.context.state.value.connections[portLocation] + if (connection !is SerialConnection.Console) return@on + + activePortLocation = portLocation + var lastSentCount = 0 + + logSubscription = scope.launch { + var disconnected = false + connection.context.state.collect { connState -> + if (disconnected) return@collect + + connState.logLines.drop(lastSentCount).forEach { line -> + receiver.sendRpc(SerialUpdateResponse(log = line + "\n")) + } + lastSentCount = connState.logLines.size + + if (!connState.connected) { + disconnected = true + activePortLocation = null + receiver.sendRpc(SerialUpdateResponse(closed = true)) + } + } + } + } + + receiver.rpcDispatcher.on { + logSubscription?.cancel() + logSubscription = null + activePortLocation = null + } + + receiver.rpcDispatcher.on { + val portLocation = activePortLocation ?: return@on + val c = serialServer.context.state.value.connections[portLocation] + if (c is SerialConnection.Console) c.handle.writeCommand("REBOOT") + } + + receiver.rpcDispatcher.on { + val portLocation = activePortLocation ?: return@on + val c = serialServer.context.state.value.connections[portLocation] + if (c is SerialConnection.Console) c.handle.writeCommand("GET INFO") + } + + receiver.rpcDispatcher.on { + val portLocation = activePortLocation ?: return@on + val c = serialServer.context.state.value.connections[portLocation] + if (c is SerialConnection.Console) c.handle.writeCommand("FRST") + } + + receiver.rpcDispatcher.on { + val portLocation = activePortLocation ?: return@on + val c = serialServer.context.state.value.connections[portLocation] + if (c is SerialConnection.Console) c.handle.writeCommand("GET WIFISCAN") + } + + receiver.rpcDispatcher.on { req -> + val portLocation = activePortLocation ?: return@on + val command = req.command ?: return@on + val c = serialServer.context.state.value.connections[portLocation] + if (c is SerialConnection.Console) c.handle.writeCommand(command) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/skeletonconfig.kt b/server/core/src/main/java/dev/slimevr/solarxr/skeletonconfig.kt new file mode 100644 index 0000000000..6a9eaa8a20 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/skeletonconfig.kt @@ -0,0 +1,80 @@ +package dev.slimevr.solarxr + +import dev.slimevr.config.UserConfig +import dev.slimevr.config.UserConfigActions +import dev.slimevr.skeleton.SKELETON_BONE_TO_BODY_PARTS +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.skeleton.computeAllDefaultProportionsByBone +import dev.slimevr.skeleton.computeDefaultProportionsByBone +import dev.slimevr.skeleton.computeUserHeight +import dev.slimevr.skeleton.expandProportions +import solarxr_protocol.rpc.ChangeSettingsRequest +import solarxr_protocol.rpc.ChangeSkeletonConfigRequest +import solarxr_protocol.rpc.SkeletonConfigRequest +import solarxr_protocol.rpc.SkeletonConfigResponse +import solarxr_protocol.rpc.SkeletonPart +import solarxr_protocol.rpc.SkeletonResetAllRequest + +private const val MIN_HEIGHT = 1.0f + +class SkeletonBehaviour( + private val userConfig: UserConfig, + private val skeleton: Skeleton, +) : SolarXRBridgeBehaviour { + + private fun buildConfigResponse(): SkeletonConfigResponse { + val proportions = userConfig.context.state.value.data.proportions + val bones = skeleton.context.state.value.bones + val skeletonParts = SKELETON_BONE_TO_BODY_PARTS.mapNotNull { (skeletonBone, bodyParts) -> + val length = proportions[skeletonBone.name] + ?: bodyParts.mapNotNull { bones[it]?.length }.average().takeIf { !it.isNaN() }?.toFloat() + ?: return@mapNotNull null + SkeletonPart(bone = skeletonBone, value = length) + } + val expanded = expandProportions(proportions) + val userHeight = if (expanded.isNotEmpty()) { + computeUserHeight(expanded).toFloat() + } else { + skeleton.context.state.value.userHeight.toFloat() + } + return SkeletonConfigResponse(skeletonParts = skeletonParts, userHeight = userHeight) + } + + override fun observe(receiver: SolarXRBridge) { + receiver.rpcDispatcher.on { + receiver.sendRpc(buildConfigResponse()) + } + + receiver.rpcDispatcher.on { req -> + req.modelSettings?.skeletonHeight?.let { skeletonHeight -> + val hmdHeight = skeletonHeight.hmdHeight ?: return@let + val floorHeight = skeletonHeight.floorHeight ?: 0f + val height = hmdHeight - floorHeight + if (height >= MIN_HEIGHT) { + userConfig.context.dispatch( + UserConfigActions.Update { + copy(userHeight = height, proportions = computeDefaultProportionsByBone(height)) + }, + ) + } + } + } + + receiver.rpcDispatcher.on { + val height = userConfig.context.state.value.data.userHeight + if (height >= MIN_HEIGHT) { + val defaults = computeAllDefaultProportionsByBone(height) + userConfig.context.dispatch(UserConfigActions.Update { copy(proportions = defaults) }) + } + receiver.sendRpc(buildConfigResponse()) + } + + receiver.rpcDispatcher.on { req -> + val bone = req.bone ?: return@on + userConfig.context.dispatch( + UserConfigActions.Update { copy(proportions = proportions + (bone.name to req.value)) }, + ) + receiver.sendRpc(buildConfigResponse()) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/trackerassignment.kt b/server/core/src/main/java/dev/slimevr/solarxr/trackerassignment.kt new file mode 100644 index 0000000000..52f525ffa2 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/trackerassignment.kt @@ -0,0 +1,31 @@ +package dev.slimevr.solarxr + +import dev.slimevr.VRServer +import dev.slimevr.tracker.TrackerActions +import io.github.axisangles.ktmath.Quaternion +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.rpc.AssignTrackerRequest + +class AssignTrackerBehaviour( + private val server: VRServer, +) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + receiver.rpcDispatcher.on { req -> + val trackerId = req.trackerId ?: return@on + val tracker = server.getTracker(trackerId.trackerNum.toInt()) + ?: return@on + + val bodyPart = req.bodyPosition?.takeIf { it != BodyPart.NONE } + val mountingOrientation = req.mountingOrientation?.let { Quaternion(it.w, it.x, it.y, it.z) } + tracker.context.dispatch( + TrackerActions.Update { + copy( + bodyPart = bodyPart, + customName = req.displayName, + mountingOrientation = mountingOrientation, + ) + }, + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/trackingchecklist.kt b/server/core/src/main/java/dev/slimevr/solarxr/trackingchecklist.kt new file mode 100644 index 0000000000..83f697849b --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/trackingchecklist.kt @@ -0,0 +1,54 @@ +package dev.slimevr.solarxr + +import dev.slimevr.config.Settings +import dev.slimevr.config.SettingsActions +import dev.slimevr.trackingchecklist.TrackingChecklist +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.rpc.IgnoreTrackingChecklistStepRequest +import solarxr_protocol.rpc.TrackingChecklistRequest +import solarxr_protocol.rpc.TrackingChecklistResponse +import solarxr_protocol.rpc.TrackingChecklistStepId + +class TrackingChecklistBehaviour( + private val checklist: TrackingChecklist, + private val settings: Settings, +) : SolarXRBridgeBehaviour { + + private fun parseMutedSteps(): Set = settings.context.state.value.data.mutedChecklistSteps + .mapNotNull { name -> TrackingChecklistStepId.entries.firstOrNull { stepId -> stepId.name == name } } + .toSet() + + private fun buildResponse(): TrackingChecklistResponse { + val steps = checklist.context.state.value.steps.mapValues { it.value.copy(id = it.key) }.values.toList() + return TrackingChecklistResponse( + steps = steps, + ignoredSteps = parseMutedSteps().toList(), + ) + } + + override fun observe(receiver: SolarXRBridge) { + combine( + checklist.context.state, + settings.context.state.map { state -> state.data.mutedChecklistSteps }, + ) { _, _ -> buildResponse() } + .onEach { response -> receiver.sendRpc(response) } + .launchIn(receiver.context.scope) + + receiver.rpcDispatcher.on { + receiver.sendRpc(buildResponse()) + } + + receiver.rpcDispatcher.on { req -> + val stepId = req.stepId ?: return@on + val name = stepId.name + settings.context.dispatch( + SettingsActions.Update { + copy(mutedChecklistSteps = if (req.ignore) mutedChecklistSteps + name else mutedChecklistSteps - name) + }, + ) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/solarxr/vrchat.kt b/server/core/src/main/java/dev/slimevr/solarxr/vrchat.kt new file mode 100644 index 0000000000..b6766ec5d6 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/vrchat.kt @@ -0,0 +1,49 @@ +package dev.slimevr.solarxr + +import dev.slimevr.VRServer +import dev.slimevr.vrchat.VRCConfigActions +import dev.slimevr.vrchat.VRCConfigManager +import dev.slimevr.vrchat.computeRecommendedValues +import dev.slimevr.vrchat.computeValidity +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.rpc.VRCConfigSettingToggleMute +import solarxr_protocol.rpc.VRCConfigStateChangeResponse +import solarxr_protocol.rpc.VRCConfigStateRequest + +class VrcBehaviour( + private val vrcManager: VRCConfigManager, + private val server: VRServer, + private val userHeight: () -> Double, +) : SolarXRBridgeBehaviour { + override fun observe(receiver: SolarXRBridge) { + fun buildCurrentResponse(): VRCConfigStateChangeResponse { + val state = vrcManager.context.state.value + val values = state.currentValues + if (!state.isSupported || values == null) return VRCConfigStateChangeResponse(isSupported = false) + val recommended = computeRecommendedValues(server, userHeight()) + return VRCConfigStateChangeResponse( + isSupported = true, + validity = computeValidity(values, recommended), + state = values, + recommended = recommended, + muted = state.mutedWarnings.toList(), + ) + } + + // Drop the initial value, we only want to push updates when the config changes + vrcManager.context.state.drop(1).onEach { + receiver.sendRpc(buildCurrentResponse()) + }.launchIn(receiver.context.scope) + + receiver.rpcDispatcher.on { + receiver.sendRpc(buildCurrentResponse()) + } + + receiver.rpcDispatcher.on { req -> + val key = req.key ?: return@on + vrcManager.context.dispatch(VRCConfigActions.ToggleMutedWarning(key)) + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt b/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt deleted file mode 100644 index bd521f9a81..0000000000 --- a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt +++ /dev/null @@ -1,39 +0,0 @@ -package dev.slimevr.status - -import solarxr_protocol.rpc.StatusDataUnion -import solarxr_protocol.rpc.StatusMessageT -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.atomic.AtomicInteger - -class StatusSystem { - private val listeners: MutableList = CopyOnWriteArrayList() - private val statuses: MutableMap = ConcurrentHashMap() - private val prioritizedStatuses: MutableSet = ConcurrentHashMap.newKeySet() - private val idCounter = AtomicInteger(1) - - fun addListener(listener: StatusListener) { - listeners.add(listener) - } - - fun removeListener(listener: StatusListener) { - listeners.remove(listener) - } - - fun getStatuses(): Array = statuses.map { (id, message) -> - val status = StatusMessageT() - status.id = id.toUInt().toLong() - status.data = message - status.prioritized = prioritizedStatuses.contains(id) - status - }.toTypedArray() - - fun hasStatusType(dataType: Byte): Boolean = statuses.any { - it.value.type == dataType - } -} - -interface StatusListener { - fun onStatusChanged(id: UInt, message: StatusDataUnion, prioritized: Boolean) - fun onStatusRemoved(id: UInt) -} diff --git a/server/core/src/main/java/dev/slimevr/tracker/behaviours.kt b/server/core/src/main/java/dev/slimevr/tracker/behaviours.kt new file mode 100644 index 0000000000..ef251dd950 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/behaviours.kt @@ -0,0 +1,114 @@ +package dev.slimevr.tracker + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.incrementAndFetch +import kotlin.time.TimeSource + +private const val NS_CONVERTER = 1.0e9f +private const val CLUMP_TIME_NS = 0.06f * NS_CONVERTER +private const val NEEDED_ACCEL_DELTA = 4.0f +private const val ALLOWED_BODY_ACCEL = 2.0f +private const val ALLOWED_BODY_ACCEL_SQUARED = ALLOWED_BODY_ACCEL * ALLOWED_BODY_ACCEL +private const val TAP_WINDOW_PER_TAP_S = 0.3f + +object TrackerTapDetectionBehaviour : TrackerBehaviour { + override fun observe(receiver: Tracker) { + val accelList = ArrayDeque>() + val tapTimestamps = ArrayDeque() + var waitForLowAccel = false + + // TODO: only enable this on the trackers that have taps assigned to them + receiver.context.state + .distinctUntilChangedBy { it.acceleration } + .onEach { current -> + val now = System.nanoTime() + val magnitude = current.acceleration.len() + + accelList.add(magnitude to now) + while (accelList.isNotEmpty() && now - accelList.first().second > CLUMP_TIME_NS) { + accelList.removeFirst() + } + + val max = accelList.maxOfOrNull { it.first } ?: 0f + val min = accelList.minOfOrNull { it.first } ?: 0f + val accelDelta = max - min + + if (accelDelta > NEEDED_ACCEL_DELTA && !waitForLowAccel) { + val othersOverThreshold = receiver.server.context.state.value.trackers.values + .count { it.context.state.value.id != current.id && it.context.state.value.acceleration.lenSq() > ALLOWED_BODY_ACCEL_SQUARED } + if (othersOverThreshold <= 1) { + tapTimestamps.add(now) + waitForLowAccel = true + } + } + + if (max < ALLOWED_BODY_ACCEL) { + waitForLowAccel = false + } + + if (tapTimestamps.isNotEmpty()) { + val totalWindowNs = (TAP_WINDOW_PER_TAP_S * tapTimestamps.size * NS_CONVERTER).toLong() + while (tapTimestamps.isNotEmpty() && now - tapTimestamps.first() > totalWindowNs) { + tapTimestamps.removeFirst() + } + + if (tapTimestamps.isNotEmpty()) { + val lastTapTime = tapTimestamps.last() + if (now - lastTapTime > (TAP_WINDOW_PER_TAP_S * NS_CONVERTER).toLong()) { + val count = tapTimestamps.size + if (count >= 2) { + println("Detected $count taps on ${receiver.context.state.value.id}") + // TODO trigger the tap action + } + tapTimestamps.clear() + } + } + } + } + .launchIn(receiver.context.scope) + } +} + +object TrackerBasicBehaviour : TrackerBehaviour { + override fun reduce(state: TrackerState, action: TrackerActions) = when (action) { + is TrackerActions.Update -> action.transform(state) + + is TrackerActions.SetMagStatus -> state.copy(magStatus = action.status) + + is TrackerActions.SetStatus -> state.copy(status = action.status) + + is TrackerActions.SetRotation -> state.copy( + rawRotation = action.rotation ?: state.rawRotation, + acceleration = action.acceleration ?: state.acceleration, + ) + } +} + +object TrackerTPSBehaviour : TrackerBehaviour { + @OptIn(ExperimentalAtomicApi::class) + override fun observe(receiver: Tracker) { + val count = AtomicInt(0) + + receiver.context.state.distinctUntilChangedBy { it.rawRotation }.onEach { + count.incrementAndFetch() + }.launchIn(receiver.context.scope) + + receiver.context.scope.launch { + var mark = TimeSource.Monotonic.markNow() + while (isActive) { + delay(1000) + val elapsed = mark.elapsedNow() + val tps = count.exchange(0) * 1000L / elapsed.inWholeMilliseconds + receiver.context.dispatch(TrackerActions.Update { copy(tps = tps.toUShort()) }) + mark = TimeSource.Monotonic.markNow() + } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracker/config.kt b/server/core/src/main/java/dev/slimevr/tracker/config.kt new file mode 100644 index 0000000000..87d1d365b8 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/config.kt @@ -0,0 +1,48 @@ +package dev.slimevr.tracker + +import dev.slimevr.config.Settings +import dev.slimevr.config.SettingsActions +import dev.slimevr.config.TrackerConfig +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.datatypes.MagnetometerStatus + +fun restoreFromConfig(state: TrackerState, config: TrackerConfig): TrackerState = state.copy( + bodyPart = config.bodyPart?.takeIf { it != BodyPart.NONE } ?: state.bodyPart, + customName = config.customName ?: state.customName, + mountingOrientation = config.mountingOrientation ?: state.mountingOrientation, + magStatus = when (config.magEnabled) { + true -> MagnetometerStatus.ENABLED + false -> MagnetometerStatus.DISABLED + null -> state.magStatus + }, +) + +private fun applyStateToConfig(config: TrackerConfig, state: TrackerState) = config.copy( + bodyPart = state.bodyPart, + customName = state.customName, + mountingOrientation = state.mountingOrientation, + magEnabled = when (state.magStatus) { + MagnetometerStatus.ENABLED -> true + MagnetometerStatus.DISABLED -> false + MagnetometerStatus.NOT_SUPPORTED -> config.magEnabled + }, +) + +class TrackerConfigBehaviour( + private val settings: Settings, + private val hardwareId: String, +) : TrackerBehaviour { + override fun observe(receiver: Tracker) { + receiver.context.state + .distinctUntilChangedBy { it.bodyPart to it.customName to it.mountingOrientation to it.magStatus } + .drop(1) + .onEach { state -> + settings.context.dispatch(SettingsActions.UpdateTracker(hardwareId) { applyStateToConfig(this, state) }) + } + .launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracker/module.kt b/server/core/src/main/java/dev/slimevr/tracker/module.kt new file mode 100644 index 0000000000..ff2948df51 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/module.kt @@ -0,0 +1,113 @@ +package dev.slimevr.tracker + +import dev.slimevr.VRServer +import dev.slimevr.config.Settings +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.context.debug.DiffStyle +import dev.slimevr.context.debug.LoggingMiddleware +import dev.slimevr.device.DeviceOrigin +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.CoroutineScope +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.datatypes.MagnetometerStatus +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.datatypes.hardware_info.ImuType + +data class TrackerIdNum(val id: Int, val trackerNum: Int) + +data class TrackerState( + val id: Int, + val name: String, + val hardwareId: String, + val sensorType: ImuType?, + val bodyPart: BodyPart?, + val customName: String?, + val mountingOrientation: Quaternion?, + val rawRotation: Quaternion, + val acceleration: Vector3, + val deviceId: Int, + val origin: DeviceOrigin, + val tps: UShort, + val imuTemp: Float?, + val position: Vector3?, + val status: TrackerStatus, + val completedRestCalibration: Boolean?, + val magStatus: MagnetometerStatus, +) + +sealed interface TrackerActions { + data class Update(val transform: TrackerState.() -> TrackerState) : TrackerActions + data class SetMagStatus(val status: MagnetometerStatus) : TrackerActions + data class SetStatus(val status: TrackerStatus) : TrackerActions + data class SetRotation(val rotation: Quaternion? = null, val acceleration: Vector3? = null) : TrackerActions +} + +typealias TrackerContext = Context +typealias TrackerBehaviour = Behaviour + +class Tracker( + val context: TrackerContext, + val server: VRServer, +) { + companion object { + fun create( + scope: CoroutineScope, + id: Int, + deviceId: Int, + sensorType: ImuType?, + hardwareId: String, + origin: DeviceOrigin, + server: VRServer, + settings: Settings, + ): Tracker { + val trackerConfigs = settings.context.state.value.data.trackers + val savedConfig = trackerConfigs[hardwareId] + val baseState = TrackerState( + id = id, + hardwareId = hardwareId, + name = "Tracker #$id", + rawRotation = Quaternion.IDENTITY, + acceleration = Vector3.NULL, + bodyPart = null, + mountingOrientation = null, + origin = origin, + deviceId = deviceId, + customName = null, + sensorType = sensorType, + position = null, + tps = 0u, + imuTemp = null, + status = TrackerStatus.DISCONNECTED, + completedRestCalibration = false, + magStatus = MagnetometerStatus.NOT_SUPPORTED, + ) + val trackerState = if (savedConfig != null) { + restoreFromConfig(baseState, savedConfig) + } else { + baseState + } + + val behaviours = listOf( + TrackerBasicBehaviour, + TrackerConfigBehaviour(settings, hardwareId), + TrackerTPSBehaviour, + TrackerTapDetectionBehaviour, + ) + val context = Context.create( + initialState = trackerState, + scope = scope, + behaviours = behaviours, + debugMiddleware = LoggingMiddleware( + "Tracker[$hardwareId]", + block = setOf(TrackerActions.SetRotation::class), + diffStyle = DiffStyle.MULTILINE, + ), + ) + val tracker = Tracker(context = context, server) + behaviours.forEach { it.observe(tracker) } + return tracker + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt deleted file mode 100644 index aed9d7768c..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt +++ /dev/null @@ -1,165 +0,0 @@ -package dev.slimevr.tracking.processor - -import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import solarxr_protocol.datatypes.BodyPart -import java.util.concurrent.CopyOnWriteArrayList - -/** - * Represents a bone composed of 2 joints: headNode and tailNode. - */ -class Bone(val boneType: BoneType, val rotationConstraint: Constraint) { - private val headNode = TransformNode(true) - private val tailNode = TransformNode(false) - var parent: Bone? = null - private set - val children: MutableList = CopyOnWriteArrayList() - var rotationOffset = Quaternion.IDENTITY - var attachedTracker: Tracker? = null - - init { - headNode.attachChild(tailNode) - } - - /** - * Attach another bone as a child of this. - */ - fun attachChild(bone: Bone) { - // Attach bone - require(bone.parent == null) { "The child bone must not already have a parent." } - children.add(bone) - bone.parent = this - - // Attach node - tailNode.attachChild(bone.headNode) - } - - /** - * Detach this bone from its parent and children. - */ - fun detachWithChildren() { - // Detach bones - for (child in children) child.parent = null - children.clear() - parent?.children?.remove(this) - parent = null - - // Detach nodes - headNode.detachWithChildren() - tailNode.detachWithChildren() - - // Reattach this bone - headNode.attachChild(tailNode) - } - - /** - * Computes the rotations and positions of - * this bone and all of its children. - */ - fun update() { - headNode.update() - } - - /** - * Computes the rotations and positions of - * this bone and all of its children while - * enforcing rotation constraints. - */ - fun updateWithConstraints(correctConstraints: Boolean) { - val initialRot = getGlobalRotation() - val newRot = rotationConstraint.applyConstraint(initialRot, this) - setRotationRaw(newRot) - updateThisNode() - - // Correct tracker if applicable. Do not adjust correction for hinge constraints - // or the upper chest tracker. - if (rotationConstraint.constraintType != ConstraintType.HINGE && - rotationConstraint.constraintType != ConstraintType.LOOSE_HINGE && - boneType.bodyPart != BodyPart.UPPER_CHEST - ) { - val deltaRot = newRot * initialRot.inv() - val angle = deltaRot.angleR() - - if (correctConstraints && - angle > Constraint.ANGLE_THRESHOLD && - (attachedTracker?.filteringHandler?.getFilteringImpact() ?: 1f) < Constraint.FILTER_IMPACT_THRESHOLD && - (parent?.attachedTracker?.filteringHandler?.getFilteringImpact() ?: 0f) < Constraint.FILTER_IMPACT_THRESHOLD - ) { - attachedTracker?.resetsHandler?.updateConstraintFix(deltaRot) - } - } - - // Recursively apply constraints and update children. - for (child in children) { - child.updateWithConstraints(correctConstraints) - } - } - - /** - * Computes the rotations and positions of this bone. - * Only to be used while traversing bones from top to bottom. - */ - private fun updateThisNode() { - headNode.updateThisNode() - tailNode.updateThisNode() - } - - /** - * Returns the world-aligned rotation of the bone - */ - fun getGlobalRotation(): Quaternion = headNode.worldTransform.rotation - - /** - * Returns the rotation of the bone relative to its parent - */ - fun getLocalRotation(): Quaternion = headNode.localTransform.rotation - - /** - * Sets the global rotation of the bone - */ - fun setRotation(rotation: Quaternion) { - headNode.localTransform.rotation = rotation * rotationOffset - } - - /** - * Sets the global rotation of the bone directly - */ - fun setRotationRaw(rotation: Quaternion) { - headNode.localTransform.rotation = rotation - } - - /** - * Returns the global position of the head of the bone - */ - fun getPosition(): Vector3 = headNode.worldTransform.translation - - /** - * Returns the global position of the tail of the bone - */ - fun getTailPosition(): Vector3 = tailNode.worldTransform.translation - - /** - * Sets the global position of the head of the bone. - * Note: cannot set the global position of bones with parents, - * consider changing the bone's length instead. - */ - fun setPosition(position: Vector3) { - require(parent == null) { "Cannot set the position of a child bone." } - headNode.localTransform.translation = position - } - - /** - * The length of the bone is in meters. - * This is the local translation of the tail node. - * This is also the difference between the head position and the tail position. - */ - var length: Float - get() = tailNode.localTransform.translation.len() - set(len) = updateLength(len) - - private fun updateLength(length: Float) { - tailNode.localTransform.translation = Vector3(0f, -length, 0f) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java b/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java deleted file mode 100644 index b423640103..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java +++ /dev/null @@ -1,87 +0,0 @@ -package dev.slimevr.tracking.processor; - -import solarxr_protocol.datatypes.BodyPart; - - -/** - * Keys for all the bones in the skeleton. - */ -public enum BoneType { - HEAD(BodyPart.HEAD), - HEAD_TRACKER(), - NECK(BodyPart.NECK), - UPPER_CHEST(BodyPart.UPPER_CHEST), - CHEST_TRACKER, - CHEST(BodyPart.CHEST), - WAIST(BodyPart.WAIST), - HIP(BodyPart.HIP), - HIP_TRACKER, - LEFT_HIP(BodyPart.LEFT_HIP), - RIGHT_HIP(BodyPart.RIGHT_HIP), - LEFT_UPPER_LEG(BodyPart.LEFT_UPPER_LEG), - RIGHT_UPPER_LEG(BodyPart.RIGHT_UPPER_LEG), - LEFT_KNEE_TRACKER, - RIGHT_KNEE_TRACKER, - LEFT_LOWER_LEG(BodyPart.LEFT_LOWER_LEG), - RIGHT_LOWER_LEG(BodyPart.RIGHT_LOWER_LEG), - LEFT_FOOT(BodyPart.LEFT_FOOT), - RIGHT_FOOT(BodyPart.RIGHT_FOOT), - LEFT_FOOT_TRACKER(BodyPart.LEFT_FOOT), - RIGHT_FOOT_TRACKER(BodyPart.RIGHT_FOOT), - LEFT_LOWER_ARM(BodyPart.LEFT_LOWER_ARM), - RIGHT_LOWER_ARM(BodyPart.RIGHT_LOWER_ARM), - LEFT_ELBOW_TRACKER, - RIGHT_ELBOW_TRACKER, - LEFT_UPPER_ARM(BodyPart.LEFT_UPPER_ARM), - RIGHT_UPPER_ARM(BodyPart.RIGHT_UPPER_ARM), - LEFT_SHOULDER(BodyPart.LEFT_SHOULDER), - RIGHT_SHOULDER(BodyPart.RIGHT_SHOULDER), - LEFT_UPPER_SHOULDER, - RIGHT_UPPER_SHOULDER, - LEFT_HAND(BodyPart.LEFT_HAND), - RIGHT_HAND(BodyPart.RIGHT_HAND), - LEFT_HAND_TRACKER, - RIGHT_HAND_TRACKER, - LEFT_THUMB_METACARPAL(BodyPart.LEFT_THUMB_METACARPAL), - LEFT_THUMB_PROXIMAL(BodyPart.LEFT_THUMB_PROXIMAL), - LEFT_THUMB_DISTAL(BodyPart.LEFT_THUMB_DISTAL), - LEFT_INDEX_PROXIMAL(BodyPart.LEFT_INDEX_PROXIMAL), - LEFT_INDEX_INTERMEDIATE(BodyPart.LEFT_INDEX_INTERMEDIATE), - LEFT_INDEX_DISTAL(BodyPart.LEFT_INDEX_DISTAL), - LEFT_MIDDLE_PROXIMAL(BodyPart.LEFT_MIDDLE_PROXIMAL), - LEFT_MIDDLE_INTERMEDIATE(BodyPart.LEFT_MIDDLE_INTERMEDIATE), - LEFT_MIDDLE_DISTAL(BodyPart.LEFT_MIDDLE_DISTAL), - LEFT_RING_PROXIMAL(BodyPart.LEFT_RING_PROXIMAL), - LEFT_RING_INTERMEDIATE(BodyPart.LEFT_RING_INTERMEDIATE), - LEFT_RING_DISTAL(BodyPart.LEFT_RING_DISTAL), - LEFT_LITTLE_PROXIMAL(BodyPart.LEFT_LITTLE_PROXIMAL), - LEFT_LITTLE_INTERMEDIATE(BodyPart.LEFT_LITTLE_INTERMEDIATE), - LEFT_LITTLE_DISTAL(BodyPart.LEFT_LITTLE_DISTAL), - RIGHT_THUMB_METACARPAL(BodyPart.RIGHT_THUMB_METACARPAL), - RIGHT_THUMB_PROXIMAL(BodyPart.RIGHT_THUMB_PROXIMAL), - RIGHT_THUMB_DISTAL(BodyPart.RIGHT_THUMB_DISTAL), - RIGHT_INDEX_PROXIMAL(BodyPart.RIGHT_INDEX_PROXIMAL), - RIGHT_INDEX_INTERMEDIATE(BodyPart.RIGHT_INDEX_INTERMEDIATE), - RIGHT_INDEX_DISTAL(BodyPart.RIGHT_INDEX_DISTAL), - RIGHT_MIDDLE_PROXIMAL(BodyPart.RIGHT_MIDDLE_PROXIMAL), - RIGHT_MIDDLE_INTERMEDIATE(BodyPart.RIGHT_MIDDLE_INTERMEDIATE), - RIGHT_MIDDLE_DISTAL(BodyPart.RIGHT_MIDDLE_DISTAL), - RIGHT_RING_PROXIMAL(BodyPart.RIGHT_RING_PROXIMAL), - RIGHT_RING_INTERMEDIATE(BodyPart.RIGHT_RING_INTERMEDIATE), - RIGHT_RING_DISTAL(BodyPart.RIGHT_RING_DISTAL), - RIGHT_LITTLE_PROXIMAL(BodyPart.RIGHT_LITTLE_PROXIMAL), - RIGHT_LITTLE_INTERMEDIATE(BodyPart.RIGHT_LITTLE_INTERMEDIATE), - RIGHT_LITTLE_DISTAL(BodyPart.RIGHT_LITTLE_DISTAL); - - public static final BoneType[] values = values(); - - public final int bodyPart; - - BoneType() { - this.bodyPart = BodyPart.NONE; - } - - BoneType(int associatedBodyPart) { - this.bodyPart = associatedBodyPart; - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt deleted file mode 100644 index ac33c9d2a3..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt +++ /dev/null @@ -1,190 +0,0 @@ -package dev.slimevr.tracking.processor - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/** - * Represents a function that applies a rotational constraint. - */ -typealias ConstraintFunction = (localRotation: Quaternion, limit1: Float, limit2: Float, limit3: Float) -> Quaternion - -/** - * Represents the rotational limits of a Bone relative to its parent, - * twist and swing are the max and min when constraintType is a hinge. - * Twist, swing, allowedDeviation, and maxDeviationFromTracker represent - * an angle in degrees. - */ -class Constraint( - val constraintType: ConstraintType, - twist: Float = 0.0f, - swing: Float = 0.0f, - allowedDeviation: Float = 0f, - maxDeviationFromTracker: Float = 15f, -) { - private val constraintFunction = constraintTypeToFunc(constraintType) - private val twistRad = twist * FastMath.DEG_TO_RAD - private val swingRad = swing * FastMath.DEG_TO_RAD - private val allowedDeviationRad = allowedDeviation * FastMath.DEG_TO_RAD - private val maxDeviationFromTrackerRad = maxDeviationFromTracker * FastMath.DEG_TO_RAD - - /** - * allowModification may be false for reasons other than a tracker being on this bone - * while hasTrackerRotation is only true if this bone has a tracker. These values are - * to be used with an IK solver and are not currently set accurately - */ - var allowModifications = true - var hasTrackerRotation = false - - /** - * The rotation before any IK solve takes place. Again this value is not currently set accurately - */ - var initialRotation = Quaternion.IDENTITY - - /** - * Apply rotational constraints and if applicable force the rotation - * to be unchanged unless it violates the constraints - */ - fun applyConstraint(rotation: Quaternion, thisBone: Bone): Quaternion { - // When constraints are being used during a IK solve the input rotation is not necessarily - // the bones global rotation, thus complete constraints must be specifically handled. - if (constraintType == ConstraintType.COMPLETE) return thisBone.getGlobalRotation() - - // If there is no parent and this is not a complete constraint accept the rotation as is. - // TODO: This was changed due to a race condition with the RPC thread, see - // https://github.com/SlimeVR/SlimeVR-Server/issues/1534 for more information. - val parent = thisBone.parent ?: return rotation - - val localRotation = getLocalRotation(rotation, thisBone, parent) - val constrainedRotation = constraintFunction(localRotation, swingRad, twistRad, allowedDeviationRad) - return getWorldRotationFromLocal(constrainedRotation, thisBone, parent) - } - - /** - * Force the given rotation to be within allowedDeviation degrees away from - * initialRotation on both the twist and swing axis - */ - fun constrainToInitialRotation(rotation: Quaternion): Quaternion { - val rotationLocal = rotation * initialRotation.inv() - var (swingQ, twistQ) = decompose(rotationLocal, Vector3.NEG_Y) - swingQ = constrain(swingQ, maxDeviationFromTrackerRad) - twistQ = constrain(twistQ, maxDeviationFromTrackerRad) - return initialRotation * (swingQ * twistQ) - } - - companion object { - const val ANGLE_THRESHOLD = 0.004f // == 0.25 degrees - const val FILTER_IMPACT_THRESHOLD = 0.0349f // == 2 degrees - - enum class ConstraintType { - TWIST_SWING, - HINGE, - LOOSE_HINGE, - COMPLETE, - } - - private fun constraintTypeToFunc(type: ConstraintType) = when (type) { - ConstraintType.COMPLETE -> completeConstraint - ConstraintType.TWIST_SWING -> twistSwingConstraint - ConstraintType.HINGE -> hingeConstraint - ConstraintType.LOOSE_HINGE -> looseHingeConstraint - } - - private fun getLocalRotation(rotation: Quaternion, thisBone: Bone, parent: Bone): Quaternion { - val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset - return (parent.getGlobalRotation() * localRotationOffset).inv() * rotation - } - - private fun getWorldRotationFromLocal(rotation: Quaternion, thisBone: Bone, parent: Bone): Quaternion { - val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset - return (parent.getGlobalRotation() * localRotationOffset * rotation).unit() - } - - private fun decompose( - rotation: Quaternion, - twistAxis: Vector3, - ): Pair { - val projection = rotation.project(twistAxis).unit() - val twist = Quaternion(sqrt(1.0f - projection.xyz.lenSq()) * if (rotation.w >= 0f) 1f else -1f, projection.xyz).unit() - val swing = (rotation * twist.inv()).unit() - return Pair(swing, twist) - } - - private fun constrain(rotation: Quaternion, angle: Float): Quaternion { - // Use angle to get the maximum magnitude the vector part of rotation can be - // before it has violated a constraint. - // Multiplying by 0.5 uniquely maps angles 0-180 degrees to 0-1 which works - // nicely with unit quaternions. - val magnitude = sin(angle * 0.5f) - val magnitudeSqr = magnitude * magnitude - val sign = if (rotation.w >= 0f) 1f else -1f - var vector = rotation.xyz - var rot = rotation - - if (vector.lenSq() > magnitudeSqr) { - vector = vector.unit() * magnitude - rot = Quaternion(sqrt(1.0f - magnitudeSqr) * sign, vector) - } - - return rot.unit() - } - - private fun constrain(rotation: Quaternion, minAngle: Float, maxAngle: Float, axis: Vector3): Quaternion { - val magnitudeMin = sin(minAngle * 0.5f) - val magnitudeMax = sin(maxAngle * 0.5f) - val magnitudeSqrMin = magnitudeMin * magnitudeMin * if (minAngle >= 0f) 1f else -1f - val magnitudeSqrMax = magnitudeMax * magnitudeMax * if (maxAngle >= 0f) 1f else -1f - var vector = rotation.xyz - var rot = rotation - - val rotMagnitude = vector.lenSq() * if (vector.dot(axis) * sign(rot.w) < 0) -1f else 1f - if (rotMagnitude < magnitudeSqrMin || rotMagnitude > magnitudeSqrMax) { - val distToMin = min(abs(rotMagnitude - magnitudeSqrMin), abs(rotMagnitude + magnitudeSqrMin)) - val distToMax = min(abs(rotMagnitude - magnitudeSqrMax), abs(rotMagnitude + magnitudeSqrMax)) - - val magnitude = if (distToMin < distToMax) magnitudeMin else magnitudeMax - val magnitudeSqr = abs(if (distToMin < distToMax) magnitudeSqrMin else magnitudeSqrMax) - vector = vector.unit() * -magnitude - - rot = Quaternion(sqrt(1.0f - magnitudeSqr), vector) - } - - return rot.unit() - } - - // Constraint function for TwistSwingConstraint - private val twistSwingConstraint: ConstraintFunction = - { rotation: Quaternion, swingRad: Float, twistRad: Float, _: Float -> - var (swingQ, twistQ) = decompose(rotation, Vector3.NEG_Y) - swingQ = constrain(swingQ, swingRad) - twistQ = constrain(twistQ, twistRad) - - swingQ * twistQ - } - - // Constraint function for a hinge constraint with min and max angles - private val hingeConstraint: ConstraintFunction = - { rotation: Quaternion, min: Float, max: Float, _: Float -> - val (_, hingeAxisRot) = decompose(rotation, Vector3.NEG_X) - - constrain(hingeAxisRot, min, max, Vector3.NEG_X) - } - - // Constraint function for a hinge constraint with min and max angles that allows nonHingeDeviation - // rotation on all axis but the hinge - private val looseHingeConstraint: ConstraintFunction = - { rotation: Quaternion, min: Float, max: Float, nonHingeDeviation: Float -> - var (nonHingeRot, hingeAxisRot) = decompose(rotation, Vector3.NEG_X) - hingeAxisRot = constrain(hingeAxisRot, min, max, Vector3.NEG_X) - nonHingeRot = constrain(nonHingeRot, nonHingeDeviation) - - nonHingeRot * hingeAxisRot - } - - // Constraint function for CompleteConstraint - private val completeConstraint: ConstraintFunction = { rotation: Quaternion, _: Float, _: Float, _: Float -> - rotation - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt deleted file mode 100644 index f1fa122695..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt +++ /dev/null @@ -1,691 +0,0 @@ -package dev.slimevr.tracking.processor - -import com.jme3.math.FastMath -import dev.slimevr.VRServer -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.autobone.errors.BodyProportionError -import dev.slimevr.config.ConfigManager -import dev.slimevr.tracking.processor.config.SkeletonConfigManager -import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.processor.config.SkeletonConfigValues -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.tracking.trackers.* -import dev.slimevr.trackingpause.TrackingPauseHandler -import dev.slimevr.util.ann.VRServerThread -import io.eiren.util.ann.ThreadSafe -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY -import io.github.axisangles.ktmath.Vector3 -import io.github.axisangles.ktmath.Vector3.Companion.POS_Y -import org.apache.commons.math3.util.Precision -import java.util.function.Consumer -import kotlin.math.* - -/** - * Class to handle communicate between classes in "skeleton" package and outside - * @param server the used VRServer - */ -class HumanPoseManager(val server: VRServer?) { - val computedTrackers: MutableList = FastList() - private val onSkeletonUpdated: MutableList> = FastList() - private val skeletonConfigManager = SkeletonConfigManager(true, this) - - @get:ThreadSafe - lateinit var skeleton: HumanSkeleton - private var timeAtLastReset: Long = 0 - val trackingPauseHandler: TrackingPauseHandler = TrackingPauseHandler() - - // #region Constructors - init { - initializeComputedHumanPoseTracker() - } - - init { - if (server != null) { - skeleton = HumanSkeleton(this, server) - // This computes all node offsets, so the defaults don't need to be - // explicitly loaded into the skeleton (no need for - // `computeAllNodeOffsets()`) - loadFromConfig(server.configManager) - for (sc in onSkeletonUpdated) sc.accept(skeleton) - } - } - - /** - * Creates a new HumanPoseManager that uses the given trackers for the - * HumanSkeleton and the default config - * - * @param trackers a list of all trackers - */ - constructor(trackers: List?) : this(server = null) { - skeleton = HumanSkeleton(this, trackers) - // Set default node offsets on the new skeleton - skeletonConfigManager.computeAllNodeOffsets() - skeletonConfigManager.updateSettingsInSkeleton() - } - - /** - * Creates a new HumanPoseManager that uses the given trackers for the - * HumanSkeleton and the given offsets for config - * - * @param trackers a list of all trackers - * @param offsetConfigs a map of the SkeletonConfigOffsets and values for - * them - */ - constructor( - trackers: List?, - offsetConfigs: Map?, - ) : this(server = null) { - skeleton = HumanSkeleton(this, trackers) - // Set offsetConfigs from given offsetConfigs on creation - // This computes all node offsets, so the defaults don't need to be - // explicitly loaded into the skeleton (no need for `computeAllNodeOffsets()`) - skeletonConfigManager.setOffsets(offsetConfigs) - skeletonConfigManager.updateSettingsInSkeleton() - } - - /** - * Creates a new HumanPoseManager that uses the given trackers for the - * HumanSkeleton and the given offsets for config - * - * @param trackers a list of all trackers - * @param offsetConfigs a map of the SkeletonConfigOffsets and values for - * them - * @param altOffsetConfigs an alternative map of the SkeletonConfigOffsets - * and values for them - */ - constructor( - trackers: List?, - offsetConfigs: Map?, - altOffsetConfigs: Map?, - ) : this(server = null) { - skeleton = HumanSkeleton(this, trackers) - // Set offsetConfigs from given offsetConfigs on creation - if (altOffsetConfigs != null) { - // Set alts first, so if there's any overlap it doesn't affect - // the values - skeletonConfigManager.setOffsets(altOffsetConfigs) - } - // This computes all node offsets, so the defaults don't need to be - // explicitly loaded into the skeleton (no need for `computeAllNodeOffsets()`) - skeletonConfigManager.setOffsets(offsetConfigs) - skeletonConfigManager.updateSettingsInSkeleton() - } - - // #endregion - // #region private methods - private fun makeComputedTracker(name: String, display: String, pos: TrackerPosition): Tracker = Tracker( - null, - getNextLocalTrackerId(), - name, - display, - pos, - hasPosition = true, - hasRotation = true, - isInternal = true, - isComputed = true, - allowFiltering = false, - // Do not track polarity, moving avg de-syncs ticks and breaks leg tweaks - trackRotDirection = false, - ) - - private fun initializeComputedHumanPoseTracker() { - computedTrackers - .add( - makeComputedTracker( - "human://HEAD", - "Computed head", - TrackerPosition.HEAD, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://CHEST", - "Computed chest", - TrackerPosition.UPPER_CHEST, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://WAIST", - "Computed hip", - TrackerPosition.HIP, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://LEFT_KNEE", - "Computed left knee", - TrackerPosition.LEFT_UPPER_LEG, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://RIGHT_KNEE", - "Computed right knee", - TrackerPosition.RIGHT_UPPER_LEG, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://LEFT_FOOT", - "Computed left foot", - TrackerPosition.LEFT_FOOT, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://RIGHT_FOOT", - "Computed right foot", - TrackerPosition.RIGHT_FOOT, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://LEFT_ELBOW", - "Computed left elbow", - TrackerPosition.LEFT_UPPER_ARM, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://RIGHT_ELBOW", - "Computed right elbow", - TrackerPosition.RIGHT_UPPER_ARM, - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://LEFT_HAND", - "Computed left hand", - TrackerPosition.LEFT_HAND, - - ), - ) - computedTrackers - .add( - makeComputedTracker( - "human://RIGHT_HAND", - "Computed right hand", - TrackerPosition.RIGHT_HAND, - ), - ) - - connectComputedHumanPoseTrackers() - } - - fun loadFromConfig(configManager: ConfigManager) { - skeletonConfigManager.loadFromConfig(configManager) - } - - @VRServerThread - fun updateSkeletonModelFromServer() { - skeleton.setTrackersFromList(server!!.allTrackers) - } - - private fun connectComputedHumanPoseTrackers() { - for (t in computedTrackers) { - t.status = TrackerStatus.OK - } - } - - // #endregion - // #region public methods - // #region skeleton methods - @VRServerThread - fun trackerAdded(tracker: Tracker?) { - } - - @VRServerThread - fun trackerUpdated(tracker: Tracker?) { - } - - @VRServerThread - fun addSkeletonUpdatedCallback(consumer: Consumer) { - onSkeletonUpdated.add(consumer) - consumer.accept(skeleton) - } - - /** - * @return False if the skeleton isn't yet initialized - */ - @get:ThreadSafe - val isSkeletonPresent: Boolean - get() = this::skeleton.isInitialized - - /** - * Updates the pose of the skeleton from trackers rotations - */ - @VRServerThread - fun update() { - skeleton.updatePose() - } - - /** - * Get the corresponding computed tracker for a given TrackerRole - * - * @param trackerRole the role of the tracker which we want - * @return the corresponding computed tracker for the trackerRole - */ - @ThreadSafe - fun getComputedTracker(trackerRole: TrackerRole): Tracker = skeleton.getComputedTracker(trackerRole) - - /** - * @return the head bone, which is the root of the skeleton - */ - @get:ThreadSafe - val headBone: Bone - get() = skeleton.headBone - - /** - * Get a specified bone from the passed BoneType - * - * @param boneType the type of the bone we want - * @return the specified bone - */ - @ThreadSafe - fun getBone(boneType: BoneType): Bone = skeleton.getBone(boneType) - - /** - * @return the HMD's y position from the skeleton - */ - @get:ThreadSafe - val hmdHeight: Float - get() = skeleton.hmdHeight - - /** - * Runs checks to know if we should (and are) performing the tracking of the - * left arm from the controller. - * - * @return a bool telling us if we are tracking the left arm from the - * controller or not. - */ - @get:ThreadSafe - val isTrackingLeftArmFromController: Boolean - get() = skeleton.isTrackingLeftArmFromController - - /** - * Runs checks to know if we should (and are) performing the tracking of the - * right arm from the controller. - * - * @return a bool telling us if we are tracking the right arm from the - * controller or not. - */ - @get:ThreadSafe - val isTrackingRightArmFromController: Boolean - get() = skeleton.isTrackingRightArmFromController - - /** - * @return All non-tracker bones - */ - @get:ThreadSafe - val allBones: List - get() = listOf(*skeleton.allHumanBones) - - // #endregion - // #region config methods - - /** - * @param key the offset from which to get the corresponding value - * @return the offset in config corresponding to the key - */ - @ThreadSafe - fun getOffset(key: SkeletonConfigOffsets?): Float = skeletonConfigManager.getOffset(key) - - /** - * @param key the offset to set the length to - * @param newLength the new attributed length to the offset - */ - @ThreadSafe - fun setOffset(key: SkeletonConfigOffsets, newLength: Float?) { - skeletonConfigManager.setOffset(key, newLength) - } - - /** - * Resets all the offsets in the current SkeletonConfigManager - */ - @ThreadSafe - fun resetOffsets() { - skeletonConfigManager.resetOffsets() - } - - /** - * @param key the toggle from which to get the corresponding value - * @return the toggle in config corresponding to the key - */ - @ThreadSafe - fun getToggle(key: SkeletonConfigToggles?): Boolean = skeletonConfigManager.getToggle(key) - - /** - * @param key the toggle to set the value to - * @param newValue the new attributed value to the toggle - */ - @ThreadSafe - fun setToggle(key: SkeletonConfigToggles, newValue: Boolean?) { - skeletonConfigManager.setToggle(key, newValue) - } - - /** - * Resets all the toggles in the current SkeletonConfigManager - */ - @ThreadSafe - fun resetToggles() { - skeletonConfigManager.resetToggles() - } - - /** - * @param key the value from which to get the corresponding value - * @return the value in config corresponding to the key - */ - @ThreadSafe - fun getValue(key: SkeletonConfigValues): Float = skeletonConfigManager.getValue(key) - - /** - * @param key the value to set the value to - * @param newValue the new attributed value to the value - */ - @ThreadSafe - fun setValue(key: SkeletonConfigValues, newValue: Float?) { - skeletonConfigManager.setValue(key, newValue) - } - - /** - * Resets all the values in the current SkeletonConfigManager - */ - @ThreadSafe - fun resetValues() { - skeletonConfigManager.resetValues() - } - - /** - * Resets all the skeleton configs in the current SkeletonConfigManager - */ - @ThreadSafe - fun resetAllConfigs() { - skeletonConfigManager.resetAllConfigs() - } - - /** - * Writes the skeleton configs - */ - @ThreadSafe - fun saveConfig() { - skeletonConfigManager.save() - } - - /** - * Update the given bone with the given offset - * - * @param boneType the type of the bone to update - * @param offset the new offset to apply to the boneType - */ - @ThreadSafe - fun updateNodeOffset(boneType: BoneType, offset: Vector3) { - if (!isSkeletonPresent) return - skeleton.updateNodeOffset(boneType, offset) - } - - /** - * Updates all the node offsets in the skeleton - */ - fun updateNodeOffsetsInSkeleton() { - if (!isSkeletonPresent) return - skeletonConfigManager.updateNodeOffsetsInSkeleton() - } - - /** - * Update the given toggle to the new given value in the skeleton - * - * @param configToggle the toggle to update - * @param newValue the new value to apply to the toggle - */ - @ThreadSafe - fun updateToggleState(configToggle: SkeletonConfigToggles, newValue: Boolean) { - skeleton.updateToggleState(configToggle, newValue) - } - - /** - * Update the given value to the new given value in the skeleton - * - * @param configValue the value to update - * @param newValue the new value to apply to the value - */ - @ThreadSafe - fun updateValueState(configValue: SkeletonConfigValues, newValue: Float) { - skeleton.updateValueState(configValue, newValue) - } - - /** - * Compute the offset for the given node and apply the new offset - * - * @param node the node to update - */ - fun computeNodeOffset(node: BoneType) { - skeletonConfigManager.computeNodeOffset(node) - } - - @JvmOverloads - fun resetTrackersFull(resetSourceName: String?, bodyParts: List = ArrayList()) { - skeleton.resetTrackersFull(resetSourceName, bodyParts) - if (server != null) { - if (skeleton.headTracker == null && skeleton.neckTracker == null) { - server.vrcOSCHandler.yawAlign(IDENTITY) - } else { - server.vrcOSCHandler - .yawAlign( - headBone.getGlobalRotation().project(POS_Y), - ) - } - server.vMCHandler.alignVMCTracking(headBone.getGlobalRotation()) - logTrackersDrift() - } - } - - @JvmOverloads - fun resetTrackersYaw(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { - skeleton.resetTrackersYaw(resetSourceName, bodyParts) - if (server != null) { - if (skeleton.headTracker == null && skeleton.neckTracker == null) { - server.vrcOSCHandler.yawAlign(IDENTITY) - } else { - server.vrcOSCHandler - .yawAlign( - headBone.getGlobalRotation().project(POS_Y), - ) - } - server.vMCHandler.alignVMCTracking(headBone.getGlobalRotation()) - logTrackersDrift() - } - } - - private fun logTrackersDrift() { - if (timeAtLastReset == 0L) timeAtLastReset = System.currentTimeMillis() - - // Get time since last reset in seconds - val timeSinceLastReset = (System.currentTimeMillis() - timeAtLastReset) / 1000L - timeAtLastReset = System.currentTimeMillis() - - // Build String for trackers drifts - val trackersDriftText = StringBuilder() - for (tracker in server!!.allTrackers) { - if (( - tracker.isImu() && - tracker.allowReset - ) && - tracker.resetsHandler.lastResetQuaternion != null - ) { - if (trackersDriftText.isNotEmpty()) { - trackersDriftText.append(" | ") - } - - // Get the difference between last reset and now - val difference = tracker - .getRotationNoResetSmooth() * - tracker.resetsHandler.lastResetQuaternion!!.inv() - // Get the pure yaw - var trackerDriftAngle = abs( - ( - atan2(difference.y, difference.w) * - 2 * - FastMath.RAD_TO_DEG - ), - ) - // Fix for polarity or something - if (trackerDriftAngle > 180) trackerDriftAngle = abs((trackerDriftAngle - 360)) - - // Calculate drift per minute - val driftPerMin = trackerDriftAngle / (timeSinceLastReset / 60f) - - trackersDriftText.append(tracker.name) - val trackerPosition = tracker.trackerPosition - if (trackerPosition != null) trackersDriftText.append(" (").append(trackerPosition.name).append(")") - - trackersDriftText - .append(", ") - .append(Precision.round(trackerDriftAngle, 4)) - .append(" deg (") - .append(Precision.round(driftPerMin, 4)) - .append(" deg/min)") - } - } - - if (trackersDriftText.isNotEmpty()) { - LogManager - .info( - "[HumanPoseManager] $timeSinceLastReset seconds since last reset. Tracker yaw drifts: $trackersDriftText", - ) - } - } - - @JvmOverloads - fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) { - val finalBodyParts = bodyParts - ?: if (server?.configManager?.vrConfig?.resetsConfig?.resetMountingFeet == true) { - TrackerUtils.allBodyPartsButFingers - } else { - TrackerUtils.allBodyPartsButFingersAndFeets - } - - skeleton.resetTrackersMounting(resetSourceName, finalBodyParts) - } - - fun clearTrackersMounting(resetSourceName: String?) { - skeleton.clearTrackersMounting(resetSourceName) - } - - @get:ThreadSafe - val legTweaksState: BooleanArray - get() = skeleton.legTweaksState - - @VRServerThread - fun setLegTweaksEnabled(value: Boolean) { - skeleton.setLegTweaksEnabled(value) - } - - @VRServerThread - fun setIKSolverEnabled(value: Boolean) { - skeleton.setIKSolverEnabled(value) - } - - @VRServerThread - fun setFloorClipEnabled(value: Boolean) { - skeleton.setFloorclipEnabled(value) - if (server != null) { - server.configManager - .vrConfig - .skeleton - .getToggles()[SkeletonConfigToggles.FLOOR_CLIP.configKey] = value - server.configManager.saveConfig() - } - } - - @VRServerThread - fun setSkatingCorrectionEnabled(value: Boolean) { - skeleton.setSkatingCorrectionEnabled(value) - if (server != null) { - server.configManager - .vrConfig - .skeleton - .getToggles()[SkeletonConfigToggles.SKATING_CORRECTION.configKey] = value - server.configManager.saveConfig() - } - } - - fun setLegTweaksStateTemp( - skatingCorrection: Boolean, - floorClip: Boolean, - toeSnap: Boolean, - footPlant: Boolean, - ) { - skeleton.setLegTweaksStateTemp(skatingCorrection, floorClip, toeSnap, footPlant) - } - - fun clearLegTweaksStateTemp( - skatingCorrection: Boolean, - floorClip: Boolean, - toeSnap: Boolean, - footPlant: Boolean, - ) { - skeleton.clearLegTweaksStateTemp(skatingCorrection, floorClip, toeSnap, footPlant) - } - - fun updateTapDetectionConfig() { - skeleton.updateTapDetectionConfig() - } - - fun updateLegTweaksConfig() { - skeleton.updateLegTweaksConfig() - } - - @get:ThreadSafe - val userHeightFromConfig: Float - get() = skeletonConfigManager.userHeightFromOffsets - - @get:ThreadSafe - val userNeckHeightFromConfig: Float - get() = skeletonConfigManager.userNeckHeightFromOffsets - - @get:ThreadSafe - val realUserHeight: Float - get() = skeletonConfigManager.userHeightFromOffsets / BodyProportionError.eyeHeightToHeightRatio - - // #endregion - fun getPauseTracking(): Boolean = skeleton.getPauseTracking() - - fun setPauseTracking(pauseTracking: Boolean, sourceName: String?) { - skeleton.setPauseTracking(pauseTracking, sourceName) - } - - fun togglePauseTracking(sourceName: String?): Boolean = skeleton.togglePauseTracking(sourceName) - - // This should be executed when the head tracker is changed - fun checkTrackersRequiringReset() { - // Checks if this is main human pose manager (having server) or - // skeleton doesn't have a head tracker or not an HMD one - if (server == null || - skeleton.headTracker?.isComputed != true - ) { - return - } - server.allTrackers - .filter { it.trackerPosition != null } - .forEach { - if (it.allowReset && !it.needReset) { - it.needReset = true - } - } - } - - // #endregion -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt deleted file mode 100644 index 626cf35cbe..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.slimevr.tracking.processor - -import io.eiren.util.ann.ThreadSafe -import io.github.axisangles.ktmath.Transform -import java.util.concurrent.CopyOnWriteArrayList -import java.util.function.Consumer - -/** - * Represents a joint - */ -class TransformNode(val localRotation: Boolean) { - val localTransform = Transform() - val worldTransform = Transform() - var parent: TransformNode? = null - private set - val children: MutableList = CopyOnWriteArrayList() - - fun attachChild(node: TransformNode) { - require(node.parent == null) { "The child node must not already have a parent." } - children.add(node) - node.parent = this - } - - @ThreadSafe - fun update() { - // Update transform - updateWorldTransforms() - - // Update children - for (node in children) { - node.update() - } - } - - @ThreadSafe - fun updateThisNode() { - updateWorldTransforms() - } - - @Synchronized - private fun updateWorldTransforms() { - worldTransform.set(localTransform) - parent?.let { - if (localRotation) { - worldTransform.combineWithParent(it.worldTransform) - } else { - combineWithParentGlobalRotation(it.worldTransform) - } - } - } - - fun depthFirstTraversal(visitor: Consumer) { - for (node in children) { - node.depthFirstTraversal(visitor) - } - visitor.accept(this) - } - - private fun combineWithParentGlobalRotation(parent: Transform) { - worldTransform.scale = worldTransform.scale hadamard parent.scale - val scaledTranslation = worldTransform.translation hadamard parent.scale - worldTransform.translation = parent.rotation.sandwich(scaledTranslation) + parent.translation - } - - fun detachWithChildren() { - for (child in children) { - child.parent = null - } - children.clear() - parent?.children?.remove(this) - parent = null - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt deleted file mode 100644 index 45ea6a0548..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt +++ /dev/null @@ -1,556 +0,0 @@ -package dev.slimevr.tracking.processor.config - -import dev.slimevr.VRServer.Companion.instance -import dev.slimevr.VRServer.Companion.instanceInitialized -import dev.slimevr.autobone.AutoBone -import dev.slimevr.autobone.errors.BodyProportionError.Companion.proportionLimitMap -import dev.slimevr.config.ConfigManager -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.HumanPoseManager -import io.github.axisangles.ktmath.Vector3 -import java.util.* - -class SkeletonConfigManager( - private val autoUpdateOffsets: Boolean, - private val humanPoseManager: HumanPoseManager? = null, -) { - private val configOffsets: EnumMap = EnumMap( - SkeletonConfigOffsets::class.java, - ) - private val configToggles: EnumMap = EnumMap( - SkeletonConfigToggles::class.java, - ) - private val configValues: EnumMap = EnumMap( - SkeletonConfigValues::class.java, - ) - - private var changedToggles: BooleanArray = BooleanArray(SkeletonConfigToggles.values.size) - private var changedValues: BooleanArray = BooleanArray(SkeletonConfigValues.values.size) - - private val nodeOffsets: EnumMap = EnumMap( - BoneType::class.java, - ) - - var userHeightFromOffsets: Float = calculateUserHeight() - private set - - var userNeckHeightFromOffsets: Float = calculateUserHeight() - private set - - init { - if (humanPoseManager?.isSkeletonPresent != false) { - updateSettingsInSkeleton() - - if (autoUpdateOffsets) { - computeAllNodeOffsets() - } - } - } - - fun updateSettingsInSkeleton() { - if (humanPoseManager == null) return - - for (config in SkeletonConfigToggles.values) { - val configToggle = configToggles[config] - humanPoseManager.updateToggleState(config, configToggle ?: config.defaultValue) - } - - for (config in SkeletonConfigValues.values) { - val configValue = configValues[config] - humanPoseManager.updateValueState(config, configValue ?: config.defaultValue) - } - } - - fun updateNodeOffsetsInSkeleton() { - if (humanPoseManager == null) return - - for (config in BoneType.values) { - val nodeOffset = nodeOffsets[config] - if (nodeOffset != null) humanPoseManager.updateNodeOffset(config, nodeOffset) - } - } - - fun setOffset( - config: SkeletonConfigOffsets, - newValue: Float?, - computeOffsets: Boolean, - ) { - if (newValue != null) { - configOffsets[config] = newValue - } else { - configOffsets.remove(config) - } - - // Re-compute the affected offsets - if (computeOffsets && autoUpdateOffsets && config.affectedOffsets != null) { - for (offset in config.affectedOffsets) { - computeNodeOffset(offset) - } - } - - // Re-calculate user height - userHeightFromOffsets = calculateUserHeight() - userNeckHeightFromOffsets = userHeightFromOffsets - getOffset(SkeletonConfigOffsets.NECK) - - // Update vrc config checker if user height change - humanPoseManager?.server?.vrcConfigManager?.forceUpdate() - } - - fun setOffset(config: SkeletonConfigOffsets, newValue: Float?) { - setOffset(config, newValue, true) - } - - fun getOffset(config: SkeletonConfigOffsets?): Float { - if (config == null) { - return 0f - } - - val configOffset = configOffsets[config] - return configOffset ?: config.defaultValue - } - - private fun calculateUserHeight(): Float { - var height = 0f - for (offset in HEIGHT_OFFSETS) { - height += getOffset(offset) - } - return height - } - - fun setToggle(config: SkeletonConfigToggles, newValue: Boolean?) { - if (newValue != null) { - if (configToggles[config] != null && (newValue != configToggles[config])) { - changedToggles[config.ordinal] = true - } - configToggles[config] = newValue - } else { - configToggles.remove(config) - } - - // Updates in skeleton - humanPoseManager?.updateToggleState(config, newValue ?: config.defaultValue) - } - - fun getToggle(config: SkeletonConfigToggles?): Boolean { - if (config == null) { - return false - } - - val configToggle = configToggles[config] - return configToggle ?: config.defaultValue - } - - fun setValue(config: SkeletonConfigValues, newValue: Float?) { - if (newValue != null) { - if (configValues[config] != null && (newValue != configValues[config])) { - changedValues[config.ordinal] = true - } - configValues[config] = newValue - } else { - configValues.remove(config) - } - - // Updates in skeleton - humanPoseManager?.updateValueState(config, newValue ?: config.defaultValue) - } - - fun getValue(config: SkeletonConfigValues?): Float { - if (config == null) { - return 0f - } - - val configValue = configValues[config] - return configValue ?: config.defaultValue - } - - protected fun setNodeOffset(nodeOffset: BoneType, x: Float, y: Float, z: Float) { - val offset = Vector3(x, y, z) - nodeOffsets[nodeOffset] = offset - - // Updates in skeleton - humanPoseManager?.updateNodeOffset(nodeOffset, offset) - } - - fun computeNodeOffset(nodeOffset: BoneType) { - when (nodeOffset) { - BoneType.HEAD -> setNodeOffset(nodeOffset, 0f, 0f, getOffset(SkeletonConfigOffsets.HEAD)) - - BoneType.NECK -> setNodeOffset(nodeOffset, 0f, -getOffset(SkeletonConfigOffsets.NECK), 0f) - - BoneType.UPPER_CHEST -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.UPPER_CHEST), - 0f, - ) - - BoneType.CHEST_TRACKER -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.CHEST_OFFSET) - - getOffset(SkeletonConfigOffsets.CHEST), - -getOffset(SkeletonConfigOffsets.SKELETON_OFFSET), - ) - - BoneType.CHEST -> setNodeOffset(nodeOffset, 0f, -getOffset(SkeletonConfigOffsets.CHEST), 0f) - - BoneType.WAIST -> setNodeOffset(nodeOffset, 0f, -getOffset(SkeletonConfigOffsets.WAIST), 0f) - - BoneType.HIP -> setNodeOffset(nodeOffset, 0f, -getOffset(SkeletonConfigOffsets.HIP), 0f) - - BoneType.HIP_TRACKER -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HIP_OFFSET), - -getOffset(SkeletonConfigOffsets.SKELETON_OFFSET), - ) - - BoneType.LEFT_HIP -> setNodeOffset( - nodeOffset, - -getOffset(SkeletonConfigOffsets.HIPS_WIDTH) / 2f, - 0f, - 0f, - ) - - BoneType.RIGHT_HIP -> setNodeOffset( - nodeOffset, - getOffset(SkeletonConfigOffsets.HIPS_WIDTH) / 2f, - 0f, - 0f, - ) - - BoneType.LEFT_UPPER_LEG, BoneType.RIGHT_UPPER_LEG -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.UPPER_LEG), - 0f, - ) - - BoneType.LEFT_KNEE_TRACKER, BoneType.RIGHT_KNEE_TRACKER, BoneType.LEFT_FOOT_TRACKER, BoneType.RIGHT_FOOT_TRACKER -> setNodeOffset( - nodeOffset, - 0f, - 0f, - -getOffset(SkeletonConfigOffsets.SKELETON_OFFSET), - ) - - BoneType.LEFT_LOWER_LEG, BoneType.RIGHT_LOWER_LEG -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.LOWER_LEG), - -getOffset(SkeletonConfigOffsets.FOOT_SHIFT), - ) - - BoneType.LEFT_FOOT, BoneType.RIGHT_FOOT -> setNodeOffset( - nodeOffset, - 0f, - 0f, - -getOffset(SkeletonConfigOffsets.FOOT_LENGTH), - ) - - BoneType.LEFT_UPPER_SHOULDER -> setNodeOffset( - nodeOffset, - 0f, - 0f, - 0f, - ) - - BoneType.RIGHT_UPPER_SHOULDER -> setNodeOffset( - nodeOffset, - 0f, - 0f, - 0f, - ) - - BoneType.LEFT_SHOULDER -> setNodeOffset( - nodeOffset, - -getOffset(SkeletonConfigOffsets.SHOULDERS_WIDTH) / 2f, - -getOffset(SkeletonConfigOffsets.SHOULDERS_DISTANCE), - 0f, - ) - - BoneType.RIGHT_SHOULDER -> setNodeOffset( - nodeOffset, - getOffset(SkeletonConfigOffsets.SHOULDERS_WIDTH) / 2f, - -getOffset(SkeletonConfigOffsets.SHOULDERS_DISTANCE), - 0f, - ) - - BoneType.LEFT_UPPER_ARM, BoneType.RIGHT_UPPER_ARM -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.UPPER_ARM), - 0f, - ) - - BoneType.LEFT_LOWER_ARM, BoneType.RIGHT_LOWER_ARM -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.LOWER_ARM), - 0f, - ) - - BoneType.LEFT_HAND, BoneType.RIGHT_HAND -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y), - -getOffset(SkeletonConfigOffsets.HAND_Z), - ) - - BoneType.LEFT_ELBOW_TRACKER, BoneType.RIGHT_ELBOW_TRACKER -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.ELBOW_OFFSET), - 0f, - ) - - BoneType.LEFT_THUMB_METACARPAL, BoneType.LEFT_THUMB_PROXIMAL, BoneType.LEFT_THUMB_DISTAL, - BoneType.RIGHT_THUMB_METACARPAL, BoneType.RIGHT_THUMB_PROXIMAL, BoneType.RIGHT_THUMB_DISTAL, - -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.2f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.1f, - ) - - BoneType.LEFT_INDEX_PROXIMAL, BoneType.LEFT_INDEX_INTERMEDIATE, BoneType.LEFT_INDEX_DISTAL, - BoneType.RIGHT_INDEX_PROXIMAL, BoneType.RIGHT_INDEX_INTERMEDIATE, BoneType.RIGHT_INDEX_DISTAL, - -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.25f, - 0f, - ) - - BoneType.LEFT_MIDDLE_PROXIMAL, BoneType.LEFT_MIDDLE_INTERMEDIATE, BoneType.LEFT_MIDDLE_DISTAL, - BoneType.RIGHT_MIDDLE_PROXIMAL, BoneType.RIGHT_MIDDLE_INTERMEDIATE, BoneType.RIGHT_MIDDLE_DISTAL, - -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.3f, - 0f, - ) - - BoneType.LEFT_RING_PROXIMAL, BoneType.LEFT_RING_INTERMEDIATE, BoneType.LEFT_RING_DISTAL, - BoneType.RIGHT_RING_PROXIMAL, BoneType.RIGHT_RING_INTERMEDIATE, BoneType.RIGHT_RING_DISTAL, - -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.28f, - 0f, - ) - - BoneType.LEFT_LITTLE_PROXIMAL, BoneType.LEFT_LITTLE_INTERMEDIATE, BoneType.LEFT_LITTLE_DISTAL, - BoneType.RIGHT_LITTLE_PROXIMAL, BoneType.RIGHT_LITTLE_INTERMEDIATE, BoneType.RIGHT_LITTLE_DISTAL, - -> setNodeOffset( - nodeOffset, - 0f, - -getOffset(SkeletonConfigOffsets.HAND_Y) * 0.2f, - 0f, - ) - - else -> {} - } - } - - fun computeAllNodeOffsets() { - for (offset in BoneType.values) { - computeNodeOffset(offset) - } - } - - fun setOffsets( - configOffsets: Map?, - computeOffsets: Boolean, - ) { - configOffsets?.forEach { (key: SkeletonConfigOffsets, value: Float?) -> - // Do not recalculate the offsets, these are done in bulk at the - // end - setOffset(key, value, false) - } - - if (computeOffsets && autoUpdateOffsets) { - computeAllNodeOffsets() - } - } - - fun setOffsets( - configOffsets: Map?, - ) { - setOffsets(configOffsets, true) - } - - fun setOffsets(skeletonConfigManager: SkeletonConfigManager) { - // Don't recalculate node offsets, just re-use them from skeletonConfig - setOffsets( - skeletonConfigManager.configOffsets, - false, - ) - - // Copy skeletonConfig's nodeOffsets as the configs are all the same - skeletonConfigManager.nodeOffsets.forEach { (key: BoneType, value: Vector3) -> - setNodeOffset(key, value.x, value.y, value.z) - } - } - - fun resetOffsets() { - if (humanPoseManager != null) { - for (config in SkeletonConfigOffsets.values) { - resetOffset(config) - } - } else { - configOffsets.clear() - if (autoUpdateOffsets) { - computeAllNodeOffsets() - } - } - } - - fun resetToggles() { - configToggles.clear() - - // Updates in skeleton - if (humanPoseManager != null) { - for (config in SkeletonConfigToggles.values) { - humanPoseManager.updateToggleState(config, config.defaultValue) - } - } - - // Remove from config to use default if they change in the future. - Arrays.fill(changedToggles, false) - if (instanceInitialized) { - for (value in SkeletonConfigToggles.values) { - instance.configManager - .vrConfig - .skeleton - .getToggles() - .remove(value.configKey) - } - } - } - - fun resetValues() { - configValues.clear() - - // Updates in skeleton - if (humanPoseManager != null) { - for (config in SkeletonConfigValues.values) { - humanPoseManager.updateValueState(config, config.defaultValue) - } - } - - // Remove from config to use default if they change in the future. - Arrays.fill(changedValues, false) - if (instanceInitialized) { - for (value in SkeletonConfigValues.values) { - instance.configManager - .vrConfig - .skeleton - .getValues() - .remove(value.configKey) - } - } - } - - fun resetAllConfigs() { - resetOffsets() - resetToggles() - resetValues() - } - - fun resetOffset(config: SkeletonConfigOffsets) { - val height = humanPoseManager?.server?.configManager?.vrConfig?.skeleton?.userHeight ?: -1f - // Only scale if the height is within range - if (height > AutoBone.MIN_HEIGHT) { - val proportionLimiter = proportionLimitMap[config] - if (proportionLimiter != null && proportionLimiter.scaleByHeight) { - setOffset( - config, - height * proportionLimiter.targetRatio, - ) - } else { - setOffset(config, null) - } - } else { - setOffset(config, null) - } - } - - fun loadFromConfig(configManager: ConfigManager) { - val skeletonConfig = configManager.vrConfig.skeleton - - // Load offsets - val offsets = skeletonConfig.getOffsets() - for (configValue in SkeletonConfigOffsets.values) { - val offset = offsets[configValue.configKey] - if (offset != null) { - // Do not recalculate the offsets, these are done in bulk at the - // end - setOffset(configValue, offset, false) - } - } - - // Load toggles - val toggles = skeletonConfig.getToggles() - for (configValue in SkeletonConfigToggles.values) { - val toggle = toggles[configValue.configKey] - if (toggle != null) { - setToggle(configValue, toggle) - } else { - humanPoseManager?.updateToggleState(configValue, configValue.defaultValue) - } - } - - // Load values - val values = skeletonConfig.getValues() - for (configValue in SkeletonConfigValues.values) { - val value = values[configValue.configKey] - if (value != null) { - setValue(configValue, value) - } else { - humanPoseManager?.updateValueState(configValue, configValue.defaultValue) - } - } - - // Updates all offsets - if (autoUpdateOffsets) { - computeAllNodeOffsets() - } - } - - fun save() { - require(instanceInitialized) { "VRServer instance is not initialized, config cannot be saved." } - - val skeletonConfig = instance.configManager - .vrConfig - .skeleton - - // Write all possible values to keep consistent even if defaults changed - for (value in SkeletonConfigOffsets.values) { - skeletonConfig.getOffsets()[value.configKey] = getOffset(value) - } - - // Only write changed values to keep using defaults if not changed - for (value in SkeletonConfigToggles.values) { - if (changedToggles[value.ordinal]) skeletonConfig.getToggles()[value.configKey] = getToggle(value) - } - - // Only write changed values to keep using defaults if not changed - for (value in SkeletonConfigValues.values) { - if (changedValues[value.ordinal]) skeletonConfig.getValues()[value.configKey] = getValue(value) - } - } - - companion object { - val HEIGHT_OFFSETS: Array = arrayOf( - SkeletonConfigOffsets.NECK, - SkeletonConfigOffsets.UPPER_CHEST, - SkeletonConfigOffsets.CHEST, - SkeletonConfigOffsets.WAIST, - SkeletonConfigOffsets.HIP, - SkeletonConfigOffsets.UPPER_LEG, - SkeletonConfigOffsets.LOWER_LEG, - ) - - const val FLOOR_OFFSET: Float = 0.05f - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigOffsets.java b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigOffsets.java deleted file mode 100644 index f65e962467..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigOffsets.java +++ /dev/null @@ -1,172 +0,0 @@ -package dev.slimevr.tracking.processor.config; - -import dev.slimevr.tracking.processor.BoneType; - -import java.util.HashMap; -import java.util.Map; - - -public enum SkeletonConfigOffsets { - // Note: GUI uses this order to sort (not ID) - HEAD( - 1, - "headShift", - 0.1f, - new BoneType[] { BoneType.HEAD } - ), - NECK( - 2, - "neckLength", - 0.1f, - new BoneType[] { BoneType.NECK } - ), - UPPER_CHEST( - 21, - "upperChestLength", - 0.16f, - new BoneType[] { BoneType.UPPER_CHEST, BoneType.CHEST_TRACKER } - ), - CHEST( - 3, - "chestLength", - 0.16f, - new BoneType[] { BoneType.CHEST, BoneType.CHEST_TRACKER } - ), - CHEST_OFFSET( - 4, - "chestOffset", - 0.0f, - new BoneType[] { BoneType.CHEST_TRACKER } - ), - WAIST( - 5, - "waistLength", - 0.20f, - new BoneType[] { BoneType.WAIST } - ), - HIP( - 6, - "hipLength", - 0.04f, - new BoneType[] { BoneType.HIP } - ), - HIP_OFFSET( - 7, - "hipOffset", - 0.0f, - new BoneType[] { BoneType.HIP_TRACKER } - ), - HIPS_WIDTH( - 8, - "hipsWidth", - 0.26f, - new BoneType[] { BoneType.LEFT_HIP, BoneType.RIGHT_HIP } - ), - UPPER_LEG( - 9, - "upperLegLength", - 0.42f, - new BoneType[] { BoneType.LEFT_UPPER_LEG, BoneType.RIGHT_UPPER_LEG } - ), - LOWER_LEG( - 10, - "lowerLegLength", - 0.50f, - new BoneType[] { BoneType.LEFT_LOWER_LEG, BoneType.RIGHT_LOWER_LEG } - ), - FOOT_LENGTH( - 11, - "footLength", - 0.05f, - new BoneType[] { BoneType.LEFT_FOOT, BoneType.RIGHT_FOOT } - ), - FOOT_SHIFT( - 12, - "footShift", - -0.05f, - new BoneType[] { BoneType.LEFT_LOWER_LEG, BoneType.RIGHT_LOWER_LEG } - ), - SKELETON_OFFSET( - 13, - "skeletonOffset", - 0.0f, - new BoneType[] { BoneType.CHEST_TRACKER, BoneType.HIP_TRACKER, - BoneType.LEFT_KNEE_TRACKER, BoneType.RIGHT_KNEE_TRACKER, - BoneType.LEFT_FOOT_TRACKER, BoneType.RIGHT_FOOT_TRACKER } - ), - SHOULDERS_DISTANCE( - 14, - "shouldersDistance", - 0.08f, - new BoneType[] { BoneType.LEFT_SHOULDER, BoneType.RIGHT_SHOULDER } - ), - SHOULDERS_WIDTH( - 15, - "shouldersWidth", - 0.35f, - new BoneType[] { BoneType.LEFT_SHOULDER, BoneType.RIGHT_SHOULDER } - ), - UPPER_ARM( - 16, - "upperArmLength", - 0.26f, - new BoneType[] { BoneType.LEFT_UPPER_ARM, BoneType.RIGHT_UPPER_ARM } - ), - LOWER_ARM( - 17, - "lowerArmLength", - 0.26f, - new BoneType[] { BoneType.LEFT_LOWER_ARM, BoneType.RIGHT_LOWER_ARM } - ), - HAND_Y( - 18, - "handDistanceY", - 0.035f, - new BoneType[] { BoneType.LEFT_HAND, BoneType.RIGHT_HAND } - ), - HAND_Z( - 19, - "handDistanceZ", - 0.13f, - new BoneType[] { BoneType.LEFT_HAND, BoneType.RIGHT_HAND } - ), - ELBOW_OFFSET( - 20, - "elbowOffset", - 0.0f, - new BoneType[] { BoneType.LEFT_ELBOW_TRACKER, BoneType.RIGHT_ELBOW_TRACKER } - ),; - - public static final SkeletonConfigOffsets[] values = values(); - private static final Map byIdVal = new HashMap<>(); - - static { - for (SkeletonConfigOffsets configVal : values()) { - byIdVal.put(configVal.id, configVal); - } - } - - public final int id; - public final String configKey; - public final float defaultValue; - public final BoneType[] affectedOffsets; - - SkeletonConfigOffsets( - int id, - String configKey, - float defaultValue, - BoneType[] affectedOffsets - ) { - this.id = id; // id of SkeletonBone in solarxr - this.configKey = configKey; - - this.defaultValue = defaultValue; - - this.affectedOffsets = affectedOffsets - == null ? new BoneType[0] : affectedOffsets; - } - - public static SkeletonConfigOffsets getById(int id) { - return byIdVal.get(id); - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java deleted file mode 100644 index f6f8c967ff..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.slimevr.tracking.processor.config; - -import java.util.HashMap; -import java.util.Map; - - -public enum SkeletonConfigToggles { - - EXTENDED_SPINE_MODEL(1, "Extended spine model", "extendedSpine", true), - EXTENDED_PELVIS_MODEL(2, "Extended pelvis model", "extendedPelvis", true), - EXTENDED_KNEE_MODEL(3, "Extended knee model", "extendedKnee", true), - FORCE_ARMS_FROM_HMD(4, "Force arms from HMD", "forceArmsFromHMD", true), - FLOOR_CLIP(5, "Floor clip", "floorClip", true), - SKATING_CORRECTION(6, "Skating correction", "skatingCorrection", true), - TOE_SNAP(8, "Toe Snap", "toeSnap", false), - FOOT_PLANT(9, "Foot Plant", "footPlant", true), - SELF_LOCALIZATION(10, "Self Localization", "selfLocalization", false), - USE_POSITION(11, "Use Position", "usePosition", true), - ENFORCE_CONSTRAINTS(12, "Enforce Constraints", "enforceConstraints", true), - CORRECT_CONSTRAINTS(13, "Correct Constraints", "correctConstraints", true),; - - public static final SkeletonConfigToggles[] values = values(); - private static final Map byStringVal = new HashMap<>(); - - private static final Map byIdVal = new HashMap<>(); - - static { - for (SkeletonConfigToggles configVal : values()) { - byIdVal.put(configVal.id, configVal); - byStringVal.put(configVal.stringVal.toLowerCase(), configVal); - } - } - - public final int id; - public final String stringVal; - public final String configKey; - public final boolean defaultValue; - - SkeletonConfigToggles(int id, String stringVal, String configKey, boolean defaultValue) { - this.id = id; - this.stringVal = stringVal; - this.configKey = configKey; - - this.defaultValue = defaultValue; - } - - public static SkeletonConfigToggles getByStringValue(String stringVal) { - return stringVal == null ? null : byStringVal.get(stringVal.toLowerCase()); - } - - public static SkeletonConfigToggles getById(int id) { - return byIdVal.get(id); - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigValues.java b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigValues.java deleted file mode 100644 index c0689dd6f3..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigValues.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.slimevr.tracking.processor.config; - -import java.util.HashMap; -import java.util.Map; - - -public enum SkeletonConfigValues { - // @formatter:off - WAIST_FROM_CHEST_HIP_AVERAGING(1, "waistFromChestHipAveraging", 0.30f), - WAIST_FROM_CHEST_LEGS_AVERAGING(2, "waistFromChestLegsAveraging", 0.30f), - HIP_FROM_CHEST_LEGS_AVERAGING(3, "hipFromChestLegsAveraging", 0.50f), - HIP_FROM_WAIST_LEGS_AVERAGING(4, "hipFromWaistLegsAveraging", 0.40f), - HIP_LEGS_AVERAGING(5, "hipLegsAveraging", 0.25f), - KNEE_TRACKER_ANKLE_AVERAGING(6, "kneeTrackerAnkleAveraging", 0.85f), - KNEE_ANKLE_AVERAGING(7, "kneeAnkleAveraging", 0.00f),; - // @formatter:on - - public static final SkeletonConfigValues[] values = values(); - private static final Map byIdVal = new HashMap<>(); - - static { - for (SkeletonConfigValues configVal : values()) { - byIdVal.put(configVal.id, configVal); - } - } - - public final int id; - public final String configKey; - public final float defaultValue; - - SkeletonConfigValues( - int id, - String configKey, - float defaultValue - ) { - this.id = id; - this.configKey = configKey; - - this.defaultValue = defaultValue; - } - - public static SkeletonConfigValues getById(int id) { - return byIdVal.get(id); - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt deleted file mode 100644 index 2baef0bcd5..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ /dev/null @@ -1,1791 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import dev.slimevr.VRServer -import dev.slimevr.config.MountingMethods -import dev.slimevr.config.StayAlignedConfig -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.BoneType -import dev.slimevr.tracking.processor.Constraint -import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.processor.config.SkeletonConfigValues -import dev.slimevr.tracking.processor.stayaligned.StayAligned -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerRole -import dev.slimevr.tracking.trackers.TrackerUtils -import dev.slimevr.tracking.trackers.TrackerUtils.getFirstAvailableTracker -import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton -import dev.slimevr.tracking.trackers.udp.TrackerDataType -import dev.slimevr.util.ann.VRServerThread -import io.eiren.util.ann.ThreadSafe -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Quaternion.Companion.I -import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY -import io.github.axisangles.ktmath.Quaternion.Companion.fromTo -import io.github.axisangles.ktmath.Vector3 -import io.github.axisangles.ktmath.Vector3.Companion.NEG_Y -import io.github.axisangles.ktmath.Vector3.Companion.NULL -import io.github.axisangles.ktmath.Vector3.Companion.POS_Y -import solarxr_protocol.datatypes.BodyPart -import java.lang.IllegalArgumentException -import kotlin.properties.Delegates - -class HumanSkeleton( - val humanPoseManager: HumanPoseManager, -) { - // Upper body bones - val headBone = Bone(BoneType.HEAD, Constraint(ConstraintType.COMPLETE)) - val neckBone = Bone(BoneType.NECK, Constraint(ConstraintType.COMPLETE)) - val upperChestBone = Bone(BoneType.UPPER_CHEST, Constraint(ConstraintType.TWIST_SWING, 90f, 120f)) - val chestBone = Bone(BoneType.CHEST, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) - val waistBone = Bone(BoneType.WAIST, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) - val hipBone = Bone(BoneType.HIP, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) - - // Lower body bones - val leftHipBone = Bone(BoneType.LEFT_HIP, Constraint(ConstraintType.TWIST_SWING, 0f, 15f)) - val rightHipBone = Bone(BoneType.RIGHT_HIP, Constraint(ConstraintType.TWIST_SWING, 0f, 15f)) - val leftUpperLegBone = Bone(BoneType.LEFT_UPPER_LEG, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) - val rightUpperLegBone = Bone(BoneType.RIGHT_UPPER_LEG, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) - val leftLowerLegBone = Bone(BoneType.LEFT_LOWER_LEG, Constraint(ConstraintType.LOOSE_HINGE, 180f, 0f, 50f)) - val rightLowerLegBone = Bone(BoneType.RIGHT_LOWER_LEG, Constraint(ConstraintType.LOOSE_HINGE, 180f, 0f, 50f)) - val leftFootBone = Bone(BoneType.LEFT_FOOT, Constraint(ConstraintType.TWIST_SWING, 60f, 60f)) - val rightFootBone = Bone(BoneType.RIGHT_FOOT, Constraint(ConstraintType.TWIST_SWING, 60f, 60f)) - - // Arm bones - val leftUpperShoulderBone = Bone(BoneType.LEFT_UPPER_SHOULDER, Constraint(ConstraintType.COMPLETE)) - val rightUpperShoulderBone = Bone(BoneType.RIGHT_UPPER_SHOULDER, Constraint(ConstraintType.COMPLETE)) - val leftShoulderBone = Bone(BoneType.LEFT_SHOULDER, Constraint(ConstraintType.TWIST_SWING, 0f, 30f)) - val rightShoulderBone = Bone(BoneType.RIGHT_SHOULDER, Constraint(ConstraintType.TWIST_SWING, 0f, 30f)) - val leftUpperArmBone = Bone(BoneType.LEFT_UPPER_ARM, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) - val rightUpperArmBone = Bone(BoneType.RIGHT_UPPER_ARM, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) - val leftLowerArmBone = Bone(BoneType.LEFT_LOWER_ARM, Constraint(ConstraintType.LOOSE_HINGE, 0f, -180f, 40f)) - val rightLowerArmBone = Bone(BoneType.RIGHT_LOWER_ARM, Constraint(ConstraintType.LOOSE_HINGE, 0f, -180f, 40f)) - val leftHandBone = Bone(BoneType.LEFT_HAND, Constraint(ConstraintType.TWIST_SWING, 120f, 120f)) - val rightHandBone = Bone(BoneType.RIGHT_HAND, Constraint(ConstraintType.TWIST_SWING, 120f, 120f)) - - // Finger bones - val leftThumbMetacarpalBone = Bone(BoneType.LEFT_THUMB_METACARPAL, Constraint(ConstraintType.COMPLETE)) - val leftThumbProximalBone = Bone(BoneType.LEFT_THUMB_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val leftThumbDistalBone = Bone(BoneType.LEFT_THUMB_DISTAL, Constraint(ConstraintType.COMPLETE)) - val leftIndexProximalBone = Bone(BoneType.LEFT_INDEX_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val leftIndexIntermediateBone = Bone(BoneType.LEFT_INDEX_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val leftIndexDistalBone = Bone(BoneType.LEFT_INDEX_DISTAL, Constraint(ConstraintType.COMPLETE)) - val leftMiddleProximalBone = Bone(BoneType.LEFT_MIDDLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val leftMiddleIntermediateBone = Bone(BoneType.LEFT_MIDDLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val leftMiddleDistalBone = Bone(BoneType.LEFT_MIDDLE_DISTAL, Constraint(ConstraintType.COMPLETE)) - val leftRingProximalBone = Bone(BoneType.LEFT_RING_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val leftRingIntermediateBone = Bone(BoneType.LEFT_RING_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val leftRingDistalBone = Bone(BoneType.LEFT_RING_DISTAL, Constraint(ConstraintType.COMPLETE)) - val leftLittleProximalBone = Bone(BoneType.LEFT_LITTLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val leftLittleIntermediateBone = Bone(BoneType.LEFT_LITTLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val leftLittleDistalBone = Bone(BoneType.LEFT_LITTLE_DISTAL, Constraint(ConstraintType.COMPLETE)) - val rightThumbMetacarpalBone = Bone(BoneType.RIGHT_THUMB_METACARPAL, Constraint(ConstraintType.COMPLETE)) - val rightThumbProximalBone = Bone(BoneType.RIGHT_THUMB_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val rightThumbDistalBone = Bone(BoneType.RIGHT_THUMB_DISTAL, Constraint(ConstraintType.COMPLETE)) - val rightIndexProximalBone = Bone(BoneType.RIGHT_INDEX_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val rightIndexIntermediateBone = Bone(BoneType.RIGHT_INDEX_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val rightIndexDistalBone = Bone(BoneType.RIGHT_INDEX_DISTAL, Constraint(ConstraintType.COMPLETE)) - val rightMiddleProximalBone = Bone(BoneType.RIGHT_MIDDLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val rightMiddleIntermediateBone = Bone(BoneType.RIGHT_MIDDLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val rightMiddleDistalBone = Bone(BoneType.RIGHT_MIDDLE_DISTAL, Constraint(ConstraintType.COMPLETE)) - val rightRingProximalBone = Bone(BoneType.RIGHT_RING_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val rightRingIntermediateBone = Bone(BoneType.RIGHT_RING_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val rightRingDistalBone = Bone(BoneType.RIGHT_RING_DISTAL, Constraint(ConstraintType.COMPLETE)) - val rightLittleProximalBone = Bone(BoneType.RIGHT_LITTLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) - val rightLittleIntermediateBone = Bone(BoneType.RIGHT_LITTLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) - val rightLittleDistalBone = Bone(BoneType.RIGHT_LITTLE_DISTAL, Constraint(ConstraintType.COMPLETE)) - - // Tracker bones - val headTrackerBone = Bone(BoneType.HEAD_TRACKER, Constraint(ConstraintType.COMPLETE)) - val chestTrackerBone = Bone(BoneType.CHEST_TRACKER, Constraint(ConstraintType.COMPLETE)) - val hipTrackerBone = Bone(BoneType.HIP_TRACKER, Constraint(ConstraintType.COMPLETE)) - val leftKneeTrackerBone = Bone(BoneType.LEFT_KNEE_TRACKER, Constraint(ConstraintType.COMPLETE)) - val rightKneeTrackerBone = Bone(BoneType.RIGHT_KNEE_TRACKER, Constraint(ConstraintType.COMPLETE)) - val leftFootTrackerBone = Bone(BoneType.LEFT_FOOT_TRACKER, Constraint(ConstraintType.COMPLETE)) - val rightFootTrackerBone = Bone(BoneType.RIGHT_FOOT_TRACKER, Constraint(ConstraintType.COMPLETE)) - val leftElbowTrackerBone = Bone(BoneType.LEFT_ELBOW_TRACKER, Constraint(ConstraintType.COMPLETE)) - val rightElbowTrackerBone = Bone(BoneType.RIGHT_ELBOW_TRACKER, Constraint(ConstraintType.COMPLETE)) - val leftHandTrackerBone = Bone(BoneType.LEFT_HAND_TRACKER, Constraint(ConstraintType.COMPLETE)) - val rightHandTrackerBone = Bone(BoneType.RIGHT_HAND_TRACKER, Constraint(ConstraintType.COMPLETE)) - - // Buffers - var hasSpineTracker = false - var hasKneeTrackers = false - var hasLeftArmTracker = false - var hasRightArmTracker = false - var hasLeftFingerTracker = false - var hasRightFingerTracker = false - - // Input trackers - var headTracker: Tracker? by Delegates.observable(null) { _, old, new -> - if (old == new) return@observable - - humanPoseManager.checkTrackersRequiringReset() - } - var neckTracker: Tracker? = null - var upperChestTracker: Tracker? = null - var chestTracker: Tracker? = null - var waistTracker: Tracker? = null - var hipTracker: Tracker? = null - var leftUpperLegTracker: Tracker? = null - var leftLowerLegTracker: Tracker? = null - var leftFootTracker: Tracker? = null - var rightUpperLegTracker: Tracker? = null - var rightLowerLegTracker: Tracker? = null - var rightFootTracker: Tracker? = null - var leftLowerArmTracker: Tracker? = null - var rightLowerArmTracker: Tracker? = null - var leftUpperArmTracker: Tracker? = null - var rightUpperArmTracker: Tracker? = null - var leftHandTracker: Tracker? = null - var rightHandTracker: Tracker? = null - var leftShoulderTracker: Tracker? = null - var rightShoulderTracker: Tracker? = null - var leftThumbMetacarpalTracker: Tracker? = null - var leftThumbProximalTracker: Tracker? = null - var leftThumbDistalTracker: Tracker? = null - var leftIndexProximalTracker: Tracker? = null - var leftIndexIntermediateTracker: Tracker? = null - var leftIndexDistalTracker: Tracker? = null - var leftMiddleProximalTracker: Tracker? = null - var leftMiddleIntermediateTracker: Tracker? = null - var leftMiddleDistalTracker: Tracker? = null - var leftRingProximalTracker: Tracker? = null - var leftRingIntermediateTracker: Tracker? = null - var leftRingDistalTracker: Tracker? = null - var leftLittleProximalTracker: Tracker? = null - var leftLittleIntermediateTracker: Tracker? = null - var leftLittleDistalTracker: Tracker? = null - var rightThumbMetacarpalTracker: Tracker? = null - var rightThumbProximalTracker: Tracker? = null - var rightThumbDistalTracker: Tracker? = null - var rightIndexProximalTracker: Tracker? = null - var rightIndexIntermediateTracker: Tracker? = null - var rightIndexDistalTracker: Tracker? = null - var rightMiddleProximalTracker: Tracker? = null - var rightMiddleIntermediateTracker: Tracker? = null - var rightMiddleDistalTracker: Tracker? = null - var rightRingProximalTracker: Tracker? = null - var rightRingIntermediateTracker: Tracker? = null - var rightRingDistalTracker: Tracker? = null - var rightLittleProximalTracker: Tracker? = null - var rightLittleIntermediateTracker: Tracker? = null - var rightLittleDistalTracker: Tracker? = null - - // Output trackers - var computedHeadTracker: Tracker? = null - var computedChestTracker: Tracker? = null - var computedHipTracker: Tracker? = null - var computedLeftKneeTracker: Tracker? = null - var computedLeftFootTracker: Tracker? = null - var computedRightKneeTracker: Tracker? = null - var computedRightFootTracker: Tracker? = null - var computedLeftElbowTracker: Tracker? = null - var computedRightElbowTracker: Tracker? = null - var computedLeftHandTracker: Tracker? = null - var computedRightHandTracker: Tracker? = null - - // Toggles - private var extendedSpineModel = false - private var extendedPelvisModel = false - private var extendedKneeModel = false - private var forceArmsFromHMD = true - private var enforceConstraints = true - private var correctConstraints = true - - // Ratios - private var waistFromChestHipAveraging = 0f - private var waistFromChestLegsAveraging = 0f - private var hipFromChestLegsAveraging = 0f - private var hipFromWaistLegsAveraging = 0f - private var hipLegsAveraging = 0f - private var kneeTrackerAnkleAveraging = 0f - private var kneeAnkleAveraging = 0f - - // Others - private var pauseTracking = false // Pauses skeleton tracking if true, resumes skeleton tracking if false - - // Modules - var legTweaks = LegTweaks(this) - var tapDetectionManager: TapDetectionManager? = null - var localizer = Localizer(this) - var ikSolver = IKSolver(headBone) - var userHeightCalibration: UserHeightCalibration? = null - - // Stay Aligned - var trackerSkeleton = TrackerSkeleton(this) - var stayAlignedConfig = StayAlignedConfig() - - // Constructors - init { - assembleSkeleton() - setComputedTrackers(humanPoseManager.computedTrackers) - } - - constructor( - humanPoseManager: HumanPoseManager, - server: VRServer, - ) : this(humanPoseManager) { - setTrackersFromList(server.allTrackers) - tapDetectionManager = TapDetectionManager( - server, - this, - humanPoseManager, - ) - userHeightCalibration = UserHeightCalibration(server, humanPoseManager) - legTweaks.setConfig(server.configManager.vrConfig.legTweaks) - localizer.setEnabled(humanPoseManager.getToggle(SkeletonConfigToggles.SELF_LOCALIZATION)) - stayAlignedConfig = server.configManager.vrConfig.stayAlignedConfig - } - - constructor( - humanPoseManager: HumanPoseManager, - trackers: List?, - ) : this(humanPoseManager) { - var trackersList = trackers - if (trackersList == null) { - trackersList = FastList(0) - } - setTrackersFromList(trackersList) - } - - /** - * Assembles the whole skeleton - */ - @ThreadSafe - fun assembleSkeleton() { - // Assemble upper skeleton (head to hip) - headBone.attachChild(neckBone) - neckBone.attachChild(upperChestBone) - upperChestBone.attachChild(chestBone) - chestBone.attachChild(waistBone) - waistBone.attachChild(hipBone) - - // Assemble lower skeleton (hip to feet) - hipBone.attachChild(leftHipBone) - hipBone.attachChild(rightHipBone) - leftHipBone.attachChild(leftUpperLegBone) - rightHipBone.attachChild(rightUpperLegBone) - leftUpperLegBone.attachChild(leftLowerLegBone) - rightUpperLegBone.attachChild(rightLowerLegBone) - leftLowerLegBone.attachChild(leftFootBone) - rightLowerLegBone.attachChild(rightFootBone) - - // Attach tracker bones for tracker offsets - neckBone.attachChild(headTrackerBone) - upperChestBone.attachChild(chestTrackerBone) - hipBone.attachChild(hipTrackerBone) - leftUpperLegBone.attachChild(leftKneeTrackerBone) - rightUpperLegBone.attachChild(rightKneeTrackerBone) - leftFootBone.attachChild(leftFootTrackerBone) - rightFootBone.attachChild(rightFootTrackerBone) - - // Attach arms - assembleSkeletonArms(false) - } - - /** - * Dynamically assembles the arms of the skeleton - * - * @param reset disassemble before reassembling - */ - @ThreadSafe - fun assembleSkeletonArms(reset: Boolean) { - if (reset) { - for (bone in allArmBones) { - bone.detachWithChildren() - } - } - - // Shoulders - neckBone.attachChild(leftUpperShoulderBone) - neckBone.attachChild(rightUpperShoulderBone) - leftUpperShoulderBone.attachChild(leftShoulderBone) - rightUpperShoulderBone.attachChild(rightShoulderBone) - - // Upper arm - leftShoulderBone.attachChild(leftUpperArmBone) - rightShoulderBone.attachChild(rightUpperArmBone) - - // Lower arm and hand - if (isTrackingLeftArmFromController) { - leftHandTrackerBone.attachChild(leftHandBone) - leftHandBone.attachChild(leftLowerArmBone) - leftLowerArmBone.attachChild(leftElbowTrackerBone) - } else { - leftUpperArmBone.attachChild(leftLowerArmBone) - leftUpperArmBone.attachChild(leftElbowTrackerBone) - leftLowerArmBone.attachChild(leftHandBone) - leftHandBone.attachChild(leftHandTrackerBone) - } - if (isTrackingRightArmFromController) { - rightHandTrackerBone.attachChild(rightHandBone) - rightHandBone.attachChild(rightLowerArmBone) - rightLowerArmBone.attachChild(rightElbowTrackerBone) - } else { - rightUpperArmBone.attachChild(rightLowerArmBone) - rightUpperArmBone.attachChild(rightElbowTrackerBone) - rightLowerArmBone.attachChild(rightHandBone) - rightHandBone.attachChild(rightHandTrackerBone) - } - - // Fingers - leftHandBone.attachChild(leftThumbMetacarpalBone) - leftThumbMetacarpalBone.attachChild(leftThumbProximalBone) - leftThumbProximalBone.attachChild(leftThumbDistalBone) - leftHandBone.attachChild(leftIndexProximalBone) - leftIndexProximalBone.attachChild(leftIndexIntermediateBone) - leftIndexIntermediateBone.attachChild(leftIndexDistalBone) - leftHandBone.attachChild(leftMiddleProximalBone) - leftMiddleProximalBone.attachChild(leftMiddleIntermediateBone) - leftMiddleIntermediateBone.attachChild(leftMiddleDistalBone) - leftHandBone.attachChild(leftRingProximalBone) - leftRingProximalBone.attachChild(leftRingIntermediateBone) - leftRingIntermediateBone.attachChild(leftRingDistalBone) - leftHandBone.attachChild(leftLittleProximalBone) - leftLittleProximalBone.attachChild(leftLittleIntermediateBone) - leftLittleIntermediateBone.attachChild(leftLittleDistalBone) - rightHandBone.attachChild(rightThumbMetacarpalBone) - rightThumbMetacarpalBone.attachChild(rightThumbProximalBone) - rightThumbProximalBone.attachChild(rightThumbDistalBone) - rightHandBone.attachChild(rightIndexProximalBone) - rightIndexProximalBone.attachChild(rightIndexIntermediateBone) - rightIndexIntermediateBone.attachChild(rightIndexDistalBone) - rightHandBone.attachChild(rightMiddleProximalBone) - rightMiddleProximalBone.attachChild(rightMiddleIntermediateBone) - rightMiddleIntermediateBone.attachChild(rightMiddleDistalBone) - rightHandBone.attachChild(rightRingProximalBone) - rightRingProximalBone.attachChild(rightRingIntermediateBone) - rightRingIntermediateBone.attachChild(rightRingDistalBone) - rightHandBone.attachChild(rightLittleProximalBone) - rightLittleProximalBone.attachChild(rightLittleIntermediateBone) - rightLittleIntermediateBone.attachChild(rightLittleDistalBone) - } - - /** - * Set input trackers from a list - */ - fun setTrackersFromList(trackers: List) { - // Head - headTracker = getTrackerForSkeleton(trackers, TrackerPosition.HEAD) - neckTracker = getTrackerForSkeleton(trackers, TrackerPosition.NECK) - - // Spine - upperChestTracker = getTrackerForSkeleton(trackers, TrackerPosition.UPPER_CHEST) - chestTracker = getTrackerForSkeleton(trackers, TrackerPosition.CHEST) - waistTracker = getTrackerForSkeleton(trackers, TrackerPosition.WAIST) - hipTracker = getTrackerForSkeleton(trackers, TrackerPosition.HIP) - - // Legs - leftUpperLegTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_UPPER_LEG) - leftLowerLegTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_LOWER_LEG) - leftFootTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_FOOT) - rightUpperLegTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_UPPER_LEG) - rightLowerLegTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_LOWER_LEG) - rightFootTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_FOOT) - - // Arms - leftLowerArmTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_LOWER_ARM) - rightLowerArmTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_LOWER_ARM) - leftUpperArmTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_UPPER_ARM) - rightUpperArmTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_UPPER_ARM) - leftHandTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_HAND) - rightHandTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_HAND) - leftShoulderTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_SHOULDER) - rightShoulderTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_SHOULDER) - - // Fingers - leftThumbMetacarpalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_THUMB_METACARPAL) - leftThumbProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_THUMB_PROXIMAL) - leftThumbDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_THUMB_DISTAL) - leftIndexProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_INDEX_PROXIMAL) - leftIndexIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_INDEX_INTERMEDIATE) - leftIndexDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_INDEX_DISTAL) - leftMiddleProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_MIDDLE_PROXIMAL) - leftMiddleIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_MIDDLE_INTERMEDIATE) - leftMiddleDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_MIDDLE_DISTAL) - leftRingProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_RING_PROXIMAL) - leftRingIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_RING_INTERMEDIATE) - leftRingDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_RING_DISTAL) - leftLittleProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_LITTLE_PROXIMAL) - leftLittleIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_LITTLE_INTERMEDIATE) - leftLittleDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.LEFT_LITTLE_DISTAL) - rightThumbMetacarpalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_THUMB_METACARPAL) - rightThumbProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_THUMB_PROXIMAL) - rightThumbDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_THUMB_DISTAL) - rightIndexProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_INDEX_PROXIMAL) - rightIndexIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_INDEX_INTERMEDIATE) - rightIndexDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_INDEX_DISTAL) - rightMiddleProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_MIDDLE_PROXIMAL) - rightMiddleIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_MIDDLE_INTERMEDIATE) - rightMiddleDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_MIDDLE_DISTAL) - rightRingProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_RING_PROXIMAL) - rightRingIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_RING_INTERMEDIATE) - rightRingDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_RING_DISTAL) - rightLittleProximalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_LITTLE_PROXIMAL) - rightLittleIntermediateTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_LITTLE_INTERMEDIATE) - rightLittleDistalTracker = getTrackerForSkeleton(trackers, TrackerPosition.RIGHT_LITTLE_DISTAL) - - // Check for specific conditions and cache them - hasSpineTracker = upperChestTracker != null || chestTracker != null || waistTracker != null || hipTracker != null - hasKneeTrackers = leftUpperLegTracker != null && rightUpperLegTracker != null - hasLeftArmTracker = leftLowerArmTracker != null || leftUpperArmTracker != null - hasRightArmTracker = rightLowerArmTracker != null || rightUpperArmTracker != null - hasLeftFingerTracker = leftThumbMetacarpalTracker != null || - leftThumbProximalTracker != null || - leftThumbDistalTracker != null || - leftIndexProximalTracker != null || - leftIndexIntermediateTracker != null || - leftIndexDistalTracker != null || - leftMiddleProximalTracker != null || - leftMiddleIntermediateTracker != null || - leftMiddleDistalTracker != null || - leftRingProximalTracker != null || - leftRingIntermediateTracker != null || - leftRingDistalTracker != null || - leftLittleProximalTracker != null || - leftLittleIntermediateTracker != null || - leftLittleDistalTracker != null - hasRightFingerTracker = rightThumbMetacarpalTracker != null || - rightThumbProximalTracker != null || - rightThumbDistalTracker != null || - rightIndexProximalTracker != null || - rightIndexIntermediateTracker != null || - rightIndexDistalTracker != null || - rightMiddleProximalTracker != null || - rightMiddleIntermediateTracker != null || - rightMiddleDistalTracker != null || - rightRingProximalTracker != null || - rightRingIntermediateTracker != null || - rightRingDistalTracker != null || - rightLittleProximalTracker != null || - rightLittleIntermediateTracker != null || - rightLittleDistalTracker != null - - // Rebuilds the arm skeleton nodes attachments - assembleSkeletonArms(true) - - // Refresh all skeleton node offsets based on new trackers - humanPoseManager.updateNodeOffsetsInSkeleton() - - // Update tap detection's trackers - tapDetectionManager?.refresh() - - userHeightCalibration?.checkTrackers() - - // Rebuild Ik Solver - ikSolver.buildChains(trackers) - - // Update bones tracker field - refreshBoneTracker() - - // Update tracker skeleton - trackerSkeleton = TrackerSkeleton(this) - } - - /** - * Set computed trackers from list - */ - private fun setComputedTrackers(trackers: List) { - for (t in trackers) { - setComputedTracker(t) - } - } - - /** - * Set computed tracker - */ - private fun setComputedTracker(tracker: Tracker) { - when (tracker.trackerPosition) { - TrackerPosition.HEAD -> computedHeadTracker = tracker - TrackerPosition.UPPER_CHEST -> computedChestTracker = tracker - TrackerPosition.HIP -> computedHipTracker = tracker - TrackerPosition.LEFT_UPPER_LEG -> computedLeftKneeTracker = tracker - TrackerPosition.LEFT_FOOT -> computedLeftFootTracker = tracker - TrackerPosition.RIGHT_UPPER_LEG -> computedRightKneeTracker = tracker - TrackerPosition.RIGHT_FOOT -> computedRightFootTracker = tracker - TrackerPosition.LEFT_UPPER_ARM -> computedLeftElbowTracker = tracker - TrackerPosition.RIGHT_UPPER_ARM -> computedRightElbowTracker = tracker - TrackerPosition.LEFT_HAND -> computedLeftHandTracker = tracker - TrackerPosition.RIGHT_HAND -> computedRightHandTracker = tracker - else -> {} - } - } - - /** - * Get output tracker from TrackerRole - */ - fun getComputedTracker(trackerRole: TrackerRole): Tracker = when (trackerRole) { - TrackerRole.HEAD -> computedHeadTracker!! - TrackerRole.CHEST -> computedChestTracker!! - TrackerRole.WAIST -> computedHipTracker!! - TrackerRole.LEFT_KNEE -> computedLeftKneeTracker!! - TrackerRole.LEFT_FOOT -> computedLeftFootTracker!! - TrackerRole.RIGHT_KNEE -> computedRightKneeTracker!! - TrackerRole.RIGHT_FOOT -> computedRightFootTracker!! - TrackerRole.LEFT_ELBOW -> computedLeftElbowTracker!! - TrackerRole.RIGHT_ELBOW -> computedRightElbowTracker!! - TrackerRole.LEFT_HAND -> computedLeftHandTracker!! - TrackerRole.RIGHT_HAND -> computedRightHandTracker!! - else -> throw IllegalArgumentException("Unsupported computed tracker's TrackerRole in HumanSkeleton") - } - - /** - * Updates the pose from tracker positions - */ - @VRServerThread - fun updatePose() { - tapDetectionManager?.update() - userHeightCalibration?.tick() - - StayAligned.adjustNextTracker(trackerSkeleton, stayAlignedConfig) - - updateTransforms() - updateBones() - if (enforceConstraints) { - // TODO re-enable toggling correctConstraints once - // https://github.com/SlimeVR/SlimeVR-Server/issues/1297 is solved - headBone.updateWithConstraints(false) - } - updateComputedTrackers() - - // Don't run post-processing if the tracking is paused - if (pauseTracking) return - - legTweaks.tweakLegs() - localizer.update() - } - - /** - * Refresh the attachedTracker field in each bone - */ - private fun refreshBoneTracker() { - for (bone in allHumanBones) { - bone.attachedTracker = getTrackerForBone(bone.boneType) - } - } - - /** - * Update all the bones by updating the roots - */ - @ThreadSafe - fun updateBones() { - headBone.update() - if (isTrackingLeftArmFromController) leftHandTrackerBone.update() - if (isTrackingRightArmFromController) rightHandTrackerBone.update() - } - - /** - * Update all the bones' transforms from trackers - */ - private fun updateTransforms() { - // Head - updateHeadTransforms() - - // Stop at head (for the body to follow) if tracking is paused - if (pauseTracking) return - - // Spine - updateSpineTransforms() - - // Left leg - updateLegTransforms( - leftUpperLegBone, - leftKneeTrackerBone, - leftLowerLegBone, - leftFootBone, - leftFootTrackerBone, - leftUpperLegTracker, - leftLowerLegTracker, - leftFootTracker, - ) - - // Right leg - updateLegTransforms( - rightUpperLegBone, - rightKneeTrackerBone, - rightLowerLegBone, - rightFootBone, - rightFootTrackerBone, - rightUpperLegTracker, - rightLowerLegTracker, - rightFootTracker, - ) - - // Left arm - updateArmTransforms( - isTrackingLeftArmFromController, - leftUpperShoulderBone, - leftShoulderBone, - leftUpperArmBone, - leftElbowTrackerBone, - leftLowerArmBone, - leftHandBone, - leftHandTrackerBone, - leftShoulderTracker, - leftUpperArmTracker, - leftLowerArmTracker, - leftHandTracker, - ) - - // Right arm - updateArmTransforms( - isTrackingRightArmFromController, - rightUpperShoulderBone, - rightShoulderBone, - rightUpperArmBone, - rightElbowTrackerBone, - rightLowerArmBone, - rightHandBone, - rightHandTrackerBone, - rightShoulderTracker, - rightUpperArmTracker, - rightLowerArmTracker, - rightHandTracker, - ) - - // Left thumb - updateFingerTransforms( - leftHandTrackerBone.getGlobalRotation(), - leftThumbMetacarpalBone, - leftThumbProximalBone, - leftThumbDistalBone, - leftThumbMetacarpalTracker, - leftThumbProximalTracker, - leftThumbDistalTracker, - ) - - // Left index - updateFingerTransforms( - leftHandTrackerBone.getGlobalRotation(), - leftIndexProximalBone, - leftIndexIntermediateBone, - leftIndexDistalBone, - leftIndexProximalTracker, - leftIndexIntermediateTracker, - leftIndexDistalTracker, - ) - - // Left middle - updateFingerTransforms( - leftHandTrackerBone.getGlobalRotation(), - leftMiddleProximalBone, - leftMiddleIntermediateBone, - leftMiddleDistalBone, - leftMiddleProximalTracker, - leftMiddleIntermediateTracker, - leftMiddleDistalTracker, - ) - - // Left ring - updateFingerTransforms( - leftHandTrackerBone.getGlobalRotation(), - leftRingProximalBone, - leftRingIntermediateBone, - leftRingDistalBone, - leftRingProximalTracker, - leftRingIntermediateTracker, - leftRingDistalTracker, - ) - - // Left little - updateFingerTransforms( - leftHandTrackerBone.getGlobalRotation(), - leftLittleProximalBone, - leftLittleIntermediateBone, - leftLittleDistalBone, - leftLittleProximalTracker, - leftLittleIntermediateTracker, - leftLittleDistalTracker, - ) - - // Right thumb - updateFingerTransforms( - rightHandTrackerBone.getGlobalRotation(), - rightThumbMetacarpalBone, - rightThumbProximalBone, - rightThumbDistalBone, - rightThumbMetacarpalTracker, - rightThumbProximalTracker, - rightThumbDistalTracker, - ) - - // Right index - updateFingerTransforms( - rightHandTrackerBone.getGlobalRotation(), - rightIndexProximalBone, - rightIndexIntermediateBone, - rightIndexDistalBone, - rightIndexProximalTracker, - rightIndexIntermediateTracker, - rightIndexDistalTracker, - ) - - // Right middle - updateFingerTransforms( - rightHandTrackerBone.getGlobalRotation(), - rightMiddleProximalBone, - rightMiddleIntermediateBone, - rightMiddleDistalBone, - rightMiddleProximalTracker, - rightMiddleIntermediateTracker, - rightMiddleDistalTracker, - ) - - // Right ring - updateFingerTransforms( - rightHandTrackerBone.getGlobalRotation(), - rightRingProximalBone, - rightRingIntermediateBone, - rightRingDistalBone, - rightRingProximalTracker, - rightRingIntermediateTracker, - rightRingDistalTracker, - ) - - // Right little - updateFingerTransforms( - rightHandTrackerBone.getGlobalRotation(), - rightLittleProximalBone, - rightLittleIntermediateBone, - rightLittleDistalBone, - rightLittleProximalTracker, - rightLittleIntermediateTracker, - rightLittleDistalTracker, - ) - } - - /** - * Update the head and neck bone transforms - */ - private fun updateHeadTransforms() { - var headRot = IDENTITY - headTracker?.let { head -> - // Set head position - if (head.hasPosition) headBone.setPosition(head.position) - - // Get head rotation - headRot = head.getRotation() - - // Set head rotation - headBone.setRotation(headRot) - headTrackerBone.setRotation(headRot) - - // Get neck rotation - neckTracker?.let { headRot = it.getRotation() } - - // Set neck rotation - neckBone.setRotation(headRot) - } ?: run { - // Set head position - if (!localizer.getEnabled()) headBone.setPosition(NULL) - - // Get neck or spine rotation (else is identity) - getFirstAvailableTracker( - neckTracker, - upperChestTracker, - chestTracker, - waistTracker, - hipTracker, - )?.let { headRot = it.getRotation() } - - headBone.setRotation(headRot) - headTrackerBone.setRotation(headRot) - neckBone.setRotation(headRot) - } - } - - /** - * Update the spine transforms, from the upper chest to the hip - */ - private fun updateSpineTransforms() { - if (hasSpineTracker) { - // Upper chest and chest tracker - getFirstAvailableTracker(upperChestTracker, chestTracker, waistTracker, hipTracker)?.let { - upperChestBone.setRotation(it.getRotation()) - chestTrackerBone.setRotation(it.getRotation()) - } - - // Chest - getFirstAvailableTracker(chestTracker, upperChestTracker, waistTracker, hipTracker)?.let { - chestBone.setRotation(it.getRotation()) - } - - // Waist - getFirstAvailableTracker(waistTracker, chestTracker, hipTracker, upperChestTracker)?.let { - waistBone.setRotation(it.getRotation()) - } - - // Hip and hip tracker - getFirstAvailableTracker(hipTracker, waistTracker, chestTracker, upperChestTracker)?.let { - hipBone.setRotation(it.getRotation()) - hipTrackerBone.setRotation(it.getRotation()) - } - } else if (headTracker != null) { - // Align with neck's yaw - val yawRot = neckBone.getGlobalRotation().project(POS_Y).unit() - upperChestBone.setRotation(yawRot) - chestTrackerBone.setRotation(yawRot) - chestBone.setRotation(yawRot) - waistBone.setRotation(yawRot) - hipBone.setRotation(yawRot) - hipTrackerBone.setRotation(yawRot) - } - - // Extended spine model - if (extendedSpineModel && hasSpineTracker) { - // Tries to guess missing lower spine trackers by interpolating rotations - if (waistTracker == null) { - getFirstAvailableTracker(chestTracker, upperChestTracker)?.let { chest -> - hipTracker?.let { - // Calculates waist from chest + hip - var hipRot = it.getRotation() - var chestRot = chest.getRotation() - - // Interpolate between the chest and the hip - chestRot = chestRot.interpQ(hipRot, waistFromChestHipAveraging) - - // Set waist's rotation - waistBone.setRotation(chestRot) - } ?: run { - if (hasKneeTrackers) { - // Calculates waist from chest + legs - var leftLegRot = leftUpperLegTracker?.getRotation() ?: IDENTITY - var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY - var chestRot = chest.getRotation() - - // Interpolate between the pelvis, averaged from the legs, and the chest - chestRot = chestRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), waistFromChestLegsAveraging).unit() - - // Set waist's rotation - waistBone.setRotation(chestRot) - } - } - } - } - if (hipTracker == null && hasKneeTrackers) { - waistTracker?.let { - // Calculates hip from waist + legs - var leftLegRot = leftUpperLegTracker?.getRotation() ?: IDENTITY - var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY - var waistRot = it.getRotation() - - // Interpolate between the pelvis, averaged from the legs, and the chest - waistRot = waistRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), hipFromWaistLegsAveraging).unit() - - // Set hip rotation - hipBone.setRotation(waistRot) - hipTrackerBone.setRotation(waistRot) - } ?: run { - getFirstAvailableTracker(chestTracker, upperChestTracker)?.let { - // Calculates hip from chest + legs - var leftLegRot = leftUpperLegTracker?.getRotation() ?: IDENTITY - var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY - var chestRot = it.getRotation() - - // Interpolate between the pelvis, averaged from the legs, and the chest - chestRot = chestRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), hipFromChestLegsAveraging).unit() - - // Set hip rotation - hipBone.setRotation(chestRot) - hipTrackerBone.setRotation(chestRot) - } - } - } - } - - // Extended pelvis model - if (extendedPelvisModel && hasKneeTrackers && hipTracker == null) { - val leftLegRot = leftUpperLegTracker?.getRotation() ?: IDENTITY - val rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY - val hipRot = hipBone.getLocalRotation() - - val extendedPelvisRot = extendedPelvisYawRoll(leftLegRot, rightLegRot, hipRot) - - // Interpolate between the hipRot and extendedPelvisRot - val newHipRot = hipRot.interpR( - if (extendedPelvisRot.lenSq() != 0.0f) extendedPelvisRot else IDENTITY, - hipLegsAveraging, - ) - - // Set new hip rotation - hipBone.setRotation(newHipRot) - hipTrackerBone.setRotation(newHipRot) - } - - // Set left and right hip rotations to the hip's - leftHipBone.setRotation(hipBone.getLocalRotation()) - rightHipBone.setRotation(hipBone.getLocalRotation()) - } - - /** - * Update a leg's transforms, from its hip to its foot - */ - private fun updateLegTransforms( - upperLegBone: Bone, - kneeTrackerBone: Bone, - lowerLegBone: Bone, - footBone: Bone, - footTrackerBone: Bone, - upperLegTracker: Tracker?, - lowerLegTracker: Tracker?, - footTracker: Tracker?, - ) { - var legRot = IDENTITY - - upperLegTracker?.let { - // Get upper leg rotation - legRot = it.getRotation() - } ?: run { - // Use hip's yaw - legRot = hipBone.getLocalRotation().project(POS_Y).unit() - } - // Set upper leg rotation - upperLegBone.setRotation(legRot) - kneeTrackerBone.setRotation(legRot) - - lowerLegTracker?.let { - // Get lower leg rotation - legRot = it.getRotation() - } ?: run { - // Use lower leg or hip's yaw - legRot = legRot.project(POS_Y).unit() - } - // Set lower leg rotation - lowerLegBone.setRotation(legRot) - - // Get foot rotation - footTracker?.let { legRot = it.getRotation() } - // Set foot rotation - footBone.setRotation(legRot) - footTrackerBone.setRotation(legRot) - - // Extended knee model - if (extendedKneeModel) { - upperLegTracker?.let { upper -> - lowerLegTracker?.let { lower -> - // Averages the upper leg's rotation with the local lower leg's - // pitch and roll and apply to the tracker node. - val upperRot = upper.getRotation() - val lowerRot = lower.getRotation() - val extendedRot = extendedKneeYawRoll(upperRot, lowerRot) - - upperLegBone.setRotation(upperRot.interpR(extendedRot, kneeAnkleAveraging)) - kneeTrackerBone.setRotation(upperRot.interpR(extendedRot, kneeTrackerAnkleAveraging)) - } - } - } - } - - /** - * Update an arm's transforms, from its shoulder to its hand - */ - private fun updateArmTransforms( - isTrackingFromController: Boolean, - upperShoulderBone: Bone, - shoulderBone: Bone, - upperArmBone: Bone, - elbowTrackerBone: Bone, - lowerArmBone: Bone, - handBone: Bone, - handTrackerBone: Bone, - shoulderTracker: Tracker?, - upperArmTracker: Tracker?, - lowerArmTracker: Tracker?, - handTracker: Tracker?, - ) { - if (isTrackingFromController) { // From controller - // Set hand rotation and position from tracker - handTracker?.let { - handTrackerBone.setPosition(it.position) - handTrackerBone.setRotation(it.getRotation()) - handBone.setRotation(it.getRotation()) - } - - // Get lower arm rotation - var armRot = getFirstAvailableTracker(lowerArmTracker, upperArmTracker)?.getRotation() ?: IDENTITY - // Set lower arm rotation - lowerArmBone.setRotation(armRot) - - // Get upper arm rotation - armRot = getFirstAvailableTracker(upperArmTracker, lowerArmTracker)?.getRotation() ?: IDENTITY - // Set elbow tracker rotation - elbowTrackerBone.setRotation(armRot) - } else { // From HMD - // Get shoulder rotation - var armRot = shoulderTracker?.getRotation() ?: upperChestBone.getLocalRotation() - // Set shoulder rotation - upperShoulderBone.setRotation(upperChestBone.getLocalRotation()) - shoulderBone.setRotation(armRot) - - if (upperArmTracker != null || lowerArmTracker != null) { - // Get upper arm rotation - getFirstAvailableTracker(upperArmTracker, lowerArmTracker)?.let { armRot = it.getRotation() } - // Set upper arm and elbow tracker rotation - upperArmBone.setRotation(armRot) - elbowTrackerBone.setRotation(armRot) - - // Get lower arm rotation - getFirstAvailableTracker(lowerArmTracker, upperArmTracker)?.let { armRot = it.getRotation() } - // Set lower arm rotation - lowerArmBone.setRotation(armRot) - } else { - // Fallback arm rotation as upper chest - armRot = upperChestBone.getLocalRotation() - upperArmBone.setRotation(armRot) - elbowTrackerBone.setRotation(armRot) - lowerArmBone.setRotation(armRot) - } - - // Get hand rotation - handTracker?.let { armRot = it.getRotation() } - // Set hand, and hand tracker rotation - handBone.setRotation(armRot) - handTrackerBone.setRotation(armRot) - } - } - - /** - * Update a finger's 3 bones' transforms - */ - private fun updateFingerTransforms( - handRotation: Quaternion, - proximalBone: Bone, - intermediateBone: Bone, - distalBone: Bone, - proximalTracker: Tracker?, - intermediateTracker: Tracker?, - distalTracker: Tracker?, - ) { - if (distalTracker == null && intermediateTracker == null && proximalTracker == null) { - // Set fingers' rotations to the hand's if no finger tracker - proximalBone.setRotation(handRotation) - intermediateBone.setRotation(handRotation) - distalBone.setRotation(handRotation) - } - - // Note: we use interpQ instead of interpR in order to slerp over 180 degrees. - // Start of finger - proximalTracker?.let { - val fingerRot = if (it.trackerDataType == TrackerDataType.FLEX_RESISTANCE || - it.trackerDataType == TrackerDataType.FLEX_ANGLE - ) { - handRotation * it.getRotation() - } else { - it.getRotation() - } - - proximalBone.setRotation(fingerRot) - if (intermediateTracker == null) intermediateBone.setRotation(handRotation.interpQ(fingerRot, 2.12f)) - if (distalTracker == null) distalBone.setRotation(handRotation.interpQ(fingerRot, 3.03f)) - } - // Middle of finger - intermediateTracker?.let { - val fingerRot = if (it.trackerDataType == TrackerDataType.FLEX_RESISTANCE || - it.trackerDataType == TrackerDataType.FLEX_ANGLE - ) { - handRotation * it.getRotation() - } else { - it.getRotation() - } - - if (proximalTracker == null) proximalBone.setRotation(handRotation.interpQ(fingerRot, 0.47f)) - intermediateBone.setRotation(fingerRot) - if (distalTracker == null) distalBone.setRotation(handRotation.interpQ(fingerRot, 1.43f)) - } - // Tip of finger - distalTracker?.let { - val fingerRot = if (it.trackerDataType == TrackerDataType.FLEX_RESISTANCE || - it.trackerDataType == TrackerDataType.FLEX_ANGLE - ) { - handRotation * it.getRotation() - } else { - it.getRotation() - } - - if (proximalTracker == null && intermediateTracker == null) proximalBone.setRotation(handRotation.interpQ(fingerRot, 0.33f)) - if (intermediateTracker == null) intermediateBone.setRotation(handRotation.interpQ(fingerRot, 0.7f)) - distalBone.setRotation(fingerRot) - } - } - - /** - * Rotates the first Quaternion to match its yaw and roll to the rotation of - * the second Quaternion - * - * @param knee the first Quaternion - * @param ankle the second Quaternion - * @return the rotated Quaternion - */ - private fun extendedKneeYawRoll(knee: Quaternion, ankle: Quaternion): Quaternion { - val r = knee.inv() * ankle - val c = Quaternion(r.w, -r.x, 0f, 0f) - return (knee * r * c).unit() - } - - /** - * Rotates the third Quaternion to match its yaw and roll to the rotation of - * the average of the first and second quaternions. - * - * @param leftKnee the first Quaternion - * @param rightKnee the second Quaternion - * @param hip the third Quaternion - * @return the rotated Quaternion - */ - private fun extendedPelvisYawRoll( - leftKnee: Quaternion, - rightKnee: Quaternion, - hip: Quaternion, - ): Quaternion { - // R = InverseHip * (LeftLeft + RightLeg) - // C = Quaternion(R.w, -R.x, 0, 0) - // Pelvis = Hip * R * C - // normalize(Pelvis) - val r = hip.inv() * (leftKnee + rightKnee) - val c = Quaternion(r.w, -r.x, 0f, 0f) - return (hip * r * c).unit() - } - - // Update the output trackers - private fun updateComputedTrackers() { - updateComputedTracker(computedHeadTracker, headTrackerBone) - updateComputedTracker(computedChestTracker, chestTrackerBone) - updateComputedTracker(computedHipTracker, hipTrackerBone) - updateComputedTracker(computedLeftKneeTracker, leftKneeTrackerBone) - updateComputedTracker(computedRightKneeTracker, rightKneeTrackerBone) - updateComputedTracker(computedLeftFootTracker, leftFootTrackerBone) - updateComputedTracker(computedRightFootTracker, rightFootTrackerBone) - updateComputedTracker(computedLeftElbowTracker, leftElbowTrackerBone) - updateComputedTracker(computedRightElbowTracker, rightElbowTrackerBone) - updateComputedTracker(computedLeftHandTracker, leftHandTrackerBone) - updateComputedTracker(computedRightHandTracker, rightHandTrackerBone) - } - - private fun updateComputedTracker(computedTracker: Tracker?, trackerBone: Bone) { - computedTracker?.let { - it.position = trackerBone.getTailPosition() - it.setRotation(trackerBone.getGlobalRotation() * trackerBone.rotationOffset.inv()) - it.dataTick() - } - } - - // Skeleton Config toggles - fun updateToggleState(configToggle: SkeletonConfigToggles, newValue: Boolean) { - when (configToggle) { - SkeletonConfigToggles.EXTENDED_SPINE_MODEL -> extendedSpineModel = newValue - - SkeletonConfigToggles.EXTENDED_PELVIS_MODEL -> extendedPelvisModel = newValue - - SkeletonConfigToggles.EXTENDED_KNEE_MODEL -> extendedKneeModel = newValue - - SkeletonConfigToggles.FORCE_ARMS_FROM_HMD -> { - forceArmsFromHMD = newValue - assembleSkeletonArms(true) // Rebuilds the arm skeleton nodes attachments - computeDependentArmOffsets() // Refresh node offsets for arms - } - - SkeletonConfigToggles.SKATING_CORRECTION -> legTweaks.setSkatingCorrectionEnabled(newValue) - - SkeletonConfigToggles.FLOOR_CLIP -> legTweaks.setFloorClipEnabled(newValue) - - SkeletonConfigToggles.TOE_SNAP -> legTweaks.toeSnapEnabled = newValue - - SkeletonConfigToggles.FOOT_PLANT -> legTweaks.footPlantEnabled = newValue - - SkeletonConfigToggles.SELF_LOCALIZATION -> localizer.setEnabled(newValue) - - SkeletonConfigToggles.USE_POSITION -> ikSolver.enabled = newValue - - SkeletonConfigToggles.ENFORCE_CONSTRAINTS -> enforceConstraints = newValue - - SkeletonConfigToggles.CORRECT_CONSTRAINTS -> correctConstraints = newValue - } - } - - // Skeleton Config ratios - fun updateValueState(configValue: SkeletonConfigValues, newValue: Float) { - when (configValue) { - SkeletonConfigValues.WAIST_FROM_CHEST_HIP_AVERAGING -> waistFromChestHipAveraging = newValue - SkeletonConfigValues.WAIST_FROM_CHEST_LEGS_AVERAGING -> waistFromChestLegsAveraging = newValue - SkeletonConfigValues.HIP_FROM_CHEST_LEGS_AVERAGING -> hipFromChestLegsAveraging = newValue - SkeletonConfigValues.HIP_FROM_WAIST_LEGS_AVERAGING -> hipFromWaistLegsAveraging = newValue - SkeletonConfigValues.HIP_LEGS_AVERAGING -> hipLegsAveraging = newValue - SkeletonConfigValues.KNEE_TRACKER_ANKLE_AVERAGING -> kneeTrackerAnkleAveraging = newValue - SkeletonConfigValues.KNEE_ANKLE_AVERAGING -> kneeAnkleAveraging = newValue - } - } - - // Skeleton Config bone lengths - fun updateNodeOffset(boneType: BoneType, offset: Vector3) { - var transOffset = offset - - // If no head position, headShift and neckLength = 0 - if ((boneType == BoneType.HEAD || boneType == BoneType.NECK) && (headTracker == null || !(headTracker!!.hasPosition && headTracker!!.hasRotation))) { - transOffset = NULL - } - // If trackingArmFromController, reverse - if (((boneType == BoneType.LEFT_LOWER_ARM || boneType == BoneType.LEFT_HAND) && isTrackingLeftArmFromController) || - ( - (boneType == BoneType.RIGHT_LOWER_ARM || boneType == BoneType.RIGHT_HAND) && - isTrackingRightArmFromController - ) - ) { - transOffset = -transOffset - } - - // Compute bone rotation - val rotOffset = if (transOffset.len() > 0f) { - if (transOffset.unit().y == 1f) { - I - } else { - fromTo(NEG_Y, transOffset) - } - } else { - IDENTITY - } - - // Get the bone - val bone = getBone(boneType) - - // Update bone length - bone.length = transOffset.len() - - // Set bone rotation offset - bone.rotationOffset = rotOffset - } - - private fun computeDependentArmOffsets() { - humanPoseManager.computeNodeOffset(BoneType.LEFT_LOWER_ARM) - humanPoseManager.computeNodeOffset(BoneType.RIGHT_LOWER_ARM) - humanPoseManager.computeNodeOffset(BoneType.LEFT_HAND) - humanPoseManager.computeNodeOffset(BoneType.RIGHT_HAND) - } - - fun getBone(bone: BoneType): Bone = when (bone) { - BoneType.HEAD -> headBone - BoneType.HEAD_TRACKER -> headTrackerBone - BoneType.NECK -> neckBone - BoneType.UPPER_CHEST -> upperChestBone - BoneType.CHEST_TRACKER -> chestTrackerBone - BoneType.CHEST -> chestBone - BoneType.WAIST -> waistBone - BoneType.HIP -> hipBone - BoneType.HIP_TRACKER -> hipTrackerBone - BoneType.LEFT_HIP -> leftHipBone - BoneType.RIGHT_HIP -> rightHipBone - BoneType.LEFT_UPPER_LEG -> leftUpperLegBone - BoneType.RIGHT_UPPER_LEG -> rightUpperLegBone - BoneType.LEFT_KNEE_TRACKER -> leftKneeTrackerBone - BoneType.RIGHT_KNEE_TRACKER -> rightKneeTrackerBone - BoneType.LEFT_LOWER_LEG -> leftLowerLegBone - BoneType.RIGHT_LOWER_LEG -> rightLowerLegBone - BoneType.LEFT_FOOT -> leftFootBone - BoneType.RIGHT_FOOT -> rightFootBone - BoneType.LEFT_FOOT_TRACKER -> leftFootTrackerBone - BoneType.RIGHT_FOOT_TRACKER -> rightFootTrackerBone - BoneType.LEFT_UPPER_SHOULDER -> leftUpperShoulderBone - BoneType.RIGHT_UPPER_SHOULDER -> rightUpperShoulderBone - BoneType.LEFT_SHOULDER -> leftShoulderBone - BoneType.RIGHT_SHOULDER -> rightShoulderBone - BoneType.LEFT_UPPER_ARM -> leftUpperArmBone - BoneType.RIGHT_UPPER_ARM -> rightUpperArmBone - BoneType.LEFT_ELBOW_TRACKER -> leftElbowTrackerBone - BoneType.RIGHT_ELBOW_TRACKER -> rightElbowTrackerBone - BoneType.LEFT_LOWER_ARM -> leftLowerArmBone - BoneType.RIGHT_LOWER_ARM -> rightLowerArmBone - BoneType.LEFT_HAND -> leftHandBone - BoneType.RIGHT_HAND -> rightHandBone - BoneType.LEFT_HAND_TRACKER -> leftHandTrackerBone - BoneType.RIGHT_HAND_TRACKER -> rightHandTrackerBone - BoneType.LEFT_THUMB_METACARPAL -> leftThumbMetacarpalBone - BoneType.LEFT_THUMB_PROXIMAL -> leftThumbProximalBone - BoneType.LEFT_THUMB_DISTAL -> leftThumbDistalBone - BoneType.LEFT_INDEX_PROXIMAL -> leftIndexProximalBone - BoneType.LEFT_INDEX_INTERMEDIATE -> leftIndexIntermediateBone - BoneType.LEFT_INDEX_DISTAL -> leftIndexDistalBone - BoneType.LEFT_MIDDLE_PROXIMAL -> leftMiddleProximalBone - BoneType.LEFT_MIDDLE_INTERMEDIATE -> leftMiddleIntermediateBone - BoneType.LEFT_MIDDLE_DISTAL -> leftMiddleDistalBone - BoneType.LEFT_RING_PROXIMAL -> leftRingProximalBone - BoneType.LEFT_RING_INTERMEDIATE -> leftRingIntermediateBone - BoneType.LEFT_RING_DISTAL -> leftRingDistalBone - BoneType.LEFT_LITTLE_PROXIMAL -> leftLittleProximalBone - BoneType.LEFT_LITTLE_INTERMEDIATE -> leftLittleIntermediateBone - BoneType.LEFT_LITTLE_DISTAL -> leftLittleDistalBone - BoneType.RIGHT_THUMB_METACARPAL -> rightThumbMetacarpalBone - BoneType.RIGHT_THUMB_PROXIMAL -> rightThumbProximalBone - BoneType.RIGHT_THUMB_DISTAL -> rightThumbDistalBone - BoneType.RIGHT_INDEX_PROXIMAL -> rightIndexProximalBone - BoneType.RIGHT_INDEX_INTERMEDIATE -> rightIndexIntermediateBone - BoneType.RIGHT_INDEX_DISTAL -> rightIndexDistalBone - BoneType.RIGHT_MIDDLE_PROXIMAL -> rightMiddleProximalBone - BoneType.RIGHT_MIDDLE_INTERMEDIATE -> rightMiddleIntermediateBone - BoneType.RIGHT_MIDDLE_DISTAL -> rightMiddleDistalBone - BoneType.RIGHT_RING_PROXIMAL -> rightRingProximalBone - BoneType.RIGHT_RING_INTERMEDIATE -> rightRingIntermediateBone - BoneType.RIGHT_RING_DISTAL -> rightRingDistalBone - BoneType.RIGHT_LITTLE_PROXIMAL -> rightLittleProximalBone - BoneType.RIGHT_LITTLE_INTERMEDIATE -> rightLittleIntermediateBone - BoneType.RIGHT_LITTLE_DISTAL -> rightLittleDistalBone - } - - private fun getTrackerForBone(bone: BoneType?): Tracker? = when (bone) { - BoneType.HEAD -> headTracker - BoneType.NECK -> neckTracker - BoneType.UPPER_CHEST -> upperChestTracker - BoneType.CHEST -> chestTracker - BoneType.WAIST -> waistTracker - BoneType.HIP -> hipTracker - BoneType.LEFT_UPPER_LEG -> leftUpperLegTracker - BoneType.RIGHT_UPPER_LEG -> rightUpperLegTracker - BoneType.LEFT_LOWER_LEG -> leftLowerLegTracker - BoneType.RIGHT_LOWER_LEG -> rightLowerLegTracker - BoneType.LEFT_FOOT -> leftFootTracker - BoneType.RIGHT_FOOT -> rightFootTracker - BoneType.LEFT_SHOULDER -> leftShoulderTracker - BoneType.RIGHT_SHOULDER -> rightShoulderTracker - BoneType.LEFT_UPPER_ARM -> leftUpperArmTracker - BoneType.RIGHT_UPPER_ARM -> rightUpperArmTracker - BoneType.LEFT_LOWER_ARM -> leftLowerArmTracker - BoneType.RIGHT_LOWER_ARM -> rightLowerArmTracker - BoneType.LEFT_HAND -> leftHandTracker - BoneType.RIGHT_HAND -> rightHandTracker - else -> null - } - - /** - * Returns an array of all the non-tracker bones. - */ - val allHumanBones: Array - get() = arrayOf( - headBone, - neckBone, - upperChestBone, - chestBone, - waistBone, - hipBone, - leftHipBone, - rightHipBone, - leftUpperLegBone, - rightUpperLegBone, - leftLowerLegBone, - rightLowerLegBone, - leftFootBone, - rightFootBone, - leftUpperShoulderBone, - rightUpperShoulderBone, - leftShoulderBone, - rightShoulderBone, - leftUpperArmBone, - rightUpperArmBone, - leftLowerArmBone, - rightLowerArmBone, - leftHandBone, - rightHandBone, - leftThumbMetacarpalBone, - leftThumbProximalBone, - leftThumbDistalBone, - leftIndexProximalBone, - leftIndexIntermediateBone, - leftIndexDistalBone, - leftMiddleProximalBone, - leftMiddleIntermediateBone, - leftMiddleDistalBone, - leftRingProximalBone, - leftRingIntermediateBone, - leftRingDistalBone, - leftLittleProximalBone, - leftLittleIntermediateBone, - leftLittleDistalBone, - rightThumbMetacarpalBone, - rightThumbProximalBone, - rightThumbDistalBone, - rightIndexProximalBone, - rightIndexIntermediateBone, - rightIndexDistalBone, - rightMiddleProximalBone, - rightMiddleIntermediateBone, - rightMiddleDistalBone, - rightRingProximalBone, - rightRingIntermediateBone, - rightRingDistalBone, - rightLittleProximalBone, - rightLittleIntermediateBone, - rightLittleDistalBone, - ) - - /** - * Returns all the arm bones, tracker or not. - */ - private val allArmBones: Array - get() = arrayOf( - leftUpperShoulderBone, - rightUpperShoulderBone, - leftShoulderBone, - rightShoulderBone, - leftUpperArmBone, - rightUpperArmBone, - leftElbowTrackerBone, - rightElbowTrackerBone, - leftLowerArmBone, - rightLowerArmBone, - leftHandBone, - rightHandBone, - leftHandTrackerBone, - rightHandTrackerBone, - leftThumbMetacarpalBone, - leftThumbProximalBone, - leftThumbDistalBone, - leftIndexProximalBone, - leftIndexIntermediateBone, - leftIndexDistalBone, - leftMiddleProximalBone, - leftMiddleIntermediateBone, - leftMiddleDistalBone, - leftRingProximalBone, - leftRingIntermediateBone, - leftRingDistalBone, - leftLittleProximalBone, - leftLittleIntermediateBone, - leftLittleDistalBone, - rightThumbMetacarpalBone, - rightThumbProximalBone, - rightThumbDistalBone, - rightIndexProximalBone, - rightIndexIntermediateBone, - rightIndexDistalBone, - rightMiddleProximalBone, - rightMiddleIntermediateBone, - rightMiddleDistalBone, - rightRingProximalBone, - rightRingIntermediateBone, - rightRingDistalBone, - rightLittleProximalBone, - rightLittleIntermediateBone, - rightLittleDistalBone, - ) - - val hmdHeight: Float - get() = headTracker?.position?.y ?: 0f - - /** - * Runs checks to know if we should (and are) performing the tracking of the - * left arm from the controller. - * - * @return a bool telling us if we are tracking the left arm from the - * controller or not. - */ - val isTrackingLeftArmFromController: Boolean - get() = leftHandTracker != null && leftHandTracker!!.hasPosition && !forceArmsFromHMD - - /** - * Runs checks to know if we should (and are) performing the tracking of the - * right arm from the controller. - * - * @return a bool telling us if we are tracking the right arm from the - * controller or not. - */ - val isTrackingRightArmFromController: Boolean - get() = rightHandTracker != null && rightHandTracker!!.hasPosition && !forceArmsFromHMD - - val trackersToReset: List - get() = listOf( - neckTracker, - upperChestTracker, - chestTracker, - waistTracker, - hipTracker, - leftUpperLegTracker, - leftLowerLegTracker, - leftFootTracker, - rightUpperLegTracker, - rightLowerLegTracker, - rightFootTracker, - leftLowerArmTracker, - rightLowerArmTracker, - leftUpperArmTracker, - rightUpperArmTracker, - leftHandTracker, - rightHandTracker, - leftShoulderTracker, - rightShoulderTracker, - leftThumbMetacarpalTracker, - leftThumbProximalTracker, - leftThumbDistalTracker, - leftIndexProximalTracker, - leftIndexIntermediateTracker, - leftIndexDistalTracker, - leftMiddleProximalTracker, - leftMiddleIntermediateTracker, - leftMiddleDistalTracker, - leftRingProximalTracker, - leftRingIntermediateTracker, - leftRingDistalTracker, - leftLittleProximalTracker, - leftLittleIntermediateTracker, - leftLittleDistalTracker, - rightThumbMetacarpalTracker, - rightThumbProximalTracker, - rightThumbDistalTracker, - rightIndexProximalTracker, - rightIndexIntermediateTracker, - rightIndexDistalTracker, - rightMiddleProximalTracker, - rightMiddleIntermediateTracker, - rightMiddleDistalTracker, - rightRingProximalTracker, - rightRingIntermediateTracker, - rightRingDistalTracker, - rightLittleProximalTracker, - rightLittleIntermediateTracker, - rightLittleDistalTracker, - ) - - @JvmOverloads - fun resetTrackersFull(resetSourceName: String?, bodyParts: List = ArrayList()) { - humanPoseManager.server?.serverGuards?.onFullReset() - - var referenceRotation = IDENTITY - headTracker?.let { - if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) { - // Always reset the head (ifs in resetsHandler) - it.resetsHandler.resetFull(referenceRotation) - } - referenceRotation = it.getRotation() - } - - // Resets all axes of the trackers with the HMD as reference. - for (tracker in trackersToReset) { - // Only reset if tracker needsReset - if (tracker != null && (tracker.allowReset || tracker.isHmd) && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) { - tracker.resetsHandler.resetFull(referenceRotation) - } - } - - // Tell floorclip to reset its floor level on the next update - // of the computed trackers - if (!localizer.getEnabled()) { - legTweaks.resetFloorLevel() - } - legTweaks.resetBuffer() - localizer.reset() - ikSolver.resetOffsets() - LogManager.info("[HumanSkeleton] Reset: full ($resetSourceName)") - } - - @VRServerThread - @JvmOverloads - fun resetTrackersYaw(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) { - // Resets the yaw of the trackers with the head as reference. - var referenceRotation = IDENTITY - headTracker?.let { - if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) { - // Only reset if head allowReset and isn't computed - if (it.allowReset && !it.isComputed) { - it.resetsHandler.resetYaw(referenceRotation) - } - } - referenceRotation = it.getRotation() - } - for (tracker in trackersToReset) { - // Only reset if tracker allowReset - if (tracker != null && tracker.allowReset && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) { - tracker.resetsHandler.resetYaw(referenceRotation) - } - } - legTweaks.resetBuffer() - LogManager.info("[HumanSkeleton] Reset: yaw ($resetSourceName)") - } - - /** - * if bodyParts is empty, this resets mounting for all trackers. - * Keep in mind TrackerResetsHandler.kt has some logic as well for which trackers get reset (feet) - */ - @VRServerThread - @JvmOverloads - fun resetTrackersMounting(resetSourceName: String?, bodyParts: List) { - val trackersToReset = trackersToReset - - // TODO: PLEASE rewrite this handling at some point in the future... This is so - // hacky!! Surely there's a better way to check reset status - Butterscotch - // If there's a server present (required for status) and any tracker reports a - // non-zero reset status (indicates reset required), then block mounting reset, - // as it requires a full reset first - if (humanPoseManager.server != null && trackersToReset.any { it != null && it.needReset }) { - LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName) failed, reset required") - return - } - - // Resets the mounting orientation of the trackers with the HMD as reference. - var referenceRotation = IDENTITY - headTracker?.let { - if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) { - // Only reset if head allowMounting or is computed but not HMD - if (it.allowMounting || (it.isComputed && !it.isHmd)) { - it.resetsHandler.resetMounting(referenceRotation) - } - } - referenceRotation = it.getRotation() - } - - for (tracker in trackersToReset) { - // Only reset if tracker needsMounting - if (tracker != null && tracker.allowMounting && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) { - tracker.resetsHandler.resetMounting(referenceRotation) - } - } - legTweaks.resetBuffer() - localizer.reset() - - if (humanPoseManager.server != null) { - humanPoseManager.server.configManager.vrConfig.resetsConfig.lastMountingMethod = - MountingMethods.AUTOMATIC - if (!humanPoseManager.server.trackingChecklistManager.resetMountingCompleted) { - humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it -> - val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) { - TrackerUtils.allBodyPartsButFingers - } else { - TrackerUtils.allBodyPartsButFingersAndFeets - } - - return@any defaultParts.contains(it) - } - } - if (!humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted) { - humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) } - } - humanPoseManager.server.configManager.saveConfig() - } - - LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName)") - } - - @VRServerThread - fun clearTrackersMounting(resetSourceName: String?) { - headTracker?.let { - if (it.allowMounting) it.resetsHandler.clearMounting() - } - for (tracker in trackersToReset) { - if (tracker != null && - tracker.allowMounting - ) { - tracker.resetsHandler.clearMounting() - } - } - legTweaks.resetBuffer() - LogManager.info("[HumanSkeleton] Clear: mounting ($resetSourceName)") - - if (humanPoseManager.server != null) { - humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = false - humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = false - humanPoseManager.server.configManager.saveConfig() - } - } - - fun updateTapDetectionConfig() { - tapDetectionManager?.refresh() - } - - fun updateLegTweaksConfig() { - legTweaks.updateConfig() - } - - /** - * Does not save to config - */ - fun setLegTweaksStateTemp( - skatingCorrection: Boolean, - floorClip: Boolean, - toeSnap: Boolean, - footPlant: Boolean, - ) { - legTweaks.setSkatingCorrectionEnabled(skatingCorrection) - legTweaks.setFloorClipEnabled(floorClip) - legTweaks.toeSnapEnabled = toeSnap - legTweaks.footPlantEnabled = footPlant - } - - /** - * Resets to config values - */ - fun clearLegTweaksStateTemp( - skatingCorrection: Boolean, - floorClip: Boolean, - toeSnap: Boolean, - footPlant: Boolean, - ) { - // only reset the true values as they are a mask for what to reset - if (skatingCorrection) { - legTweaks - .setSkatingCorrectionEnabled( - humanPoseManager.getToggle(SkeletonConfigToggles.SKATING_CORRECTION), - ) - } - if (floorClip) { - legTweaks - .setFloorClipEnabled(humanPoseManager.getToggle(SkeletonConfigToggles.FLOOR_CLIP)) - } - if (toeSnap) legTweaks.toeSnapEnabled = humanPoseManager.getToggle(SkeletonConfigToggles.TOE_SNAP) - if (footPlant) legTweaks.footPlantEnabled = humanPoseManager.getToggle(SkeletonConfigToggles.FOOT_PLANT) - } - - val legTweaksState: BooleanArray - get() { - val state = BooleanArray(4) - state[0] = legTweaks.floorClipEnabled - state[1] = legTweaks.skatingCorrectionEnabled - state[2] = legTweaks.toeSnapEnabled - state[3] = legTweaks.footPlantEnabled - return state - } - - /** - * Master enable/disable of all leg tweaks (for Autobone) - */ - @VRServerThread - fun setLegTweaksEnabled(value: Boolean) { - legTweaks.enabled = value - } - - /** - * enable/disable IK solver (for Autobone) - */ - @VRServerThread - fun setIKSolverEnabled(value: Boolean) { - ikSolver.enabled = value - } - - @VRServerThread - fun setFloorclipEnabled(value: Boolean) { - humanPoseManager.setToggle(SkeletonConfigToggles.FLOOR_CLIP, value) - } - - @VRServerThread - fun setSkatingCorrectionEnabled(value: Boolean) { - humanPoseManager.setToggle(SkeletonConfigToggles.SKATING_CORRECTION, value) - } - - fun getPauseTracking(): Boolean = pauseTracking - - fun setPauseTracking(pauseTracking: Boolean, sourceName: String?) { - if (!pauseTracking && this.pauseTracking) { - // If unpausing tracking, clear the legtweaks buffer - legTweaks.resetBuffer() - } - this.pauseTracking = pauseTracking - LogManager.info("[HumanSkeleton] ${if (pauseTracking) "Pause" else "Unpause"} tracking ($sourceName)") - // Report the new state of tracking pause - humanPoseManager.trackingPauseHandler.sendTrackingPauseState(pauseTracking) - } - - fun togglePauseTracking(sourceName: String?): Boolean { - val newState = !pauseTracking - setPauseTracking(newState, sourceName) - return newState - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/IKChain.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/IKChain.kt deleted file mode 100644 index b34c930323..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/IKChain.kt +++ /dev/null @@ -1,159 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/* - * This class implements a chain of Bones - */ - -class IKChain( - val bones: MutableList, - var parent: IKChain?, - val level: Int, - val baseConstraint: Tracker?, - val tailConstraint: Tracker?, -) { - // State variables - private val computedBasePosition = baseConstraint?.let { IKConstraint(it) } - private val computedTailPosition = tailConstraint?.let { IKConstraint(it) } - var children = mutableListOf() - var target = Vector3.NULL - var distToTargetSqr = Float.POSITIVE_INFINITY - private var rotations = getRotationsList() - - private fun getRotationsList(): MutableList { - val rotList = mutableListOf() - for (b in bones) { - rotList.add(b.getGlobalRotation()) - } - - return rotList - } - - /* - * Populate the non-static rotations with the solve angle from the last iteration - */ - private fun prepBones() { - for (i in 0..() - private var rootChain: IKChain? = null - private var needsReset = false - - /** - * Any time the skeleton is rebuilt or trackers are assigned / unassigned the chains - * should be rebuilt. - */ - fun buildChains(trackers: List) { - chainList.clear() - - val positionalConstraints = extractPositionalConstraints(trackers) - val rotationalConstraints = extractRotationalConstraints(trackers) - - rootChain = chainBuilder(root, null, 0, positionalConstraints, rotationalConstraints) - populateChainList(rootChain!!) - addConstraints() - - // Check if there is any constraints (other than the head) in the model - rootChain = if (neededChain(rootChain!!)) rootChain else null - chainList.sortBy { -it.level } - } - - /** - * Reset the offsets of positional trackers. - */ - fun resetOffsets() { - needsReset = true - } - - /** - * Convert the skeleton in to a system of chains. - * A break in a chain is created at any point that has either - * multiple children or is positionally constrained, useless chains are discarded - * (useless chains are chains with no positional constraint at their tail). - */ - private fun chainBuilder( - root: Bone, - parent: IKChain?, - level: Int, - positionalConstraints: MutableList, - rotationalConstraints: MutableList, - ): IKChain { - val boneList = mutableListOf() - var currentBone = root - var constraint = getConstraint(currentBone, rotationalConstraints) - currentBone.rotationConstraint.allowModifications = constraint == null - currentBone.rotationConstraint.hasTrackerRotation = constraint != null - boneList.add(currentBone) - - // Get constraints - val baseConstraint = if (parent == null) { - getConstraint(boneList.first(), positionalConstraints) - } else { - parent.tailConstraint - } - var tailConstraint = getConstraint(currentBone, positionalConstraints) - - // Add bones until there is a reason to make a new chain - while (currentBone.children.size == 1 && tailConstraint == null) { - currentBone = currentBone.children.first() - constraint = getConstraint(currentBone, rotationalConstraints) - currentBone.rotationConstraint.allowModifications = constraint == null - currentBone.rotationConstraint.hasTrackerRotation = constraint != null - boneList.add(currentBone) - tailConstraint = getConstraint(currentBone, positionalConstraints) - } - - var chain = IKChain(boneList, parent, level, baseConstraint, tailConstraint) - - if (currentBone.children.isNotEmpty()) { - // Build child chains - val childrenList = mutableListOf() - for (child in currentBone.children) { - val childChain = chainBuilder(child, chain, level + 1, positionalConstraints, rotationalConstraints) - if (neededChain(childChain)) { - childrenList.add(childChain) - } - } - chain.children = childrenList - } - - // If the chain has only one child and no tail constraint combine the chains - if (chain.children.size == 1 && chain.tailConstraint == null) { - chain = combineChains(chain, chain.children.first()) - } - - return chain - } - - private fun populateChainList(chain: IKChain) { - chainList.add(chain) - for (c in chain.children) { - populateChainList(c) - } - } - - private fun combineChains(chain: IKChain, childChain: IKChain): IKChain { - val boneList = mutableListOf() - boneList.addAll(chain.bones) - boneList.addAll(childChain.bones) - - val newChain = IKChain( - boneList, - chain.parent, - chain.level, - chain.baseConstraint, - childChain.tailConstraint, - ) - - newChain.children = childChain.children - - for (c in newChain.children) { - c.parent = newChain - } - - return newChain - } - - private fun addConstraints() { - fun constrainChain(chain: IKChain) { - chain.bones.forEach { it.rotationConstraint.allowModifications = false } - } - chainList.forEach { if (it.tailConstraint == null) constrainChain(it) } - } - - private fun neededChain(chain: IKChain): Boolean { - if (chain.tailConstraint != null) { - return true - } - - for (c in chain.children) { - if (c.tailConstraint != null) { - return true - } - - if (neededChain(c)) { - return true - } - } - - return false - } - - private fun extractPositionalConstraints(trackers: List): MutableList { - val constraintList = mutableListOf() - for (t in trackers) { - if (t.hasPosition && - !t.isInternal && - !t.status.reset - ) { - constraintList.add(t) - } - } - return constraintList - } - - private fun extractRotationalConstraints(trackers: List): MutableList { - val constrainList = mutableListOf() - for (t in trackers) { - if (t.hasRotation && - !t.status.reset && - !t.isInternal - ) { - constrainList.add(t) - } - } - - return constrainList - } - - private fun getConstraint(bone: Bone, constraints: MutableList): Tracker? { - for (c in constraints) { - if (c.trackerPosition != null && bone.boneType.bodyPart == (c.trackerPosition?.bodyPart ?: 0)) { - constraints.remove(c) - return c - } - } - return null - } - - private fun solve(iterations: Int): Boolean { - var solved = false - for (i in 0..iterations) { - for (chain in chainList) { - chain.backwardsCCDIK() - } - - rootChain?.computeTargetDistance() - - // If all chains have reached their target the chain is solved - solved = true - for (chain in chainList) { - if (chain.distToTargetSqr > TOLERANCE_SQR) { - solved = false - break - } - } - - if (solved) break - } - - return solved - } - - fun solve() { - if (rootChain == null || !enabled) return - - if (needsReset) { - for (c in chainList) { - c.resetTrackerOffsets() - } - needsReset = false - } - - rootChain?.resetChain() - root.update() - - solve(MAX_ITERATIONS) - - root.update() - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaks.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaks.kt deleted file mode 100644 index 2ae9824dbf..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaks.kt +++ /dev/null @@ -1,1060 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import com.jme3.math.FastMath -import dev.slimevr.config.LegTweaksConfig -import dev.slimevr.tracking.processor.Bone -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -class LegTweaks(private val skeleton: HumanSkeleton) { - /** - * here is an explanation of each parameter that may need explaining - * STANDING_CUTOFF_VERTICAL is the percentage the hip has to be below its - * position at calibration to register as the user not standing - * MAX_DISENGAGEMENT_OFFSET is how much the floor will be shifted to allow - * an offset to happen smoothly DYNAMIC_DISPLACEMENT_CUTOFF is the percent - * of downwards rotation that can contribute to dynamic displacement - * MAX_DYNAMIC_DISPLACEMENT is the max amount the floor will be moved up to - * account for the foot rotating downward and needing to be put higher to - * avoid clipping in the game world MIN_ACCEPTABLE_ERROR and - * MAX_ACCEPTABLE_ERROR Defines the distance where CORRECTION_WEIGHT_MIN and - * CORRECTION_WEIGHT_MAX are calculating a percent of velocity to correct - * rather than using the min or max FLOOR_CALIBRATION_OFFSET is the amount - * the floor plane is shifted up. This can help the feet from floating - * slightly above the ground - */ - - companion object { - // clip correction - const val DYNAMIC_DISPLACEMENT_CUTOFF = 1.0f - const val FLOOR_CALIBRATION_OFFSET = 0.0025f - - // skating correction - private const val MIN_ACCEPTABLE_ERROR = 0.01f - private const val MAX_ACCEPTABLE_ERROR = 0.05f - private const val CORRECTION_WEIGHT_MIN = 0.55f - private const val CORRECTION_WEIGHT_MAX = 0.70f - private const val CONTINUOUS_CORRECTION_DIST = 0.5f - private const val CONTINUOUS_CORRECTION_WARMUP = 175 - - // knee / hip correction - private const val KNEE_CORRECTION_WEIGHT = 0.00f - private const val KNEE_LATERAL_WEIGHT = 0.8f - private const val WAIST_PUSH_WEIGHT = 0.2f - - // COM calculation - private const val HEAD_MASS = 0.0827f - private const val THORAX_MASS = 0.1870f - private const val ABDOMEN_MASS = 0.1320f - private const val PELVIS_MASS = 0.1530f - private const val THIGH_MASS = 0.1122f - private const val LEG_AND_FOOT_MASS = 0.0620f - private const val UPPER_ARM_MASS = 0.0263f - private const val FOREARM_AND_HAND_MASS = 0.0224f - - // rotation correction - private const val ROTATION_CORRECTION_VERTICAL = 0.1f - private const val MAXIMUM_CORRECTION_ANGLE = 0.4f - private const val MAXIMUM_CORRECTION_ANGLE_DELTA = 0.7f - private const val MAXIMUM_TOE_DOWN_ANGLE = 0.8f - private const val TOE_SNAP_COOLDOWN = 3.0f - private const val MIN_DISTANCE_FOR_PLANT = 0.025f - private const val MAX_DISTANCE_FOR_PLANT = 0.075f - - // misc - const val NEARLY_ZERO = 0.001f - private const val STANDING_CUTOFF_VERTICAL = 0.65f - private const val MAX_DISENGAGEMENT_OFFSET = 0.30f - private const val DEFAULT_ARM_DISTANCE = 0.15f - private const val MAX_CORRECTION_STRENGTH_DELTA = 1.0f - } - - // state variables - var floorLevel = 0f - private set - private var hipToFloorDist = 0f - private var currentDisengagementOffset = 0.0f - private var footLength = 0.0f - private var currentCorrectionStrength = 0.3f // default value - - private var initialized = true - var enabled = true // master switch - - private var alwaysUseFloorclip = false - var floorClipEnabled = false - private set - var skatingCorrectionEnabled = false - private set - var toeSnapEnabled = false - var footPlantEnabled = false - private var active = false - private var rightLegActive = false - private var leftLegActive = false - private var leftFramesLocked = 0 - private var rightFramesLocked = 0 - private var leftFramesUnlocked = 0 - private var rightFramesUnlocked = 0 - private var leftToeAngle = 0.0f - private var leftToeTouched = false - private var rightToeAngle = 0.0f - private var rightToeTouched = false - private var localizerMode = false - - // config - private var config: LegTweaksConfig? = null - - // leg data - private var leftFootPosition = Vector3.NULL - private var rightFootPosition = Vector3.NULL - private var leftKneePosition = Vector3.NULL - private var rightKneePosition = Vector3.NULL - private var hipPosition = Vector3.NULL - private var leftFootRotation = Quaternion.IDENTITY - private var rightFootRotation = Quaternion.IDENTITY - - private var leftFootAcceleration = Vector3.NULL - private var rightFootAcceleration = Vector3.NULL - private var leftLowerLegAcceleration = Vector3.NULL - private var rightLowerLegAcceleration = Vector3.NULL - - // buffer for holding previous frames of data - var bufferHead = LegTweaksBuffer() - private set - private var bufferInvalid = true - - constructor(skeleton: HumanSkeleton, config: LegTweaksConfig) : this(skeleton) { - this.config = config - updateConfig() - } - - fun resetFloorLevel() { - initialized = false - } - - fun setFloorClipEnabled(floorClipEnabled: Boolean) { - this.floorClipEnabled = floorClipEnabled - - // reset the buffer - bufferHead = LegTweaksBuffer() - bufferInvalid = true - } - - fun setSkatingCorrectionEnabled(skatingCorrectionEnabled: Boolean) { - this.skatingCorrectionEnabled = skatingCorrectionEnabled - - // reset the buffer - bufferHead = LegTweaksBuffer() - bufferInvalid = true - } - - fun setLocalizerMode(value: Boolean) { - localizerMode = value - if (value) setFloorLevel(0.0f) - } - - fun resetBuffer() { - bufferInvalid = true - } - - fun setConfig(config: LegTweaksConfig) { - this.config = config - updateConfig() - } - - fun updateConfig() { - if (config == null) return - - updateHyperParameters(config!!.correctionStrength) - floorClipEnabled = - skeleton.humanPoseManager.getToggle(SkeletonConfigToggles.FLOOR_CLIP) - alwaysUseFloorclip = config!!.alwaysUseFloorclip - skatingCorrectionEnabled = skeleton.humanPoseManager - .getToggle(SkeletonConfigToggles.SKATING_CORRECTION) - toeSnapEnabled = - skeleton.humanPoseManager.getToggle(SkeletonConfigToggles.TOE_SNAP) - footPlantEnabled = - skeleton.humanPoseManager.getToggle(SkeletonConfigToggles.FOOT_PLANT) - } - - // tweak the position of the legs based on data from the last frames - fun tweakLegs() { - // If user doesn't have knees or legtweaks is disabled, - // don't spend time doing calculations! - if ((!skeleton.hasKneeTrackers && !alwaysUseFloorclip) || !enabled) return - - // update the class with the latest data from the skeleton - // if false is returned something indicated that the legs should not - // be tweaked - preUpdate() - - // correct foot rotation's (Foot plant & Toe snap) - if (footPlantEnabled || toeSnapEnabled) correctFootRotations() - - // push the feet up if needed (Floor clip) - if (floorClipEnabled && !localizerMode) correctClipping() - - // correct for skating if needed (Skating correction) - if (skatingCorrectionEnabled) correctSkating() - - // determine if either leg is in a position to activate or - // deactivate - // (use the buffer to get the positions before corrections) - val leftFootDif = abs( - (bufferHead.leftFootPosition - leftFootPosition).y, - ) - val rightFootDif = abs( - (bufferHead.rightFootPosition - rightFootPosition).y, - ) - - if (!active && leftFootDif < NEARLY_ZERO) { - leftLegActive = false - } else if (active && leftFootDif < NEARLY_ZERO) { - leftLegActive = true - } - if (!active && rightFootDif < NEARLY_ZERO) { - rightLegActive = false - } else if (active && rightFootDif < NEARLY_ZERO) { - rightLegActive = true - } - - // restore the y positions of inactive legs - if (!leftLegActive) { - leftFootPosition = Vector3( - leftFootPosition.x, - bufferHead.leftFootPosition.y, - leftFootPosition.z, - ) - leftKneePosition = Vector3( - leftKneePosition.x, - bufferHead.leftKneePosition.y, - leftKneePosition.z, - ) - } - if (!rightLegActive) { - rightFootPosition = Vector3( - rightFootPosition.x, - bufferHead.rightFootPosition.y, - rightFootPosition.z, - ) - rightKneePosition = Vector3( - rightKneePosition.x, - bufferHead.rightKneePosition.y, - rightKneePosition.z, - ) - } - - // calculate the correction for the knees - if (initialized) solveLowerBody() - - // populate the corrected data into the current frame - bufferHead - .setCorrectedPositions( - leftFootPosition, - rightFootPosition, - leftKneePosition, - rightKneePosition, - hipPosition, - ) - - // Set the corrected positions in the skeleton - skeleton.computedHipTracker?.position = hipPosition - skeleton.computedLeftKneeTracker?.position = leftKneePosition - skeleton.computedRightKneeTracker?.position = rightKneePosition - skeleton.computedLeftFootTracker?.position = leftFootPosition - skeleton.computedRightFootTracker?.position = rightFootPosition - } - - // update the hyperparameters with the config - private fun updateHyperParameters(newStrength: Float) { - LegTweaksBuffer.setSkatingVelocityThreshold( - getScaledHyperParameter( - newStrength, - LegTweaksBuffer.getSkatingVelocityThreshold(), - ), - ) - LegTweaksBuffer.setSkatingAccelerationThreshold( - getScaledHyperParameter( - newStrength, - LegTweaksBuffer.getSkatingAccelerationThreshold(), - ), - ) - currentCorrectionStrength = newStrength - } - - private fun getScaledHyperParameter(newStrength: Float, currentValue: Float): Float = currentValue - currentCorrectionStrength * MAX_CORRECTION_STRENGTH_DELTA + newStrength * MAX_CORRECTION_STRENGTH_DELTA - - private fun setFloorLevel(floorLevel: Float) { - this.floorLevel = floorLevel - hipToFloorDist = hipPosition.y - floorLevel - } - - // set the vectors in this object to the vectors in the skeleton - private fun setVectors() { - // set the positions of the feet and knees to the skeleton's - // current positions - hipPosition = skeleton.computedHipTracker?.position ?: Vector3.NULL - leftKneePosition = skeleton.computedLeftKneeTracker?.position ?: Vector3.NULL - rightKneePosition = skeleton.computedRightKneeTracker?.position ?: Vector3.NULL - leftFootPosition = skeleton.computedLeftFootTracker?.position ?: Vector3.NULL - rightFootPosition = skeleton.computedRightFootTracker?.position ?: Vector3.NULL - leftFootRotation = skeleton.computedLeftFootTracker?.getRotation() ?: Quaternion.NULL - rightFootRotation = skeleton.computedRightFootTracker?.getRotation() ?: Quaternion.NULL - - // get the vector for acceleration of the feet and lower legs - leftFootAcceleration = - if (skeleton.leftFootTracker != null) skeleton.leftFootTracker!!.getAcceleration() else Vector3.NULL - rightFootAcceleration = - if (skeleton.rightFootTracker != null) skeleton.rightFootTracker!!.getAcceleration() else Vector3.NULL - leftLowerLegAcceleration = - if (skeleton.leftLowerLegTracker != null) skeleton.leftLowerLegTracker!!.getAcceleration() else Vector3.NULL - rightLowerLegAcceleration = - if (skeleton.rightLowerLegTracker != null) skeleton.rightLowerLegTracker!!.getAcceleration() else Vector3.NULL - } - - // updates the object with the latest data from the skeleton - private fun preUpdate() { - // populate the vectors with the latest data - setVectors() - - // if not initialized, we need to calculate some values from this frame - // to be used later (must happen immediately after reset) - if (!initialized) { - setFloorLevel(((leftFootPosition.y + rightFootPosition.y) / 2f + FLOOR_CALIBRATION_OFFSET)) - - // invalidate the buffer since the non-initialized output may be - // very wrong - bufferInvalid = true - initialized = true - } - - // update the foot length - footLength = skeleton.leftFootBone.length - - // if the user is standing start checking for a good time to enable leg - // tweaks - active = isStanding() - - // if the buffer is invalid add all the extra info - if (bufferInvalid && !localizerMode) { - bufferHead - .setPositions( - leftFootPosition, - rightFootPosition, - leftKneePosition, - rightKneePosition, - hipPosition, - ) - - // if active correct clipping before populating corrected positions - if (active) { - correctClipping() - } - - bufferHead - .setCorrectedPositions( - leftFootPosition, - rightFootPosition, - leftKneePosition, - rightKneePosition, - hipPosition, - ) - bufferHead.leftLegState = LegTweaksBuffer.UNLOCKED - bufferHead.rightLegState = LegTweaksBuffer.UNLOCKED - bufferInvalid = false - } - - // update the buffer - val leftFloorLevel: Float = ( - floorLevel + - footLength * - getFootOffset(leftFootRotation) - - currentDisengagementOffset - ) - val rightFloorLevel: Float = ( - floorLevel + - footLength * - getFootOffset(rightFootRotation) - - currentDisengagementOffset - ) - val leftFootAccel = - if (skeleton.leftFootTracker != null) leftFootAcceleration else leftLowerLegAcceleration - val rightFootAccel = - if (skeleton.rightFootTracker != null) rightFootAcceleration else rightLowerLegAcceleration - val detectionMode = - if (skeleton.leftFootTracker != null && skeleton.rightFootTracker != null) LegTweaksBuffer.FOOT_ACCEL else LegTweaksBuffer.ANKLE_ACCEL - val centerOfMass: Vector3 = computeCenterOfMass() - - // update the buffer head - bufferHead = LegTweaksBuffer( - leftFootPosition, - rightFootPosition, - leftKneePosition, - rightKneePosition, - leftFootRotation, - rightFootRotation, - leftFloorLevel, - rightFloorLevel, - leftFootAccel, - rightFootAccel, - detectionMode, - hipPosition, - centerOfMass, - bufferHead, - active, - ) - - // update the lock duration counters - updateLockStateCounters() - } - - // returns true if the foot is clipped and false if it is not - private fun isClipped(leftOffset: Float, rightOffset: Float): Boolean = ( - leftFootPosition.y < floorLevel + leftOffset * footLength || - rightFootPosition.y < floorLevel + rightOffset * footLength - ) - - // corrects the foot position to be above the floor level that is calculated - // on calibration - private fun correctClipping() { - // calculate how angled down the feet are as a scalar value between 0 - // and 1 (0 = flat, 1 = max angle) - val leftOffset: Float = getFootOffset(leftFootRotation) - val rightOffset: Float = getFootOffset(rightFootRotation) - var avgOffset = 0f - - // if there is no clipping, return - if (!isClipped(leftOffset, rightOffset)) return - - // move the feet to their new positions - if (leftFootPosition.y - < floorLevel + - footLength * - leftOffset - - currentDisengagementOffset - ) { - val displacement = abs( - ( - floorLevel + - footLength * - leftOffset - - leftFootPosition.y - - currentDisengagementOffset - ), - ) - leftFootPosition = Vector3( - leftFootPosition.x, - leftFootPosition.y + displacement, - leftFootPosition.z, - ) - leftKneePosition = Vector3( - leftKneePosition.x, - leftKneePosition.y + displacement, - leftKneePosition.z, - ) - avgOffset += displacement - } - - if (rightFootPosition.y - < floorLevel + - footLength * - rightOffset - - currentDisengagementOffset - ) { - val displacement = abs( - ( - floorLevel + - footLength * - rightOffset - - rightFootPosition.y - - currentDisengagementOffset - ), - ) - rightFootPosition = Vector3( - rightFootPosition.x, - rightFootPosition.y + displacement, - rightFootPosition.z, - ) - rightKneePosition = Vector3( - rightKneePosition.x, - rightKneePosition.y + displacement, - rightKneePosition.z, - ) - avgOffset += displacement - } - - hipPosition = Vector3( - hipPosition.x, - hipPosition.y + avgOffset / 2 * WAIST_PUSH_WEIGHT, - hipPosition.z, - ) - } - - // based on the data from the last frame compute a new position that reduces - // ice skating - private fun correctSkating() { - // for either foot that is locked get its position (x and z only we let - // y move freely) and set it to be there - val bufPrev = bufferHead.parent ?: return - - if (bufferHead.leftLegState == LegTweaksBuffer.LOCKED) { - leftFootPosition = Vector3( - bufPrev - .leftFootPositionCorrected - .x, - leftFootPosition.y, - bufPrev - .leftFootPositionCorrected - .z, - ) - } - if (bufferHead.rightLegState == LegTweaksBuffer.LOCKED) { - rightFootPosition = Vector3( - bufPrev - .rightFootPositionCorrected - .x, - rightFootPosition.y, - bufPrev - .rightFootPositionCorrected - .z, - ) - } - - // for either foot that is unlocked get its last position and calculate - // its position for this frame. the amount of displacement is based on - // the distance between the last position, the current position, and - // the hyperparameters - if (bufferHead.leftLegState == LegTweaksBuffer.UNLOCKED) { - leftFootPosition = correctUnlockedFootTracker(leftFootPosition, bufPrev.leftFootPosition, bufPrev.leftFootPositionCorrected, bufferHead.leftFootVelocity, leftFramesUnlocked) - } - if (bufferHead.rightLegState == LegTweaksBuffer.UNLOCKED) { - rightFootPosition = correctUnlockedFootTracker(rightFootPosition, bufPrev.rightFootPosition, bufPrev.rightFootPositionCorrected, bufferHead.rightFootVelocity, rightFramesUnlocked) - } - } - - private fun correctUnlockedFootTracker(footPosition: Vector3, previousFootPosition: Vector3, previousFootPositionCorrected: Vector3, footVelocity: Vector3, framesUnlocked: Int): Vector3 { - var newFootPosition = footPosition - var footDif = footPosition - previousFootPositionCorrected - footDif = Vector3(footDif.x, 0f, footDif.z) - - if (footDif.len() > NEARLY_ZERO) { - val leftY = footPosition.y - var temp = previousFootPositionCorrected - val (x, _, z) = footVelocity - - // first add the difference from the last frame to this frame - temp -= (previousFootPosition - footPosition) - newFootPosition = Vector3(temp.x, leftY, temp.z) - - // if velocity and dif are pointing in the same direction, - // add a small amount of velocity to the dif - // else subtract a small amount of velocity from the dif - // calculate the correction weight. - // it is also right here where the constant correction is - // applied - val weight: Float = calculateCorrectionWeight( - newFootPosition, - previousFootPositionCorrected, - ) - val constantCorrection = getConstantCorrectionQuantity(framesUnlocked) - var newX = newFootPosition.x - var newZ = newFootPosition.z - - if (x * footDif.x > 0) { - newX += ( - x * - weight + - ( - constantCorrection - * - (if (x > 0) 1 else -1) / - bufferHead.getTimeDelta() - ) - ) - } else if (x * footDif.x < 0) { - newX -= ( - x * - weight + - ( - constantCorrection - * - (if (x > 0) 1 else -1) / - bufferHead.getTimeDelta() - ) - ) - } - if (z * footDif.z > 0) { - newZ += ( - z * - weight + - ( - constantCorrection - * - (if (z > 0) 1 else -1) / - bufferHead.getTimeDelta() - ) - ) - } else if (z * footDif.z < 0) { - newZ -= ( - z * - weight + - ( - constantCorrection - * - (if (z > 0) 1 else -1) / - bufferHead.getTimeDelta() - ) - ) - } - - // if the foot overshot the target, move it back to the target - if (checkOverShoot( - footPosition.x, - previousFootPositionCorrected.x, - newX, - ) - ) { - newX = footPosition.x - } - if (checkOverShoot( - footPosition.z, - previousFootPositionCorrected.z, - newZ, - ) - ) { - newZ = footPosition.z - } - newFootPosition = Vector3(newX, newFootPosition.y, newZ) - } - - return newFootPosition - } - - // get the amount of the constant correction to apply. - private fun getConstantCorrectionQuantity(framesUnlocked: Int): Float = if (framesUnlocked >= CONTINUOUS_CORRECTION_WARMUP) { - CONTINUOUS_CORRECTION_DIST - } else { - ( - CONTINUOUS_CORRECTION_DIST - * (leftFramesUnlocked.toFloat() / CONTINUOUS_CORRECTION_WARMUP) - ) - } - - // correct the rotations of the feet - // this is done by planting the foot better and by snapping the toes to the - // ground - private fun correctFootRotations() { - if (bufferHead.parent == null) return - - // boolean for if there is a foot tracker - val leftFootTracker = skeleton.leftFootTracker != null - val rightFootTracker = skeleton.rightFootTracker != null - - // get the foot positions - var leftFootRotation = bufferHead.leftFootRotation - var rightFootRotation = bufferHead.rightFootRotation - - // between maximum correction angle and maximum correction angle delta - // the values are interpolated - val kneeAngleL = getXZAmount(leftFootPosition, leftKneePosition) - val kneeAngleR = getXZAmount(rightFootPosition, rightKneePosition) - val masterWeightL = getMasterWeight(kneeAngleL) - val masterWeightR = getMasterWeight(kneeAngleR) - - // corrects rotations when planted firmly on the ground - if (footPlantEnabled) { - // the further from the ground the foot is, the less weight it - // should have - var weightL = getFootPlantWeight(leftFootPosition) - var weightR = getFootPlantWeight(rightFootPosition) - - // if foot trackers exist add to the weights - val leftFootYaw = isolateYaw(leftFootRotation) - if (leftFootTracker) { - weightL *= getRotationalDistanceToPlant( - leftFootRotation, - leftFootYaw, - ) - } - val rightFootYaw = isolateYaw(rightFootRotation) - if (rightFootTracker) { - weightR *= getRotationalDistanceToPlant( - rightFootRotation, - rightFootYaw, - ) - } - - // perform the correction - leftFootRotation = leftFootRotation - .interpR( - leftFootYaw, - weightL * masterWeightL, - ) - rightFootRotation = rightFootRotation - .interpR( - rightFootYaw, - weightR * masterWeightR, - ) - } - - // corrects rotations when the foot is in the air by rotating the foot - // down so that the toes are touching - if (toeSnapEnabled && !(leftFootTracker && rightFootTracker)) { - // this correction step has its own weight vars - var weightL: Float - var weightR: Float - - // first compute the angle of the foot - val angleL = getToeSnapAngle(leftFootPosition) - val angleR = getToeSnapAngle(rightFootPosition) - - // then compute the weight of the correction - weightL = getToeSnapWeight(leftFootPosition) - weightR = getToeSnapWeight(rightFootPosition) - - // depending on the state variables, the correction weights should - // be clamped - if (!leftToeTouched) { - weightL = min(weightL, leftToeAngle) - } - if (!rightToeTouched) { - weightR = min(weightR, rightToeAngle) - } - - // then slerp the rotation to the new rotation based on the weight - if (!leftFootTracker) { - leftFootRotation = leftFootRotation - .interpR( - replacePitch(leftFootRotation, -angleL), - weightL * masterWeightL, - ) - } - if (!rightFootTracker) { - rightFootRotation = rightFootRotation - .interpR( - replacePitch(rightFootRotation, -angleR), - weightR * masterWeightR, - ) - } - - // update state variables regarding toe snap - if (leftFootPosition.y - floorLevel > footLength * MAXIMUM_TOE_DOWN_ANGLE) { - leftToeTouched = false - leftToeAngle = weightL - } else if (leftFootPosition.y - floorLevel <= 0.0f) { - leftToeTouched = true - leftToeAngle = 1.0f - } - if (rightFootPosition.y - floorLevel > footLength * MAXIMUM_TOE_DOWN_ANGLE) { - rightToeTouched = false - rightToeAngle = weightR - } else if (rightFootPosition.y - floorLevel <= 0.0f) { - rightToeTouched = true - rightToeAngle = 1.0f - } - } - - // update the foot rotations in the buffer - bufferHead.setCorrectedRotations(leftFootRotation, rightFootRotation) - - // update the skeleton - skeleton.computedLeftFootTracker?.setRotation(leftFootRotation) - skeleton.computedRightFootTracker?.setRotation(rightFootRotation) - } - - // returns the length of the xz components of the normalized difference - // between two vectors - private fun getXZAmount(vec1: Vector3, vec2: Vector3): Float { - val (x, _, z) = (vec1 - vec2).unit() - return Vector3(x, 0f, z).len() - } - - // returns a float between 0 and 1 that represents the master weight for - // foot rotation correciton - private fun getMasterWeight(kneeAngle: Float): Float { - val masterWeight = if (kneeAngle > MAXIMUM_CORRECTION_ANGLE && - kneeAngle < MAXIMUM_CORRECTION_ANGLE_DELTA - ) { - ( - 1.0f - - ( - (kneeAngle - MAXIMUM_CORRECTION_ANGLE) / - (MAXIMUM_CORRECTION_ANGLE_DELTA - MAXIMUM_CORRECTION_ANGLE) - ) - ) - } else { - 0.0f - } - return if (kneeAngle < MAXIMUM_CORRECTION_ANGLE) 1.0f else masterWeight - } - - // return the weight of the correction for toe snap - private fun getToeSnapWeight(footPos: Vector3): Float { - // then compute the weight of the correction - val weight = - if (footPos.y - floorLevel > footLength * TOE_SNAP_COOLDOWN) { - 0.0f - } else { - ( - 1.0f - - ( - (footPos.y - floorLevel - footLength) / - (footLength * (TOE_SNAP_COOLDOWN - 1.0f)) - ) - ) - } - return FastMath.clamp(weight, 0.0f, 1.0f) - } - - // returns the angle of the foot for toe snap - private fun getToeSnapAngle(footPos: Vector3): Float { - val angle = FastMath.clamp(footPos.y - floorLevel, 0.0f, footLength) - return if (angle > footLength * MAXIMUM_TOE_DOWN_ANGLE) { - asin( - MAXIMUM_TOE_DOWN_ANGLE, - ) - } else { - asin( - angle / footLength, - ) - } - } - - // returns the weight for floor plant - private fun getFootPlantWeight(footPos: Vector3): Float { - val weight = - if (footPos.y - floorLevel > ROTATION_CORRECTION_VERTICAL) 0.0f else 1.0f - (footPos.y - floorLevel) / ROTATION_CORRECTION_VERTICAL - return FastMath.clamp(weight, 0.0f, 1.0f) - } - - // returns the amount to slerp for foot plant when foot trackers are active - private fun getRotationalDistanceToPlant(footRot: Quaternion, footRotYaw: Quaternion): Float { - var angle = footRot.angleToR(footRotYaw) - angle = (angle / (2 * Math.PI)).toFloat() - angle = FastMath.clamp( - angle, - MIN_DISTANCE_FOR_PLANT, - MAX_DISTANCE_FOR_PLANT, - ) - return ( - 1 - - ( - (angle - MIN_DISTANCE_FOR_PLANT) / - (MAX_DISTANCE_FOR_PLANT - MIN_DISTANCE_FOR_PLANT) - ) - ) - } - - // returns true if it is likely the user is standing - private fun isStanding(): Boolean { - // if the hip is below the vertical cutoff, user is not standing - val cutoff = ( - floorLevel + - hipToFloorDist - - (hipToFloorDist * STANDING_CUTOFF_VERTICAL) - ) - if (hipPosition.y < cutoff) { - currentDisengagementOffset = ( - ( - 1 - - ( - (floorLevel - hipPosition.y) / - (floorLevel - cutoff) - ) - ) * - MAX_DISENGAGEMENT_OFFSET - ) - return false - } - currentDisengagementOffset = 0f - return true - } - - // move the knees in to a position that is closer to the truth - private fun solveLowerBody() { - // calculate the left and right hip nodes in standing space - val leftHip = hipPosition - val rightHip = hipPosition - - // before moving the knees back closer to the hip nodes, offset them - // the same amount the foot trackers where offset - val leftXDif = leftFootPosition.x - bufferHead.leftFootPosition.x - val rightXDif = rightFootPosition.x - bufferHead.rightFootPosition.x - val leftZDif = leftFootPosition.z - bufferHead.leftFootPosition.z - val rightZDif = rightFootPosition.z - bufferHead.rightFootPosition.z - val leftX = leftKneePosition.x + leftXDif * KNEE_LATERAL_WEIGHT - val leftZ = leftKneePosition.z + leftZDif * KNEE_LATERAL_WEIGHT - val rightX = rightKneePosition.x + rightXDif * KNEE_LATERAL_WEIGHT - val rightZ = rightKneePosition.z + rightZDif * KNEE_LATERAL_WEIGHT - leftKneePosition = Vector3(leftX, leftKneePosition.y, leftZ) - rightKneePosition = Vector3(rightX, rightKneePosition.y, rightZ) - - // calculate the bone distances - val leftKneeHip = (bufferHead.leftKneePosition - leftHip).len() - val rightKneeHip = (bufferHead.rightKneePosition - rightHip).len() - val leftKneeHipNew = (leftKneePosition - leftHip).len() - val rightKneeHipNew = (rightKneePosition - rightHip).len() - val leftKneeOffset = leftKneeHipNew - leftKneeHip - val rightKneeOffset = rightKneeHipNew - rightKneeHip - - // get the vector from the hip to the knee - val leftKneeVector = (leftKneePosition - leftHip).unit() * - (leftKneeOffset * KNEE_CORRECTION_WEIGHT) - val rightKneeVector = (rightKneePosition - rightHip).unit() * - (rightKneeOffset * KNEE_CORRECTION_WEIGHT) - - // correct the knees - leftKneePosition -= leftKneeVector - rightKneePosition -= rightKneeVector - } - - private fun getFootOffset(footRotation: Quaternion): Float { - val offset: Float = computeUnitVector(footRotation).y - return FastMath.clamp(offset, 0f, DYNAMIC_DISPLACEMENT_CUTOFF) - } - - // calculate the weight of foot correction - private fun calculateCorrectionWeight( - foot: Vector3, - footCorrected: Vector3, - ): Float { - var footDif = foot - footCorrected - footDif = Vector3(footDif.x, 0f, footDif.z) - if (footDif.len() < MIN_ACCEPTABLE_ERROR) { - return CORRECTION_WEIGHT_MIN - } else if (footDif.len() > MAX_ACCEPTABLE_ERROR) { - return CORRECTION_WEIGHT_MAX - } - return ( - CORRECTION_WEIGHT_MIN + - (footDif.len() - MIN_ACCEPTABLE_ERROR) / - (MAX_ACCEPTABLE_ERROR - MIN_ACCEPTABLE_ERROR) * - (CORRECTION_WEIGHT_MAX - CORRECTION_WEIGHT_MIN) - ) - } - - // calculate the center of mass of the user for the current frame - // returns a vector representing the center of mass position - private fun computeCenterOfMass(): Vector3 { - // check if arm data is available - val armsAvailable = ( - skeleton.hasLeftArmTracker && - skeleton.hasRightArmTracker - ) - var centerOfMass = Vector3(0f, 0f, 0f) - - // compute the center of mass of smaller body parts and then sum them up - // with their respective weights - val head = skeleton.headBone.getPosition() - val thorax: Vector3 = - getCenterOfJoint(skeleton.chestBone) - val abdomen = skeleton.waistBone.getPosition() - val pelvis = skeleton.hipBone.getPosition() - val leftCalf: Vector3 = - getCenterOfJoint(skeleton.leftLowerLegBone) - val rightCalf: Vector3 = - getCenterOfJoint(skeleton.rightLowerLegBone) - val leftThigh: Vector3 = - getCenterOfJoint(skeleton.leftUpperLegBone) - val rightThigh: Vector3 = - getCenterOfJoint(skeleton.rightUpperLegBone) - centerOfMass += head * HEAD_MASS - centerOfMass += thorax * THORAX_MASS - centerOfMass += abdomen * ABDOMEN_MASS - centerOfMass += pelvis * PELVIS_MASS - centerOfMass += leftCalf * LEG_AND_FOOT_MASS - centerOfMass += rightCalf * LEG_AND_FOOT_MASS - centerOfMass += leftThigh * THIGH_MASS - centerOfMass += rightThigh * THIGH_MASS - - if (armsAvailable) { - val leftUpperArm: Vector3 = getCenterOfJoint( - skeleton.leftUpperArmBone, - ) - val rightUpperArm: Vector3 = getCenterOfJoint( - skeleton.rightUpperArmBone, - ) - val leftForearm: Vector3 = getCenterOfJoint( - skeleton.leftLowerArmBone, - ) - val rightForearm: Vector3 = getCenterOfJoint( - skeleton.rightLowerArmBone, - ) - centerOfMass += leftUpperArm * UPPER_ARM_MASS - centerOfMass += rightUpperArm * UPPER_ARM_MASS - centerOfMass += leftForearm * FOREARM_AND_HAND_MASS - centerOfMass += rightForearm * FOREARM_AND_HAND_MASS - } else { - // if the arms are not available put them slightly in front - // of the upper chest. - val chestUnitVector: Vector3 = computeUnitVector( - skeleton.upperChestBone.getGlobalRotation(), - ) - val armLocation = - abdomen + (chestUnitVector * DEFAULT_ARM_DISTANCE) - centerOfMass += armLocation * (UPPER_ARM_MASS * 2.0f) - centerOfMass += armLocation * (FOREARM_AND_HAND_MASS * 2.0f) - } - - // finally translate in to tracker space - centerOfMass = hipPosition + - (centerOfMass - skeleton.hipTrackerBone.getPosition()) - return centerOfMass - } - - // get the center of two joints - private fun getCenterOfJoint(bone: Bone): Vector3 = ( - bone.getPosition() + - bone.getTailPosition() - ) * - 0.5f - - // update counters for the lock state of the feet - private fun updateLockStateCounters() { - if (bufferHead.leftLegState == LegTweaksBuffer.LOCKED) { - leftFramesUnlocked = 0 - leftFramesLocked++ - } else { - leftFramesLocked = 0 - leftFramesUnlocked++ - } - if (bufferHead.rightLegState == LegTweaksBuffer.LOCKED) { - rightFramesUnlocked = 0 - rightFramesLocked++ - } else { - rightFramesLocked = 0 - rightFramesUnlocked++ - } - } - - // isolate the euler yaw component of a given quaternion - private fun isolateYaw(quaternion: Quaternion): Quaternion = Quaternion.rotationAroundYAxis( - quaternion.toEulerAngles(EulerOrder.YZX).y, - ) - - // return a quaternion that has been rotated by the new pitch amount - private fun replacePitch(quaternion: Quaternion, newPitch: Float): Quaternion { - val (_, _, y, z) = quaternion.toEulerAngles(EulerOrder.YZX) - val newAngs = EulerAngles( - EulerOrder.YZX, - newPitch, - y, - z, - ) - return newAngs.toQuaternion() - } - - // check if correction overshot the true value - // returns true if overshot - private fun checkOverShoot( - trueVal: Float, - valBefore: Float, - valAfter: Float, - ): Boolean = (trueVal - valBefore) * (trueVal - valAfter) < 0 - - // get the unit vector of the given rotation - private fun computeUnitVector(quaternion: Quaternion): Vector3 = quaternion.toMatrix().z.unit() -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaksBuffer.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaksBuffer.kt deleted file mode 100644 index 1bd5efa1f5..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/LegTweaksBuffer.kt +++ /dev/null @@ -1,728 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/** - * class that holds data related to the state and other various attributes of - * the legs such as the position of the foot, knee, and hip, after and before - * correction, the velocity of the foot and the computed state of the feet at - * that frame. mainly calculates the state of the legs per frame using these - * rules: The conditions for an unlock are as follows: 1. the foot is too far - * from its correct position 2. a velocity higher than a threshold is achieved - * 3. a large acceleration is applied to the foot 4. angular velocity of the - * foot goes higher than a threshold. The conditions for a lock are the opposite - * of the above but require a lower value for all of the above conditions. The - * aforementioned thresholds are computed by applying scalars to a base - * threshold value. This allows one set of initial values to be applicable to a - * large range of actions and body types. - */ - -class LegTweaksBuffer @Suppress("ktlint") constructor() { - // hyperparameters / constants - companion object { - const val STATE_UNKNOWN = 0 - const val LOCKED = 1 - const val UNLOCKED = 2 - const val FOOT_ACCEL = 3 - const val ANKLE_ACCEL = 4 - const val NS_CONVERT = 1.0e9f - const val BUFFER_LEN = 10 - val GRAVITY: Vector3 = Vector3(0f, -9.81f, 0f) - val GRAVITY_MAGNITUDE: Float = GRAVITY.len() - - private const val SKATING_DISTANCE_CUTOFF = 0.5f - private const val SKATING_ROTVELOCITY_THRESHOLD = 4.5f - private const val SKATING_LOCK_ENGAGE_PERCENT = 1.1f - private const val FLOOR_DISTANCE_CUTOFF = 0.065f - private const val SIX_TRACKER_TOLERANCE = -0.10f - private val FORCE_VECTOR_TO_PRESSURE: Vector3 = Vector3(0.25f, 1.0f, 0.25f) - private val FORCE_ERROR_TOLERANCE_SQR: Float = 4.0f.pow(2) - private val FORCE_VECTOR_FALLBACK = floatArrayOf(0.1f, 0.1f) - - var SKATING_VELOCITY_THRESHOLD = 2.4f - var SKATING_ACCELERATION_THRESHOLD = 0.8f - - private var PARAM_SCALAR_MAX = 3.2f - private var PARAM_SCALAR_MIN = 0.25f - private const val PARAM_SCALAR_MID = 1.0f - - // the point at which the scalar is at the max or min depending on accel - private const val MAX_SCALAR_ACCEL = 0.2f - private const val MIN_SCALAR_ACCEL = 0.9f - - // the point at which the scalar is at it max or min in a double locked foot - // situation - private const val MAX_SCALAR_DORMANT = 0.2f - private const val MIN_SCALAR_DORMANT = 1.50f - - // the point at which the scalar is at it max or min in a single locked foot - // situation - private const val MIN_SCALAR_ACTIVE = 1.75f - private const val MAX_SCALAR_ACTIVE = 0.1f - - // maximum scalars for the pressure on each foot - private const val PRESSURE_SCALAR_MIN = 0.1f - private const val PRESSURE_SCALAR_MAX = 1.9f - - private const val SKATING_CUTOFF_ENGAGE = ( - SKATING_DISTANCE_CUTOFF - * SKATING_LOCK_ENGAGE_PERCENT - ) - private var SKATING_VELOCITY_CUTOFF_ENGAGE = ( - SKATING_VELOCITY_THRESHOLD - * SKATING_LOCK_ENGAGE_PERCENT - ) - private var SKATING_ACCELERATION_CUTOFF_ENGAGE = ( - SKATING_ACCELERATION_THRESHOLD - * SKATING_LOCK_ENGAGE_PERCENT - ) - private const val SKATING_ROTATIONAL_VELOCITY_CUTOFF_ENGAGE = - ( - SKATING_ROTVELOCITY_THRESHOLD - * SKATING_LOCK_ENGAGE_PERCENT - ) - - fun setSkatingVelocityThreshold(value: Float) { - SKATING_VELOCITY_THRESHOLD = value - SKATING_VELOCITY_CUTOFF_ENGAGE = ( - SKATING_VELOCITY_THRESHOLD - * SKATING_LOCK_ENGAGE_PERCENT - ) - } - - fun getSkatingVelocityThreshold(): Float = SKATING_VELOCITY_THRESHOLD - - fun setSkatingAccelerationThreshold(value: Float) { - SKATING_ACCELERATION_THRESHOLD = value - SKATING_ACCELERATION_CUTOFF_ENGAGE = ( - SKATING_ACCELERATION_THRESHOLD - * SKATING_LOCK_ENGAGE_PERCENT - ) - } - - fun getSkatingAccelerationThreshold(): Float = SKATING_ACCELERATION_THRESHOLD - } - - // states for the legs - var leftLegState = STATE_UNKNOWN - var rightLegState = STATE_UNKNOWN - var leftLegNumericalState = 0f - private set - var rightLegNumericalState = 0f - private set - - // positions and rotations - var leftFootPosition: Vector3 = Vector3.NULL - private set - var rightFootPosition: Vector3 = Vector3.NULL - private set - var leftKneePosition: Vector3 = Vector3.NULL - private set - var rightKneePosition: Vector3 = Vector3.NULL - private set - var hipPosition: Vector3 = Vector3.NULL - private set - var leftFootRotation: Quaternion = Quaternion.IDENTITY - private set - var rightFootRotation: Quaternion = Quaternion.IDENTITY - private set - var leftFootPositionCorrected: Vector3 = Vector3.NULL - private set - var rightFootPositionCorrected: Vector3 = Vector3.NULL - private set - var leftKneePositionCorrected: Vector3 = Vector3.NULL - private set - var rightKneePositionCorrected: Vector3 = Vector3.NULL - private set - var hipPositionCorrected: Vector3 = Vector3.NULL - private set - var leftFootRotationCorrected: Quaternion = - Quaternion.IDENTITY - private set - var rightFootRotationCorrected: Quaternion = - Quaternion.IDENTITY - private set - - // velocities - var leftFootVelocity: Vector3 = Vector3.NULL - private set - var leftFootVelocityMagnitude = 0f - private set - var rightFootVelocity: Vector3 = Vector3.NULL - private set - var rightFootVelocityMagnitude = 0f - private set - private var leftFootAngleDiff = 0f - private var rightFootAngleDiff = 0f - - // acceleration - var leftFootAcceleration: Vector3 = Vector3.NULL - private set - var leftFootAccelerationMagnitude = 0f - private set - var rightFootAcceleration: Vector3 = Vector3.NULL - private set - var rightFootAccelerationMagnitude = 0f - private set - - // center of mass - var centerOfMass: Vector3 = Vector3.NULL - private set - var centerOfMassVelocity: Vector3 = Vector3.NULL - private set - var centerOfMassAcceleration: Vector3 = Vector3.NULL - private set - - // other data - val timeOfFrame: Long = System.nanoTime() - var parent: LegTweaksBuffer? = null - private set - - private var frameNumber = 0 // higher number is older frame - private var detectionMode = ANKLE_ACCEL - private var accelerationAboveThresholdLeft = true - private var accelerationAboveThresholdRight = true - private var leftFloorLevel = 0f - private var rightFloorLevel = 0f - var isStanding = false - private set - - private var leftFootSensitivityVel = 1.0f - private var rightFootSensitivityVel = 1.0f - private var leftFootSensitivityAccel = 1.0f - private var rightFootSensitivityAccel = 1.0f - - constructor( - leftFootPosition: Vector3, - rightFootPosition: Vector3, - leftKneePosition: Vector3, - rightKneePosition: Vector3, - leftFootRotation: Quaternion, - rightFootRotation: Quaternion, - leftFloorLevel: Float, - rightFloorLevel: Float, - leftFootAcceleration: Vector3, - rightFootAcceleration: Vector3, - accelerationMode: Int, - hipPosition: Vector3, - centerOfMass: Vector3, - parent: LegTweaksBuffer, - active: Boolean, - ) : this() { - this.leftFootPosition = leftFootPosition - this.rightFootPosition = rightFootPosition - this.leftKneePosition = leftKneePosition - this.rightKneePosition = rightKneePosition - this.leftFootRotation = leftFootRotation - this.rightFootRotation = rightFootRotation - this.leftFloorLevel = leftFloorLevel - this.rightFloorLevel = rightFloorLevel - this.leftFootAcceleration = leftFootAcceleration - this.rightFootAcceleration = rightFootAcceleration - this.detectionMode = accelerationMode - this.hipPosition = hipPosition - this.centerOfMass = centerOfMass - this.parent = parent - - this.calculateFootAttributes(active) - } - - fun setPositions( - leftFootPosition: Vector3, - rightFootPosition: Vector3, - leftKneePosition: Vector3, - rightKneePosition: Vector3, - hipPosition: Vector3, - ) { - this.leftFootPosition = leftFootPosition - this.rightFootPosition = rightFootPosition - this.leftKneePosition = leftKneePosition - this.rightKneePosition = rightKneePosition - this.hipPosition = hipPosition - } - - fun setCorrectedPositions( - leftFootPosition: Vector3, - rightFootPosition: Vector3, - leftKneePosition: Vector3, - rightKneePosition: Vector3, - hipPosition: Vector3, - ) { - this.leftFootPositionCorrected = leftFootPosition - this.rightFootPositionCorrected = rightFootPosition - this.leftKneePositionCorrected = leftKneePosition - this.rightKneePositionCorrected = rightKneePosition - this.hipPositionCorrected = hipPosition - } - - fun setCorrectedRotations(leftFootRotation: Quaternion, rightFootRotation: Quaternion) { - this.leftFootRotationCorrected = leftFootRotation - this.rightFootRotationCorrected = rightFootRotation - } - - // returns 1 / delta time - fun getTimeDelta(): Float = if (parent == null) 0.0f else 1.0f / ((timeOfFrame - parent!!.timeOfFrame) / NS_CONVERT) - - // calculate movement attributes - private fun calculateFootAttributes(active: Boolean) { - updateFrameNumber(0) - - // compute attributes of the legs - computeVelocity() - computeAccelerationMagnitude() - computeComAttributes() - - // check if the acceleration triggers forced unlock - if (detectionMode == FOOT_ACCEL) { - computeAccelerationAboveThresholdFootTrackers() - } else { - computeAccelerationAboveThresholdAnkleTrackers() - } - - // calculate the scalar for other parameters - computeScalar() - - // if correction is inactive state is unknown (default to unlocked) - if (!active || parent == null) { - leftLegState = UNLOCKED - rightLegState = UNLOCKED - } else { - computeState() - } - } - - // update the frame number and discard frames older than BUFFER_LEN - private fun updateFrameNumber(frameNumber: Int) { - this.frameNumber = frameNumber - if (this.frameNumber >= BUFFER_LEN) parent = null - parent?.updateFrameNumber(frameNumber + 1) - } - - // compute the state of the legs - private fun computeState() { - // get the difference of the corrected and current positions - val leftDiff = getFootHorizontalDifference(parent!!.leftFootPositionCorrected, leftFootPosition) - val rightDiff = getFootHorizontalDifference(parent!!.rightFootPositionCorrected, rightFootPosition) - - // based on the last state of the legs compute their state for this - // individual frame - leftLegState = checkState( - parent!!.leftLegState, - leftDiff, - leftFootVelocityMagnitude, - leftFootSensitivityVel, - leftFootAngleDiff, - leftFloorLevel, - accelerationAboveThresholdLeft, - leftFootPosition, - ) - rightLegState = checkState( - parent!!.rightLegState, - rightDiff, - rightFootVelocityMagnitude, - rightFootSensitivityVel, - rightFootAngleDiff, - rightFloorLevel, - accelerationAboveThresholdRight, - rightFootPosition, - ) - - computeNumericalState() - } - - // check if a locked foot should stay locked or be released and vice versa - private fun checkState( - legState: Int, - horizontalDifference: Float, - velocityMagnitude: Float, - velocitySensitivity: Float, - angleDiff: Float, - floorLevel: Float, - accelerationAboveThreshold: Boolean, - footPosition: Vector3, - ): Int { - val timeStep = getTimeDelta() - if (legState == UNLOCKED) { - return if (horizontalDifference > SKATING_CUTOFF_ENGAGE || - (velocityMagnitude * timeStep > SKATING_VELOCITY_CUTOFF_ENGAGE * velocitySensitivity) || - (angleDiff * timeStep > SKATING_ROTATIONAL_VELOCITY_CUTOFF_ENGAGE * velocitySensitivity) || - footPosition.y > floorLevel + FLOOR_DISTANCE_CUTOFF || - accelerationAboveThreshold - ) { - UNLOCKED - } else { - LOCKED - } - } - return if (horizontalDifference > SKATING_DISTANCE_CUTOFF || - (velocityMagnitude * timeStep > SKATING_VELOCITY_THRESHOLD * velocitySensitivity) || - (angleDiff * timeStep > SKATING_ROTVELOCITY_THRESHOLD * velocitySensitivity) || - footPosition.y > floorLevel + FLOOR_DISTANCE_CUTOFF || - accelerationAboveThreshold - ) { - UNLOCKED - } else { - LOCKED - } - } - - // compute a numerical value representing how locked a foot is (bigger - // number == less locked) - private fun computeNumericalState() { - leftLegNumericalState = computeNumericalState( - leftFootVelocityMagnitude, - leftFootAccelerationMagnitude, - leftFootSensitivityAccel, - leftFootSensitivityVel, - ) - rightLegNumericalState = computeNumericalState( - rightFootVelocityMagnitude, - rightFootAccelerationMagnitude, - rightFootSensitivityAccel, - rightFootSensitivityVel, - ) - } - - // returns the average percentage the real velocity and acceleration are of - // the scaled thresholds for velocity and acceleration - private fun computeNumericalState( - velocityMagnitude: Float, - accelerationMagnitude: Float, - accelSensitivity: Float, - velSensitivity: Float, - ): Float { - val timeStep = getTimeDelta() - val velocity = velocityMagnitude * timeStep - val velocityPercentage: Float = - velocity / (SKATING_VELOCITY_THRESHOLD * velSensitivity) - val accelerationPercentage: Float = ( - accelerationMagnitude / - (SKATING_ACCELERATION_THRESHOLD * accelSensitivity) - ) - return (velocityPercentage + accelerationPercentage) * 0.5f - } - - // get the difference in feet position between the kinematic and corrected - // positions of the feet disregarding vertical displacement - private fun getFootHorizontalDifference(correctedPosition: Vector3, position: Vector3): Float { - val diff: Vector3 = correctedPosition - position - return Vector3( - diff.x, - 0f, - diff.z, - ).len() - } - - // get the angular velocity of the left foot (kinda we just want a scalar) - private fun getFootAngularVelocity(oldRot: Quaternion, rot: Quaternion): Float = (rot.toMatrix().z.unit() - oldRot.toMatrix().z.unit()).len() - - // compute the velocity of the feet from the position in the last frames - private fun computeVelocity() { - if (parent == null) return - leftFootVelocity = leftFootPosition - parent!!.leftFootPosition - leftFootVelocityMagnitude = leftFootVelocity.len() - rightFootVelocity = rightFootPosition - parent!!.rightFootPosition - rightFootVelocityMagnitude = rightFootVelocity.len() - leftFootAngleDiff = getFootAngularVelocity(parent!!.leftFootRotation, leftFootRotation) - rightFootAngleDiff = getFootAngularVelocity(parent!!.rightFootRotation, rightFootRotation) - } - - // get the nth parent of this frame - private fun getNParent(n: Int): LegTweaksBuffer? = if (n == 0 || parent == null) this else parent!!.getNParent(n - 1) - - // compute the acceleration magnitude of the feet from the acceleration - // given by the imus - private fun computeAccelerationMagnitude() { - leftFootAccelerationMagnitude = leftFootAcceleration.len() - rightFootAccelerationMagnitude = rightFootAcceleration.len() - } - - // compute the velocity and acceleration of the center of mass - private fun computeComAttributes() { - centerOfMassVelocity = centerOfMass - parent!!.centerOfMass - centerOfMassAcceleration = centerOfMassVelocity - parent!!.centerOfMassVelocity - } - - // for a setup with foot trackers the data from the imus is enough to determine lock/unlock - private fun computeAccelerationAboveThresholdFootTrackers() { - accelerationAboveThresholdLeft = ( - leftFootAccelerationMagnitude - > SKATING_ACCELERATION_CUTOFF_ENGAGE * leftFootSensitivityAccel - ) - accelerationAboveThresholdRight = ( - rightFootAccelerationMagnitude - > SKATING_ACCELERATION_CUTOFF_ENGAGE * rightFootSensitivityAccel - ) - } - - // for any setup without foot trackers the data from the imus is enough to - // determine lock/unlock but we add some tolerance - private fun computeAccelerationAboveThresholdAnkleTrackers() { - accelerationAboveThresholdLeft = ( - leftFootAccelerationMagnitude - > (SKATING_ACCELERATION_THRESHOLD + SIX_TRACKER_TOLERANCE) * leftFootSensitivityAccel - ) - accelerationAboveThresholdRight = ( - rightFootAccelerationMagnitude - > (SKATING_ACCELERATION_THRESHOLD + SIX_TRACKER_TOLERANCE) * rightFootSensitivityAccel - ) - } - - private fun computeScalar() { - // get the first set of scalars that are based on acceleration from the - // imus - val leftFootScalarAccel: Float = getFootScalarAccel(leftFootAccelerationMagnitude) - val rightFootScalarAccel: Float = getFootScalarAccel(rightFootAccelerationMagnitude) - - // get the second set of scalars that is based off of how close each - // foot is to a lock and dynamically adjusting the scalars - // (based off the assumption that if you are standing one foot is likely - // planted on the ground unless you are moving fast) - val leftFootScalarVel: Float = getFootLockLikelihood( - leftFootVelocity, - rightFootVelocity, - leftFootVelocityMagnitude, - rightFootVelocityMagnitude, - ) - val rightFootScalarVel: Float = getFootLockLikelihood( - rightFootVelocity, - leftFootVelocity, - rightFootVelocityMagnitude, - leftFootVelocityMagnitude, - ) - - // get the third set of scalars that is based on where the COM is - val pressureScalars: FloatArray = getPressurePrediction() - - // combine the scalars to get the final scalars - leftFootSensitivityVel = ( - ( - leftFootScalarAccel + - leftFootScalarVel / - 2.0f - ) * - FastMath.clamp( - pressureScalars[0] * 2.0f, - PRESSURE_SCALAR_MIN, - PRESSURE_SCALAR_MAX, - ) - ) - rightFootSensitivityVel = ( - ( - rightFootScalarAccel + - rightFootScalarVel / - 2.0f - ) * - FastMath.clamp( - pressureScalars[1] * 2.0f, - PRESSURE_SCALAR_MIN, - PRESSURE_SCALAR_MAX, - ) - ) - - leftFootSensitivityAccel = leftFootScalarVel - rightFootSensitivityAccel = rightFootScalarVel - } - - // calculate a scalar using acceleration to apply to the non acceleration - // based hyperparameters when calculating - // lock states - private fun getFootScalarAccel(accelMagnitude: Float): Float { - if (leftLegState == LOCKED) { - if (accelMagnitude < MAX_SCALAR_ACCEL) { - return PARAM_SCALAR_MAX - } else if (accelMagnitude > MIN_SCALAR_ACCEL) { - return ( - PARAM_SCALAR_MAX - * - (accelMagnitude - MIN_SCALAR_ACCEL) / - (MAX_SCALAR_ACCEL - MIN_SCALAR_ACCEL) - ) - } - } - return PARAM_SCALAR_MID - } - - // calculate a scalar using the velocity of the foot trackers and the lock - // states to calculate a scalar to apply to the non acceleration based - // hyperparameters when calculating - // lock states - private fun getFootLockLikelihood( - primaryFootVel: Vector3, - otherFootVel: Vector3, - primaryFootVelMagnitude: Float, - otherFootVelMagnitude: Float, - ): Float { - if (leftLegState == LOCKED && rightLegState == LOCKED) { - var velocityDiff: Vector3 = primaryFootVel - otherFootVel - velocityDiff = Vector3(velocityDiff.x, 0f, velocityDiff.z) - val velocityDiffMagnitude: Float = velocityDiff.len() - if (velocityDiffMagnitude < MAX_SCALAR_DORMANT) { - return PARAM_SCALAR_MAX - } else if (velocityDiffMagnitude > MIN_SCALAR_DORMANT) { - return ( - PARAM_SCALAR_MAX - * - (velocityDiffMagnitude - MIN_SCALAR_DORMANT) / - (MAX_SCALAR_DORMANT - MIN_SCALAR_DORMANT) - ) - } - } - - // calculate the 'unlockedness factor' and use that to - // determine the scalar (go as low as 0.5 and as high as - // param_scalar_max) - val velocityDiffAbs: Float = abs(primaryFootVelMagnitude - otherFootVelMagnitude) - if (velocityDiffAbs > MIN_SCALAR_ACTIVE) { - return PARAM_SCALAR_MIN - } else if (velocityDiffAbs < MAX_SCALAR_ACTIVE) { - return PARAM_SCALAR_MAX - } - return ( - PARAM_SCALAR_MAX - * - (velocityDiffAbs - MIN_SCALAR_ACTIVE) / - (MAX_SCALAR_ACTIVE - MIN_SCALAR_ACTIVE) - - PARAM_SCALAR_MID - ) - } - - // get the pressure prediction for the feet based of the center of mass - // (assume mass is 1) - // for understanding in the future this is assuming that the mass is one and - // the force of gravity - // is 9.8 m/s^2 this allows for the force sum to map directly to the - // acceleration of the center of mass - // since F = ma and if m is 1 then F = a - private fun getPressurePrediction(): FloatArray { - // get the vectors from the com to each foot - val leftFootVector: Vector3 = (leftFootPosition - centerOfMass).unit() - val rightFootVector: Vector3 = (rightFootPosition - centerOfMass).unit() - - // get the magnitude of the force on each foot - val leftFootMagnitude: Float = - GRAVITY_MAGNITUDE * leftFootVector.y / leftFootVector.len() - val rightFootMagnitude: Float = ( - GRAVITY_MAGNITUDE - * - rightFootVector.y / - rightFootVector.len() - ) - - // get the force vector each foot could apply to the com - val leftFootForce: Vector3 = leftFootVector * (leftFootMagnitude / 2.0f) - val rightFootForce: Vector3 = rightFootVector * (rightFootMagnitude / 2.0f) - - // based off the acceleration of the com, get the force each foot is - // likely applying (the expected force sum should be equal to - // centerOfMassAcceleration since the mass is 1) - val (modifiedLeftFootForce, modifiedRightFootForce) = - findForceVectors(leftFootForce, rightFootForce) - - // see if the force vectors found a reasonable solution - // if they did not we assume there is another force acting on the com - // and fall back to a low pressure prediction - if (detectOutsideForces(modifiedLeftFootForce, modifiedRightFootForce)) { - isStanding = false - return FORCE_VECTOR_FALLBACK - } - isStanding = true - - // set the pressure to the force on each foot times the force to - // pressure scalar - var leftFootPressure = modifiedLeftFootForce.hadamard(FORCE_VECTOR_TO_PRESSURE).len() - var rightFootPressure = modifiedRightFootForce.hadamard(FORCE_VECTOR_TO_PRESSURE).len() - - // distance from the ground is a factor in the pressure - // using the inverse of the distance to the ground scale the - // pressure - val leftDistance: Float = - if (leftFootPosition.y > (leftFloorLevel + FLOOR_DISTANCE_CUTOFF)) { - leftFootPosition.y - (leftFloorLevel + FLOOR_DISTANCE_CUTOFF) - } else { - LegTweaks.NEARLY_ZERO - } - leftFootPressure *= 1.0f / leftDistance - val rightDistance: Float = - if (rightFootPosition.y > (rightFloorLevel + FLOOR_DISTANCE_CUTOFF)) { - rightFootPosition.y - (rightFloorLevel + FLOOR_DISTANCE_CUTOFF) - } else { - LegTweaks.NEARLY_ZERO - } - rightFootPressure *= 1.0f / rightDistance - - // normalize the pressure values - val pressureSum = leftFootPressure + rightFootPressure - leftFootPressure /= pressureSum - rightFootPressure /= pressureSum - return floatArrayOf(leftFootPressure, rightFootPressure) - } - - // perform a gradient descent to find the force vectors that best match the - // acceleration of the com - private fun findForceVectors(leftFootForceInit: Vector3, rightFootForceInit: Vector3): List { - var leftFootForce: Vector3 = leftFootForceInit - var rightFootForce: Vector3 = rightFootForceInit - val iterations = 100 - val stepSize = 0.01f - // set up the temporary variables - var tempLeftFootForce1: Vector3 - var tempLeftFootForce2: Vector3 - var tempRightFootForce1: Vector3 - var tempRightFootForce2: Vector3 - var error: Vector3 - var error1: Vector3 - var error2: Vector3 - var error3: Vector3 - var error4: Vector3 - for (i in 0 until iterations) { - tempLeftFootForce1 = leftFootForce - tempLeftFootForce2 = leftFootForce - tempRightFootForce1 = rightFootForce - tempRightFootForce2 = rightFootForce - - // get the error at the current position - error = centerOfMassAcceleration - (leftFootForce + rightFootForce + GRAVITY) - - // add and subtract the error to the force vectors - tempLeftFootForce1 *= (1.0f + stepSize) - tempLeftFootForce2 *= (1.0f - stepSize) - tempRightFootForce1 *= (1.0f + stepSize) - tempRightFootForce2 *= (1.0f - stepSize) - - // get the error at the new position - error1 = getForceVectorError(tempLeftFootForce1, rightFootForce) - error2 = getForceVectorError(tempLeftFootForce2, rightFootForce) - error3 = getForceVectorError(tempRightFootForce1, leftFootForce) - error4 = getForceVectorError(tempRightFootForce2, leftFootForce) - - // set the new force vectors - if (error1.len() < error.len()) { - leftFootForce = tempLeftFootForce1 - } else if (error2.len() < error.len()) { - leftFootForce = tempLeftFootForce2 - } - if (error3.len() < error.len()) { - rightFootForce = tempRightFootForce1 - } else if (error4.len() < error.len()) { - rightFootForce = tempRightFootForce2 - } - } - - return listOf(leftFootForce, rightFootForce) - } - - // detect any outside forces on the body such - // as a wall or a chair. returns true if there is an outside force - private fun detectOutsideForces(f1: Vector3, f2: Vector3): Boolean { - val force: Vector3 = GRAVITY + f1 + f2 - val error: Vector3 = centerOfMassAcceleration - force - return error.lenSq() > FORCE_ERROR_TOLERANCE_SQR - } - - // simple error function for the force vector gradient descent - private fun getForceVectorError(testForce: Vector3, otherForce: Vector3): Vector3 = centerOfMassAcceleration - (testForce + otherForce + GRAVITY) -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/Localizer.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/Localizer.kt deleted file mode 100644 index 225b402f2d..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/Localizer.kt +++ /dev/null @@ -1,417 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import com.jme3.math.FastMath -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 - -/* Handles localizing the skeleton in 3d space when no 6dof device is present. - * This is accomplished by using the foot state calculated by legtweaks. This of course - * has the problem of the true position and predicted location drifting apart over time. - * Jumping is quite unreliable - */ - -enum class MovementStates { - LEFT_LOCKED, - RIGHT_LOCKED, - NONE_LOCKED, - FOLLOW_FOOT, - FOLLOW_COM, - FOLLOW_SITTING, -} - -class Localizer(humanSkeleton: HumanSkeleton) { - // hyper parameters - companion object { - private const val WARMUP_FRAMES = 100 // ~0.1 seconds - private const val MAX_FOOT_PERCENTAGE = 50.0f - private const val MAX_ACCEL_UP = 2.0f - private const val SITTING_KNEE_THRESHOLD = 1.1f - private const val SITTING_EARLY = 1000f - private const val VELOCITY_SAMPLE_RATE: Long = 100000000 // 10ms - private const val CONSTANT_ACCELERATION: Float = 2.0f - } - - private val skeleton: HumanSkeleton = humanSkeleton - private val legTweaks: LegTweaks = skeleton.legTweaks - private var bufCur: LegTweaksBuffer = legTweaks.bufferHead - private var bufPrev: LegTweaksBuffer = LegTweaksBuffer() - - // state variables - private var enabled: Boolean = false - private var targetFoot: Vector3 = Vector3.NULL - private var currentCOM: Vector3 = Vector3.NULL - private var targetCOM: Vector3 = Vector3.NULL - private var targetHip: Vector3 = Vector3.NULL - private var comVelocity: Vector3 = Vector3.NULL - private var comAccel: Vector3 = Vector3.NULL - private var plantedFoot = MovementStates.LEFT_LOCKED - private var worldReference = MovementStates.FOLLOW_FOOT - private var uncorrectedFloor = 0.0f - LegTweaks.FLOOR_CALIBRATION_OFFSET - private var floor = 0.0f - private var warmupFrames = 0 - private var comFrames = 0 - private var footFrames = 0 - private var sittingFrames = 0 - - // travel from different sources - private var footTravel: Vector3 = Vector3.NULL - private var comTravel: Vector3 = Vector3.NULL - private var sittingTravel: Vector3 = Vector3.NULL - - fun getEnabled(): Boolean = enabled - - fun setEnabled(enabled: Boolean) { - this.enabled = enabled - legTweaks.setLocalizerMode(enabled) - } - - fun update() { - if (!enabled) { - return - } - - // if there is a 6dof device just use it - if (skeleton.headTracker != null && skeleton.headTracker!!.hasPosition) { - return - } - - // set the acceleration of the com for this frame - comAccel = getTorsoAccel() - - if (warmupFrames < WARMUP_FRAMES) { - comVelocity = Vector3.NULL - targetFoot = Vector3.NULL - } - warmupFrames++ - - // set the buffers for easy access - bufCur = legTweaks.bufferHead - if (bufCur.parent == null) { - return - } - bufPrev = bufCur.parent!! - - var finalTravel: Vector3 - - // get the movement of the skeleton by foot travel - footTravel = getPlantedFootTravel() - - // get the movement of the skeleton by the previous COM velocity - comTravel = getCOMTravel() - - sittingTravel = computeSittingTravel() - - // get the metric that this frame should rely on - worldReference = getWorldReference() - - // update the final travel vector - if (worldReference == MovementStates.FOLLOW_FOOT || warmupFrames < WARMUP_FRAMES) { - finalTravel = footTravel - } else if (worldReference == MovementStates.FOLLOW_COM) { - finalTravel = comTravel - } else if (worldReference == MovementStates.FOLLOW_SITTING) { - finalTravel = sittingTravel - if (sittingFrames < SITTING_EARLY) { - finalTravel = footTravel - } - } else { - finalTravel = Vector3.NULL - } - - // update the y value - if (worldReference != MovementStates.FOLLOW_SITTING || sittingFrames < SITTING_EARLY) { - finalTravel = Vector3( - finalTravel.x, - comTravel.y, - finalTravel.z, - ) - } - - updateSkeletonPos(finalTravel) - } - - // resets to the starting position - fun reset() { - if (!enabled) return - - skeleton.headBone.setPosition(Vector3.NULL) - comVelocity = Vector3.NULL - - // when localizing without a 6 dof device we choose the floor level - // 0 happens to be an easy number to use - legTweaks.setLocalizerMode(enabled) - floor = 0.0f - uncorrectedFloor = 0.0f - LegTweaks.FLOOR_CALIBRATION_OFFSET - warmupFrames = 0 - } - - private fun getPlantedFoot(): MovementStates { - // if locked in legtweaks it's the locked foot - if (bufCur.leftLegState == LegTweaksBuffer.LOCKED) return MovementStates.LEFT_LOCKED - if (bufCur.rightLegState == LegTweaksBuffer.LOCKED) return MovementStates.RIGHT_LOCKED - - // if the state is not locked, use the numerical state to determine a - // foot to follow - val leftNumericalState = bufCur.leftLegNumericalState - val rightNumericalState = bufCur.rightLegNumericalState - - return if (leftNumericalState < rightNumericalState && - leftNumericalState < MAX_FOOT_PERCENTAGE && - bufCur.leftFootAcceleration.y < MAX_ACCEL_UP - ) { - return MovementStates.LEFT_LOCKED - } else if (rightNumericalState < leftNumericalState && - rightNumericalState < MAX_FOOT_PERCENTAGE && - bufCur.rightFootAcceleration.y < MAX_ACCEL_UP - ) { - MovementStates.RIGHT_LOCKED - } else { - MovementStates.NONE_LOCKED - } - } - - // check if the foot that is planted is actually planted - private fun getWorldReference(): MovementStates { - // check for sitting position - if (isUserSitting()) { - return MovementStates.FOLLOW_SITTING - } - - // if the foot is not on the ground, use the COM - return if (!isFootOnGround()) { - MovementStates.FOLLOW_COM - } else { - MovementStates.FOLLOW_FOOT - } - } - - // get the foot or feet that are planted - // also sets the planted foot, foot init, and target pos variables - private fun getPlantedFootTravel(): Vector3 { - // get the foot that is planted - val foot: MovementStates = getPlantedFoot() - - if (foot == MovementStates.LEFT_LOCKED) { - val footLoc: Vector3 = bufCur.leftFootPosition - updateTargetPos(footLoc, foot) - return getFootTravel(footLoc) - } else if (foot == MovementStates.RIGHT_LOCKED) { - val footLoc: Vector3 = bufCur.rightFootPosition - updateTargetPos(footLoc, foot) - return getFootTravel(footLoc) - } - return Vector3.NULL - } - - // get the travel of a foot over a frame - private fun getFootTravel(loc: Vector3): Vector3 = loc - targetFoot - - // update the target position of the foot - private fun updateTargetPos(loc: Vector3, foot: MovementStates) { - if (foot == plantedFoot) { - if (worldReference == MovementStates.FOLLOW_COM) { - targetFoot = loc - } - } else { - targetFoot = loc - plantedFoot = foot - } - } - - // get the sitting travel (emulates hip lock) - private fun computeSittingTravel(): Vector3 { - val hip = skeleton.computedHipTracker?.position ?: Vector3.NULL - - // get the distance to move the waist to the target waist - val dist: Vector3 = hip - targetHip - - val lowTracker = getLowestTracker() - - if (lowTracker != null) { - if (lowTracker.position.y < uncorrectedFloor) { - targetHip = Vector3(targetHip.x, targetHip.y + (uncorrectedFloor - lowTracker.position.y), targetHip.z) - } - } - - // if the world reference is not sitting update the target waist - if (worldReference != MovementStates.FOLLOW_SITTING || sittingFrames < SITTING_EARLY) { - targetHip = hip - } - return dist - } - - // returns the travel of the COM from its last position - private fun getCOMTravel(): Vector3 { - // update COM attributes - updateCOMAttributes() - return bufCur.centerOfMass - targetCOM - } - - // get the movement of the COM based on the last velocity - private fun updateCOMAttributes() { - getCOMVelocity() - updateTargetCOM() - - // update how long the COM has been the reference and how long the foot - // has been - comFrames = if (worldReference == MovementStates.FOLLOW_COM) comFrames + 1 else 0 - footFrames = if (worldReference == MovementStates.FOLLOW_FOOT) footFrames + 1 else 0 - sittingFrames = if (worldReference == MovementStates.FOLLOW_SITTING) sittingFrames + 1 else 0 - } - - // gets the position the COM should be at based on the velocity of the com and - // the location of the floor - private fun updateTargetCOM() { - // if not in COM tracking mode, just use the current COM - if (worldReference == MovementStates.FOLLOW_FOOT || worldReference == MovementStates.FOLLOW_SITTING) { - targetCOM = bufCur.centerOfMass - } else { - currentCOM = targetCOM - } - - targetCOM += (comVelocity / bufCur.getTimeDelta()) - - val lowTracker = getLowestTracker() - - // update the target COM and velocity to reflect this new distance - if (lowTracker != null) { - if (lowTracker.position.y < uncorrectedFloor) { - targetCOM = Vector3(targetCOM.x, targetCOM.y + (uncorrectedFloor - lowTracker.position.y), targetCOM.z) - comVelocity = Vector3(comVelocity.x, 0.0f, comVelocity.z) - } - } - } - - // get the velocity of the COM - private fun getCOMVelocity(): Vector3 { - val comY = comVelocity.y - - var buf = bufCur - val timeStart: Long = buf.timeOfFrame - var timeEnd = timeStart - VELOCITY_SAMPLE_RATE - val comPosStart: Vector3 = buf.centerOfMass - - // get the buffer that occurred VELOCITY_SAMPLE_RATE ago in time - while (buf.timeOfFrame > timeEnd && buf.parent != null) { - buf = buf.parent!! - } - - val comPosEnd: Vector3 = buf.centerOfMass - timeEnd = buf.timeOfFrame - - // calculate the velocity - comVelocity = (comPosEnd - comPosStart) / ((timeEnd - timeStart) / LegTweaksBuffer.NS_CONVERT) - - // if the feet have been the reference for a short amount of time nullify any upwards acceleration to prevent flying away - if (footFrames < WARMUP_FRAMES) { - comAccel = Vector3( - comAccel.x, - FastMath.clamp(comAccel.y, -9999.0f, 0.0f), - comAccel.z, - ) - } - - // constantly pull the skeleton down a little to account for acceleration - // inaccuracy - val gravity = comAccel.y - CONSTANT_ACCELERATION - - // add the acceleration of gravity - comVelocity = Vector3( - comVelocity.x, - comY + (gravity / bufCur.getTimeDelta()), - comVelocity.z, - ) - - return comVelocity - } - - // returns true if either foot is below 0.0 - private fun isFootOnGround(): Boolean = ( - bufCur.leftFootPosition.y <= floor || - bufCur.rightFootPosition.y <= floor - ) - - // returns the tracker closest to or the furthest in the ground - private fun getLowestTracker(): Tracker? { - val trackerList = arrayOf( - skeleton.computedHeadTracker, - skeleton.computedChestTracker, - skeleton.computedHipTracker, - skeleton.computedLeftElbowTracker, - skeleton.computedRightElbowTracker, - skeleton.computedLeftHandTracker, - skeleton.computedRightHandTracker, - skeleton.computedLeftKneeTracker, - skeleton.computedRightKneeTracker, - skeleton.computedLeftFootTracker, - skeleton.computedRightFootTracker, - ) - - var minVal = trackerList[0]?.position?.y - var retVal: Tracker? = trackerList[0] - for (tracker in trackerList) { - if (tracker == null) { - continue - } - - if (tracker.position.y < minVal!!) { - minVal = tracker.position.y - retVal = tracker - } - } - - return retVal - } - - // returns true if the user is likely sitting - // (assumes the floor is flat at 0.0) - private fun isUserSitting(): Boolean { - // based on the waist to knee vector decide if the user is sitting or - // standing (ie, if the user is sitting the vector will be pointing off - // to the side for both feet) - var leftKnee: Vector3 = bufCur.leftKneePosition - var rightKnee: Vector3 = bufCur.rightKneePosition - val hip: Vector3 = skeleton.computedHipTracker?.position ?: Vector3.NULL - leftKnee = hip - leftKnee - rightKnee = hip - rightKnee - - // if the y component of the vectors is small then the user is probably - // sitting - var left = false - var right = false - if (leftKnee.y * SITTING_KNEE_THRESHOLD < leftKnee.x + leftKnee.z) { - left = true - } - if (rightKnee.y * SITTING_KNEE_THRESHOLD < rightKnee.x + rightKnee.z) { - right = true - } - return !bufCur.isStanding || (left && right) - } - - // get the combined accel of the Torso trackers - private fun getTorsoAccel(): Vector3 { - var num = 0.0f - var accel = Vector3.NULL - if (skeleton.waistTracker != null) { - accel += skeleton.waistTracker!!.getAcceleration() - num++ - } else if (skeleton.hipTracker != null) { - accel += skeleton.hipTracker!!.getAcceleration() - num++ - } else if (skeleton.chestTracker != null) { - accel += skeleton.chestTracker!!.getAcceleration() - num++ - } - return if (num == 0f) accel else accel / num - } - - // update the head position and rotation - private fun updateSkeletonPos(travel: Vector3) { - val rot = skeleton.headTracker?.getRotation() ?: Quaternion.IDENTITY - val temp = skeleton.headBone.getPosition() - travel - - skeleton.headBone.setPosition(temp) - skeleton.headBone.setRotation(rot) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetection.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetection.kt deleted file mode 100644 index 073709eabd..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetection.kt +++ /dev/null @@ -1,163 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import dev.slimevr.tracking.trackers.Tracker -import java.util.* - -// class that monitors the acceleration of the waist, hip, chest or upper chest trackers to detect taps -// and use this to trigger a variety of resets (if your wondering why no single tap class exists, it's because -// to many false positives) -class TapDetection(val skeleton: HumanSkeleton, val trackerToWatch: Tracker, val numberTrackersOverThreshold: Int, val tapToComplete: Int, val onTapCompleted: () -> Unit) { - - private val accelList = LinkedList() - private val tapTimestamps = LinkedList() - private var timeWindowNS = 0.3f * tapToComplete * NS_CONVERTER - private var waitForLowAccel = false - - // reset the lists for detecting taps - fun reset() { - tapTimestamps.clear() - accelList.clear() - waitForLowAccel = false - } - - // main function for tap detection - fun update() { - // get the acceleration of the tracker and add it to the list - val time = System.nanoTime().toFloat() - accelList.add(floatArrayOf(trackerToWatch.getAcceleration().len(), time)) - - // remove old values from the list (if they are too old) - while (time - accelList.first()[1] > CLUMP_TIME_NS) { - accelList.removeFirst() - } - - // check for a tap - val accelDelta = accelDelta - if (accelDelta > NEEDED_ACCEL_DELTA && !waitForLowAccel) { - // after a tap is added to the list, a lower acceleration - // is needed before another tap can be added - tapTimestamps.add(time) - waitForLowAccel = true - } - - // if waiting for low accel - if (maxAccel < ALLOWED_BODY_ACCEL) { - waitForLowAccel = false - } - - // remove old taps from the list (if they are too old) - if (!tapTimestamps.isEmpty()) { - while (time - tapTimestamps.first() > timeWindowNS) { - tapTimestamps.removeFirst() - if (tapTimestamps.isEmpty()) return - } - } - - // if we have no taps within the timeframe or - // if the user is moving their body too much, reset the tap detector - if (!isUserStatic(trackerToWatch)) { - reset() - return - } - - if (tapTimestamps.size >= tapToComplete) { - onTapCompleted() - reset() - } - } - - private val accelDelta: Float - get() { - var max = -999.9f - var min = 999.9f - for (`val` in accelList) { - if (`val`[0] > max) max = `val`[0] - if (`val`[0] < min) min = `val`[0] - } - return max - min - } - - private val maxAccel: Float - get() { - var max = 0.0f - for (`val` in accelList) { - if (`val`[0] > max) { - max = `val`[0] - } - } - return max - } - - // returns true if the user is not imparting more than allowedBodyAccel of - // force on any of the torso or upper leg trackers (this sadly implies that - // you need two or more trackers for this feature to be reliable) - private fun isUserStatic(trackerToExclude: Tracker): Boolean { - var num = 0 - if (skeleton.upperChestTracker != null && - skeleton.upperChestTracker != trackerToExclude - ) { - if (skeleton.upperChestTracker!!.getAcceleration().lenSq() - > ALLOWED_BODY_ACCEL_SQUARED - ) { - num++ - } - } - if (skeleton.chestTracker != null && - skeleton.chestTracker != trackerToExclude - ) { - if (skeleton.chestTracker!!.getAcceleration().lenSq() > ALLOWED_BODY_ACCEL_SQUARED) num++ - } - if (skeleton.hipTracker != null && skeleton.hipTracker != trackerToExclude) { - if (skeleton.hipTracker!!.getAcceleration().lenSq() > ALLOWED_BODY_ACCEL_SQUARED) num++ - } - if (skeleton.waistTracker != null && - skeleton.waistTracker != trackerToExclude - ) { - if (skeleton.waistTracker!!.getAcceleration().lenSq() > ALLOWED_BODY_ACCEL_SQUARED) num++ - } - if (skeleton.leftUpperLegTracker != null && - skeleton.leftUpperLegTracker != trackerToExclude - ) { - if (skeleton.leftUpperLegTracker!!.getAcceleration().lenSq() - > ALLOWED_BODY_ACCEL_SQUARED - ) { - num++ - } - } - if (skeleton.rightUpperLegTracker != null && - skeleton.rightUpperLegTracker != trackerToExclude - ) { - if (skeleton.rightUpperLegTracker!!.getAcceleration().lenSq() - > ALLOWED_BODY_ACCEL_SQUARED - ) { - num++ - } - } - if (skeleton.leftFootTracker != null && - skeleton.leftFootTracker != trackerToExclude - ) { - if (skeleton.leftFootTracker!!.getAcceleration().lenSq() > ALLOWED_BODY_ACCEL_SQUARED) { - num++ - } - } - if (skeleton.rightFootTracker != null && - skeleton.rightFootTracker != trackerToExclude - ) { - if (skeleton.rightFootTracker!!.getAcceleration().lenSq() - > ALLOWED_BODY_ACCEL_SQUARED - ) { - num++ - } - } - return num < numberTrackersOverThreshold - } - - companion object { - // hyperparameters - private const val NS_CONVERTER = 1.0e9f - private const val NEEDED_ACCEL_DELTA = 6.0f - private const val ALLOWED_BODY_ACCEL = 2.5f - private const val ALLOWED_BODY_ACCEL_SQUARED = ALLOWED_BODY_ACCEL * ALLOWED_BODY_ACCEL - private const val CLUMP_TIME_NS = 0.06f * NS_CONVERTER - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.kt deleted file mode 100644 index db942cea71..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import dev.slimevr.VRServer -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.trackers.Tracker -import java.util.concurrent.CopyOnWriteArrayList - -class TapDetectionManager( - val server: VRServer, - val skeleton: HumanSkeleton, - val humanPoseManager: HumanPoseManager, -) { - private var tapDetectors: MutableList = CopyOnWriteArrayList() - var yawResetDetector: TapDetection? = null - var fullResetDetector: TapDetection? = null - var mountingResetDetector: TapDetection? = null - - var config = server.configManager.vrConfig.tapDetection - - init { - refresh() - } - - fun registerSingleTapDetectors() { - for (tracker in server.allTrackers) { - tapDetectors.add( - TapDetection(skeleton, tracker, config.numberTrackersOverThreshold, 2) { - server.tapSetupHandler.sendTap(tracker) - }, - ) - } - } - - fun registerResetsDetectors() { - val yawTracker = yawResetTracker - yawResetDetector = if (yawTracker != null && config.yawResetEnabled) { - TapDetection(skeleton, yawTracker, config.numberTrackersOverThreshold, config.yawResetTaps) { - server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (config.yawResetDelay * 1000).toLong()) - } - } else { - null - } - - val fullTracker = fullResetTracker - fullResetDetector = if (fullTracker != null && config.fullResetEnabled) { - TapDetection(skeleton, fullTracker, config.numberTrackersOverThreshold, config.fullResetTaps) { - server.scheduleResetTrackersFull(RESET_SOURCE_NAME, (config.fullResetDelay * 1000).toLong()) - } - } else { - null - } - - val mountingTracker = mountingResetTracker - mountingResetDetector = if (mountingTracker != null && config.mountingResetEnabled) { - TapDetection(skeleton, mountingTracker, config.numberTrackersOverThreshold, config.mountingResetTaps) { - server.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (config.mountingResetDelay * 1000).toLong()) - } - } else { - null - } - } - - /** - * Called when the list of available trackers gets updated - * or when the tap settings get changed - * it re-create the tap detectors according to the configs and available trackers - */ - fun refresh() { - tapDetectors.clear() - registerSingleTapDetectors() - registerResetsDetectors() - } - - fun update() { - // We disable the resets detectors during the assignment phase so you cant - // trigger a reset while assigning - if (config.setupMode) { - for (detector in tapDetectors) { - detector.update() - } - } else { - yawResetDetector?.update() - fullResetDetector?.update() - mountingResetDetector?.update() - } - } - - private val mountingResetTracker: Tracker? - get() { - return arrayOf( - skeleton.rightUpperLegTracker, - skeleton.rightLowerLegTracker, - ).firstNotNullOfOrNull { it } - } - - private val fullResetTracker: Tracker? - get() { - return arrayOf( - skeleton.leftUpperLegTracker, - skeleton.leftLowerLegTracker, - ).firstNotNullOfOrNull { it } - } - - private val yawResetTracker: Tracker? - get() { - return arrayOf( - skeleton.upperChestTracker, - skeleton.chestTracker, - skeleton.hipTracker, - skeleton.waistTracker, - ).firstNotNullOfOrNull { it } - } - - companion object { - const val RESET_SOURCE_NAME: String = "TapDetection" - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/UserHeightCalibration.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/UserHeightCalibration.kt deleted file mode 100644 index b87d64cf02..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/UserHeightCalibration.kt +++ /dev/null @@ -1,317 +0,0 @@ -package dev.slimevr.tracking.processor.skeleton - -import dev.slimevr.VRServer -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.github.axisangles.ktmath.Vector3 -import org.apache.commons.collections4.queue.CircularFifoQueue -import solarxr_protocol.rpc.UserHeightCalibrationStatus -import solarxr_protocol.rpc.UserHeightRecordingStatusResponseT -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.max -import kotlin.math.sqrt - -/** - * @param positionSamples The list of Vector3 samples. - * @return The standard deviation as a Float. Returns Float.MAX_VALUE if the list is empty. - */ -fun calculatePositionStdDev(positionSamples: Collection): Float { - val sampleCount = positionSamples.size - if (sampleCount == 0) return Float.MAX_VALUE - - var sumX = 0f - var sumY = 0f - var sumZ = 0f - - for (pos in positionSamples) { - sumX += pos.x - sumY += pos.y - sumZ += pos.z - } - - val meanX = sumX / sampleCount - val meanY = sumY / sampleCount - val meanZ = sumZ / sampleCount - - var varianceSum = 0f - for (pos in positionSamples) { - val dx = pos.x - meanX - val dy = pos.y - meanY - val dz = pos.z - meanZ - // Square of the Euclidean distance from the mean - varianceSum += dx * dx + dy * dy + dz * dz - } - - val variance = varianceSum / sampleCount - return sqrt(variance) -} - -fun isHmdLeveled(hmd: Tracker, threshold: Double): Boolean { - val q = hmd.getRotation() - - val worldHmdUp = q.sandwich(Vector3.POS_Y) - val dotProduct = worldHmdUp.dot(Vector3.POS_Y) - return dotProduct >= threshold -} - -fun isControllerPointingDown(controller: Tracker, threshold: Double): Boolean { - val q = controller.getRawRotation() - val controllerForwardWorld = q.sandwich(Vector3.NEG_Z) - val worldDown = Vector3.NEG_Y - val dotProduct = controllerForwardWorld.dot(worldDown) - - return dotProduct >= threshold -} - -interface UserHeightCalibrationListener { - fun onStatusChange(status: UserHeightRecordingStatusResponseT) -} - -class UserHeightCalibration(val server: VRServer, val humanPoseManager: HumanPoseManager) { - var status = UserHeightCalibrationStatus.NONE - - var currentHeight = 0f - var currentFloorLevel = 0f - - var startTime = 0L - - private val hmdPositionSamples: CircularFifoQueue = CircularFifoQueue(MAX_SAMPLES) - private var heightStableStartTime: Long? = null - - private val floorPositionSamples: CircularFifoQueue = CircularFifoQueue(MAX_SAMPLES) - private var floorStableStartTime: Long? = null - - private val listeners: MutableList = CopyOnWriteArrayList() - private var hmd: Tracker? = null - - private val handTrackers: MutableList = mutableListOf() - - fun start() { - clear() - checkTrackers() - - if (!server.serverGuards.canDoUserHeightCalibration) { - return - } - - startTime = System.nanoTime() - - status = UserHeightCalibrationStatus.RECORDING_FLOOR - currentFloorLevel = Float.MAX_VALUE - - sendStatusUpdate() - } - - fun clear() { - status = UserHeightCalibrationStatus.NONE - hmdPositionSamples.clear() - floorPositionSamples.clear() - heightStableStartTime = null - floorStableStartTime = null - currentHeight = 0f - startTime = 0L - } - - init { - clear() - checkTrackers() - } - - fun checkTrackers() { - handTrackers.clear() - handTrackers.addAll( - server.allTrackers.filter { - ( - it.trackerPosition == TrackerPosition.LEFT_HAND || - it.trackerPosition == TrackerPosition.RIGHT_HAND - ) && - !it.isInternal && - it.hasPosition && - it.status == TrackerStatus.OK - }, - ) - - hmd = server.allTrackers.find { - it.trackerPosition == TrackerPosition.HEAD && - it.hasPosition && - !it.isInternal && - it.status == TrackerStatus.OK - } - server.serverGuards.canDoUserHeightCalibration = hmd != null && handTrackers.isNotEmpty() - - currentHeight = 0f - currentFloorLevel = 0f - } - - fun applyCalibration() { - server.configManager.vrConfig.skeleton.hmdHeight = currentHeight - server.configManager.vrConfig.skeleton.floorHeight = 0f - - server.humanPoseManager.resetOffsets() - server.humanPoseManager.saveConfig() - server.configManager.saveConfig() - - server.trackingChecklistManager.resetMountingCompleted = false - server.trackingChecklistManager.feetResetMountingCompleted = false - } - - fun tick() { - if (startTime == 0L) return - - val currentTime = System.nanoTime() - if (active && currentTime - startTime > TIMEOUT_TIME) { - status = UserHeightCalibrationStatus.ERROR_TIMEOUT - sendStatusUpdate() - return - } - - when (status) { - UserHeightCalibrationStatus.RECORDING_FLOOR, UserHeightCalibrationStatus.WAITING_FOR_CONTROLLER_PITCH -> recordFloor(currentTime) - UserHeightCalibrationStatus.WAITING_FOR_RISE, UserHeightCalibrationStatus.RECORDING_HEIGHT, UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK -> recordHeight(currentTime) - } - } - - private fun recordFloor(currentTime: Long) { - val lowestTracker = handTrackers.minByOrNull { it.position.y } - val currentLowestPos = lowestTracker?.position ?: return - - if (currentLowestPos.y > MAX_FLOOR_Y) { - floorStableStartTime = null - floorPositionSamples.clear() - return - } - - if (!isControllerPointingDown(lowestTracker, CONTROLLER_ANGLE_THRESHOLD)) { - status = UserHeightCalibrationStatus.WAITING_FOR_CONTROLLER_PITCH - floorStableStartTime = null - floorPositionSamples.clear() - sendStatusUpdate() - return - } - - floorPositionSamples.add(currentLowestPos) - currentFloorLevel = minOf(currentFloorLevel, currentLowestPos.y) - - if (floorPositionSamples.isAtFullCapacity) { - val isStable = calculatePositionStdDev(floorPositionSamples) <= CONTROLLER_POSITION_STD_DEV_THRESHOLD - - if (isStable) { - if (floorStableStartTime == null) { - floorStableStartTime = currentTime - } - - val stableDuration = currentTime - floorStableStartTime!! - if (stableDuration >= CONTROLLER_STABILITY_DURATION) { - status = UserHeightCalibrationStatus.WAITING_FOR_RISE - sendStatusUpdate() - } - } else { - floorStableStartTime = null - } - } - } - - private fun recordHeight(currentTime: Long) { - val localHmd = hmd ?: return - - val currentPos = localHmd.position - val relativeY = currentPos.y - currentFloorLevel - - if (relativeY <= HMD_RISE_THRESHOLD) { - status = UserHeightCalibrationStatus.WAITING_FOR_RISE - sendStatusUpdate() - hmdPositionSamples.clear() - heightStableStartTime = null - return - } - - if (currentHeight != relativeY) { - currentHeight = relativeY - sendStatusUpdate() - } - - if (!isHmdLeveled(localHmd, HEAD_ANGLE_THRESHOLD)) { - status = UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK - heightStableStartTime = null - hmdPositionSamples.clear() - sendStatusUpdate() - return - } - - status = UserHeightCalibrationStatus.RECORDING_HEIGHT - hmdPositionSamples.add(currentPos) - - if (hmdPositionSamples.isAtFullCapacity) { - val std = calculatePositionStdDev(hmdPositionSamples) - val isStable = std <= POSITION_STD_DEV_THRESHOLD - - if (isStable) { - if (heightStableStartTime == null) { - heightStableStartTime = currentTime - } - - val stableDuration = currentTime - heightStableStartTime!! - if (stableDuration >= HEAD_STABILITY_DURATION) { - status = if (currentHeight < 1.2f) { - UserHeightCalibrationStatus.ERROR_TOO_SMALL - } else if (currentHeight > 1.936f) { - UserHeightCalibrationStatus.ERROR_TOO_HIGH - } else { - UserHeightCalibrationStatus.DONE - } - - if (status == UserHeightCalibrationStatus.DONE) { - applyCalibration() - } - - sendStatusUpdate() - } - } else { - heightStableStartTime = null - } - } - } - - fun addListener(listener: UserHeightCalibrationListener) { - listeners.add(listener) - } - - fun removeListener(listener: UserHeightCalibrationListener) { - listeners.remove(listener) - } - - fun sendStatusUpdate() { - val res = UserHeightRecordingStatusResponseT().apply { - this.status = this@UserHeightCalibration.status - this.hmdHeight = this@UserHeightCalibration.currentHeight - } - listeners.forEach { it.onStatusChange(res) } - } - - val active: Boolean - get() { - return status == UserHeightCalibrationStatus.RECORDING_HEIGHT || status == UserHeightCalibrationStatus.RECORDING_FLOOR || status == UserHeightCalibrationStatus.WAITING_FOR_RISE || status == UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK - } - - companion object { - private const val MAX_SAMPLES = 100 - - private const val POSITION_STD_DEV_THRESHOLD = 0.003f - private const val HEAD_STABILITY_DURATION = 600_000_000f - - private const val CONTROLLER_POSITION_STD_DEV_THRESHOLD = 0.005f - private const val CONTROLLER_STABILITY_DURATION = 300_000_000f - - private const val MAX_FLOOR_Y = 0.10f - private const val HMD_RISE_THRESHOLD = 1.2f - - val HEAD_ANGLE_THRESHOLD = cos((PI / 180f) * 15f) - val CONTROLLER_ANGLE_THRESHOLD = cos((PI / 180f) * 45f) - - private const val TIMEOUT_TIME = 30_000_000_000f - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAligned.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAligned.kt deleted file mode 100644 index e31d0e7fd0..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAligned.kt +++ /dev/null @@ -1,60 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned - -import dev.slimevr.VRServer -import dev.slimevr.config.StayAlignedConfig -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.IMU_TO_YAW_CORRECTION -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.YAW_CORRECTION_DEFAULT -import dev.slimevr.tracking.processor.stayaligned.adjust.AdjustTrackerYaw -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton - -/** - * Manager to keep the trackers aligned. - */ -object StayAligned { - - private var nextTrackerIndex = 0 - - /** - * Adjusts the yaw of the next tracker. - * - * We only adjust one tracker per tick to minimize CPU usage. When the server is - * running at 1000 Hz and there are 20 trackers, each tracker is still updated 50 - * times a second. - */ - fun adjustNextTracker(trackers: TrackerSkeleton, config: StayAlignedConfig) { - if (!config.enabled) { - return - } - - val numTrackers = trackers.allTrackers.size - if (numTrackers == 0) { - return - } - - val trackerToAdjust = trackers.allTrackers[nextTrackerIndex % numTrackers] - ++nextTrackerIndex - - // Update hide correction since the config could have changed - trackerToAdjust.stayAligned.hideCorrection = config.hideYawCorrection - - val yawCorrectionPerSec = - IMU_TO_YAW_CORRECTION.getOrDefault(trackerToAdjust.imuType, YAW_CORRECTION_DEFAULT) - if (yawCorrectionPerSec == Angle.ZERO) { - return - } - - // Scale yaw correction since we're only updating one tracker per tick - val yawCorrection = - yawCorrectionPerSec * - VRServer.instance.fpsTimer.timePerFrame * - numTrackers.toFloat() - - AdjustTrackerYaw.adjust( - trackerToAdjust, - trackers, - yawCorrection, - config, - ) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAlignedDefaults.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAlignedDefaults.kt deleted file mode 100644 index 330475f82e..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/StayAlignedDefaults.kt +++ /dev/null @@ -1,72 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned - -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.processor.stayaligned.trackers.RestDetector -import dev.slimevr.tracking.trackers.udp.IMUType -import kotlin.time.Duration.Companion.seconds - -/** - * All non-user-configurable defaults used by Stay Aligned, so that we can tune the - * algorithm from a single place. - */ -object StayAlignedDefaults { - - // Rest detector for detecting when trackers are at rest - fun makeRestDetector() = RestDetector( - maxRotation = Angle.ofDeg(2.0f), - enterRestTime = 1.seconds, - enterMovingTime = 3.seconds, - ) - - // Relaxed pose for kneeling. This isn't that common, so we don't want to ask - // players to provide this relaxed pose during setup. - val RELAXED_POSE_KNEELING = - RelaxedPose( - upperLeg = Angle.ofDeg(0.0f), - lowerLeg = Angle.ofDeg(0.0f), - foot = Angle.ofDeg(0.0f), - ) - - // Weights to calculate the average yaw of the skeleton - const val CENTER_ERROR_HEAD_WEIGHT = 0.5f - const val CENTER_ERROR_UPPER_BODY_WEIGHT = 1.0f - const val CENTER_ERROR_UPPER_LEG_WEIGHT = 0.4f - const val CENTER_ERROR_LOWER_LEG_WEIGHT = 0.3f - - // Weight of each force - const val YAW_ERRORS_LOCKED_ERROR_WEIGHT = 10.0f - const val YAW_ERRORS_CENTER_ERROR_WEIGHT = 2.0f - const val YAW_ERRORS_NEIGHBOR_ERROR_WEIGHT = 1.0f - - // Yaw correction for each type of IMU - val YAW_CORRECTION_IMU_GOOD = Angle.ofDeg(0.15f) - val YAW_CORRECTION_IMU_OK = Angle.ofDeg(0.20f) - val YAW_CORRECTION_IMU_BAD = Angle.ofDeg(0.40f) - val YAW_CORRECTION_IMU_DISABLED = Angle.ZERO - - val IMU_TO_YAW_CORRECTION = buildMap { - // Mag is enabled on MPU9250 but server doesn't know about it - set(IMUType.MPU9250, YAW_CORRECTION_IMU_DISABLED) - set(IMUType.MPU6500, YAW_CORRECTION_IMU_BAD) - set(IMUType.BNO080, YAW_CORRECTION_IMU_GOOD) - set(IMUType.BNO085, YAW_CORRECTION_IMU_GOOD) - set(IMUType.BNO055, YAW_CORRECTION_IMU_BAD) - set(IMUType.MPU6050, YAW_CORRECTION_IMU_BAD) - set(IMUType.BNO086, YAW_CORRECTION_IMU_GOOD) - set(IMUType.BMI160, YAW_CORRECTION_IMU_BAD) - set(IMUType.ICM20948, YAW_CORRECTION_IMU_BAD) - set(IMUType.ICM42688, YAW_CORRECTION_IMU_OK) - set(IMUType.BMI270, YAW_CORRECTION_IMU_OK) - set(IMUType.LSM6DS3TRC, YAW_CORRECTION_IMU_BAD) - set(IMUType.LSM6DSV, YAW_CORRECTION_IMU_GOOD) - set(IMUType.LSM6DSO, YAW_CORRECTION_IMU_OK) - set(IMUType.LSM6DSR, YAW_CORRECTION_IMU_GOOD) - set(IMUType.ICM45686, YAW_CORRECTION_IMU_GOOD) - set(IMUType.ICM45605, YAW_CORRECTION_IMU_GOOD) - } - - // Assume any new IMUs are at least OK, or else we wouldn't be writing firmware to - // support it. Please classify and add new IMUs to the map above! - val YAW_CORRECTION_DEFAULT = YAW_CORRECTION_IMU_OK -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/AdjustTrackerYaw.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/AdjustTrackerYaw.kt deleted file mode 100644 index 73bc8268f8..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/AdjustTrackerYaw.kt +++ /dev/null @@ -1,185 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.config.StayAlignedConfig -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.YAW_ERRORS_CENTER_ERROR_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.YAW_ERRORS_LOCKED_ERROR_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.YAW_ERRORS_NEIGHBOR_ERROR_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.poses.PlayerPose -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.processor.stayaligned.trackers.RestDetector -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton -import dev.slimevr.tracking.processor.stayaligned.trackers.YawErrors -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.udp.MagnetometerStatus - -object AdjustTrackerYaw { - - /** - * Adjusts the yaw of a tracker. - * - * Locked Trackers - * --------------- - * After a tracker is at rest for a short time, we lock it and save its initial - * rotation. We assume that locked trackers really are at rest, and that any - * rotation is due to drift. We adjust the tracker's yaw towards its initial - * rotation. If the tracker rotates beyonds a certain angle, we unlock the - * tracker. - * - * This works very well when the player is still and the tracker is supported by - * some surface, e.g. sitting in a chair or lying in a bed. However, it does not - * work well when the player is standing or moving around because the trackers - * will never lock. - * - * Centering Force - * --------------- - * When the player is moving around, we assume that the player will often be in - * a relaxed pose, or will eventually return to a relaxed pose. During setup, we - * collect the player's relaxed posed when standing, sitting and lying on their - * back. - * - * The centering force adjusts the tracker's yaw towards the relaxed pose. - * Upper body trackers are adjusted towards the average yaw of the body. Leg - * trackers are also adjusted towards the average yaw of the body, but with a - * yaw offset corresponding to their relaxed pose. - * - * This works well when the player is moving a lot. However, it doesn't work - * well when some of the trackers are locked, and others are moving. The locked - * trackers will stay in place, while the moving trackers will pull towards the - * relaxed pose, which can result in imbalanced poses. - * - * Neighbor Trackers - * ----------------- - * The neighboring force adjusts the tracker's yaw so that it is balanced - * between its neighboring trackers. For example, if the player is standing in a - * very wide stance, the neighboring force will push the upper leg tracker to a - * position that is proportional to their relaxed pose. This keeps the poses - * balanced. - * - * We use gradient descent to find the direction to apply a yaw correction. By - * applying this 50 times a second, the whole body is nudged into a reasonable - * alignment. - */ - fun adjust( - tracker: Tracker, - trackers: TrackerSkeleton, - yawCorrection: Angle, - config: StayAlignedConfig, - ) { - // Only IMUs can drift - if (!tracker.isImu()) { - return - } - - // Skip trackers that use magnetometer, because the magnetometer should know the - // absolute yaw of the tracker - if (tracker.magStatus == MagnetometerStatus.ENABLED) { - return - } - - // Clear errors, in case we don't adjust the tracker - val state = tracker.stayAligned - state.yawErrors = YawErrors() - - val restDetector = state.restDetector - when (restDetector.state) { - RestDetector.State.MOVING -> - adjustMovingTracker(tracker, trackers, yawCorrection, config) - - RestDetector.State.AT_REST -> - adjustLockedTracker(tracker, trackers, yawCorrection) - - RestDetector.State.RECENTLY_AT_REST -> { - // Do not adjust trackers that were recently at rest, to support play - // styles that are primarily at rest - } - } - } - - /** - * Adjusts a locked tracker. - */ - private fun adjustLockedTracker( - tracker: Tracker, - trackers: TrackerSkeleton, - yawCorrection: Angle, - ) { - val lockedRotation = tracker.stayAligned.lockedRotation ?: return - - adjustByError(tracker, yawCorrection) { - YawErrors().also { - trackers.visit(tracker, LockedErrorVisitor(lockedRotation, it.lockedError)) - } - } - } - - /** - * Adjusts a tracker that is moving. - */ - private fun adjustMovingTracker( - tracker: Tracker, - trackers: TrackerSkeleton, - yawCorrection: Angle, - config: StayAlignedConfig, - ) { - val centerYaw = CenterYaw.ofSkeleton(trackers) ?: return - - val pose = PlayerPose.ofTrackers(trackers) - val relaxedPose = RelaxedPose.forPose(pose, config) ?: return - - adjustByError(tracker, yawCorrection) { - YawErrors().also { - trackers.visit(tracker, CenterErrorVisitor(centerYaw, relaxedPose, it.centerError)) - trackers.visit(tracker, NeighborErrorVisitor(relaxedPose, it.neighborError)) - } - } - } - - /** - * Adjusts the yaw by applying gradient descent. - */ - private fun adjustByError( - tracker: Tracker, - yawCorrection: Angle, - errorFn: (tracker: Tracker) -> YawErrors, - ) { - val state = tracker.stayAligned - - val curYaw = state.yawCorrection - val curError = errorFn(tracker) - - val posYaw = curYaw + yawCorrection - state.yawCorrection = posYaw - val posError = errorFn(tracker) - - val negYaw = curYaw - yawCorrection - state.yawCorrection = negYaw - val negError = errorFn(tracker) - - val posYawDelta = gradient(posError, curError) - val negYawDelta = gradient(negError, curError) - - // Pick the yaw correction that minimizes the error - if ((posYawDelta < Angle.ZERO) && (posYawDelta < negYawDelta)) { - state.yawCorrection = posYaw - state.yawErrors = posError - } else if (negYawDelta < Angle.ZERO) { - state.yawCorrection = negYaw - state.yawErrors = negError - } else { - state.yawCorrection = curYaw - state.yawErrors = curError - } - } - - /** - * Calculates the gradient between two errors. A negative gradient means that there - * is less error in that direction. - */ - private fun gradient(errors: YawErrors, base: YawErrors) = (errors.lockedError.toL2Norm() - base.lockedError.toL2Norm()) * - YAW_ERRORS_LOCKED_ERROR_WEIGHT + - (errors.centerError.toL2Norm() - base.centerError.toL2Norm()) * - YAW_ERRORS_CENTER_ERROR_WEIGHT + - (errors.neighborError.toL2Norm() - base.neighborError.toL2Norm()) * - YAW_ERRORS_NEIGHBOR_ERROR_WEIGHT -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterErrorVisitor.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterErrorVisitor.kt deleted file mode 100644 index 52fcf229df..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterErrorVisitor.kt +++ /dev/null @@ -1,92 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.math.Angle -import dev.slimevr.math.AngleErrors -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.extraYaw -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.trackerYaw -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.processor.stayaligned.trackers.Side -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton -import dev.slimevr.tracking.trackers.Tracker - -/** - * Assumes that the body is centered around an average yaw, and returns the error of the - * tracker with respect to that average yaw. - */ -class CenterErrorVisitor( - val centerYaw: Angle, - val relaxedPose: RelaxedPose, - val errors: AngleErrors, -) : TrackerSkeleton.TrackerVisitor { - - override fun visitHeadTracker( - tracker: Tracker, - belowUpperBody: Tracker?, - ) { - errors.add(centerYaw - trackerYaw(tracker)) - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowUpperBody: Tracker?, - ) { - errors.add(centerYaw - trackerYaw(tracker)) - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowLeftUpperLeg: Tracker?, - belowRightUpperLeg: Tracker?, - ) { - errors.add(centerYaw - trackerYaw(tracker)) - } - - override fun visitArmTracker( - side: Side, - tracker: Tracker, - aboveUpperBodyOrArm: Tracker?, - belowHandOrArm: Tracker?, - ) { - // No error because arms can go anywhere - } - - override fun visitHandTracker( - side: Side, - tracker: Tracker, - aboveArm: Tracker?, - oppositeHand: Tracker?, - ) { - // No error because hands can go anywhere - } - - override fun visitUpperLegTracker( - side: Side, - tracker: Tracker, - aboveUpperBody: Tracker?, - belowLowerLeg: Tracker?, - oppositeUpperLeg: Tracker?, - ) { - errors.add(centerYaw + extraYaw(side, relaxedPose.upperLeg) - trackerYaw(tracker)) - } - - override fun visitLowerLegTracker( - side: Side, - tracker: Tracker, - aboveUpperLeg: Tracker?, - belowFoot: Tracker?, - oppositeLowerLeg: Tracker?, - ) { - errors.add(centerYaw + extraYaw(side, relaxedPose.lowerLeg) - trackerYaw(tracker)) - } - - override fun visitFootTracker( - side: Side, - tracker: Tracker, - aboveLowerLeg: Tracker?, - oppositeFoot: Tracker?, - ) { - errors.add(centerYaw + extraYaw(side, relaxedPose.foot) - trackerYaw(tracker)) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterYaw.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterYaw.kt deleted file mode 100644 index 9c2d6c26eb..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/CenterYaw.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.math.Angle -import dev.slimevr.math.AngleAverage -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.CENTER_ERROR_HEAD_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.CENTER_ERROR_LOWER_LEG_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.CENTER_ERROR_UPPER_BODY_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults.CENTER_ERROR_UPPER_LEG_WEIGHT -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.hasTrackerYaw -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.trackerYaw -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton - -object CenterYaw { - - fun ofSkeleton( - trackers: TrackerSkeleton, - ): Angle? { - val head = trackers.head - val upperBody = trackers.upperBody - val leftUpperLeg = trackers.leftUpperLeg - val rightUpperLeg = trackers.rightUpperLeg - val leftLowerLeg = trackers.leftLowerLeg - val rightLowerLeg = trackers.rightLowerLeg - - if ( - // Head optional, because some mocap scenarios don't use one - upperBody.isEmpty() || - leftUpperLeg == null || - rightUpperLeg == null || - leftLowerLeg == null || - rightLowerLeg == null - ) { - return null - } - - // Need a minimum set of trackers, and the trackers need to be oriented in a - // way where we can actually calculate its yaw. - val hasCenterYaw = - upperBody.all(::hasTrackerYaw) && - hasTrackerYaw(leftUpperLeg) && - hasTrackerYaw(rightUpperLeg) && - hasTrackerYaw(leftLowerLeg) && - hasTrackerYaw(rightLowerLeg) - if (!hasCenterYaw) { - return null - } - - // Calculate average yaw of the body - val averageYaw = AngleAverage() - - if (head != null && hasTrackerYaw(head)) { - averageYaw.add(trackerYaw(head), CENTER_ERROR_HEAD_WEIGHT) - } - - upperBody.forEach { - averageYaw.add(trackerYaw(it), CENTER_ERROR_UPPER_BODY_WEIGHT) - } - - averageYaw.add(trackerYaw(leftUpperLeg), CENTER_ERROR_UPPER_LEG_WEIGHT) - averageYaw.add(trackerYaw(rightUpperLeg), CENTER_ERROR_UPPER_LEG_WEIGHT) - - averageYaw.add(trackerYaw(leftLowerLeg), CENTER_ERROR_LOWER_LEG_WEIGHT) - averageYaw.add(trackerYaw(rightLowerLeg), CENTER_ERROR_LOWER_LEG_WEIGHT) - - return averageYaw.toAngle() - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/LockedErrorVisitor.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/LockedErrorVisitor.kt deleted file mode 100644 index 0d8f04050a..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/LockedErrorVisitor.kt +++ /dev/null @@ -1,143 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.math.Angle -import dev.slimevr.math.AngleErrors -import dev.slimevr.tracking.processor.stayaligned.trackers.Side -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/** - * Error between a locked tracker's yaw and its yaw when it was initially locked. - */ -class LockedErrorVisitor( - val lockedRotation: Quaternion, - val errors: AngleErrors, -) : TrackerSkeleton.TrackerVisitor { - - override fun visitHeadTracker( - tracker: Tracker, - belowUpperBody: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowUpperBody: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowLeftUpperLeg: Tracker?, - belowRightUpperLeg: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitArmTracker( - side: Side, - tracker: Tracker, - aboveUpperBodyOrArm: Tracker?, - belowHandOrArm: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitHandTracker( - side: Side, - tracker: Tracker, - aboveArm: Tracker?, - oppositeHand: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitUpperLegTracker( - side: Side, - tracker: Tracker, - aboveUpperBody: Tracker?, - belowLowerLeg: Tracker?, - oppositeUpperLeg: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitLowerLegTracker( - side: Side, - tracker: Tracker, - aboveUpperLeg: Tracker?, - belowFoot: Tracker?, - oppositeLowerLeg: Tracker?, - ) { - errors.add(error(tracker)) - } - - override fun visitFootTracker( - side: Side, - tracker: Tracker, - aboveLowerLeg: Tracker?, - oppositeFoot: Tracker?, - ) { - errors.add(error(tracker)) - } - - private fun error(tracker: Tracker): Angle = yawDifference(tracker.getAdjustedRotationForceStayAligned(), lockedRotation) - - companion object { - - /** - * Gets the yaw between two rotations, for small rotations. - * - * A locked tracker can be in any rotation, so we cannot use - * TrackerYaw::trackerYaw, which doesn't work for a tracker that is on its - * side. - * - * WARNING: DO NOT USE for large rotations because the chosen axis might have - * a very small projection on the yaw plane, which yields a low confidence yaw. - * - * TODO: It might be possible to pick a different EulerOrder when we encounter - * singularities, but I wasn't able to get this working correctly. - */ - private fun yawDifference( - rotation: Quaternion, - targetRotation: Quaternion, - ): Angle { - val targetX = targetRotation.sandwichUnitX() - val targetY = targetRotation.sandwichUnitY() - val targetZ = targetRotation.sandwichUnitZ() - - // Find the axis that is closest to the yaw plane - val axis: Vector3 - val targetAxis: Vector3 - - val targetXScore = abs(targetX.dot(Vector3.POS_Y)) - val targetYScore = abs(targetY.dot(Vector3.POS_Y)) - val targetZScore = abs(targetZ.dot(Vector3.POS_Y)) - - // The axis that is closest to the yaw plane has the smallest absolute dot - // product with the Y axis - if ((targetXScore <= targetYScore) && (targetXScore <= targetZScore)) { - axis = rotation.sandwichUnitX() - targetAxis = targetX - } else if ((targetYScore <= targetXScore) && (targetYScore <= targetZScore)) { - axis = rotation.sandwichUnitY() - targetAxis = targetY - } else { - axis = rotation.sandwichUnitZ() - targetAxis = targetZ - } - - val yaw = Angle.ofRad(atan2(axis.z, axis.x)) - val targetYaw = Angle.ofRad(atan2(targetAxis.z, targetAxis.x)) - - return targetYaw - yaw - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/NeighborErrorVisitor.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/NeighborErrorVisitor.kt deleted file mode 100644 index e9bb0f07a0..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/NeighborErrorVisitor.kt +++ /dev/null @@ -1,164 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.math.AngleErrors -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.extraYaw -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.trackerYaw -import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose -import dev.slimevr.tracking.processor.stayaligned.trackers.Side -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition - -/** - * Error between a tracker's yaw and its neighbors' yaws. - */ -class NeighborErrorVisitor( - val relaxedPose: RelaxedPose, - val errors: AngleErrors, -) : TrackerSkeleton.TrackerVisitor { - - override fun visitHeadTracker( - tracker: Tracker, - belowUpperBody: Tracker?, - ) { - if (belowUpperBody != null) { - errors.add(trackerYaw(belowUpperBody) - trackerYaw(tracker)) - } - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowUpperBody: Tracker?, - ) { - if ( - aboveHeadOrUpperBody != null && - // Head often drags the upper body trackers off to the side, so ignore it - aboveHeadOrUpperBody.trackerPosition != TrackerPosition.HEAD - ) { - errors.add(trackerYaw(aboveHeadOrUpperBody) - trackerYaw(tracker)) - } - - if (belowUpperBody != null) { - errors.add(trackerYaw(belowUpperBody) - trackerYaw(tracker)) - } - } - - override fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowLeftUpperLeg: Tracker?, - belowRightUpperLeg: Tracker?, - ) { - if ( - aboveHeadOrUpperBody != null && - // Head often drags the upper body trackers off to the side, so ignore it - aboveHeadOrUpperBody.trackerPosition != TrackerPosition.HEAD - ) { - errors.add(trackerYaw(aboveHeadOrUpperBody) - trackerYaw(tracker)) - } - - // Only consider upper leg trackers if both are available, so that the upper - // body tracker can be balanced between both - if ( - belowLeftUpperLeg != null && - belowRightUpperLeg != null - ) { - errors.add( - trackerYaw(belowLeftUpperLeg) - - extraYaw(Side.LEFT, relaxedPose.upperLeg) - - trackerYaw(tracker), - ) - errors.add( - trackerYaw(belowRightUpperLeg) - - extraYaw(Side.RIGHT, relaxedPose.upperLeg) - - trackerYaw(tracker), - ) - } - } - - override fun visitArmTracker( - side: Side, - tracker: Tracker, - aboveUpperBodyOrArm: Tracker?, - belowHandOrArm: Tracker?, - ) { - // No error because arms can go anywhere - } - - override fun visitHandTracker( - side: Side, - tracker: Tracker, - aboveArm: Tracker?, - oppositeHand: Tracker?, - ) { - // No error because hands can go anywhere - } - - override fun visitUpperLegTracker( - side: Side, - tracker: Tracker, - aboveUpperBody: Tracker?, - belowLowerLeg: Tracker?, - oppositeUpperLeg: Tracker?, - ) { - if (aboveUpperBody != null) { - errors.add( - trackerYaw(aboveUpperBody) + - extraYaw(side, relaxedPose.upperLeg) - - trackerYaw(tracker), - ) - } - - if (belowLowerLeg != null) { - errors.add( - trackerYaw(belowLowerLeg) - - extraYaw(side, relaxedPose.lowerLeg) + - extraYaw(side, relaxedPose.upperLeg) - - trackerYaw(tracker), - ) - } - } - - override fun visitLowerLegTracker( - side: Side, - tracker: Tracker, - aboveUpperLeg: Tracker?, - belowFoot: Tracker?, - oppositeLowerLeg: Tracker?, - ) { - if (aboveUpperLeg != null) { - errors.add( - trackerYaw(aboveUpperLeg) - - extraYaw(side, relaxedPose.upperLeg) + - extraYaw(side, relaxedPose.lowerLeg) - - trackerYaw(tracker), - ) - } - - if (belowFoot != null) { - errors.add( - trackerYaw(belowFoot) - - extraYaw(side, relaxedPose.foot) + - extraYaw(side, relaxedPose.lowerLeg) - - trackerYaw(tracker), - ) - } - } - - override fun visitFootTracker( - side: Side, - tracker: Tracker, - aboveLowerLeg: Tracker?, - oppositeFoot: Tracker?, - ) { - if (aboveLowerLeg != null) { - errors.add( - trackerYaw(aboveLowerLeg) - - extraYaw(side, relaxedPose.lowerLeg) + - extraYaw(side, relaxedPose.foot) - - trackerYaw(tracker), - ) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/TrackerYaw.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/TrackerYaw.kt deleted file mode 100644 index fb903538c7..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/adjust/TrackerYaw.kt +++ /dev/null @@ -1,56 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.adjust - -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.stayaligned.trackers.Side -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Vector3 - -/** - * Utilities for tracker yaw. - * - * The SlimeVR coordinate system is x-right, y-up, z-back, which is a right-handed - * coordinate system. - * - * Rotations follow the right-hand rule, for example, a positive rotation around the - * y-axis is a counter-clockwise rotation from z to x. From the perspective of a player, - * left is positive yaw, right is negative yaw. - */ -object TrackerYaw { - - /** - * Whether we can get the yaw of a tracker. - */ - fun hasTrackerYaw(tracker: Tracker) = Angle.absBetween( - tracker.getAdjustedRotationForceStayAligned().sandwichUnitX(), - Vector3.POS_Y, - ) > MIN_ON_SIDE_ANGLE - - /** - * Gets the yaw of the tracker, for trackers that are not on its side. - * - * WARNING: DO NOT USE for a tracker that is on its side. Euler YZX angles have a - * singularity for a tracker that is on its side, and can yield arbitrary yaws. - * For example, the Euler YZX angles (Y=0°, Z=90°, X=30°) and (Y=30°, Z=90°, X=0°) - * are equivalent but yield completely different yaws. - * - * WARNING: It is possible to use another EulerOrder which does not have a - * singularity for this rotation to get "some" yaw, but this yaw will be very - * different from the from YZX. DO NOT ATTEMPT! - */ - fun trackerYaw(tracker: Tracker) = Angle.ofRad( - tracker.getAdjustedRotationForceStayAligned() - .toEulerAngles(EulerOrder.YZX) - .y, - ) - - /** - * Applies an extra yaw in the specified direction. - */ - fun extraYaw(direction: Side, angle: Angle) = when (direction) { - Side.LEFT -> angle - Side.RIGHT -> -angle - } - - private val MIN_ON_SIDE_ANGLE = Angle.ofDeg(30.0f) -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/PlayerPose.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/PlayerPose.kt deleted file mode 100644 index 349c5dea36..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/PlayerPose.kt +++ /dev/null @@ -1,89 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.poses - -import dev.slimevr.tracking.processor.stayaligned.trackers.TrackerSkeleton - -/** - * The pose of the player. - */ -enum class PlayerPose { - UNKNOWN, - STANDING, - SITTING_IN_CHAIR, - SITTING_ON_GROUND, - LYING_ON_BACK, - KNEELING, - ; - - companion object { - - fun ofTrackers(trackers: TrackerSkeleton): PlayerPose { - val poses = - TrackerPoses( - trackers.upperBody.map(TrackerPose.Companion::ofTracker), - TrackerPose.ofTracker(trackers.leftUpperLeg), - TrackerPose.ofTracker(trackers.rightUpperLeg), - TrackerPose.ofTracker(trackers.leftLowerLeg), - TrackerPose.ofTracker(trackers.rightLowerLeg), - ) - - return ( - if (isStanding(poses)) { - STANDING - } else if (isSittingInChair(poses)) { - SITTING_IN_CHAIR - } else if (isSittingOnGround(poses)) { - SITTING_ON_GROUND - } else if (isLyingOnBack(poses)) { - LYING_ON_BACK - } else if (isKneeling(poses)) { - KNEELING - } else { - UNKNOWN - } - ) - } - - private class TrackerPoses( - val upperBody: List, - val leftUpperLeg: TrackerPose, - val rightUpperLeg: TrackerPose, - val leftLowerLeg: TrackerPose, - val rightLowerLeg: TrackerPose, - ) - - private fun isStanding(pose: TrackerPoses) = pose.upperBody.all { it == TrackerPose.TOP_FACING_UP } && - pose.leftUpperLeg == TrackerPose.TOP_FACING_UP && - pose.rightUpperLeg == TrackerPose.TOP_FACING_UP && - pose.leftLowerLeg == TrackerPose.TOP_FACING_UP && - pose.rightLowerLeg == TrackerPose.TOP_FACING_UP - - private fun isSittingInChair(pose: TrackerPoses) = pose.upperBody.isNotEmpty() && - pose.upperBody[0] == TrackerPose.TOP_FACING_UP && - pose.upperBody.all { it == TrackerPose.TOP_FACING_UP || it == TrackerPose.FRONT_FACING_UP } && - pose.leftUpperLeg == TrackerPose.FRONT_FACING_UP && - pose.rightUpperLeg == TrackerPose.FRONT_FACING_UP && - pose.leftLowerLeg == TrackerPose.TOP_FACING_UP && - pose.rightLowerLeg == TrackerPose.TOP_FACING_UP - - private fun isSittingOnGround(pose: TrackerPoses) = pose.upperBody.isNotEmpty() && - pose.upperBody[0] == TrackerPose.TOP_FACING_UP && - pose.upperBody.all { it == TrackerPose.TOP_FACING_UP || it == TrackerPose.FRONT_FACING_UP } && - // Allow legs to be flat on ground, or knees-up - pose.leftUpperLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_DOWN } && - pose.rightUpperLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_DOWN } && - pose.leftLowerLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_UP } && - pose.rightLowerLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_UP } - - private fun isLyingOnBack(pose: TrackerPoses) = pose.upperBody.all { it == TrackerPose.FRONT_FACING_UP } && - // Allow legs to be flat on ground, or knees-up - pose.leftUpperLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_DOWN } && - pose.rightUpperLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_DOWN } && - pose.leftLowerLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_UP } && - pose.rightLowerLeg.let { it == TrackerPose.FRONT_FACING_UP || it == TrackerPose.TOP_FACING_UP } - - private fun isKneeling(pose: TrackerPoses) = pose.leftUpperLeg.let { it == TrackerPose.TOP_FACING_UP || it == TrackerPose.FRONT_FACING_UP } && - pose.rightUpperLeg.let { it == TrackerPose.TOP_FACING_UP || it == TrackerPose.FRONT_FACING_UP } && - pose.leftLowerLeg == TrackerPose.FRONT_FACING_DOWN && - pose.rightLowerLeg == TrackerPose.FRONT_FACING_DOWN - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/RelaxedPose.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/RelaxedPose.kt deleted file mode 100644 index cebc3bc526..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/RelaxedPose.kt +++ /dev/null @@ -1,109 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.poses - -import dev.slimevr.config.StayAlignedConfig -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults -import dev.slimevr.tracking.processor.stayaligned.adjust.TrackerYaw.trackerYaw -import dev.slimevr.tracking.trackers.Tracker - -class RelaxedPose( - val upperLeg: Angle, - val lowerLeg: Angle, - val foot: Angle, -) { - override fun toString(): String = "upperLeg=$upperLeg lowerLeg=$lowerLeg foot=$foot" - - companion object { - - val ZERO = RelaxedPose(Angle.ZERO, Angle.ZERO, Angle.ZERO) - - /** - * Gets the relaxed angles for a particular pose. May provide defaults if the - * angles aren't configured for the pose. - */ - fun forPose( - playerPose: PlayerPose, - config: StayAlignedConfig, - ) = when (playerPose) { - PlayerPose.STANDING -> { - val poseConfig = config.standingRelaxedPose - if (poseConfig.enabled) { - RelaxedPose( - Angle.ofDeg(poseConfig.upperLegAngleInDeg), - Angle.ofDeg(poseConfig.lowerLegAngleInDeg), - Angle.ofDeg(poseConfig.footAngleInDeg), - ) - } else { - null - } - } - - PlayerPose.SITTING_IN_CHAIR -> { - val poseConfig = config.sittingRelaxedPose - if (poseConfig.enabled) { - RelaxedPose( - Angle.ofDeg(poseConfig.upperLegAngleInDeg), - Angle.ofDeg(poseConfig.lowerLegAngleInDeg), - Angle.ofDeg(poseConfig.footAngleInDeg), - ) - } else { - null - } - } - - PlayerPose.SITTING_ON_GROUND, - PlayerPose.LYING_ON_BACK, - -> { - val poseConfig = config.flatRelaxedPose - if (poseConfig.enabled) { - RelaxedPose( - Angle.ofDeg(poseConfig.upperLegAngleInDeg), - Angle.ofDeg(poseConfig.lowerLegAngleInDeg), - Angle.ofDeg(poseConfig.footAngleInDeg), - ) - } else { - null - } - } - - PlayerPose.KNEELING -> - StayAlignedDefaults.RELAXED_POSE_KNEELING - - else -> - null - } - - /** - * Gets the relaxed angles from the trackers. - */ - fun fromTrackers(humanSkeleton: HumanSkeleton): RelaxedPose { - val halfAngleBetween = { left: Tracker, right: Tracker -> - (trackerYaw(left) - trackerYaw(right)) * 0.5f - } - - var upperLegAngle = Angle.ZERO - humanSkeleton.leftUpperLegTracker?.let { left -> - humanSkeleton.rightUpperLegTracker?.let { right -> - upperLegAngle = halfAngleBetween(left, right) - } - } - - var lowerLegAngle = Angle.ZERO - humanSkeleton.leftLowerLegTracker?.let { left -> - humanSkeleton.rightLowerLegTracker?.let { right -> - lowerLegAngle = halfAngleBetween(left, right) - } - } - - var footAngle = Angle.ZERO - humanSkeleton.leftFootTracker?.let { left -> - humanSkeleton.rightFootTracker?.let { right -> - footAngle = halfAngleBetween(left, right) - } - } - - return RelaxedPose(upperLegAngle, lowerLegAngle, footAngle) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/TrackerPose.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/TrackerPose.kt deleted file mode 100644 index 2f3cd35ee5..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/poses/TrackerPose.kt +++ /dev/null @@ -1,61 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.poses - -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -/** - * The orientation of a tracker. - */ -enum class TrackerPose { - NONE, - TOP_FACING_UP, - TOP_FACING_DOWN, - FRONT_FACING_UP, - FRONT_FACING_DOWN, - ON_SIDE, - ; - - companion object { - - fun ofTracker(tracker: Tracker?): TrackerPose { - if (tracker == null) { - return NONE - } - - val rotation = tracker.getAdjustedRotationForceStayAligned() - - val x = rotation.sandwichUnitX() - val y = rotation.sandwichUnitY() - val z = rotation.sandwichUnitZ() - - val xDot = x.dot(Vector3.POS_Y) - val yDot = y.dot(Vector3.POS_Y) - val zDot = z.dot(Vector3.POS_Y) - - val xAbsDot = abs(xDot) - val yAbsDot = abs(yDot) - val zAbsDot = abs(zDot) - - val pose = - if ((xAbsDot >= yAbsDot) && (xAbsDot >= zAbsDot)) { - ON_SIDE - } else if ((yAbsDot >= xAbsDot) && (yAbsDot >= zAbsDot)) { - if (yDot >= 0) { - TOP_FACING_UP - } else { - TOP_FACING_DOWN - } - } else { - // Tracker local POS_Z is behind - if (zDot >= 0) { - FRONT_FACING_DOWN - } else { - FRONT_FACING_UP - } - } - - return pose - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/RestDetector.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/RestDetector.kt deleted file mode 100644 index cf385cd979..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/RestDetector.kt +++ /dev/null @@ -1,95 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.trackers - -import dev.slimevr.math.Angle -import io.github.axisangles.ktmath.Quaternion -import kotlin.time.Duration -import kotlin.time.TimeSource - -/** - * Detects whether a tracker is at rest. - * - * A tracker is at rest when it stays within a certain rotational range for a given - * amount of time. If it rotates past that range, it is no longer at rest. - * - * TODO: In practice this is good enough for Stay Aligned, but we could also consider - * acceleration if we want to make this a general purpose rest detector. - */ -class RestDetector( - private val maxRotation: Angle, - private val enterRestTime: Duration, - private val enterMovingTime: Duration, -) { - enum class State { - MOVING, - AT_REST, - RECENTLY_AT_REST, - } - - var state = State.MOVING - private set - - // Instant that we entered the current state - private var startTime = TimeSource.Monotonic.markNow() - - // Rotation which could - private var lastRotation = Quaternion.IDENTITY - private var lastRotationTime = TimeSource.Monotonic.markNow() - - /** - * Resets the detector - */ - fun reset() { - val now = TimeSource.Monotonic.markNow() - - state = State.MOVING - startTime = now - lastRotation = Quaternion.IDENTITY - lastRotationTime = now - } - - /** - * Provides a new rotation sample to the detector. - * - * @return whether the tracker is at rest - */ - fun update(rotation: Quaternion) { - val now = TimeSource.Monotonic.markNow() - - if ( - state == State.RECENTLY_AT_REST && - now > startTime.plus(enterMovingTime) - ) { - state = State.MOVING - startTime = now - lastRotation = rotation - lastRotationTime = now - } - - when (state) { - State.MOVING, - State.RECENTLY_AT_REST, - -> - if (Angle.absBetween(lastRotation, rotation) > maxRotation) { - lastRotation = rotation - lastRotationTime = now - } else { - // When we detect the tracker is at rest, use the current rotation as the - // new start rotation for continuing to detect the tracker is at rest - if (now > lastRotationTime.plus(enterRestTime)) { - state = State.AT_REST - startTime = now - lastRotation = rotation - lastRotationTime = now - } - } - - State.AT_REST -> - if (Angle.absBetween(lastRotation, rotation) > maxRotation) { - state = State.RECENTLY_AT_REST - startTime = now - lastRotation = rotation - lastRotationTime = now - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/Side.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/Side.kt deleted file mode 100644 index f2da3ba6b7..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/Side.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.trackers - -enum class Side { - LEFT, - RIGHT, -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/StayAlignedTrackerState.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/StayAlignedTrackerState.kt deleted file mode 100644 index c49bc7af72..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/StayAlignedTrackerState.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.trackers - -import dev.slimevr.math.Angle -import dev.slimevr.tracking.processor.stayaligned.StayAlignedDefaults -import dev.slimevr.tracking.trackers.Tracker -import io.github.axisangles.ktmath.Quaternion - -class StayAlignedTrackerState( - val tracker: Tracker, -) { - // Whether to hide the yaw correction - var hideCorrection = false - - // Detects whether the tracker is at rest - val restDetector = StayAlignedDefaults.makeRestDetector() - - // Rotation of the tracker when it was locked - var lockedRotation: Quaternion? = null - - // Yaw correction to apply to tracker rotation - var yawCorrection = Angle.ZERO - - // Alignment error that yaw correction attempts to minimize - var yawErrors = YawErrors() - - fun update() { - restDetector.update(tracker.getRawRotation()) - if (restDetector.state == RestDetector.State.AT_REST) { - if (lockedRotation == null) { - lockedRotation = tracker.getAdjustedRotationForceStayAligned() - } - } else { - lockedRotation = null - } - } - - fun reset() { - restDetector.reset() - lockedRotation = null - yawCorrection = Angle.ZERO - yawErrors = YawErrors() - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/TrackerSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/TrackerSkeleton.kt deleted file mode 100644 index dbf5962b7e..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/TrackerSkeleton.kt +++ /dev/null @@ -1,415 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.trackers - -import dev.slimevr.tracking.processor.skeleton.HumanSkeleton -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition - -/** - * Represents a skeleton of trackers. - * - * The skeleton consists of: - * - An upper body group - * - A head tracker, connected to the top of the upper body group - * - Two arm groups, connected to the top of the upper body group - * - Two hands connected to the bottom of the corresponding arm group - * - Two upper legs, connected to the bottom of the upper body group - * - Two lower legs, connected to the bottom of each corresponding upper leg - * - Two feet, connected to the bottom of each corresponding lower leg - */ -class TrackerSkeleton(skeleton: HumanSkeleton) { - - val allTrackers = with(skeleton) { - listOfNotNull( - headTracker, - // Upper body - neckTracker, - upperChestTracker, - chestTracker, - waistTracker, - hipTracker, - // Left arm - leftShoulderTracker, - leftUpperArmTracker, - leftLowerArmTracker, - leftHandTracker, - // Right arm - rightShoulderTracker, - rightUpperArmTracker, - rightLowerArmTracker, - rightHandTracker, - // Left leg - leftUpperLegTracker, - leftLowerLegTracker, - leftFootTracker, - // Right leg - rightUpperLegTracker, - rightLowerLegTracker, - rightFootTracker, - ) - } - - // Tracker groups - val upperBody = with(skeleton) { - listOfNotNull( - neckTracker, - upperChestTracker, - chestTracker, - waistTracker, - hipTracker, - ) - } - - val leftArm = with(skeleton) { - listOfNotNull( - leftShoulderTracker, - leftUpperArmTracker, - leftLowerArmTracker, - ) - } - - val rightArm = with(skeleton) { - listOfNotNull( - rightShoulderTracker, - rightUpperArmTracker, - rightLowerArmTracker, - ) - } - - // Individual trackers - val head = skeleton.headTracker - val leftHand = skeleton.leftHandTracker - val rightHand = skeleton.rightHandTracker - val leftUpperLeg = skeleton.leftUpperLegTracker - val leftLowerLeg = skeleton.leftLowerLegTracker - val leftFoot = skeleton.leftFootTracker - val rightUpperLeg = skeleton.rightUpperLegTracker - val rightLowerLeg = skeleton.rightLowerLegTracker - val rightFoot = skeleton.rightFootTracker - - /** - * Visits a tracker within the skeleton. - */ - fun visit( - tracker: Tracker, - visitor: TrackerVisitor, - ) { - when (tracker.trackerPosition) { - TrackerPosition.HEAD -> - if (tracker == head) { - visitor.visitHeadTracker(tracker, upperBody.firstOrNull()) - } - - // Upper body - TrackerPosition.NECK, - TrackerPosition.UPPER_CHEST, - TrackerPosition.CHEST, - TrackerPosition.WAIST, - TrackerPosition.HIP, - -> - visitUpperBodyTrackers( - tracker, - visitor, - head, - upperBody, - leftUpperLeg, - rightUpperLeg, - ) - - // Left arm - TrackerPosition.LEFT_SHOULDER, - TrackerPosition.LEFT_UPPER_ARM, - TrackerPosition.LEFT_LOWER_ARM, - -> - visitArmTrackers( - tracker, - visitor, - Side.LEFT, - upperBody.firstOrNull(), - leftArm, - leftHand, - ) - - // Right arm - TrackerPosition.RIGHT_SHOULDER, - TrackerPosition.RIGHT_UPPER_ARM, - TrackerPosition.RIGHT_LOWER_ARM, - -> - visitArmTrackers( - tracker, - visitor, - Side.RIGHT, - upperBody.firstOrNull(), - rightArm, - rightHand, - ) - - TrackerPosition.LEFT_HAND -> - if (tracker == leftHand) { - visitor.visitHandTracker( - Side.LEFT, - tracker, - leftArm.lastOrNull(), - rightHand, - ) - } - - TrackerPosition.RIGHT_HAND -> - if (tracker == rightHand) { - visitor.visitHandTracker( - Side.RIGHT, - tracker, - rightArm.lastOrNull(), - leftHand, - ) - } - - TrackerPosition.LEFT_UPPER_LEG -> - if (tracker == leftUpperLeg) { - visitor.visitUpperLegTracker( - Side.LEFT, - tracker, - upperBody.lastOrNull(), - leftLowerLeg, - rightUpperLeg, - ) - } - - TrackerPosition.RIGHT_UPPER_LEG -> - if (tracker == rightUpperLeg) { - visitor.visitUpperLegTracker( - Side.RIGHT, - tracker, - upperBody.lastOrNull(), - rightLowerLeg, - leftUpperLeg, - ) - } - - TrackerPosition.LEFT_LOWER_LEG -> - if (tracker == leftLowerLeg) { - visitor.visitLowerLegTracker( - Side.LEFT, - tracker, - leftUpperLeg, - leftFoot, - rightLowerLeg, - ) - } - - TrackerPosition.RIGHT_LOWER_LEG -> - if (tracker == rightLowerLeg) { - visitor.visitLowerLegTracker( - Side.RIGHT, - tracker, - rightUpperLeg, - rightFoot, - leftLowerLeg, - ) - } - - TrackerPosition.LEFT_FOOT -> - if (tracker == leftFoot) { - visitor.visitFootTracker( - Side.LEFT, - tracker, - leftLowerLeg, - rightFoot, - ) - } - - TrackerPosition.RIGHT_FOOT -> - if (tracker == rightFoot) { - visitor.visitFootTracker( - Side.RIGHT, - tracker, - rightLowerLeg, - leftFoot, - ) - } - - else -> { - // No tracker to visit - } - } - } - - private fun visitUpperBodyTrackers( - tracker: Tracker, - visitor: TrackerVisitor, - head: Tracker?, - upperBody: List, - leftUpperLeg: Tracker?, - rightUpperLeg: Tracker?, - ) { - val index = upperBody.indexOf(tracker) - if (index < 0) { - return - } - - if (index == 0) { - if (upperBody.size == 1) { - // Only upper body tracker - visitor.visitUpperBodyTracker( - tracker, - head, - leftUpperLeg, - rightUpperLeg, - ) - } else { - // First upper body tracker - visitor.visitUpperBodyTracker( - tracker, - head, - upperBody[1], - ) - } - } else if (index < upperBody.size - 1) { - // Middle upper body tracker - visitor.visitUpperBodyTracker( - tracker, - upperBody[index - 1], - upperBody[index + 1], - ) - } else { - // Last upper body tracker - visitor.visitUpperBodyTracker( - tracker, - upperBody[index - 1], - leftUpperLeg, - rightUpperLeg, - ) - } - } - - private fun visitArmTrackers( - tracker: Tracker, - visitor: TrackerVisitor, - side: Side, - upperBody: Tracker?, - arm: List, - hand: Tracker?, - ) { - val index = arm.indexOf(tracker) - if (index < 0) { - return - } - - if (index == 0) { - if (arm.size == 1) { - // Only arm tracker - visitor.visitArmTracker( - side, - tracker, - upperBody, - hand, - ) - } else { - // First arm tracker - visitor.visitArmTracker( - side, - tracker, - upperBody, - arm[1], - ) - } - } else if (index < arm.size - 1) { - // Middle arm tracker - visitor.visitArmTracker( - side, - tracker, - arm[index - 1], - arm[index + 1], - ) - } else { - // Last arm tracker - visitor.visitArmTracker( - side, - tracker, - arm[index - 1], - hand, - ) - } - } - - interface TrackerVisitor { - - /** - * Visits the head tracker. - */ - fun visitHeadTracker( - tracker: Tracker, - belowUpperBody: Tracker?, - ) - - /** - * Visits an upper body tracker (except for the bottom-most tracker). - */ - fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowUpperBody: Tracker?, - ) - - /** - * Visits the bottom-most upper body tracker. - */ - fun visitUpperBodyTracker( - tracker: Tracker, - aboveHeadOrUpperBody: Tracker?, - belowLeftUpperLeg: Tracker?, - belowRightUpperLeg: Tracker?, - ) - - /** - * Visits an arm tracker. - */ - fun visitArmTracker( - side: Side, - tracker: Tracker, - aboveUpperBodyOrArm: Tracker?, - belowHandOrArm: Tracker?, - ) - - /** - * Visits a hand tracker. - */ - fun visitHandTracker( - side: Side, - tracker: Tracker, - aboveArm: Tracker?, - oppositeHand: Tracker?, - ) - - /** - * Visits an upper leg tracker. - */ - fun visitUpperLegTracker( - side: Side, - tracker: Tracker, - aboveUpperBody: Tracker?, - belowLowerLeg: Tracker?, - oppositeUpperLeg: Tracker?, - ) - - /** - * Visits a lower leg tracker. - */ - fun visitLowerLegTracker( - side: Side, - tracker: Tracker, - aboveUpperLeg: Tracker?, - belowFoot: Tracker?, - oppositeLowerLeg: Tracker?, - ) - - /** - * Visits a foot tracker. - */ - fun visitFootTracker( - side: Side, - tracker: Tracker, - aboveLowerLeg: Tracker?, - oppositeFoot: Tracker?, - ) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/YawErrors.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/YawErrors.kt deleted file mode 100644 index 4658669b4a..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/stayaligned/trackers/YawErrors.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.slimevr.tracking.processor.stayaligned.trackers - -import dev.slimevr.math.AngleErrors - -/** - * Aggregates the yaw errors from multiple forces. - */ -class YawErrors { - var lockedError = AngleErrors() - var centerError = AngleErrors() - var neighborError = AngleErrors() -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt deleted file mode 100644 index d92f60d04a..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.slimevr.tracking.trackers - -import dev.slimevr.tracking.trackers.udp.BoardType -import dev.slimevr.tracking.trackers.udp.MCUType -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger - -open class Device(val magSupport: Boolean = false) { - open val id: Int = nextLocalDeviceId.incrementAndGet() - open var name: String? = null - open var firmwareVersion: String? = null - open var firmwareDate: String? = null - open var manufacturer: String? = null - open val trackers: MutableMap = ConcurrentHashMap() - - /** - * Implement toString() to return a string that uniquely identifies the board type - * SHOULDN'T RETURN NULL WHEN toString() IS CALLED - */ - open val boardType: BoardType = BoardType.UNKNOWN - open val mcuType: MCUType = MCUType.UNKNOWN - - open val hardwareIdentifier: String = "Unknown" - - val isOpenVrDevice: Boolean - get() = manufacturer == "OpenVR" - - init { - CoroutineScope(Job()).launch { - // Wait a little for device to get configured - delay(1000) - } - } - - /** - * Enables or disables magnetometers in all the trackers of the device - * if `sensorId` null or in the specified tracker - * @param sensorId If null, every sensor will be modified - */ - open suspend fun setMag(state: Boolean, sensorId: Int = 255) { - TODO("Not implemented because no mag support: $magSupport") - } - - companion object { - @JvmStatic - protected val nextLocalDeviceId = AtomicInteger() - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt deleted file mode 100644 index dd409623a6..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.slimevr.tracking.trackers - -import dev.slimevr.VRServer -import io.eiren.util.collections.FastList - -class DeviceManager(private val server: VRServer) { - val devices = FastList() - fun createDevice(name: String?, version: String?, manufacturer: String?): Device { - val device = Device() - device.name = name - device.firmwareVersion = version - device.manufacturer = manufacturer - return device - } - - fun addDevice(device: Device) { - server.queueTask { devices.add(device) } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt deleted file mode 100644 index bb70fbc830..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ /dev/null @@ -1,453 +0,0 @@ -package dev.slimevr.tracking.trackers - -import dev.slimevr.VRServer -import dev.slimevr.config.TrackerConfig -import dev.slimevr.tracking.processor.stayaligned.trackers.StayAlignedTrackerState -import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByDesignation -import dev.slimevr.tracking.trackers.udp.IMUType -import dev.slimevr.tracking.trackers.udp.MagnetometerStatus -import dev.slimevr.tracking.trackers.udp.TrackerDataType -import dev.slimevr.util.InterpolationHandler -import io.eiren.util.BufferedTimer -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.properties.Delegates - -const val TIMEOUT_MS = 2_000L -const val DISCONNECT_MS = 3_000L + TIMEOUT_MS - -/** - * Generic tracker class for input and output tracker, - * with flags on instantiation. - */ -class Tracker @JvmOverloads constructor( - val device: Device?, - /** - * VRServer.nextLocalTrackerId - */ - val id: Int, - /** - * unique, for config - */ - val name: String, - /** - * default display GUI name - */ - val displayName: String = "Tracker #$id", - trackerPosition: TrackerPosition?, - /** - * It's like the ID, but it should be local to the device if it has one - */ - trackerNum: Int? = null, - val hasPosition: Boolean = false, - val hasRotation: Boolean = false, - val hasAcceleration: Boolean = false, - /** - * User can change TrackerPosition, mounting... - */ - val userEditable: Boolean = false, - /** - * Is used within SlimeVR (shareable trackers) - */ - val isInternal: Boolean = false, - /** - * Has solved position + rotation (Vive trackers) - */ - val isComputed: Boolean = false, - val imuType: IMUType? = null, - /** - * Automatically set the status to DISCONNECTED - */ - val usesTimeout: Boolean = false, - /** - * If true, smoothing and prediction may be enabled. If either are enabled, then - * rotations will be updated with [tick]. This will not have any effect if - * [trackRotDirection] is set to false. - */ - val allowFiltering: Boolean = false, - - /** - * If true, the tracker can be reset - */ - val allowReset: Boolean = false, - /** - * If true, the tracker can do mounting calibration - */ - val allowMounting: Boolean = false, - - val isHmd: Boolean = false, - - /** - * If true, the tracker need the user to perform a reset - */ - var needReset: Boolean = false, - - /** - * Whether to track the direction of the tracker's rotation - * (positive vs negative rotation). This needs to be disabled for AutoBone and - * unit tests, where the rotation is absolute and not temporal. - * - * If true, the output rotation will only be updated after [dataTick]. If false, the - * output rotation will be updated immediately with the raw rotation. - */ - val trackRotDirection: Boolean = true, - magStatus: MagnetometerStatus = MagnetometerStatus.NOT_SUPPORTED, - /** - * Rotation by default. - * NOT the same as hasRotation (other data types emulate rotation) - */ - val trackerDataType: TrackerDataType = TrackerDataType.ROTATION, -) { - private val timer = BufferedTimer(1f) - private var timeAtLastUpdate: Long = System.currentTimeMillis() - private var _rotation = Quaternion.IDENTITY - - // IMU: +z forward, +x left, +y up - // SlimeVR: +z backward, +x right, +y up - private var _acceleration = Vector3.NULL - private var _magVector = Vector3.NULL - var position = Vector3.NULL - val resetsHandler: TrackerResetsHandler = TrackerResetsHandler(this) - val filteringHandler: TrackerFilteringHandler = TrackerFilteringHandler() - val trackerFlexHandler: TrackerFlexHandler = TrackerFlexHandler(this) - var batteryVoltage: Float? = null - var batteryLevel: Float? = null - var batteryRemainingRuntime: Long? = null - var ping: Int? = null - var signalStrength: Int? = null - var temperature: Float? = null - var button: Int? = null - var packetsReceived: Int? = null - var packetsLost: Int? = null - var packetLoss: Float? = null - var customName: String? = null - var magStatus: MagnetometerStatus = magStatus - private set - - /** - * Watch the rest calibration status - */ - var hasCompletedRestCalibration: Boolean? = null - - /** - * If the tracker has gotten disconnected after it was initialized first time - */ - var status: TrackerStatus by Delegates.observable(TrackerStatus.DISCONNECTED) { _, old, new -> - if (old == new) return@observable - - if (allowReset && !old.reset && new.reset && !needReset) { - needReset = true - } - - if (!isInternal && VRServer.instanceInitialized) { - // If the status of a non-internal tracker has changed, inform - // the VRServer to recreate the skeleton, as it may need to - // assign or un-assign the tracker to a body part - VRServer.instance.updateSkeletonModel() - VRServer.instance.refreshTrackersDriftCompensationEnabled() - VRServer.instance.trackerStatusChanged(this, old, new) - } - } - - var trackerPosition: TrackerPosition? by Delegates.observable(trackerPosition) { _, old, new -> - if (old == new) return@observable - - if (allowReset && !needReset) { - needReset = true - } - - if (!isInternal) { - // Set default mounting orientation for that body part - new?.let { resetsHandler.mountingOrientation = it.defaultMounting() } - } - } - - // Computed value to simplify availability checks - val hasAdjustedRotation = hasRotation && (allowFiltering || allowReset) - - /** - * It's like the ID, but it should be local to the device if it has one - */ - val trackerNum: Int = trackerNum ?: id - - val stayAligned = StayAlignedTrackerState(this) - val yawResetSmoothing = InterpolationHandler() - - init { - // IMPORTANT: Look here for the required states of inputs - require(!allowReset || (hasRotation && allowReset)) { - "If ${::allowReset.name} is true, then ${::hasRotation.name} must also be true" - } - require(!allowMounting || (allowReset && allowMounting)) { - "If ${::allowMounting.name} is true, then ${::allowReset.name} must also be true" - } - require(!isHmd || (hasPosition && isHmd)) { - "If ${::isHmd.name} is true, then ${::hasPosition.name} must also be true" - } -// require(device != null && _trackerNum == null) { -// "If ${::device.name} exists, then ${::trackerNum.name} must not be null" -// } - } - - /** - * Reads/loads from the given config - */ - fun readConfig(config: TrackerConfig) { - config.customName?.let { - customName = it - } - config.designation?.let { designation -> - getByDesignation(designation)?.let { trackerPosition = it } - } ?: run { trackerPosition = null } - if (allowMounting) { - // Load manual mounting - config.mountingOrientation?.let { resetsHandler.mountingOrientation = it.toValue() } - } - if (this.isImu() && config.allowDriftCompensation == null) { - // If value didn't exist, default to true and save - resetsHandler.allowDriftCompensation = true - VRServer.instance.configManager.vrConfig.getTracker(this).allowDriftCompensation = true - VRServer.instance.configManager.saveConfig() - } else { - config.allowDriftCompensation?.let { - resetsHandler.allowDriftCompensation = it - } - } - } - - /** - * Writes/saves to the given config - */ - fun writeConfig(config: TrackerConfig) { - trackerPosition?.let { config.designation = it.designation } ?: run { config.designation = null } - customName?.let { config.customName = it } - if (allowMounting) { - // Save manual mounting - config.mountingOrientation = resetsHandler.mountingOrientation.toObject() - } - if (this.isImu()) { - config.allowDriftCompensation = resetsHandler.allowDriftCompensation - } - } - - /** - * Loads the mounting reset quaternion from disk - */ - fun saveMountingResetOrientation(config: TrackerConfig) { - // Load automatic mounting - config.mountingResetOrientation?.let { - resetsHandler.trySetMountingReset(it.toValue()) - } - } - - /** - * Saves the mounting reset quaternion to disk - */ - fun saveMountingResetOrientation(quat: Quaternion?) { - val configManager = VRServer.instance.configManager - configManager.vrConfig.getTracker(this).mountingResetOrientation = quat?.toObject() - configManager.saveConfig() - } - - /** - * Synchronized with the VRServer's 1000hz while loop - */ - fun tick(deltaTime: Float) { - if (usesTimeout) { - if (System.currentTimeMillis() - timeAtLastUpdate > DISCONNECT_MS) { - status = TrackerStatus.DISCONNECTED - } else if (System.currentTimeMillis() - timeAtLastUpdate > TIMEOUT_MS) { - status = TrackerStatus.TIMED_OUT - } - } - - filteringHandler.update() - yawResetSmoothing.tick(deltaTime) - stayAligned.update() - } - - /** - * Tells the tracker that it received new data - * NOTE: Use only when rotation is received - */ - fun dataTick() { - timer.update() - timeAtLastUpdate = System.currentTimeMillis() - if (trackRotDirection) { - filteringHandler.dataTick(getAdjustedRotation()) - } - } - - /** - * A way to delay the timeout of the tracker - */ - fun heartbeat() { - timeAtLastUpdate = System.currentTimeMillis() - } - - /** - * Gets the adjusted tracker rotation after the resetsHandler's corrections - * (reset, mounting and drift compensation). - * This is the rotation that is applied on the SlimeVR skeleton bones. - * Warning: This performs several Quaternion multiplications, so calling - * it too much should be avoided for performance reasons. - */ - private fun getAdjustedRotation(): Quaternion { - var rot = _rotation - - if (!stayAligned.hideCorrection) { - // Yaw drift happens in the raw rotation space - rot = Quaternion.rotationAroundYAxis(stayAligned.yawCorrection.toRad()) * rot - } - - // Reset if needed and is not computed and internal - return if (allowReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) { - // Adjust to reset, mounting and drift compensation - resetsHandler.getReferenceAdjustedDriftRotationFrom(rot) - } else { - rot - } - } - - /** - * Same as getAdjustedRotation except that Stay Aligned correction is always - * applied. This allows Stay Aligned to do gradient descent with the tracker's - * rotation. - */ - fun getAdjustedRotationForceStayAligned(): Quaternion { - var rot = _rotation - - // Yaw drift happens in the raw rotation space - rot = Quaternion.rotationAroundYAxis(stayAligned.yawCorrection.toRad()) * rot - - // Reset if needed and is not computed and internal - return if (allowReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) { - // Adjust to reset, mounting and drift compensation - resetsHandler.getReferenceAdjustedDriftRotationFrom(rot) - } else { - rot - } - } - - /** - * Gets the identity-adjusted tracker rotation after the resetsHandler's corrections - * (identity reset, drift and identity mounting). - * This is used for debugging/visualizing tracker data - */ - fun getIdentityAdjustedRotation(): Quaternion { - var rot = _rotation - - if (!stayAligned.hideCorrection) { - // Yaw drift happens in the raw rotation space - rot = Quaternion.rotationAroundYAxis(stayAligned.yawCorrection.toRad()) * rot - } - - // Reset if needed or is a computed tracker besides head - return if (allowReset && !(isComputed && trackerPosition != TrackerPosition.HEAD) && trackerDataType == TrackerDataType.ROTATION) { - // Adjust to reset and mounting - resetsHandler.getIdentityAdjustedDriftRotationFrom(rot) - } else { - rot - } - } - - /** - * Get the rotation of the tracker after the resetsHandler's corrections, filtering, - * and reset smoothing if applicable - */ - fun getRotation(): Quaternion { - var rot = getRotationNoResetSmooth() - - if (yawResetSmoothing.remainingTime > 0f) { - rot = yawResetSmoothing.curRotation * rot - } - - return rot - } - - /** - * Get the rotation of the tracker after the resetsHandler's corrections and - * filtering if applicable - */ - fun getRotationNoResetSmooth(): Quaternion = if (trackRotDirection) { - filteringHandler.getFilteredRotation() - } else { - // Get non-filtered rotation - getAdjustedRotation() - } - - /** - * Gets the world-adjusted acceleration - */ - fun getAcceleration(): Vector3 = if (allowReset) { - resetsHandler.getReferenceAdjustedAccel(_rotation, _acceleration) - } else { - _acceleration - } - - /** - * Gets the raw (unadjusted) rotation of the tracker. - * If this is an IMU, this will be the raw sensor rotation. - */ - fun getRawRotation() = _rotation - - /** - * Sets the raw (unadjusted) rotation of the tracker. - */ - fun setRotation(rotation: Quaternion) { - this._rotation = rotation - } - - /** - * Sets the raw (unadjusted) acceleration of the tracker. - */ - fun setAcceleration(vec: Vector3) { - this._acceleration = vec - } - - /** - * True if the raw rotation is coming directly from an IMU (no cameras or lighthouses) - * For example, flex sensor trackers are not considered as IMU trackers (see TrackerDataType) - */ - fun isImu(): Boolean = imuType != null && trackerDataType == TrackerDataType.ROTATION - - /** - * Please don't use this and instead set it via [Device.setMag] - */ - internal fun setMagPrivate(mag: Boolean) { - magStatus = if (mag) { - MagnetometerStatus.ENABLED - } else { - MagnetometerStatus.DISABLED - } - } - - /** - * Gets the magnetic field vector, in mGauss. - */ - fun getMagVector() = if (allowReset) { - resetsHandler.getReferenceAdjustedAccel(_rotation, _magVector) - } else { - _magVector - } - - /** - * Sets the magnetic field vector. - */ - fun setMagVector(vec: Vector3) { - this._magVector = vec - } - - /** - * Gets the current TPS of the tracker - */ - val tps: Float - get() = timer.averageFPS - - /** - * Call when doing a full reset to reset the tracking of rotations >180 degrees - */ - fun resetFilteringQuats(reference: Quaternion) { - filteringHandler.resetMovingAverage(getAdjustedRotation(), reference) - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt deleted file mode 100644 index d660c50cb5..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt +++ /dev/null @@ -1,69 +0,0 @@ -package dev.slimevr.tracking.trackers - -import dev.slimevr.config.FiltersConfig -import dev.slimevr.filtering.QuaternionMovingAverage -import dev.slimevr.filtering.TrackerFilters -import io.github.axisangles.ktmath.Quaternion - -/** - * Class taking care of filtering logic - * (smoothing and prediction) - * See QuaternionMovingAverage.kt for the quaternion math. - */ -class TrackerFilteringHandler { - // Instantiated by default in case config doesn't get read (if tracker doesn't support filtering) - private var movingAverage = QuaternionMovingAverage(TrackerFilters.NONE) - var filteringEnabled = false - - /** - * Reads/loads filtering settings from given config - */ - fun readFilteringConfig(config: FiltersConfig, currentRotation: Quaternion) { - val type = TrackerFilters.getByConfigkey(config.type) - if (type == TrackerFilters.SMOOTHING || type == TrackerFilters.PREDICTION) { - movingAverage = QuaternionMovingAverage( - type, - config.amount, - currentRotation, - ) - filteringEnabled = true - } else { - movingAverage = QuaternionMovingAverage( - TrackerFilters.NONE, - initialRotation = currentRotation, - ) - filteringEnabled = false - } - } - - /** - * Update the moving average to make it smooth - */ - fun update() { - movingAverage.update() - } - - /** - * Updates the latest rotation - */ - fun dataTick(currentRawRotation: Quaternion) { - movingAverage.addQuaternion(currentRawRotation) - } - - /** - * Call when doing a full reset to reset the tracking of rotations >180 degrees - */ - fun resetMovingAverage(currentRotation: Quaternion, reference: Quaternion) { - movingAverage.resetQuats(currentRotation, reference) - } - - /** - * Get the filtered rotation from the moving average (either prediction/smoothing or just >180 degs) - */ - fun getFilteredRotation() = movingAverage.filteredQuaternion - - /** - * Get the impact filtering has on the rotation - */ - fun getFilteringImpact(): Float = movingAverage.filteringImpact -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFlexHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFlexHandler.kt deleted file mode 100644 index 653292cfb0..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFlexHandler.kt +++ /dev/null @@ -1,176 +0,0 @@ -package dev.slimevr.tracking.trackers - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import kotlin.math.* - -/** - * Class handling flex sensor data (angle and resistance) - * Resistance is expected to go up with bend by default, but a mounting reset allows the contrary - */ -class TrackerFlexHandler(val tracker: Tracker) { - private var minResistance = Float.MIN_VALUE - private var maxResistance = Float.MAX_VALUE - - // Used to support resistance going both ways. - // Default is higher = more bend, but can change after a full and mounting reset. - private var lastMinResetResistance = Float.MIN_VALUE - private var resistanceReversed = false - private var lastResistance = 0f - private val thumbInitialOffset = FastMath.PI / 8 // 22.5 deg - - /** - * Resets the min resistance from the last resistance value received. - * Triggered from full reset - */ - fun resetMin() { - minResistance = lastResistance - lastMinResetResistance = lastResistance - - setFlexResistance(lastResistance) - tracker.dataTick() - } - - /** - * Resets the max resistance from the last resistance value received. - * Triggered from mounting reset - */ - fun resetMax() { - // Account for the resistance being able to be reversed - if (resistanceReversed != lastResistance < lastMinResetResistance) { - // Switching - resistanceReversed = lastResistance < lastMinResetResistance - minResistance = maxResistance - maxResistance = lastMinResetResistance - } else { - // Not switching - maxResistance = lastResistance - } - - setFlexResistance(lastResistance) - tracker.dataTick() - } - - /** - * Sets the flex resistance which is then calculated into an angle - */ - fun setFlexResistance(resistance: Float) { - // Dynamically calibrate the minimum resistance - minResistance = if (minResistance == Float.MIN_VALUE) { - resistance - } else if (!resistanceReversed) { - min(minResistance, resistance) - } else { - max(minResistance, resistance) - } - - // Dynamically calibrate the maximum resistance - maxResistance = if (maxResistance == Float.MAX_VALUE) { - resistance - } else if (!resistanceReversed) { - max(maxResistance, resistance) - } else { - min(maxResistance, resistance) - } - - // Get max angle - val maxBend = getMaxAngleForTrackerPosition(tracker.trackerPosition) - - // Get angle and set it - val angle = if (minResistance == maxResistance) { - // Avoid division by 0 - 0f - } else { - maxBend * (resistance - minResistance) / (maxResistance - minResistance) - } - setFlexAngle(angle) - - lastResistance = resistance - } - - /** - * Sets an angle (rad) about the X axis - */ - fun setFlexAngle(angle: Float) { - // Sets the rotation of the tracker by the angle about a given axis depending on - // the tracker's TrackerPosition - when (tracker.trackerPosition) { - TrackerPosition.LEFT_INDEX_PROXIMAL, TrackerPosition.LEFT_INDEX_INTERMEDIATE, - TrackerPosition.LEFT_INDEX_DISTAL, TrackerPosition.LEFT_MIDDLE_PROXIMAL, - TrackerPosition.LEFT_MIDDLE_INTERMEDIATE, TrackerPosition.LEFT_MIDDLE_DISTAL, - TrackerPosition.LEFT_RING_PROXIMAL, TrackerPosition.LEFT_RING_INTERMEDIATE, - TrackerPosition.LEFT_RING_DISTAL, TrackerPosition.LEFT_LITTLE_PROXIMAL, - TrackerPosition.LEFT_LITTLE_INTERMEDIATE, TrackerPosition.LEFT_LITTLE_DISTAL, - TrackerPosition.RIGHT_SHOULDER, - -> tracker.setRotation(EulerAngles(EulerOrder.YZX, 0f, 0f, angle).toQuaternion()) - - TrackerPosition.RIGHT_INDEX_PROXIMAL, TrackerPosition.RIGHT_INDEX_INTERMEDIATE, - TrackerPosition.RIGHT_INDEX_DISTAL, TrackerPosition.RIGHT_MIDDLE_PROXIMAL, - TrackerPosition.RIGHT_MIDDLE_INTERMEDIATE, TrackerPosition.RIGHT_MIDDLE_DISTAL, - TrackerPosition.RIGHT_RING_PROXIMAL, TrackerPosition.RIGHT_RING_INTERMEDIATE, - TrackerPosition.RIGHT_RING_DISTAL, TrackerPosition.RIGHT_LITTLE_PROXIMAL, - TrackerPosition.RIGHT_LITTLE_INTERMEDIATE, TrackerPosition.RIGHT_LITTLE_DISTAL, - TrackerPosition.LEFT_SHOULDER, - -> tracker.setRotation(EulerAngles(EulerOrder.YZX, 0f, 0f, -angle).toQuaternion()) - - TrackerPosition.LEFT_THUMB_METACARPAL, TrackerPosition.LEFT_THUMB_PROXIMAL, TrackerPosition.LEFT_THUMB_DISTAL, - -> tracker.setRotation(EulerAngles(EulerOrder.YZX, thumbInitialOffset - angle, -angle * 0.05f, angle * 0.1f).toQuaternion()) - - TrackerPosition.RIGHT_THUMB_METACARPAL, TrackerPosition.RIGHT_THUMB_PROXIMAL, TrackerPosition.RIGHT_THUMB_DISTAL, - -> tracker.setRotation(EulerAngles(EulerOrder.YZX, thumbInitialOffset - angle, angle * 0.05f, -angle * 0.1f).toQuaternion()) - - // Default to X axis (pitch) - else -> tracker.setRotation(EulerAngles(EulerOrder.YZX, angle, 0f, 0f).toQuaternion()) - } - } - - /** - * Gets the max angle for a TrackerPosition - */ - private fun getMaxAngleForTrackerPosition(trackerPosition: TrackerPosition?): Float { - if (trackerPosition == null) return FastMath.PI // 180 degrees - - return when (trackerPosition) { - // 270 degrees - TrackerPosition.LEFT_INDEX_DISTAL, TrackerPosition.LEFT_MIDDLE_DISTAL, - TrackerPosition.LEFT_RING_DISTAL, TrackerPosition.LEFT_LITTLE_DISTAL, - TrackerPosition.RIGHT_INDEX_DISTAL, TrackerPosition.RIGHT_MIDDLE_DISTAL, - TrackerPosition.RIGHT_RING_DISTAL, TrackerPosition.RIGHT_LITTLE_DISTAL, - -> FastMath.PI + FastMath.HALF_PI - - // 202.5 degrees - TrackerPosition.LEFT_THUMB_DISTAL, TrackerPosition.RIGHT_THUMB_DISTAL, - -> FastMath.PI + thumbInitialOffset - - // 180 degrees - TrackerPosition.LEFT_INDEX_INTERMEDIATE, TrackerPosition.LEFT_MIDDLE_INTERMEDIATE, - TrackerPosition.LEFT_RING_INTERMEDIATE, TrackerPosition.LEFT_LITTLE_INTERMEDIATE, - TrackerPosition.RIGHT_INDEX_INTERMEDIATE, TrackerPosition.RIGHT_MIDDLE_INTERMEDIATE, - TrackerPosition.RIGHT_RING_INTERMEDIATE, TrackerPosition.RIGHT_LITTLE_INTERMEDIATE, - -> FastMath.PI - - // 112.5 degrees - TrackerPosition.LEFT_THUMB_PROXIMAL, TrackerPosition.RIGHT_THUMB_PROXIMAL, - -> FastMath.HALF_PI + thumbInitialOffset - - // 90 degrees - TrackerPosition.LEFT_INDEX_PROXIMAL, TrackerPosition.LEFT_MIDDLE_PROXIMAL, - TrackerPosition.LEFT_RING_PROXIMAL, TrackerPosition.LEFT_LITTLE_PROXIMAL, - TrackerPosition.RIGHT_INDEX_PROXIMAL, TrackerPosition.RIGHT_MIDDLE_PROXIMAL, - TrackerPosition.RIGHT_RING_PROXIMAL, TrackerPosition.RIGHT_LITTLE_PROXIMAL, - -> FastMath.HALF_PI - - // 67.5 degrees - TrackerPosition.LEFT_THUMB_METACARPAL, TrackerPosition.RIGHT_THUMB_METACARPAL, - -> FastMath.QUARTER_PI + thumbInitialOffset - - // 45 degrees - TrackerPosition.LEFT_SHOULDER, TrackerPosition.RIGHT_SHOULDER, - -> FastMath.QUARTER_PI - - // 135 degrees - else -> FastMath.HALF_PI + FastMath.QUARTER_PI - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt deleted file mode 100644 index 65a6c79afa..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerPosition.kt +++ /dev/null @@ -1,236 +0,0 @@ -@file:Suppress("ktlint:standard:no-wildcard-imports") - -package dev.slimevr.tracking.trackers - -import dev.slimevr.tracking.trackers.TrackerPosition.* -import io.github.axisangles.ktmath.Quaternion -import solarxr_protocol.datatypes.BodyPart - -fun TrackerPosition?.isThigh(): Boolean { - this?.let { - return it == LEFT_UPPER_LEG || - it == RIGHT_UPPER_LEG - } - return false -} - -fun TrackerPosition?.isLeftArm(): Boolean { - this?.let { - return it == LEFT_SHOULDER || - it == LEFT_UPPER_ARM || - it == LEFT_LOWER_ARM || - it == LEFT_HAND - } - return false -} - -fun TrackerPosition?.isRightArm(): Boolean { - this?.let { - return it == RIGHT_SHOULDER || - it == RIGHT_UPPER_ARM || - it == RIGHT_LOWER_ARM || - it == RIGHT_HAND - } - return false -} - -fun TrackerPosition?.isLeftLowerArm(): Boolean { - this?.let { - return it == LEFT_LOWER_ARM || - it == LEFT_HAND - } - return false -} - -fun TrackerPosition?.isRightLowerArm(): Boolean { - this?.let { - return it == RIGHT_LOWER_ARM || - it == RIGHT_HAND - } - return false -} - -fun TrackerPosition?.isFoot(): Boolean { - this?.let { - return it == LEFT_FOOT || - it == RIGHT_FOOT - } - return false -} - -fun TrackerPosition?.isLeftFinger(): Boolean { - this?.let { - return it == LEFT_THUMB_METACARPAL || - it == LEFT_THUMB_PROXIMAL || - it == LEFT_THUMB_DISTAL || - it == LEFT_INDEX_PROXIMAL || - it == LEFT_INDEX_INTERMEDIATE || - it == LEFT_INDEX_DISTAL || - it == LEFT_MIDDLE_PROXIMAL || - it == LEFT_MIDDLE_INTERMEDIATE || - it == LEFT_MIDDLE_DISTAL || - it == LEFT_RING_PROXIMAL || - it == LEFT_RING_INTERMEDIATE || - it == LEFT_RING_DISTAL || - it == LEFT_LITTLE_PROXIMAL || - it == LEFT_LITTLE_INTERMEDIATE || - it == LEFT_LITTLE_DISTAL - } - return false -} - -fun TrackerPosition?.isRightFinger(): Boolean { - this?.let { - return it == RIGHT_THUMB_METACARPAL || - it == RIGHT_THUMB_PROXIMAL || - it == RIGHT_THUMB_DISTAL || - it == RIGHT_INDEX_PROXIMAL || - it == RIGHT_INDEX_INTERMEDIATE || - it == RIGHT_INDEX_DISTAL || - it == RIGHT_MIDDLE_PROXIMAL || - it == RIGHT_MIDDLE_INTERMEDIATE || - it == RIGHT_MIDDLE_DISTAL || - it == RIGHT_RING_PROXIMAL || - it == RIGHT_RING_INTERMEDIATE || - it == RIGHT_RING_DISTAL || - it == RIGHT_LITTLE_PROXIMAL || - it == RIGHT_LITTLE_INTERMEDIATE || - it == RIGHT_LITTLE_DISTAL - } - return false -} - -/** - * Represents a position on the body that a tracker could be placed. Any bone is - * a valid position. - * - * TrackerPosition intentionally lacks a numerical id to avoid breakage. - */ -enum class TrackerPosition( - val designation: String, - val trackerRole: TrackerRole?, - val bodyPart: Int, - val id: Int, -) { - // If updating BodyPart of a TrackerRole, - // please also update SteamVRBridge#updateShareSettingsAutomatically() - HEAD("body:head", TrackerRole.HMD, BodyPart.HEAD, 1), - NECK("body:neck", TrackerRole.NECK, BodyPart.NECK, 2), - UPPER_CHEST("body:upper_chest", TrackerRole.CHEST, BodyPart.UPPER_CHEST, 3), - CHEST("body:chest", null, BodyPart.CHEST, 4), - WAIST("body:waist", null, BodyPart.WAIST, 5), - HIP("body:hip", TrackerRole.WAIST, BodyPart.HIP, 6), - LEFT_UPPER_LEG("body:left_upper_leg", TrackerRole.LEFT_KNEE, BodyPart.LEFT_UPPER_LEG, 7), - RIGHT_UPPER_LEG("body:right_upper_leg", TrackerRole.RIGHT_KNEE, BodyPart.RIGHT_UPPER_LEG, 8), - LEFT_LOWER_LEG("body:left_lower_leg", null, BodyPart.LEFT_LOWER_LEG, 9), - RIGHT_LOWER_LEG("body:right_lower_leg", null, BodyPart.RIGHT_LOWER_LEG, 10), - LEFT_FOOT("body:left_foot", TrackerRole.LEFT_FOOT, BodyPart.LEFT_FOOT, 11), - RIGHT_FOOT("body:right_foot", TrackerRole.RIGHT_FOOT, BodyPart.RIGHT_FOOT, 12), - LEFT_LOWER_ARM("body:left_lower_arm", null, BodyPart.LEFT_LOWER_ARM, 13), - RIGHT_LOWER_ARM("body:right_lower_arm", null, BodyPart.RIGHT_LOWER_ARM, 14), - LEFT_UPPER_ARM("body:left_upper_arm", TrackerRole.LEFT_ELBOW, BodyPart.LEFT_UPPER_ARM, 15), - RIGHT_UPPER_ARM("body:right_upper_arm", TrackerRole.RIGHT_ELBOW, BodyPart.RIGHT_UPPER_ARM, 16), - LEFT_HAND("body:left_hand", TrackerRole.LEFT_HAND, BodyPart.LEFT_HAND, 17), - RIGHT_HAND("body:right_hand", TrackerRole.RIGHT_HAND, BodyPart.RIGHT_HAND, 18), - LEFT_SHOULDER("body:left_shoulder", TrackerRole.LEFT_SHOULDER, BodyPart.LEFT_SHOULDER, 19), - RIGHT_SHOULDER("body:right_shoulder", TrackerRole.RIGHT_SHOULDER, BodyPart.RIGHT_SHOULDER, 20), - LEFT_THUMB_METACARPAL("body:left_thumb_metacarpal", null, BodyPart.LEFT_THUMB_METACARPAL, 21), - LEFT_THUMB_PROXIMAL("body:left_thumb_proximal", null, BodyPart.LEFT_THUMB_PROXIMAL, 22), - LEFT_THUMB_DISTAL("body:left_thumb_distal", null, BodyPart.LEFT_THUMB_DISTAL, 23), - LEFT_INDEX_PROXIMAL("body:left_index_proximal", null, BodyPart.LEFT_INDEX_PROXIMAL, 24), - LEFT_INDEX_INTERMEDIATE("body:left_index_intermediate", null, BodyPart.LEFT_INDEX_INTERMEDIATE, 25), - LEFT_INDEX_DISTAL("body:left_index_distal", null, BodyPart.LEFT_INDEX_DISTAL, 26), - LEFT_MIDDLE_PROXIMAL("body:left_middle_proximal", null, BodyPart.LEFT_MIDDLE_PROXIMAL, 27), - LEFT_MIDDLE_INTERMEDIATE("body:left_middle_intermediate", null, BodyPart.LEFT_MIDDLE_INTERMEDIATE, 28), - LEFT_MIDDLE_DISTAL("body:left_middle_distal", null, BodyPart.LEFT_MIDDLE_DISTAL, 29), - LEFT_RING_PROXIMAL("body:left_ring_proximal", null, BodyPart.LEFT_RING_PROXIMAL, 30), - LEFT_RING_INTERMEDIATE("body:left_ring_intermediate", null, BodyPart.LEFT_RING_INTERMEDIATE, 31), - LEFT_RING_DISTAL("body:left_ring_distal", null, BodyPart.LEFT_RING_DISTAL, 32), - LEFT_LITTLE_PROXIMAL("body:left_little_proximal", null, BodyPart.LEFT_LITTLE_PROXIMAL, 33), - LEFT_LITTLE_INTERMEDIATE("body:left_little_intermediate", null, BodyPart.LEFT_LITTLE_INTERMEDIATE, 34), - LEFT_LITTLE_DISTAL("body:left_little_distal", null, BodyPart.LEFT_LITTLE_DISTAL, 35), - RIGHT_THUMB_METACARPAL("body:right_thumb_metacarpal", null, BodyPart.RIGHT_THUMB_METACARPAL, 36), - RIGHT_THUMB_PROXIMAL("body:right_thumb_proximal", null, BodyPart.RIGHT_THUMB_PROXIMAL, 37), - RIGHT_THUMB_DISTAL("body:right_thumb_distal", null, BodyPart.RIGHT_THUMB_DISTAL, 38), - RIGHT_INDEX_PROXIMAL("body:right_index_proximal", null, BodyPart.RIGHT_INDEX_PROXIMAL, 39), - RIGHT_INDEX_INTERMEDIATE("body:right_index_intermediate", null, BodyPart.RIGHT_INDEX_INTERMEDIATE, 40), - RIGHT_INDEX_DISTAL("body:right_index_distal", null, BodyPart.RIGHT_INDEX_DISTAL, 41), - RIGHT_MIDDLE_PROXIMAL("body:right_middle_proximal", null, BodyPart.RIGHT_MIDDLE_PROXIMAL, 42), - RIGHT_MIDDLE_INTERMEDIATE("body:right_middle_intermediate", null, BodyPart.RIGHT_MIDDLE_INTERMEDIATE, 43), - RIGHT_MIDDLE_DISTAL("body:right_middle_distal", null, BodyPart.RIGHT_MIDDLE_DISTAL, 44), - RIGHT_RING_PROXIMAL("body:right_ring_proximal", null, BodyPart.RIGHT_RING_PROXIMAL, 45), - RIGHT_RING_INTERMEDIATE("body:right_ring_intermediate", null, BodyPart.RIGHT_RING_INTERMEDIATE, 46), - RIGHT_RING_DISTAL("body:right_ring_distal", null, BodyPart.RIGHT_RING_DISTAL, 47), - RIGHT_LITTLE_PROXIMAL("body:right_little_proximal", null, BodyPart.RIGHT_LITTLE_PROXIMAL, 48), - RIGHT_LITTLE_INTERMEDIATE("body:right_little_intermediate", null, BodyPart.RIGHT_LITTLE_INTERMEDIATE, 49), - RIGHT_LITTLE_DISTAL("body:right_little_distal", null, BodyPart.RIGHT_LITTLE_DISTAL, 50), - ; - - /** - * Returns the default mounting orientation for the body part - */ - fun defaultMounting(): Quaternion = when (this) { - LEFT_LOWER_ARM, LEFT_HAND, - LEFT_INDEX_PROXIMAL, LEFT_INDEX_INTERMEDIATE, - LEFT_INDEX_DISTAL, LEFT_MIDDLE_PROXIMAL, - LEFT_MIDDLE_INTERMEDIATE, LEFT_MIDDLE_DISTAL, - LEFT_RING_PROXIMAL, LEFT_RING_INTERMEDIATE, - LEFT_RING_DISTAL, LEFT_LITTLE_PROXIMAL, - LEFT_LITTLE_INTERMEDIATE, LEFT_LITTLE_DISTAL, - -> Quaternion.SLIMEVR.LEFT - - RIGHT_LOWER_ARM, RIGHT_HAND, - RIGHT_INDEX_PROXIMAL, RIGHT_INDEX_INTERMEDIATE, - RIGHT_INDEX_DISTAL, RIGHT_MIDDLE_PROXIMAL, - RIGHT_MIDDLE_INTERMEDIATE, RIGHT_MIDDLE_DISTAL, - RIGHT_RING_PROXIMAL, RIGHT_RING_INTERMEDIATE, - RIGHT_RING_DISTAL, RIGHT_LITTLE_PROXIMAL, - RIGHT_LITTLE_INTERMEDIATE, RIGHT_LITTLE_DISTAL, - -> Quaternion.SLIMEVR.RIGHT - - LEFT_UPPER_ARM, LEFT_LOWER_LEG -> Quaternion.SLIMEVR.FRONT_LEFT - - RIGHT_UPPER_ARM, RIGHT_LOWER_LEG -> Quaternion.SLIMEVR.FRONT_RIGHT - - else -> Quaternion.SLIMEVR.FRONT - } - - companion object { - /** Indexed by `BodyPart` int value. EFFICIENCY FTW */ - private val byBodyPart: Array = arrayOfNulls(BodyPart.names.size).apply { - for (position in entries) { - this[position.bodyPart] = position - } - } - private val byId = entries.associateBy { it.id } - private val byDesignation = entries.associateBy { it.designation.lowercase() } - private val byTrackerRole = entries.filter { it.trackerRole != null }.associateBy { it.trackerRole!! } - - /** - * Gets the `TrackerPosition` by its string designation. - * - * @return Returns an optional as not all strings are valid designators. - */ - @JvmStatic - fun getByDesignation(designation: String): TrackerPosition? = byDesignation[designation.lowercase()] - - @JvmStatic - fun getByTrackerRole(role: TrackerRole): TrackerPosition? { - // Hands TrackerPositions are bound to the hands TrackerRoles, - // so we hardcode getting those. - if (role == TrackerRole.LEFT_CONTROLLER) { - return LEFT_HAND - } - if (role == TrackerRole.RIGHT_CONTROLLER) { - return RIGHT_HAND - } - return byTrackerRole[role] - } - - @JvmStatic - fun getByBodyPart(bodyPart: Int): TrackerPosition? = byBodyPart[bodyPart] - - @JvmStatic - fun getById(id: Int): TrackerPosition? = byId[id] - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt deleted file mode 100644 index 25a5fd3426..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt +++ /dev/null @@ -1,599 +0,0 @@ -package dev.slimevr.tracking.trackers - -import com.jme3.math.FastMath -import dev.slimevr.VRServer -import dev.slimevr.config.ArmsResetModes -import dev.slimevr.config.DriftCompensationConfig -import dev.slimevr.config.ResetsConfig -import dev.slimevr.filtering.CircularArrayList -import dev.slimevr.tracking.trackers.udp.TrackerDataType -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -private const val DRIFT_COOLDOWN_MS = 50000L - -/** Class taking care of full reset, yaw reset, mounting reset, and drift compensation logic. */ -class TrackerResetsHandler(val tracker: Tracker) { - - private val HalfHorizontal = EulerAngles( - EulerOrder.YZX, - 0f, - Math.PI.toFloat(), - 0f, - ).toQuaternion() - private var driftAmount = 0f - private var averagedDriftQuat = Quaternion.IDENTITY - private var rotationSinceReset = Quaternion.IDENTITY - private var driftQuats = CircularArrayList(0) - private var driftTimes = CircularArrayList(0) - private var totalDriftTime: Long = 0 - private var driftSince: Long = 0 - private var timeAtLastReset: Long = 0 - private var compensateDrift = false - private var driftPrediction = false - private var driftCompensationEnabled = false - private var armsResetMode = ArmsResetModes.BACK - private var yawResetSmoothTime = 0.0f - var saveMountingReset = false - var resetHmdPitch = false - var allowDriftCompensation = false - var lastResetQuaternion: Quaternion? = null - - // Manual mounting orientation - var mountingOrientation = HalfHorizontal - set(value) { - field = value - // Clear the mounting reset now that it's been set manually - clearMounting() - } - - // Reference adjustment quats - - /** - * Gyro fix is set by full reset. This sets the current y rotation to 0, correcting - * for initial yaw rotation and the rotation incurred by mounting orientation. This - * is a local offset in rotation and does not affect the axes of rotation. - * - * This rotation is only used to compute [attachmentFix], otherwise [yawFix] would - * correct for the same rotation. - */ - private var gyroFix = Quaternion.IDENTITY - - /** - * Attachment fix is set by full reset. This sets the current x and z rotations to - * 0, correcting for initial pitch and roll rotation. This is a global offset in - * rotation and affects the axes of rotation. - * - * This effectively sets the rotation at the moment of a full reset to be - * zero-reference in the x and z axes. - */ - private var attachmentFix = Quaternion.IDENTITY - - /** - * Mounting rotation fix is set by mounting reset. This corrects for the mounting - * orientation, then the inverse is used to correct for the rotation incurred. This - * value is computed after [yawFix], but takes effect before [yawFix]. This affects - * the axes of rotation, but does not incur an offset in rotation. - * - * This rotation is done in addition to [mountingOrientation] as to not interfere - * with the functionality of manual mounting orientation. This effectively sets the - * rotation at the moment of a mounting reset to be zero-reference in the y-axis. If - * no mounting reset is done, then this rotation will not be used and only - * [mountingOrientation] will apply. - */ - var mountRotFix = Quaternion.IDENTITY - private set - - /** - * Yaw fix is set by yaw reset. This sets the current y rotation to match the - * provided reference, correlating the tracker to the provided frame of reference. - * This is a local offset in rotation and does not affect the axes of rotation. - * - * This effectively aligns the current yaw rotation to the head tracker's yaw - * rotation. - */ - private var yawFix = Quaternion.IDENTITY - - /** - * Constraint fix is set by skeleton constraints. This corrects for any yaw rotation - * that violates the skeleton constraints. This is a local offset in rotation and - * does not affect the axes of rotation. - */ - private var constraintFix = Quaternion.IDENTITY - - // Zero-reference/identity adjustment quats for IMU debugging - private var gyroFixNoMounting = Quaternion.IDENTITY - private var attachmentFixNoMounting = Quaternion.IDENTITY - private var yawFixZeroReference = Quaternion.IDENTITY - - /** - * T-Pose down fix is set by full reset. This corrects for the pitch of the rotation - * assuming a t-pose reference, adjusting to match our expected i-pose reference. - * This is a global offset in rotation and affects the axes of rotation. - */ - private var tposeDownFix = Quaternion.IDENTITY - - /** - * Reads/loads drift compensation settings from given config - */ - fun readDriftCompensationConfig(config: DriftCompensationConfig) { - compensateDrift = false - driftPrediction = config.prediction - driftAmount = config.amount - val maxResets = config.maxResets - - if (compensateDrift && maxResets != driftQuats.capacity()) { - driftQuats = CircularArrayList(maxResets) - driftTimes = CircularArrayList(maxResets) - } - - refreshDriftCompensationEnabled() - } - - /** - * Clears drift compensation data - */ - fun clearDriftCompensation() { - driftSince = 0L - timeAtLastReset = 0L - totalDriftTime = 0L - driftQuats.clear() - driftTimes.clear() - } - - /** - * Checks for compensateDrift, allowDriftCompensation, and if - * a computed head tracker exists. - */ - fun refreshDriftCompensationEnabled() { - driftCompensationEnabled = compensateDrift && - allowDriftCompensation && - TrackerUtils.getNonInternalNonImuTrackerForBodyPosition( - VRServer.instance.allTrackers, - TrackerPosition.HEAD, - ) != null - } - - /** - * Reads/loads reset settings from the given config - */ - fun readResetConfig(config: ResetsConfig) { - armsResetMode = config.mode - yawResetSmoothTime = config.yawResetSmoothTime - saveMountingReset = config.saveMountingReset - resetHmdPitch = config.resetHmdPitch - } - - fun trySetMountingReset(quat: Quaternion) { - if (saveMountingReset) { - mountRotFix = quat - } - } - - /** - * Takes a rotation and adjusts it to resets, mounting, - * and drift compensation, with the HMD as the reference. - */ - fun getReferenceAdjustedDriftRotationFrom(rotation: Quaternion): Quaternion = adjustToDrift(adjustToReference(rotation)) - - /** - * Takes a rotation and adjusts it to resets and mounting, - * with the identity Quaternion as the reference. - */ - fun getIdentityAdjustedDriftRotationFrom(rotation: Quaternion): Quaternion = adjustToDrift(adjustToIdentity(rotation)) - - /** - * Get the reference adjusted accel. - */ - // TODO: Make this actually adjusted to the corrected IMU heading. The current - // implementation for heading correction doesn't appear to be correct and may simply - // make acceleration worse, so I'm just leaving this until we work that out. The - // output of this will be world space, but with an unknown offset to heading (yaw). - // - Butterscotch - fun getReferenceAdjustedAccel(rawRot: Quaternion, accel: Vector3): Vector3 = rawRot.sandwich(accel) - - /** - * Converts raw or filtered rotation into reference- and - * mounting-reset-adjusted by applying quaternions produced after - * full reset, yaw rest and mounting reset - */ - private fun adjustToReference(rotation: Quaternion): Quaternion { - var rot = rotation - // Align heading axis with bone space - if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) { - rot *= mountingOrientation - } - // Heading correction assuming manual orientation is correct - rot = gyroFix * rot - // Align attitude axes with bone space - rot *= attachmentFix - // Secondary heading axis alignment with bone space for automatic mounting - // Note: Applying an inverse amount of heading correction corresponding to the - // axis alignment quaternion will leave the correction to another variable - rot = mountRotFix.inv() * (rot * mountRotFix) - // More attitude axes alignment specifically for the t-pose configuration, this - // probably shouldn't be a separate variable from attachmentFix? - rot *= tposeDownFix - // More heading correction - rot = yawFix * rot - rot = constraintFix * rot - return rot - } - - /** - * Converts raw or filtered rotation into zero-reference-adjusted by - * applying quaternions produced after full reset and yaw reset only - */ - // This is essentially just adjustToReference but aligning to quaternion identity - // rather than to the bone. - private fun adjustToIdentity(rotation: Quaternion): Quaternion { - var rot = rotation - rot = gyroFixNoMounting * rot - rot *= attachmentFixNoMounting - rot = yawFixZeroReference * rot - rot = constraintFix * rot - return rot - } - - /** - * Adjust the given rotation for drift compensation if enabled, - * and returns it - */ - private fun adjustToDrift(rotation: Quaternion): Quaternion { - if (driftCompensationEnabled && totalDriftTime > 0) { - var driftTimeRatio = ((System.currentTimeMillis() - driftSince).toFloat() / totalDriftTime) - if (!driftPrediction) { - driftTimeRatio = min(1.0f, driftTimeRatio) - } - return averagedDriftQuat.pow(driftAmount * driftTimeRatio) * rotation - } - return rotation - } - - /** - * Reset the tracker so that its current rotation is counted as (0, HMD Yaw, - * 0). This allows the tracker to be strapped to body at any pitch and roll. - */ - fun resetFull(reference: Quaternion) { - constraintFix = Quaternion.IDENTITY - - if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) { - tracker.trackerFlexHandler.resetMin() - postProcessResetFull(reference) - return - } else if (tracker.trackerDataType == TrackerDataType.FLEX_ANGLE) { - postProcessResetFull(reference) - return - } - - // Adjust for T-Pose (down) - tposeDownFix = if (((tracker.trackerPosition.isLeftArm() || tracker.trackerPosition.isLeftFinger()) && armsResetMode == ArmsResetModes.TPOSE_DOWN)) { - EulerAngles(EulerOrder.YZX, 0f, 0f, -FastMath.HALF_PI).toQuaternion() - } else if (((tracker.trackerPosition.isRightArm() || tracker.trackerPosition.isRightFinger()) && armsResetMode == ArmsResetModes.TPOSE_DOWN)) { - EulerAngles(EulerOrder.YZX, 0f, 0f, FastMath.HALF_PI).toQuaternion() - } else { - Quaternion.IDENTITY - } - - // Old rot for drift compensation - val oldRot = adjustToReference(tracker.getRawRotation()) - lastResetQuaternion = oldRot - - // Adjust raw rotation to mountingOrientation - val mountingAdjustedRotation = tracker.getRawRotation() * mountingOrientation - - // Gyrofix - if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) { - gyroFix = if (tracker.isComputed) { - fixGyroscope(tracker.getRawRotation()) - } else { - fixGyroscope(mountingAdjustedRotation * tposeDownFix) - } - } - - // Mounting for computed trackers - if (tracker.isComputed && tracker.trackerPosition != TrackerPosition.HEAD) { - // Set mounting to the reference's yaw so that a computed - // tracker goes forward according to the head tracker. - mountRotFix = getYawQuaternion(reference) - } - - // Attachment fix - attachmentFix = if (tracker.trackerPosition == TrackerPosition.HEAD && tracker.isHmd) { - if (resetHmdPitch) { - // Reset the HMD's pitch if it's assigned to head and resetHmdPitch is true - // Get rotation without yaw (make sure to use the raw rotation directly!) - val rotBuf = getYawQuaternion(tracker.getRawRotation()).inv() * tracker.getRawRotation() - // Isolate pitch - Quaternion(rotBuf.w, -rotBuf.x, 0f, 0f).unit() - } else { - // Don't reset the HMD at all - Quaternion.IDENTITY - } - } else { - fixAttachment(mountingAdjustedRotation) - } - - // Rotate attachmentFix by 180 degrees as a workaround for t-pose (down) - if (tposeDownFix != Quaternion.IDENTITY && tracker.allowMounting) { - attachmentFix *= HalfHorizontal - } - - makeIdentityAdjustmentQuatsFull() - - // Don't adjust yaw if head and computed - if (tracker.trackerPosition != TrackerPosition.HEAD || !tracker.isComputed) { - yawFix = fixYaw(mountingAdjustedRotation, reference) - tracker.yawResetSmoothing.reset() - } - - calculateDrift(oldRot) - - // Reset Stay Aligned (before resetting filtering, which depends on the - // tracker's rotation) - tracker.stayAligned.reset() - - postProcessResetFull(reference) - } - - private fun postProcessResetFull(reference: Quaternion) { - if (this.tracker.needReset) { - this.tracker.needReset = false - } - - tracker.resetFilteringQuats(reference) - } - - /** - * Reset the tracker so that its current yaw rotation is aligned with the HMD's - * Yaw. This allows the tracker to have yaw independent of the HMD. Tracker - * should still report yaw as if it was mounted facing HMD, mounting - * position should be corrected in the source. - */ - fun resetYaw(reference: Quaternion) { - // TODO HMD doesn't get yaw reset, which makes it so tracker.resetFilteringQuats() doesn't get called - - constraintFix = Quaternion.IDENTITY - - if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE || - tracker.trackerDataType == TrackerDataType.FLEX_ANGLE - ) { - // Don't do anything as these don't have yaw anyways - return - } - - // Old rot for drift compensation - val oldRot = adjustToReference(tracker.getRawRotation()) - lastResetQuaternion = oldRot - - val yawFixOld = yawFix - yawFix = fixYaw(tracker.getRawRotation() * mountingOrientation, reference) - tracker.yawResetSmoothing.reset() - - makeIdentityAdjustmentQuatsYaw() - - calculateDrift(oldRot) - - // Start at yaw before reset if smoothing enabled - if (yawResetSmoothTime > 0.0f) { - tracker.yawResetSmoothing.interpolate( - yawFixOld / yawFix, - Quaternion.IDENTITY, - yawResetSmoothTime, - ) - } - - // Reset Stay Aligned (before resetting filtering, which depends on the - // tracker's rotation) - tracker.stayAligned.reset() - - tracker.resetFilteringQuats(reference) - } - - /** - * Perform the math to align the tracker to go forward - * and stores it in mountRotFix, and adjusts yawFix - */ - fun resetMounting(reference: Quaternion) { - if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) { - tracker.trackerFlexHandler.resetMax() - tracker.resetFilteringQuats(reference) - return - } else if (tracker.trackerDataType == TrackerDataType.FLEX_ANGLE) { - return - } - - constraintFix = Quaternion.IDENTITY - - // Get the current calibrated rotation - var rotBuf = adjustToDrift(tracker.getRawRotation() * mountingOrientation) - rotBuf = gyroFix * rotBuf - rotBuf *= attachmentFix - rotBuf = yawFix * rotBuf - - // Adjust buffer to reference - rotBuf = reference.project(Vector3.POS_Y).inv().unit() * rotBuf - - // Rotate a vector pointing up by the quat - val rotVector = rotBuf.sandwich(Vector3.POS_Y) - - // Calculate the yaw angle using tan - var yawAngle = atan2(rotVector.x, rotVector.z) - - // Adjust for T-Pose and fingers - if ((tracker.trackerPosition.isLeftArm() && armsResetMode == ArmsResetModes.TPOSE_DOWN) || - (tracker.trackerPosition.isRightArm() && armsResetMode == ArmsResetModes.TPOSE_UP) || - tracker.trackerPosition.isLeftFinger() - ) { - // Tracker goes right - yawAngle -= FastMath.HALF_PI - } - if ((tracker.trackerPosition.isLeftArm() && armsResetMode == ArmsResetModes.TPOSE_UP) || - (tracker.trackerPosition.isRightArm() && armsResetMode == ArmsResetModes.TPOSE_DOWN) || - tracker.trackerPosition.isRightFinger() - ) { - // Tracker goes left - yawAngle += FastMath.HALF_PI - } - - // Adjust for forward/back arms and thighs - val isLowerArmBack = armsResetMode == ArmsResetModes.BACK && (tracker.trackerPosition.isLeftLowerArm() || tracker.trackerPosition.isRightLowerArm()) - val isArmForward = armsResetMode == ArmsResetModes.FORWARD && (tracker.trackerPosition.isLeftArm() || tracker.trackerPosition.isRightArm()) - if (!tracker.trackerPosition.isThigh() && !isArmForward && !isLowerArmBack) { - // Tracker goes back - yawAngle -= FastMath.PI - } - - // Make an adjustment quaternion from the angle - mountRotFix = EulerAngles(EulerOrder.YZX, 0f, yawAngle, 0f).toQuaternion() - - // save mounting reset - if (saveMountingReset) tracker.saveMountingResetOrientation(mountRotFix) - - tracker.resetFilteringQuats(reference) - } - - /** - * Apply a corrective rotation to the gyroFix - */ - fun updateConstraintFix(correctedRotation: Quaternion) { - constraintFix *= correctedRotation - } - - fun clearMounting() { - mountRotFix = Quaternion.IDENTITY - } - - private fun fixGyroscope(sensorRotation: Quaternion): Quaternion = getYawQuaternion(sensorRotation).inv() - - private fun fixAttachment(sensorRotation: Quaternion): Quaternion = (gyroFix * sensorRotation).inv() - - private fun fixYaw(sensorRotation: Quaternion, reference: Quaternion): Quaternion { - var rot = gyroFix * sensorRotation - rot *= attachmentFix - rot = mountRotFix.inv() * (rot * mountRotFix) - rot = getYawQuaternion(rot) - return rot.inv() * reference.project(Vector3.POS_Y).unit() - } - - // TODO : isolating yaw for yaw reset bad. - // The way we isolate the tracker's yaw for yaw reset is - // incorrect. Projection around the Y-axis is worse. - // In both cases, the isolated yaw value changes - // with the tracker's roll when pointing forward. - // calling twinNearest() makes sure this rotation has the wanted polarity (+-). - private fun getYawQuaternion(rot: Quaternion): Quaternion = EulerAngles(EulerOrder.YZX, 0f, rot.toEulerAngles(EulerOrder.YZX).y, 0f).toQuaternion().twinNearest(rot) - - private fun makeIdentityAdjustmentQuatsFull() { - val sensorRotation = tracker.getRawRotation() - gyroFixNoMounting = fixGyroscope(sensorRotation) - attachmentFixNoMounting = (gyroFixNoMounting * sensorRotation).inv() - yawFixZeroReference = Quaternion.IDENTITY - } - - private fun makeIdentityAdjustmentQuatsYaw() { - var sensorRotation = tracker.getRawRotation() - sensorRotation = gyroFixNoMounting * sensorRotation - sensorRotation *= attachmentFixNoMounting - yawFixZeroReference = fixGyroscope(sensorRotation) - } - - /** - * Calculates drift since last reset and store the data related to it in - * driftQuat and timeAtLastReset - */ - private fun calculateDrift(beforeQuat: Quaternion) { - if (driftCompensationEnabled) { - val rotQuat = adjustToReference(tracker.getRawRotation()) - - if (driftSince > 0 && System.currentTimeMillis() - timeAtLastReset > DRIFT_COOLDOWN_MS) { - // Check and remove from lists to keep them under the reset limit - if (driftQuats.size == driftQuats.capacity()) { - driftQuats.removeLast() - driftTimes.removeLast() - } - - // Add new drift quaternion - driftQuats.add(getYawQuaternion(rotQuat) / getYawQuaternion(beforeQuat)) - - // Add drift time to total - driftTimes.add(System.currentTimeMillis() - driftSince) - totalDriftTime = 0 - for (time in driftTimes) { - totalDriftTime += time - } - - // Calculate drift Quaternions' weights - val driftWeights = ArrayList(driftTimes.size) - for (time in driftTimes) { - driftWeights.add(time.toFloat() / totalDriftTime.toFloat()) - } - - // Make it so recent Quaternions weigh more - for (i in driftWeights.size - 1 downTo 1) { - // Add some of i-1's value to i - driftWeights[i] = driftWeights[i] + driftWeights[i - 1] / driftWeights.size - // Remove the value that was added to i from i-1 - driftWeights[i - 1] = driftWeights[i - 1] - driftWeights[i - 1] / driftWeights.size - } - - // Set final averaged drift Quaternion - averagedDriftQuat = fromAveragedQuaternions(driftQuats, driftWeights) - - // Save tracker rotation and current time - rotationSinceReset = driftQuats.latest - timeAtLastReset = System.currentTimeMillis() - } else if (System.currentTimeMillis() - timeAtLastReset < DRIFT_COOLDOWN_MS && driftQuats.size > 0) { - // Replace latest drift quaternion - rotationSinceReset *= (getYawQuaternion(rotQuat) / getYawQuaternion(beforeQuat)) - driftQuats[driftQuats.size - 1] = rotationSinceReset - - // Add drift time to total - driftTimes[driftTimes.size - 1] = driftTimes.latest + System.currentTimeMillis() - driftSince - totalDriftTime = 0 - for (time in driftTimes) { - totalDriftTime += time - } - - // Calculate drift Quaternions' weights - val driftWeights = ArrayList(driftTimes.size) - for (time in driftTimes) { - driftWeights.add(time.toFloat() / totalDriftTime.toFloat()) - } - - // Make it so recent Quaternions weigh more - for (i in driftWeights.size - 1 downTo 1) { - // Add some of i-1's value to i - driftWeights[i] = driftWeights[i] + driftWeights[i - 1] / driftWeights.size - // Remove the value that was added to i from i-1 - driftWeights[i - 1] = driftWeights[i - 1] - driftWeights[i - 1] / driftWeights.size - } - - // Set final averaged drift Quaternion - averagedDriftQuat = fromAveragedQuaternions(driftQuats, driftWeights) - } else { - timeAtLastReset = System.currentTimeMillis() - } - - driftSince = System.currentTimeMillis() - } - } - - /** - * Calculates and returns the averaged Quaternion - * from the given Quaternions and weights. - */ - private fun fromAveragedQuaternions( - qn: CircularArrayList, - tn: ArrayList, - ): Quaternion { - var totalMatrix = qn[0].toMatrix() * tn[0] - for (i in 1 until qn.size) { - totalMatrix += (qn[i].toMatrix() * tn[i]) - } - return totalMatrix.toQuaternion() - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerRole.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerRole.kt deleted file mode 100644 index 081bb5ed86..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerRole.kt +++ /dev/null @@ -1,42 +0,0 @@ -package dev.slimevr.tracking.trackers - -/** - * The tracker role classifies the position and the role of a tracker on user's - * body or playspace (like CAMERA or BEACON). Tracker roles are hints for - * interacting programs what the tracker means, and they do not correspond to - * body's bones on purpose. Example: virtual vive trackers for SteamVR vs actual - * SlimeVR trackers. - */ -enum class TrackerRole(val id: Int, val roleHint: String, val viveRole: String) { - // @formatter:off - NONE(0, "", ""), - WAIST(1, "vive_tracker_waist", "TrackerRole_Waist"), - LEFT_FOOT(2, "vive_tracker_left_foot", "TrackerRole_LeftFoot"), - RIGHT_FOOT(3, "vive_tracker_right_foot", "TrackerRole_RightFoot"), - CHEST(4, "vive_tracker_chest", "TrackerRole_Chest"), - LEFT_KNEE(5, "vive_tracker_left_knee", "TrackerRole_LeftKnee"), - RIGHT_KNEE(6, "vive_tracker_right_knee", "TrackerRole_RightKnee"), - LEFT_ELBOW(7, "vive_tracker_left_elbow", "TrackerRole_LeftElbow"), - RIGHT_ELBOW(8, "vive_tracker_right_elbow", "TrackerRole_RightElbow"), - LEFT_SHOULDER(9, "vive_tracker_left_shoulder", "TrackerRole_LeftShoulder"), - RIGHT_SHOULDER(10, "vive_tracker_right_shoulder", "TrackerRole_RightShoulder"), - LEFT_HAND(11, "vive_tracker_handed", "TrackerRole_Handed"), - RIGHT_HAND(12, "vive_tracker_handed", "TrackerRole_Handed"), - LEFT_CONTROLLER(13, "vive_tracker_handed", "TrackerRole_Handed"), - RIGHT_CONTROLLER(14, "vive_tracker_handed", "TrackerRole_Handed"), - HEAD(15, "", ""), - NECK(16, "", ""), - CAMERA(17, "vive_tracker_camera", "TrackerRole_Camera"), - KEYBOARD(18, "vive_tracker_keyboard", "TrackerRole_Keyboard"), - HMD(19, "", ""), - BEACON(20, "", ""), - GENERIC_CONTROLLER(21, "vive_tracker_handed", "TrackerRole_Handed"), - ; - - companion object { - private val byId = values().associateBy { it.id } - - @JvmStatic - fun getById(id: Int): TrackerRole? = byId[id] - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt deleted file mode 100644 index 3ba3973d9a..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slimevr.tracking.trackers - -enum class TrackerStatus(val id: Int, val sendData: Boolean, val reset: Boolean) { - - DISCONNECTED(0, false, true), - OK(1, true, false), - BUSY(2, true, false), - ERROR(3, false, true), - OCCLUDED(4, false, false), - TIMED_OUT(5, false, false), - ; - - companion object { - - private val byId = entries.associateBy { it.id } - - @JvmStatic - fun getById(id: Int): TrackerStatus? = byId[id] - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatusListener.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatusListener.kt deleted file mode 100644 index 4f4069be67..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatusListener.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.tracking.trackers - -interface TrackerStatusListener { - - fun onTrackerStatusChanged(tracker: Tracker, oldStatus: TrackerStatus, newStatus: TrackerStatus) -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt deleted file mode 100644 index f3e73b3eee..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt +++ /dev/null @@ -1,119 +0,0 @@ -package dev.slimevr.tracking.trackers - -import solarxr_protocol.datatypes.BodyPart - -object TrackerUtils { - - /** - * Finds a suitable tracker for use in the SlimeVR skeleton - * in allTrackers matching the position. - * - * This won't return disconnected, errored or internal trackers, - * but it will return timed out trackers, and they have a lower - * priority than normal trackers. - * - * @return A tracker for use in the SlimeVR skeleton - */ - @JvmStatic - fun getTrackerForSkeleton( - allTrackers: List, - position: TrackerPosition, - ): Tracker? = getNonInternalTrackerForBodyPosition(allTrackers, position) - - /** - * Finds the first non-internal tracker from allTrackers - * matching the position, that is not `TrackerStatus.reset`. - * It will also choose timed out trackers, but they have a lower - * priority than the rest of the trackers - * - * @return A non-internal tracker - */ - private fun getNonInternalTrackerForBodyPosition( - allTrackers: List, - position: TrackerPosition, - ): Tracker? { - val resetTrackers = allTrackers.filter { - it.trackerPosition == position && - !it.isInternal && - !it.status.reset - } - return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() - } - - /** - * Finds the first non-internal non-computed tracker from allTrackers - * matching the position, that is not `TrackerStatus.reset`. - * It will also choose timed out trackers, but they have a lower - * priority than the rest of the trackers - * - * @return A non-internal non-computed tracker - */ - private fun getNonInternalNonComputedTrackerForBodyPosition( - allTrackers: List, - position: TrackerPosition, - ): Tracker? { - val resetTrackers = allTrackers.filter { - it.trackerPosition == position && - !it.isComputed && - !it.isInternal && - !it.status.reset - } - return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() - } - - /** - * Finds the first non-internal and non-imu tracker from allTrackers - * matching the position, that is not `TrackerStatus.reset`. - * It will also choose timed out trackers, but they have a lower - * priority than the rest of the trackers - * @return A non-internal non-imu tracker - */ - @JvmStatic - fun getNonInternalNonImuTrackerForBodyPosition( - allTrackers: List, - position: TrackerPosition, - ): Tracker? { - val resetTrackers = allTrackers.filter { - it.trackerPosition == position && - !it.isImu() && - !it.isInternal && - !it.status.reset - } - return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() - } - - /** - * Returns the first tracker that isn't null out of the n trackers passed as - * arguments. - * - * @return The first non-null tracker or null - */ - @JvmStatic - fun getFirstAvailableTracker( - vararg trackers: Tracker?, - ): Tracker? = trackers.firstOrNull { it != null } - - val allBodyPartsButFingers = listOf( - BodyPart.HEAD, BodyPart.NECK, BodyPart.UPPER_CHEST, - BodyPart.CHEST, BodyPart.WAIST, BodyPart.HIP, - BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG, BodyPart.LEFT_LOWER_LEG, - BodyPart.RIGHT_LOWER_LEG, BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM, - BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, BodyPart.LEFT_HAND, - BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER, - BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT, - ) - - val allBodyPartsButFingersAndFeets = listOf( - BodyPart.HEAD, BodyPart.NECK, BodyPart.UPPER_CHEST, - BodyPart.CHEST, BodyPart.WAIST, BodyPart.HIP, - BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG, BodyPart.LEFT_LOWER_LEG, - BodyPart.RIGHT_LOWER_LEG, BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM, - BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, BodyPart.LEFT_HAND, - BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER, - ) - - val feetsBodyParts = listOf( - BodyPart.LEFT_FOOT, - BodyPart.RIGHT_FOOT, - ) -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt deleted file mode 100644 index b1be8d1330..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt +++ /dev/null @@ -1,383 +0,0 @@ -package dev.slimevr.tracking.trackers.hid - -import com.jme3.math.FastMath -import dev.slimevr.VRServer -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.udp.BoardType -import dev.slimevr.tracking.trackers.udp.IMUType -import dev.slimevr.tracking.trackers.udp.MCUType -import dev.slimevr.tracking.trackers.udp.MagnetometerStatus -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Quaternion.Companion.fromRotationVector -import io.github.axisangles.ktmath.Vector3 -import java.nio.ByteBuffer -import java.util.function.Consumer -import kotlin.collections.set -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt - -/** - * A collection of shared HID functions between OS specific HID implementations. - */ -class HIDCommon { - companion object { - const val HID_TRACKER_RECEIVER_VID = 0x1209 - const val HID_TRACKER_RECEIVER_PID = 0x7690 - const val HID_TRACKER_PID = 0x7692 - - const val PACKET_SIZE = 16 - - private val AXES_OFFSET = fromRotationVector(-FastMath.HALF_PI, 0f, 0f) - - fun deviceIdLookup( - hidDevices: MutableList, - hidSerialNumber: String?, - deviceId: Int, - deviceName: String? = null, - deviceList: MutableList, - ): HIDDevice? { - synchronized(hidDevices) { - deviceList.map { hidDevices[it] }.find { it.hidId == deviceId }?.let { return it } - if (deviceName == null) { // not registered yet - return null - } - val device = HIDDevice(deviceId) - // server wants tracker to be unique, so use combination of hid serial and full id // TODO: use the tracker "address" instead - // TODO: the server should not setup any device, only when the receiver associates the id with the tracker "address" and sends this packet (0xff?) which it will do occasionally - // device.name = hidDevice.serialNumber ?: "Unknown HID Device" - // device.name += "-$deviceId" - device.name = deviceName - device.manufacturer = "HID Device" // TODO: - // device.manufacturer = hidDevice.manufacturer ?: "HID Device" -// device.hardwareIdentifier = hidDevice.serialNumber // hardwareIdentifier is not used to identify the tracker, so also display the receiver serial -// device.hardwareIdentifier += "-$deviceId/$deviceName" // receiver serial + assigned id in receiver + device address - device.hardwareIdentifier = deviceName // the rest of identifier wont fit in gui - hidDevices.add(device) - deviceList.add(hidDevices.size - 1) - VRServer.instance.deviceManager.addDevice(device) // actually add device to the server - LogManager - .info( - "[TrackerServer] Added device $deviceName for ${hidSerialNumber ?: "Unknown HID Device"}, id $deviceId", - ) - return device - } - } - - private fun setUpSensor( - device: HIDDevice, - trackerId: Int, - sensorType: IMUType, - sensorStatus: TrackerStatus, - magStatus: MagnetometerStatus, - trackersConsumer: Consumer, - ) { - // LogManager.info("[TrackerServer] Sensor $trackerId for ${device.name}, status $sensorStatus") - var imuTracker = device.getTracker(trackerId) - if (imuTracker == null) { - var formattedHWID = device.hardwareIdentifier.replace(":", "").takeLast(5) - imuTracker = Tracker( - device, - VRServer.getNextLocalTrackerId(), - device.name + "/" + trackerId, - "Tracker $formattedHWID", - null, - trackerNum = trackerId, - hasRotation = true, - hasAcceleration = true, - userEditable = true, - imuType = sensorType, - allowFiltering = true, - allowReset = true, - allowMounting = true, - usesTimeout = false, - magStatus = magStatus, - ) - // usesTimeout false because HID trackers aren't "Disconnected" unless receiver is physically removed probably - // TODO: Could tracker maybe use "Timed out" status without marking as disconnecting? - // TODO: can be marked as "Disconnected" by timeout if the tracker has enabled activity timeouts - device.trackers[trackerId] = imuTracker - trackersConsumer.accept(imuTracker) - imuTracker.status = sensorStatus - LogManager - .info( - "[TrackerServer] Added sensor $trackerId for ${device.name}, type $sensorType", - ) - } - } - - fun processPacket( - dataReceived: ByteArray, - i: Int, - packetType: Int, - device: HIDDevice, - q: IntArray, - a: IntArray, - m: IntArray, - trackersConsumer: Consumer, - ) { - val trackerId = 0 // no concept of extensions - - // Register tracker - if (packetType == 0) { // Tracker register packet (device info) - val imu_id = dataReceived[i + 8].toUByte().toInt() - val mag_id = dataReceived[i + 9].toUByte().toInt() - val sensorType = IMUType.getById(imu_id.toUInt()) - // only able to register magnetometer status, not magnetometer type - val magStatus = MagnetometerStatus.getById(mag_id.toUByte()) - if (sensorType != null && magStatus != null) { - setUpSensor(device, trackerId, sensorType, TrackerStatus.OK, magStatus, trackersConsumer) - } - } - - val tracker: Tracker? = device.getTracker(trackerId) - if (tracker == null) { // not registered yet - return - } - - // Packet data - var runtime: Long? = null - var batt: Int? = null - var batt_v: Int? = null - var temp: Int? = null - var brd_id: Int? = null - var mcu_id: Int? = null - var button: Int? = null - // var imu_id: Int? = null - // var mag_id: Int? = null - var fw_date: Int? = null - var fw_major: Int? = null - var fw_minor: Int? = null - var fw_patch: Int? = null - var svr_status: Int? = null - // var status: Int? = null // raw status from tracker - var rssi: Int? = null - var packets_received: Int? = null - var packets_lost: Int? = null - var windows_hit: Int? = null - var windows_missed: Int? = null - - // Tracker packets - when (packetType) { - 0 -> { // device info - batt = dataReceived[i + 2].toUByte().toInt() - batt_v = dataReceived[i + 3].toUByte().toInt() - temp = dataReceived[i + 4].toUByte().toInt() - brd_id = dataReceived[i + 5].toUByte().toInt() - mcu_id = dataReceived[i + 6].toUByte().toInt() - // imu_id = dataReceived[i + 8].toUByte().toInt() - // mag_id = dataReceived[i + 9].toUByte().toInt() - // ushort little endian - fw_date = dataReceived[i + 11].toUByte().toInt() shl 8 or dataReceived[i + 10].toUByte().toInt() - fw_major = dataReceived[i + 12].toUByte().toInt() - fw_minor = dataReceived[i + 13].toUByte().toInt() - fw_patch = dataReceived[i + 14].toUByte().toInt() - rssi = dataReceived[i + 15].toUByte().toInt() - } - - 1 -> { // full precision quat and accel, no extra data - // Q15: 1 is represented as 0x7FFF, -1 as 0x8000 - // The sender can use integer saturation to avoid overflow - for (j in 0..3) { // quat received as fixed Q15 - // Q15 as short little endian - q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt() - } - for (j in 0..2) { // accel received as fixed 7, in m/s^2 - // Q7 as short little endian - a[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt() - } - } - - 2 -> { // reduced precision quat and accel with data - batt = dataReceived[i + 2].toUByte().toInt() - batt_v = dataReceived[i + 3].toUByte().toInt() - temp = dataReceived[i + 4].toUByte().toInt() - // quaternion is quantized as exponential map - // X = 10 bits, Y/Z = 11 bits - val buffer = ByteBuffer.wrap(dataReceived, i + 5, 4) - buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN) - val q_buf = buffer.getInt().toUInt() - q[0] = (q_buf and 1023u).toInt() - q[1] = (q_buf shr 10 and 2047u).toInt() - q[2] = (q_buf shr 21 and 2047u).toInt() - for (j in 0..2) { // accel received as fixed 7, in m/s^2 - // Q7 as short little endian - a[j] = dataReceived[i + 9 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 9 + j * 2].toUByte().toInt() - } - rssi = dataReceived[i + 15].toUByte().toInt() - } - - 3 -> { // status - svr_status = dataReceived[i + 2].toUByte().toInt() - // status = dataReceived[i + 3].toUByte().toInt() - packets_received = dataReceived[i + 4].toUByte().toInt() - packets_lost = dataReceived[i + 5].toUByte().toInt() - windows_hit = dataReceived[i + 6].toUByte().toInt() - windows_missed = dataReceived[i + 7].toUByte().toInt() - rssi = dataReceived[i + 15].toUByte().toInt() - } - - 4 -> { // full precision quat and mag, no extra data - for (j in 0..3) { // quat received as fixed Q15 - // Q15 as short little endian - q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt() - } - for (j in 0..2) { // mag received as fixed 10, in gauss - // Q10 as short little endian - m[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt() - } - } - - 5 -> { // runtime - // ulong as little endian - runtime = (dataReceived[i + 9].toUByte().toLong() shl 56) or (dataReceived[i + 8].toUByte().toLong() shl 48) or (dataReceived[i + 7].toUByte().toLong() shl 40) or (dataReceived[i + 6].toUByte().toLong() shl 32) or (dataReceived[i + 5].toUByte().toLong() shl 24) or (dataReceived[i + 4].toUByte().toLong() shl 16) or (dataReceived[i + 3].toUByte().toLong() shl 8) or dataReceived[i + 2].toUByte().toLong() - } - - 6 -> { // data - button = dataReceived[i + 2].toUByte().toInt() - rssi = dataReceived[i + 15].toUByte().toInt() - } - - 7 -> { // reduced precision quat and accel with data - button = dataReceived[i + 2].toUByte().toInt() - // quaternion is quantized as exponential map - // X = 10 bits, Y/Z = 11 bits - val buffer = ByteBuffer.wrap(dataReceived, i + 5, 4) - buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN) - val q_buf = buffer.getInt().toUInt() - q[0] = (q_buf and 1023u).toInt() - q[1] = (q_buf shr 10 and 2047u).toInt() - q[2] = (q_buf shr 21 and 2047u).toInt() - for (j in 0..2) { // accel received as fixed 7, in m/s^2 - // Q7 as short little endian - a[j] = dataReceived[i + 9 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 9 + j * 2].toUByte().toInt() - } - rssi = dataReceived[i + 15].toUByte().toInt() - } - - else -> { - } - } - - // Assign data - if (runtime != null && runtime >= 0) { - tracker.batteryRemainingRuntime = runtime - } - // -1: Not known (e.g. not yet calculated after wake up, reusing known value is okay), 0: N/A (e.g. charging) - if (batt != null) { - tracker.batteryLevel = if (batt == 128) 1f else (batt and 127).toFloat() - } - // Server still won't display battery at 0% at all - if (batt_v != null) { - tracker.batteryVoltage = (batt_v.toFloat() + 245f) / 100f - } - if (temp != null) { - tracker.temperature = if (temp > 0) temp.toFloat() / 2f - 39f else null - } - // Range 1 - 255 -> -38.5 - +88.5 C - if (brd_id != null) { - val boardType = BoardType.getById(brd_id.toUInt()) - if (boardType != null) { - device.boardType = boardType!! - } - } - if (mcu_id != null) { - val mcuType = MCUType.getById(mcu_id.toUInt()) - if (mcuType != null) { - device.mcuType = mcuType!! - } - } - if (button != null) { - if (tracker.button == null) { - tracker.button = 0 - } - if (button != tracker.button) { - button = button and tracker.button!!.inv() - // Nothing to do now.. - } - } - if (fw_date != null) { - val firmwareYear = 2020 + (fw_date shr 9 and 127) - val firmwareMonth = fw_date shr 5 and 15 - val firmwareDay = fw_date and 31 - device.firmwareDate = String.format("%04d-%02d-%02d", firmwareYear, firmwareMonth, firmwareDay) - } - if (fw_major != null && fw_minor != null && fw_patch != null) { - device.firmwareVersion = "$fw_major.$fw_minor.$fw_patch" - } - if (svr_status != null) { - val status = TrackerStatus.getById(svr_status) - if (status != null) { - tracker.status = status!! - } - } - if (rssi != null) { - tracker.signalStrength = -rssi - } - if (packets_received != null && packets_lost != null) { - tracker.packetsReceived = packets_received - tracker.packetsLost = packets_lost - tracker.packetLoss = if (packets_lost == 0) 0.0f else packets_lost.toFloat() / (packets_received + packets_lost).toFloat() - } - - // Assign rotation and acceleration - if (packetType == 1 || packetType == 4) { - // The data comes in the same order as in the UDP protocol - // x y z w -> w x y z - var rot = Quaternion(q[3].toFloat(), q[0].toFloat(), q[1].toFloat(), q[2].toFloat()) - val scaleRot = 1 / (1 shl 15).toFloat() // compile time evaluation - rot = AXES_OFFSET.times(scaleRot).times(rot) // no division - tracker.setRotation(rot) - } - if (packetType == 2 || packetType == 7) { - val v = floatArrayOf(q[0].toFloat(), q[1].toFloat(), q[2].toFloat()) // used q array for quantized data - v[0] /= (1 shl 10).toFloat() - v[1] /= (1 shl 11).toFloat() - v[2] /= (1 shl 11).toFloat() - for (i in 0..2) { - v[i] = v[i] * 2 - 1 - } - // http://marc-b-reynolds.github.io/quaternions/2017/05/02/QuatQuantPart1.html#fnref:pos:3 - // https://github.com/Marc-B-Reynolds/Stand-alone-junk/blob/559bd78893a3a95cdee1845834c632141b945a45/src/Posts/quatquant0.c#L898 - val d = v[0] * v[0] + v[1] * v[1] + v[2] * v[2] - val invSqrtD = 1 / sqrt(d + 1e-6f) - val a = (PI.toFloat() / 2) * d * invSqrtD - val s = sin(a) - val k = s * invSqrtD - var rot = Quaternion(cos(a), k * v[0], k * v[1], k * v[2]) - rot = AXES_OFFSET.times(rot) // no division - tracker.setRotation(rot) - } - if (packetType == 1 || packetType == 2 || packetType == 7) { - // Acceleration is in local device frame - // On flat surface / face up: - // Right side of the device is +X - // Front side (facing up) is +Z - // Mounted on body / standing up: - // Top side of the device is +Y - // Front side (facing out) is +Z - val scaleAccel = 1 / (1 shl 7).toFloat() // compile time evaluation - val acceleration = Vector3(a[0].toFloat(), a[1].toFloat(), a[2].toFloat()).times(scaleAccel) // no division - tracker.setAcceleration(acceleration) - } - if (packetType == 4) { - // Magnetometer is in local device frame - // On flat surface / face up: - // Right side of the device is +X - // Front side (facing up) is +Z - // Mounted on body / standing up: - // Top side of the device is +Y - // Front side (facing out) is +Z - val scaleMag = 1000 / (1 shl 10).toFloat() // compile time evaluation, and change gauss to milligauss - val magnetometer = Vector3(m[0].toFloat(), m[1].toFloat(), m[2].toFloat()).times(scaleMag) // no division - tracker.setMagVector(magnetometer) - } - if (packetType == 1 || packetType == 2 || packetType == 4 || packetType == 7) { - tracker.dataTick() // only data tick if there is rotation data - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt deleted file mode 100644 index a0fee7f7f8..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.slimevr.tracking.trackers.hid - -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.udp.BoardType -import dev.slimevr.tracking.trackers.udp.MCUType - -class HIDDevice(val hidId: Int) : Device() { - override var hardwareIdentifier: String = "Unknown" - override var boardType: BoardType = BoardType.UNKNOWN - override var mcuType: MCUType = MCUType.UNKNOWN - fun getTracker(id: Int): Tracker? = trackers[id] -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FeatureFlags.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FeatureFlags.kt deleted file mode 100644 index 640af58078..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FeatureFlags.kt +++ /dev/null @@ -1,80 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -import java.nio.ByteBuffer - -/** - * Bit packed flags, enum values start with 0 and indicate which bit it is. - * - * Change the enums and `flagsEnabled` inside to extend. - */ -class FirmwareFeatures { - enum class FirmwareFeatureFlags { - // EXAMPLE_FEATURE, - - // Add new flags here - REMOTE_COMMAND, - B64_WIFI_SCANNING, - SENSOR_CONFIG, - - BITS_TOTAL, - } - - fun has(flag: FirmwareFeatureFlags): Boolean { - val bit = flag.ordinal - return (flags[bit / 8].toInt() and (1 shl (bit % 8))) != 0 - } - - /** - * Whether the firmware supports the "feature flags" feature, - * set to true when we've received flags packet from the firmware. - */ - var available = false - private set - - companion object { - fun from(received: ByteBuffer, length: Int): FirmwareFeatures { - val res = FirmwareFeatures() - res.available = true - received.get(res.flags, 0, res.flags.size.coerceAtMost(length)) - return res - } - } - - private val flags = ByteArray(FirmwareFeatureFlags.BITS_TOTAL.ordinal / 8 + 1) -} - -enum class ServerFeatureFlags { - /** Server can parse bundle packets: `PACKET_BUNDLE` = 100 (0x64). */ - PROTOCOL_BUNDLE_SUPPORT, - - /** Server can parse bundle packets with compact headers and packed IMU rotation/acceleration frames: - - `PACKET_BUNDLE_COMPACT` = 101 (0x65), - - `PACKET_ROTATION_AND_ACCELERATION` = 23 (0x17). */ - PROTOCOL_BUNDLE_COMPACT_SUPPORT, - - // Add new flags here - - BITS_TOTAL, ; - - companion object { - val flagsEnabled: Set = setOf( - PROTOCOL_BUNDLE_SUPPORT, - PROTOCOL_BUNDLE_COMPACT_SUPPORT, - - // Add enabled flags here - ) - - val packed = run { - val byteLength = BITS_TOTAL.ordinal / 8 + 1 - val tempPacked = ByteArray(byteLength) - - for (flag in flagsEnabled) { - val bit = flag.ordinal - tempPacked[bit / 8] = - (tempPacked[bit / 8].toInt() or (1 shl (bit % 8))).toByte() - } - - tempPacked - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FirmwareConstants.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FirmwareConstants.kt deleted file mode 100644 index bac7d126b9..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/FirmwareConstants.kt +++ /dev/null @@ -1,179 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -enum class IMUType(val id: UInt) { - UNKNOWN(0u), - MPU9250(1u), - MPU6500(2u), - BNO080(3u), - BNO085(4u), - BNO055(5u), - MPU6050(6u), - BNO086(7u), - BMI160(8u), - ICM20948(9u), - ICM42688(10u), - BMI270(11u), - LSM6DS3TRC(12u), - LSM6DSV(13u), - LSM6DSO(14u), - LSM6DSR(15u), - ICM45686(16u), - ICM45605(17u), - ADC_RESISTANCE(18u), - DEV_RESERVED(250u), - ; - - fun getSolarType(): Int = this.id.toInt() - - companion object { - private val byId = entries.associateBy { it.id } - - @JvmStatic - fun getById(id: UInt): IMUType? = byId[id] - } -} - -enum class BoardType(val id: UInt) { - UNKNOWN(0u), - SLIMEVR_LEGACY(1u), - SLIMEVR_DEV(2u), - NODEMCU(3u), - CUSTOM(4u), - WROOM32(5u), - WEMOSD1MINI(6u), - TTGO_TBASE(7u), - ESP01(8u), - SLIMEVR(9u), - LOLIN_C3_MINI(10u), - BEETLE32C3(11u), - ESP32C3DEVKITM1(12u), - OWOTRACK(13u), - WRANGLER(14u), - MOCOPI(15u), - WEMOSWROOM02(16u), - XIAO_ESP32C3(17u), - HARITORA(18u), - ESP32C6DEVKITC1(19u), - GLOVE_IMU_SLIMEVR_DEV(20u), - GESTURES(21u), - SLIMEVR_V1_2(22u), - ESP32S3_SUPERMINI(23u), - GENERIC_NRF(24u), - SLIMEVR_BUTTERFLY_DEV(25u), - SLIMEVR_BUTTERFLY(26u), - DEV_RESERVED(250u), - ; - - fun getSolarType(): Int = this.id.toInt() - - override fun toString(): String = when (this) { - UNKNOWN -> "Unknown" - SLIMEVR_LEGACY -> "SlimeVR Legacy" - SLIMEVR_DEV -> "SlimeVR Dev" - NODEMCU -> "NodeMCU" - CUSTOM -> "Custom Board" - WROOM32 -> "WROOM32" - WEMOSD1MINI -> "Wemos D1 Mini" - TTGO_TBASE -> "TTGO T-Base" - ESP01 -> "ESP-01" - SLIMEVR -> "SlimeVR" - LOLIN_C3_MINI -> "Lolin C3 Mini" - BEETLE32C3 -> "Beetle ESP32-C3" - ESP32C3DEVKITM1 -> "Espressif ESP32-C3 DevKitM-1" - OWOTRACK -> "owoTrack" - WRANGLER -> "Wrangler Joycons" - MOCOPI -> "Sony Mocopi" - WEMOSWROOM02 -> "Wemos Wroom-02 D1 Mini" - GESTURES -> "litten Yº by Gestures" - XIAO_ESP32C3 -> "Seeed Studio XIAO ESP32C3" - HARITORA -> "Haritora" - ESP32C6DEVKITC1 -> "Espressif ESP32-C6 DevKitC-1" - GLOVE_IMU_SLIMEVR_DEV -> "SlimeVR Dev IMU Glove" - SLIMEVR_V1_2 -> "SlimeVR v1.2" - ESP32S3_SUPERMINI -> "ESP32-S3 SuperMini" - GENERIC_NRF -> "Generic nRF" - SLIMEVR_BUTTERFLY_DEV -> "SlimeVR Dev Butterfly" - SLIMEVR_BUTTERFLY -> "SlimeVR Butterfly" - DEV_RESERVED -> "Prototype" - } - - companion object { - private val byId = entries.associateBy { it.id } - - @JvmStatic - fun getById(id: UInt): BoardType? = byId[id] - } -} - -enum class MCUType(val id: UInt) { - UNKNOWN(0u), - ESP8266(1u), - ESP32(2u), - OWOTRACK_ANDROID(3u), - WRANGLER(4u), - OWOTRACK_IOS(5u), - ESP32_C3(6u), - MOCOPI(7u), - HARITORA(8u), - NRF52(9u), - NRF54L(10u), - DEV_RESERVED(250u), - ; - - fun getSolarType(): Int = this.id.toInt() - - companion object { - private val byId = entries.associateBy { it.id } - - @JvmStatic - fun getById(id: UInt): MCUType? = byId[id] - } -} - -enum class TrackerDataType(val id: UInt) { - ROTATION(0u), - FLEX_RESISTANCE(1u), - FLEX_ANGLE(2u), - ; - - fun getSolarType(): Int = this.id.toInt() - - companion object { - private val byId = entries.associateBy { it.id } - - @JvmStatic - fun getById(id: UInt): TrackerDataType? = byId[id] - } -} - -@JvmInline -value class ConfigTypeId(val v: UShort) - -enum class MagnetometerStatus { - NOT_SUPPORTED, - DISABLED, - ENABLED, - ; - - fun getSolarType(): Int = this.ordinal - - companion object { - private val byId = entries.associateBy { it.ordinal.toUByte() } - - @JvmStatic - fun getById(id: UByte): MagnetometerStatus? = byId[id] - } -} - -@JvmInline -value class SensorConfig(val v: UShort) { - val magStatus - get(): MagnetometerStatus { - if ((v.toUInt() shr 1) and 1u == 0u) return MagnetometerStatus.NOT_SUPPORTED - return if ((v and 1u) == 1u.toUShort()) { - MagnetometerStatus.ENABLED - } else { - MagnetometerStatus.DISABLED - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/SensorTap.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/SensorTap.kt deleted file mode 100644 index 2f4a07b6b7..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/SensorTap.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -data class SensorTap(val tapBits: Int) { - val doubleTap = tapBits and 0x40 > 0 - - enum class TapAxis { - X, - Y, - Z, - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt deleted file mode 100644 index ea5c6d5ba1..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt +++ /dev/null @@ -1,681 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -import com.jme3.math.FastMath -import dev.slimevr.NetworkProtocol -import dev.slimevr.VRServer -import dev.slimevr.config.config -import dev.slimevr.protocol.rpc.MAG_TIMEOUT -import dev.slimevr.tracking.trackers.* -import io.eiren.util.Util -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Quaternion.Companion.fromRotationVector -import kotlinx.coroutines.* -import solarxr_protocol.rpc.ResetType -import java.net.DatagramPacket -import java.net.DatagramSocket -import java.net.InetSocketAddress -import java.net.NetworkInterface -import java.net.SocketAddress -import java.net.SocketTimeoutException -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import java.util.function.Consumer -import kotlin.collections.HashMap -import kotlin.coroutines.resume - -/** - * Receives trackers data by UDP using extended owoTrack protocol. - */ -class TrackersUDPServer(private val port: Int, name: String, private val trackersConsumer: Consumer) : Thread(name) { - private val random = Random() - private val connections: MutableList = FastList() - private val connectionsByAddress: MutableMap = HashMap() - private val connectionsByMAC: MutableMap = HashMap() - private val broadcastAddresses: List = try { - NetworkInterface.getNetworkInterfaces().asSequence().filter { - // Ignore loopback, PPP, virtual and disabled interfaces - !it.isLoopback && it.isUp && !it.isPointToPoint && !it.isVirtual - }.flatMap { - it.interfaceAddresses.asSequence() - }.map { - // This ignores IPv6 addresses - it.broadcast - }.filter { it != null && it.isSiteLocalAddress }.map { InetSocketAddress(it, this.port) }.toList() - } catch (e: Exception) { - LogManager.severe("[TrackerServer] Can't enumerate network interfaces", e) - emptyList() - } - private val parser = UDPProtocolParser() - - // 1500 is a common network MTU. 1472 is the maximum size of a UDP packet (1500 - 20 for IPv4 header - 8 for UDP header) - private val rcvBuffer = ByteArray(1500 - 20 - 8) - private val bb = ByteBuffer.wrap(rcvBuffer).order(ByteOrder.BIG_ENDIAN) - - // Gets initialized in this.run() - private lateinit var socket: DatagramSocket - private var lastKeepup = System.currentTimeMillis() - - private fun setUpNewConnection(handshakePacket: DatagramPacket, handshake: UDPPacket3Handshake) { - LogManager.info("[TrackerServer] Handshake received from ${handshakePacket.address}:${handshakePacket.port}") - val addr = handshakePacket.address - val socketAddr = handshakePacket.socketAddress - - // Check if it's a known device - VRServer.instance.configManager.vrConfig.let { vrConfig -> - if (vrConfig.isKnownDevice(handshake.macString)) return@let - val mac = handshake.macString ?: return@let - - VRServer.instance.handshakeHandler.sendUnknownHandshake(mac) - return - } - - // Get a connection either by an existing one, or by creating a new one - val connection: UDPDevice = synchronized(connections) { - connectionsByMAC[handshake.macString]?.apply { - // Look for an existing connection by the MAC address and update the - // connection information - connectionsByAddress.remove(address) - address = socketAddr - lastPacketNumber = 0 - ipAddress = addr - name = handshake.macString?.let { "udp://$it" } - descriptiveName = "udp:/$addr" - protocolVersion = handshake.protocolVersion - firmwareVersion = handshake.firmware - connectionsByAddress[address] = this - - val i = connections.indexOf(this) - LogManager - .info( - """ - [TrackerServer] Tracker $i handed over to address $socketAddr. - Board type: ${handshake.boardType}, - firmware name: ${handshake.firmware}, - protocol version: $protocolVersion, - mac: ${handshake.macString}, - name: $name - """.trimIndent(), - ) - } ?: connectionsByAddress[socketAddr]?.apply { - // Look for an existing connection by the socket address (IP and port) - // and update the connection information - lastPacketNumber = 0 - ipAddress = addr - name = handshake.macString?.let { "udp://$it" } - ?: "udp:/$addr" - descriptiveName = "udp:/$addr" - protocolVersion = handshake.protocolVersion - firmwareVersion = handshake.firmware - val i = connections.indexOf(this) - LogManager - .info( - """ - [TrackerServer] Tracker $i reconnected from address $socketAddr. - Board type: ${handshake.boardType}, - firmware name: ${handshake.firmware}, - protocol version: $protocolVersion, - mac: ${handshake.macString}, - name: $name - """.trimIndent(), - ) - } - } ?: run { - // No existing connection could be found, create a new one - - val connection = UDPDevice( - socketAddr, - addr, - handshake.macString ?: addr.hostAddress, - handshake.boardType, - handshake.mcuType, - ) - VRServer.instance.deviceManager.addDevice(connection) - connection.protocolVersion = handshake.protocolVersion - connection.protocol = if (handshake.firmware?.isEmpty() == true) { - // Only old owoTrack doesn't report firmware and have different packet IDs with SlimeVR - NetworkProtocol.OWO_LEGACY - } else { - NetworkProtocol.SLIMEVR_RAW - } - connection.name = handshake.macString?.let { "udp://$it" } - ?: "udp:/$addr" - // TODO: The missing slash in udp:// was intended because InetAddress.toString() - // returns "hostname/address" but it wasn't known that if hostname is empty - // string it just looks like "/address" lol. - // Fixing this would break config! - connection.descriptiveName = "udp:/$addr" - connection.firmwareVersion = handshake.firmware - synchronized(connections) { - // Register the new connection - val i = connections.size - connections.add(connection) - connectionsByAddress[socketAddr] = connection - if (handshake.macString != null) { - connectionsByMAC[handshake.macString!!] = connection - } - LogManager - .info( - """ - [TrackerServer] Tracker $i connected from address $socketAddr. - Board type: ${handshake.boardType}, - firmware name: ${handshake.firmware}, - protocol version: ${connection.protocolVersion}, - mac: ${handshake.macString}, - name: ${connection.name} - """.trimIndent(), - ) - } - if (connection.protocol == NetworkProtocol.OWO_LEGACY || connection.protocolVersion < 9) { - // Set up new sensor for older firmware. - // Firmware after 7 should send sensor status packet and sensor - // will be created when it's received - setUpSensor( - connection, - 0, - handshake.imuType, - 1, - MagnetometerStatus.NOT_SUPPORTED, - null, - TrackerDataType.ROTATION, - null, - ) - } - connection - } - connection.firmwareFeatures = FirmwareFeatures() - bb.limit(bb.capacity()) - bb.rewind() - parser.writeHandshakeResponse(bb, connection) - socket.send(DatagramPacket(rcvBuffer, bb.position(), connection.address)) - } - - private val mainScope = CoroutineScope(SupervisorJob()) - private fun setUpSensor( - connection: UDPDevice, - trackerId: Int, - sensorType: IMUType, - sensorStatus: Int, - magStatus: MagnetometerStatus, - trackerPosition: TrackerPosition?, - trackerDataType: TrackerDataType, - hasCompletedRestCalibration: Boolean?, - ) { - LogManager.info("[TrackerServer] Sensor $trackerId for ${connection.name} status: $sensorStatus") - var imuTracker = connection.getTracker(trackerId) - if (imuTracker == null) { - var formattedHWID = connection.hardwareIdentifier.replace(":", "").takeLast(5) - if (trackerId != 0) { - formattedHWID += " Extension" - if (trackerId > 1) { - formattedHWID += " $trackerId" - } - } - - imuTracker = Tracker( - connection, - VRServer.getNextLocalTrackerId(), - connection.name + "/" + trackerId, - "Tracker $formattedHWID", - trackerPosition, - trackerNum = trackerId, - hasRotation = true, - hasAcceleration = true, - userEditable = true, - imuType = if (trackerDataType == TrackerDataType.ROTATION) sensorType else null, - allowFiltering = true, - allowReset = true, - allowMounting = true, - usesTimeout = true, - magStatus = magStatus, - trackerDataType = trackerDataType, - ) - connection.trackers[trackerId] = imuTracker - trackersConsumer.accept(imuTracker) - LogManager.info("[TrackerServer] Added sensor $trackerId for ${connection.name}, ImuType $sensorType, DataType $trackerDataType, default TrackerPosition $trackerPosition") - } - val status = UDPPacket15SensorInfo.getStatus(sensorStatus) - if (status != null) imuTracker.status = status - - imuTracker.hasCompletedRestCalibration = hasCompletedRestCalibration - - if (magStatus == MagnetometerStatus.NOT_SUPPORTED) return - if (magStatus == MagnetometerStatus.ENABLED && - (!VRServer.instance.configManager.vrConfig.server.useMagnetometerOnAllTrackers || imuTracker.config.shouldHaveMagEnabled == false) - ) { - mainScope.launch { - withTimeoutOrNull(MAG_TIMEOUT) { - connection.setMag(false, trackerId) - } - } - } else if (magStatus == MagnetometerStatus.DISABLED && - VRServer.instance.configManager.vrConfig.server.useMagnetometerOnAllTrackers && - imuTracker.config.shouldHaveMagEnabled == true - ) { - mainScope.launch { - withTimeoutOrNull(MAG_TIMEOUT) { - connection.setMag(true, trackerId) - } - } - } - } - - private data class ConfigStateWaiter( - val expectedState: Boolean, - val channel: CancellableContinuation, - var ran: Boolean = false, - ) - - private val queues: MutableMap, Deque> = ConcurrentHashMap() - suspend fun setConfigFlag(device: UDPDevice, configTypeId: ConfigTypeId, state: Boolean, sensorId: Int = 255) { - if (device.timedOut) return - val triple = Triple(device.address, configTypeId, sensorId) - val queue = queues.computeIfAbsent(triple) { _ -> ConcurrentLinkedDeque() } - - suspendCancellableCoroutine { - val waiter = ConfigStateWaiter(state, it) - queue.add(waiter) - it.invokeOnCancellation { - queue.remove(waiter) - } - } - } - - private fun actualSetConfigFlag(device: UDPDevice, configTypeId: ConfigTypeId, state: Boolean, sensorId: Int) { - val packet = UDPPacket25SetConfigFlag(sensorId, configTypeId, state) - bb.limit(bb.capacity()) - bb.rewind() - parser.write(bb, null, packet) - socket.send(DatagramPacket(rcvBuffer, bb.position(), device.address)) - } - - override fun run() { - val serialBuffer2 = StringBuilder() - try { - socket = DatagramSocket(port) - var prevPacketTime = System.currentTimeMillis() - socket.soTimeout = 250 - while (true) { - var received: DatagramPacket? = null - try { - val hasActiveTrackers = connections.any { it.trackers.size > 0 } - if (!hasActiveTrackers) { - val discoveryPacketTime = System.currentTimeMillis() - if (discoveryPacketTime - prevPacketTime >= 2000) { - for (addr in broadcastAddresses) { - bb.limit(bb.capacity()) - bb.rewind() - parser.write(bb, null, UDPPacket0Heartbeat) - socket.send(DatagramPacket(rcvBuffer, bb.position(), addr)) - } - prevPacketTime = discoveryPacketTime - } - } - received = DatagramPacket(rcvBuffer, rcvBuffer.size) - socket.receive(received) - bb.limit(received.length) - bb.rewind() - val connection = synchronized(connections) { connectionsByAddress[received.socketAddress] } - parser.parse(bb, connection) - .filterNotNull() - .forEach { processPacket(received, it, connection) } - - queues.forEach { (t, p) -> - val q = p.firstOrNull() ?: return@forEach - if (q.ran) return@forEach - - val device = connectionsByAddress[t.first] ?: run { - p.removeFirst() - LogManager.info("[TrackerServer] Device ${t.first} not connected, so can't communicate with it") - return@forEach - } - actualSetConfigFlag(device, t.second, q.expectedState, t.third) - if (!device.timedOut) q.ran = true - } - } catch (ignored: SocketTimeoutException) { - } catch (e: Exception) { - LogManager.warning( - "[TrackerServer] Error parsing packet ${packetToString(received)}", - e, - ) - } - if (lastKeepup + 500 < System.currentTimeMillis()) { - lastKeepup = System.currentTimeMillis() - synchronized(connections) { - for (conn in connections) { - bb.limit(bb.capacity()) - bb.rewind() - parser.write(bb, conn, UDPPacket1Heartbeat) - socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) - if (conn.lastPacket + 1000 < System.currentTimeMillis()) { - if (!conn.timedOut) { - conn.timedOut = true - LogManager.info("[TrackerServer] Tracker timed out: $conn") - } - } else { - for (value in conn.trackers.values) { - if (value.status == TrackerStatus.DISCONNECTED || - value.status == TrackerStatus.TIMED_OUT - ) { - value.status = TrackerStatus.OK - } - } - conn.timedOut = false - } - - if (conn.serialBuffer.isNotEmpty() && - conn.lastSerialUpdate + 500L < System.currentTimeMillis() - ) { - serialBuffer2 - .append('[') - .append(conn.name) - .append("] ") - .append(conn.serialBuffer) - println(serialBuffer2) - serialBuffer2.setLength(0) - conn.serialBuffer.setLength(0) - } - - if (conn.lastPingPacketTime + 500 < System.currentTimeMillis()) { - conn.lastPingPacketId = random.nextInt() - conn.lastPingPacketTime = System.currentTimeMillis() - bb.limit(bb.capacity()) - bb.rewind() - bb.putInt(10) - bb.putLong(0) - bb.putInt(conn.lastPingPacketId) - socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) - } - } - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - if (::socket.isInitialized) { - Util.close(socket) - } - } - } - - private fun processPacket(received: DatagramPacket, packet: UDPPacket, connection: UDPDevice?) { - val tracker: Tracker? - when (packet) { - is UDPPacket0Heartbeat, is UDPPacket1Heartbeat, is UDPPacket25SetConfigFlag -> {} - - is UDPPacket3Handshake -> setUpNewConnection(received, packet) - - is RotationPacket -> { - var rot = packet.rotation - rot = AXES_OFFSET.times(rot) - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - tracker.setRotation(rot) - if (packet is UDPPacket23RotationAndAcceleration) { - // sensorOffset is applied correctly since protocol 22 - // See: https://github.com/SlimeVR/SlimeVR-Tracker-ESP/pull/480 - if (connection.protocolVersion >= 22) { - tracker.setAcceleration(packet.acceleration) - } else { - tracker.setAcceleration(SENSOR_OFFSET_CORRECTION.sandwich(packet.acceleration)) - } - } - tracker.dataTick() - } - - is UDPPacket17RotationData -> { - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - var rot17 = packet.rotation - rot17 = AXES_OFFSET * rot17 - when (packet.dataType) { - UDPPacket17RotationData.DATA_TYPE_NORMAL -> { - tracker.setRotation(rot17) - tracker.dataTick() - // tracker.calibrationStatus = rotationData.calibrationInfo; - // Not implemented in server - } - - UDPPacket17RotationData.DATA_TYPE_CORRECTION -> { -// tracker.rotMagQuaternion.set(rot17); -// tracker.magCalibrationStatus = rotationData.calibrationInfo; -// tracker.hasNewCorrectionData = true; - // Not implemented in server - } - } - } - - is UDPPacket18MagnetometerAccuracy -> {} - - is UDPPacket4Acceleration -> { - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - // sensorOffset is applied correctly since protocol 22 - // See: https://github.com/SlimeVR/SlimeVR-Tracker-ESP/pull/480 - if (connection.protocolVersion >= 22) { - tracker.setAcceleration(packet.acceleration) - } else { - tracker.setAcceleration(SENSOR_OFFSET_CORRECTION.sandwich(packet.acceleration)) - } - } - - is UDPPacket10PingPong -> { - if (connection == null) return - if (connection.lastPingPacketId == packet.pingId) { - for (t in connection.trackers.values) { - t.ping = (System.currentTimeMillis() - connection.lastPingPacketTime).toInt() / 2 - t.dataTick() - } - } else { - LogManager.debug( - "[TrackerServer] Wrong ping id ${packet.pingId} != ${connection.lastPingPacketId}", - ) - } - } - - is UDPPacket11Serial -> { - if (connection == null) return - println("[${connection.name}] ${packet.serial}") - } - - is UDPPacket12BatteryLevel -> connection?.trackers?.values?.forEach { - it.batteryVoltage = packet.voltage - it.batteryLevel = packet.level * 100 - } - - is UDPPacket13Tap -> { - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - LogManager.info( - "[TrackerServer] Tap packet received from ${tracker.name}: ${packet.tap}", - ) - } - - is UDPPacket14Error -> { - LogManager.severe( - "[TrackerServer] Error received from ${received.socketAddress}: ${packet.errorNumber}", - ) - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - tracker.status = TrackerStatus.ERROR - } - - is UDPPacket15SensorInfo -> { - if (connection == null) return - val magStatus = packet.sensorConfig?.magStatus ?: MagnetometerStatus.NOT_SUPPORTED - setUpSensor( - connection, - packet.sensorId, - packet.sensorType, - packet.sensorStatus, - magStatus, - packet.trackerPosition, - packet.trackerDataType, - packet.hasCompletedRestCalibration, - ) - // Send ack - bb.limit(bb.capacity()) - bb.rewind() - parser.writeSensorInfoResponse(bb, connection, packet) - socket.send(DatagramPacket(rcvBuffer, bb.position(), connection.address)) - LogManager.info( - "[TrackerServer] Sensor info for ${connection.descriptiveName}/${packet.sensorId}: ${packet.sensorStatus}, mag $magStatus", - ) - } - - is UDPPacket19SignalStrength -> connection?.trackers?.values?.forEach { - it.signalStrength = packet.signalStrength - } - - is UDPPacket20Temperature -> { - tracker = connection?.getTracker(packet.sensorId) ?: return - tracker.temperature = packet.temperature - } - - is UDPPacket21UserAction -> { - if (connection == null) return - var name = "" - when (packet.type) { - UDPPacket21UserAction.RESET_FULL -> { - name = "Full reset" - VRServer.instance.scheduleResetTrackersFull( - RESET_SOURCE_NAME, - (VRServer.instance.configManager.vrConfig.resetsConfig.fullResetDelay * 1000).toLong(), - ) - } - - UDPPacket21UserAction.RESET_YAW -> { - name = "Yaw reset" - VRServer.instance.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (VRServer.instance.configManager.vrConfig.resetsConfig.yawResetDelay * 1000).toLong()) - } - - UDPPacket21UserAction.RESET_MOUNTING -> { - name = "Mounting reset" - VRServer - .instance - .resetHandler - .sendStarted(ResetType.Mounting) - VRServer.instance.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (VRServer.instance.configManager.vrConfig.resetsConfig.mountingResetDelay * 1000).toLong()) - } - - UDPPacket21UserAction.PAUSE_TRACKING -> { - name = "Pause tracking toggle" - VRServer.instance.togglePauseTracking(RESET_SOURCE_NAME) - } - } - - LogManager.info( - "[TrackerServer] User action from ${connection.descriptiveName} received. $name performed.", - ) - } - - is UDPPacket22FeatureFlags -> { - if (connection == null) return - // Respond with server flags - bb.limit(bb.capacity()) - bb.rewind() - parser.write(bb, connection, packet) - socket.send(DatagramPacket(rcvBuffer, bb.position(), connection.address)) - connection.firmwareFeatures = packet.firmwareFeatures - } - - is UDPPacket24AckConfigChange -> { - if (connection == null) return - val queue = queues[Triple(connection.address, packet.configType, packet.sensorId)] ?: run { - LogManager.severe("[TrackerServer] Error, acknowledgment of config change that we don't have in our queue.") - return - } - val changed = queue.removeFirst() - changed.channel.resume(true) - val trackers = if (SensorSpecificPacket.isGlobal(packet.sensorId)) { - connection.trackers.values.toList() - } else { - listOf(connection.getTracker(packet.sensorId) ?: return) - } - LogManager.info("[TrackerServer] Acknowledged config change on ${connection.descriptiveName} (${trackers.map { it.trackerNum }.joinToString()}). Config changed on ${packet.configType}") - } - - is UDPPacket26FlexData -> { - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) { - tracker.trackerFlexHandler.setFlexResistance(packet.flexData) - } else if (tracker.trackerDataType == TrackerDataType.FLEX_ANGLE) { - tracker.trackerFlexHandler.setFlexAngle(packet.flexData) - } - tracker.dataTick() - } - - is UDPPacket27Position -> { - tracker = connection?.getTracker(packet.sensorId) - if (tracker == null) return - tracker.position = packet.position - // dont call dataTick here as this is just position update - } - - is UDPPacket200ProtocolChange -> {} - } - } - - fun getConnections(): List = connections - - // FIXME: for some reason it ends up disconnecting after 30 seconds have passed instead of immediately - fun disconnectDevice(device: UDPDevice) { - synchronized(connections) { - connections.remove(device) - } - synchronized(connectionsByAddress) { - connectionsByAddress.filter { (_, dev) -> dev.id == device.id }.keys.forEach( - connectionsByAddress::remove, - ) - } - device.trackers.forEach { (_, tracker) -> - tracker.status = TrackerStatus.DISCONNECTED - } - - LogManager.info( - "[TrackerServer] Forcefully disconnected ${device.hardwareIdentifier} device.", - ) - } - - companion object { - /** - * Change between IMU axes and OpenGL/SteamVR axes - */ - private val AXES_OFFSET = fromRotationVector(-FastMath.HALF_PI, 0f, 0f) - - // TODO: Set this offset to Quaternion.IDENTITY when the firmware is corrected! - // 270 deg (-90 deg) default for officials - private val SENSOR_OFFSET_CORRECTION = Quaternion.rotationAroundZAxis(-FastMath.HALF_PI) - private const val RESET_SOURCE_NAME = "TrackerServer" - - private val hexFormat = HexFormat { - bytes.byteSeparator = "," - } - - private fun packetToString(packet: DatagramPacket?): String { - val sb = StringBuilder() - sb.append("DatagramPacket{") - if (packet == null) { - sb.append("null") - } else { - sb.append(packet.address.toString()) - sb.append(':') - sb.append(packet.port) - sb.append(',') - sb.append(packet.length) - sb.append(',') - sb.append('{') - sb.append(packet.data.toHexString(0, packet.length, hexFormat)) - sb.append('}') - } - sb.append('}') - return sb.toString() - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt deleted file mode 100644 index bed58741c8..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt +++ /dev/null @@ -1,85 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -import dev.slimevr.NetworkProtocol -import dev.slimevr.VRServer -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import java.net.InetAddress -import java.net.SocketAddress -import java.util.concurrent.ConcurrentHashMap - -class UDPDevice( - var address: SocketAddress, - var ipAddress: InetAddress, - override val hardwareIdentifier: String, - override val boardType: BoardType = BoardType.UNKNOWN, - override val mcuType: MCUType = MCUType.UNKNOWN, -) : Device(true) { - - override val id: Int = nextLocalDeviceId.incrementAndGet() - - @JvmField - var lastPacket = System.currentTimeMillis() - - @JvmField - var lastPingPacketId = -1 - - @JvmField - var lastPingPacketTime: Long = 0 - override var name: String? = null - set(name) { - super.name = name - field = name - } - - @JvmField - var descriptiveName: String? = null - - @JvmField - var serialBuffer = StringBuilder() - - @JvmField - var lastSerialUpdate: Long = 0 - - @JvmField - var lastPacketNumber: Long = -1 - - @JvmField - var protocol: NetworkProtocol? = null - - @JvmField - var protocolVersion = 0 - - @JvmField - var timedOut = false - override val trackers = ConcurrentHashMap() - - override suspend fun setMag(state: Boolean, sensorId: Int) { - if (sensorId == 255) { - VRServer.instance.trackersServer.setConfigFlag(this, ConfigTypeId(1u), state) - trackers.forEach { (_, t) -> t.setMagPrivate(state) } - } else { - require(trackers[sensorId] != null) { "There is no tracker $sensorId in device ${toString()}" } - VRServer.instance.trackersServer.setConfigFlag(this, ConfigTypeId(1u), state, sensorId) - trackers[sensorId]!!.setMagPrivate(state) - } - } - - var firmwareFeatures = FirmwareFeatures() - - fun isNextPacket(packetId: Long): Boolean { - if (packetId != 0L && packetId <= lastPacketNumber) return false - lastPacketNumber = packetId - return true - } - - override fun toString(): String = "udp:/$ipAddress" - - override var manufacturer: String? - get() = "SlimeVR" - set(manufacturer) { - super.manufacturer = manufacturer - } - - fun getTracker(id: Int): Tracker? = trackers[id] -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPPacket.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPPacket.kt deleted file mode 100644 index c83277b2cc..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPPacket.kt +++ /dev/null @@ -1,454 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import java.io.IOException -import java.nio.BufferUnderflowException -import java.nio.ByteBuffer - -sealed class UDPPacket(val packetId: Int) { - @Throws(IOException::class, BufferUnderflowException::class) - open fun readData(buf: ByteBuffer) {} - - @Throws(IOException::class) - open fun writeData(buf: ByteBuffer) {} - - companion object { - /** - * Naively read null-terminated ASCII string from the byte buffer - * - * @param buf - * @return - * @throws IOException - */ - @Throws(IOException::class) - fun readASCIIString(buf: ByteBuffer): String { - val sb = StringBuilder() - while (true) { - val c = (buf.get().toInt() and 0xFF).toChar() - if (c.code == 0) break - sb.append(c) - } - return sb.toString() - } - - @JvmStatic - @Throws(IOException::class) - fun readASCIIString(buf: ByteBuffer, length: Int): String { - var length = length - val sb = StringBuilder() - while (length-- > 0) { - val c = (buf.get().toInt() and 0xFF).toChar() - if (c.code == 0) break - sb.append(c) - } - return sb.toString() - } - - /** - * Naively write null-terminated ASCII string to byte buffer - * - * @param str - * @param buf - * @throws IOException - */ - @Throws(IOException::class) - fun writeASCIIString(str: String, buf: ByteBuffer) { - for (element in str) { - buf.put((element.code and 0xFF).toByte()) - } - buf.put(0.toByte()) - } - } -} - -sealed interface SensorSpecificPacket { - val sensorId: Int - companion object { - /** - * Sensor with id 255 is "global" representing a whole device - * - * @param sensorId - * @return - */ - fun isGlobal(sensorId: Int): Boolean = sensorId == 255 - } -} - -sealed interface RotationPacket : SensorSpecificPacket { - var rotation: Quaternion -} - -data object UDPPacket0Heartbeat : UDPPacket(0) -data object UDPPacket1Heartbeat : UDPPacket(1) -data class UDPPacket1Rotation(override var rotation: Quaternion = Quaternion.IDENTITY) : - UDPPacket(1), - RotationPacket { - override val sensorId = 0 - override fun readData(buf: ByteBuffer) { - rotation = UDPUtils.getSafeBufferQuaternion(buf) - } -} - -data class UDPPacket3Handshake( - var boardType: BoardType = BoardType.UNKNOWN, - var imuType: IMUType = IMUType.UNKNOWN, - var mcuType: MCUType = MCUType.UNKNOWN, - var protocolVersion: Int = 0, - var firmware: String? = null, - var macString: String? = null, -) : UDPPacket(3) { - override fun readData(buf: ByteBuffer) { - if (buf.remaining() == 0) return - if (buf.remaining() > 3) { - boardType = BoardType.getById(buf.int.toUInt()) ?: BoardType.UNKNOWN - } - if (buf.remaining() > 3) { - imuType = IMUType.getById(buf.int.toUInt()) ?: IMUType.UNKNOWN - } - if (buf.remaining() > 3) { - mcuType = MCUType.getById(buf.int.toUInt()) ?: MCUType.UNKNOWN - } // MCU TYPE - if (buf.remaining() > 11) { - buf.int - buf.int - buf.int // IMU info - } - if (buf.remaining() > 3) protocolVersion = buf.int - var length = 0 - if (buf.remaining() > 0) length = buf.get().toInt() - // firmware version length is 1 longer than - // that because it's nul-terminated - firmware = readASCIIString(buf, length) - val mac = ByteArray(6) - if (buf.remaining() >= mac.size) { - buf.get(mac) - macString = String.format( - "%02X:%02X:%02X:%02X:%02X:%02X", - mac[0], - mac[1], - mac[2], - mac[3], - mac[4], - mac[5], - ) - if (macString == "00:00:00:00:00:00") macString = null - } - } - - override fun writeData(buf: ByteBuffer) { - // Never sent back in current protocol - // Handshake for RAW SlimeVR and legacy owoTrack has different packet id - // byte - // order from normal packets - // So it's handled by raw protocol call - } -} - -data class UDPPacket4Acceleration(var acceleration: Vector3 = Vector3.NULL) : - UDPPacket(4), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - acceleration = Vector3(UDPUtils.getSafeBufferFloat(buf), UDPUtils.getSafeBufferFloat(buf), UDPUtils.getSafeBufferFloat(buf)) - - sensorId = try { - buf.get().toInt() and 0xFF - } catch (e: BufferUnderflowException) { - // for owo track app - 0 - } - } -} - -data class UDPPacket10PingPong(var pingId: Int = 0) : UDPPacket(10) { - override fun readData(buf: ByteBuffer) { - pingId = buf.int - } - - override fun writeData(buf: ByteBuffer) { - buf.putInt(pingId) - } -} - -data class UDPPacket11Serial(var serial: String = "") : UDPPacket(11) { - override fun readData(buf: ByteBuffer) { - val length = buf.int - val sb = StringBuilder(length) - for (i in 0 until length) { - val ch = Char(buf.get().toUShort()) - sb.append(ch) - } - serial = sb.toString() - } -} - -data class UDPPacket12BatteryLevel( - var voltage: Float = 0.0f, - var level: Float = 0.0f, -) : UDPPacket(12) { - - override fun readData(buf: ByteBuffer) { - voltage = UDPUtils.getSafeBufferFloat(buf) - if (buf.remaining() > 3) { - level = UDPUtils.getSafeBufferFloat(buf) - } else { - level = voltage - voltage = 0.0f - } - } -} - -data class UDPPacket13Tap(var tap: SensorTap = SensorTap(0)) : - UDPPacket(13), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - tap = SensorTap(buf.get().toInt() and 0xFF) - } -} - -data class UDPPacket14Error(var errorNumber: Int = 0) : - UDPPacket(14), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - errorNumber = buf.get().toInt() and 0xFF - } -} - -data class UDPPacket15SensorInfo( - var sensorStatus: Int = 0, - var sensorType: IMUType = IMUType.UNKNOWN, - var sensorConfig: SensorConfig? = null, - var hasCompletedRestCalibration: Boolean? = null, - var trackerPosition: TrackerPosition? = null, - var trackerDataType: TrackerDataType = TrackerDataType.ROTATION, -) : UDPPacket(15), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - sensorStatus = buf.get().toInt() and 0xFF - if (buf.remaining() > 0) { - sensorType = IMUType.getById(buf.get().toUInt() and 0xFFu) ?: IMUType.UNKNOWN - } - if (buf.remaining() > 1) { - sensorConfig = SensorConfig(buf.getShort().toUShort()) - } - if (buf.remaining() > 0) hasCompletedRestCalibration = buf.get().toInt() and 0xFF != 0 - if (buf.remaining() > 0) trackerPosition = TrackerPosition.getById(buf.get().toInt() and 0xFF) - if (buf.remaining() > 0) trackerDataType = TrackerDataType.getById(buf.get().toUInt() and 0xFFu) ?: TrackerDataType.ROTATION - } - - companion object { - fun getStatus(sensorStatus: Int): TrackerStatus? = when (sensorStatus) { - 0 -> TrackerStatus.DISCONNECTED - 1 -> TrackerStatus.OK - 2 -> TrackerStatus.ERROR - else -> null - } - } -} - -data class UDPPacket16Rotation2(override var rotation: Quaternion = Quaternion.IDENTITY) : - UDPPacket(16), - RotationPacket { - override val sensorId = 1 - override fun readData(buf: ByteBuffer) { - rotation = UDPUtils.getSafeBufferQuaternion(buf) - } -} - -data class UDPPacket17RotationData( - var rotation: Quaternion = Quaternion.IDENTITY, - var dataType: Int = 0, - var calibrationInfo: Int = 0, -) : UDPPacket(17), - SensorSpecificPacket { - override var sensorId: Int = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - dataType = buf.get().toInt() and 0xFF - rotation = UDPUtils.getSafeBufferQuaternion(buf) - calibrationInfo = buf.get().toInt() and 0xFF - } - - companion object { - const val DATA_TYPE_NORMAL = 1 - const val DATA_TYPE_CORRECTION = 2 - } -} - -data class UDPPacket18MagnetometerAccuracy(var accuracyInfo: Float = 0.0f) : - UDPPacket(18), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - accuracyInfo = UDPUtils.getSafeBufferFloat(buf) - } -} - -data class UDPPacket19SignalStrength(var signalStrength: Int = 0) : - UDPPacket(19), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - signalStrength = buf.get().toInt() - } -} - -data class UDPPacket20Temperature(var temperature: Float = 0.0f) : - UDPPacket(20), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - temperature = UDPUtils.getSafeBufferFloat(buf) - } -} - -data class UDPPacket21UserAction(var type: Int = 0) : UDPPacket(21) { - override fun readData(buf: ByteBuffer) { - type = buf.get().toInt() and 0xFF - } - - companion object { - const val RESET_FULL = 2 - const val RESET_YAW = 3 - const val RESET_MOUNTING = 4 - const val PAUSE_TRACKING = 5 - } -} - -class UDPPacket22FeatureFlags( - var firmwareFeatures: FirmwareFeatures = FirmwareFeatures(), -) : UDPPacket(22) { - override fun readData(buf: ByteBuffer) { - firmwareFeatures = FirmwareFeatures.from(buf, buf.remaining()) - } - - override fun writeData(buf: ByteBuffer) { - buf.put(ServerFeatureFlags.packed) - } -} - -data class UDPPacket23RotationAndAcceleration( - override var rotation: Quaternion = Quaternion.IDENTITY, - var acceleration: Vector3 = Vector3.NULL, -) : UDPPacket(23), - RotationPacket { - override var sensorId: Int = 0 - override fun readData(buf: ByteBuffer) { - // s16 s16 s16 s16 s16 s16 s16 - // qX qY qZ qW aX aY aZ - sensorId = buf.get().toInt() and 0xFF - val scaleR = 1 / (1 shl 15).toFloat() // Q15: 1 is represented as 0x7FFF and -1 as 0x8000 - val x = buf.short * scaleR - val y = buf.short * scaleR - val z = buf.short * scaleR - val w = buf.short * scaleR - rotation = Quaternion(w, x, y, z).unit() - val scaleA = 1 / (1 shl 7).toFloat() // The same as the HID scale - acceleration = Vector3(buf.short * scaleA, buf.short * scaleA, buf.short * scaleA) - } -} - -data class UDPPacket24AckConfigChange( - override var sensorId: Int = 0, - var configType: ConfigTypeId = ConfigTypeId(0u), -) : UDPPacket(24), - SensorSpecificPacket { - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - configType = ConfigTypeId(buf.getShort().toUShort()) - } -} - -data class UDPPacket25SetConfigFlag( - override var sensorId: Int = 255, - var configType: ConfigTypeId, - var state: Boolean, -) : UDPPacket(25), - SensorSpecificPacket { - override fun writeData(buf: ByteBuffer) { - buf.put(sensorId.toByte()) - buf.putShort(configType.v.toShort()) - buf.put(if (state) 1 else 0) - } -} - -data class UDPPacket26FlexData( - var flexData: Float = 0f, -) : UDPPacket(26), - SensorSpecificPacket { - - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - flexData = UDPUtils.getSafeBufferFloat(buf) - } -} - -data class UDPPacket27Position( - var position: Vector3 = Vector3.NULL, -) : UDPPacket(27), - SensorSpecificPacket { - override var sensorId = 0 - override fun readData(buf: ByteBuffer) { - sensorId = buf.get().toInt() and 0xFF - val x = UDPUtils.getSafeBufferFloat(buf) - val y = UDPUtils.getSafeBufferFloat(buf) - val z = UDPUtils.getSafeBufferFloat(buf) - position = Vector3(x, y, z) - } -} - -data class UDPPacket200ProtocolChange( - var targetProtocol: Int = 0, - var targetProtocolVersion: Int = 0, -) : UDPPacket(200) { - override fun readData(buf: ByteBuffer) { - targetProtocol = buf.get().toInt() and 0xFF - targetProtocolVersion = buf.get().toInt() and 0xFF - } - - override fun writeData(buf: ByteBuffer) { - buf.put(targetProtocol.toByte()) - buf.put(targetProtocolVersion.toByte()) - } -} - -class UDPUtils { - companion object { - fun getSafeBufferQuaternion(byteBuffer: ByteBuffer): Quaternion { - val x = byteBuffer.getFloat() - val y = byteBuffer.getFloat() - val z = byteBuffer.getFloat() - val w = byteBuffer.getFloat() - - return if ( - (x.isNaN() || y.isNaN() || z.isNaN() || w.isNaN()) || - (x == 0f && y == 0f && z == 0f && w == 0f) - ) { - Quaternion.IDENTITY - } else { - Quaternion(w, x, y, z) - } - } - fun getSafeBufferFloat(byteBuffer: ByteBuffer): Float { - val value = byteBuffer.getFloat() - return if (value.isNaN()) { - 0f - } else { - value - } - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt deleted file mode 100644 index bf73740b1c..0000000000 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt +++ /dev/null @@ -1,169 +0,0 @@ -package dev.slimevr.tracking.trackers.udp - -import java.io.IOException -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets - -class UDPProtocolParser { - @Throws(IOException::class) - fun parse(buf: ByteBuffer, connection: UDPDevice?): Array { - val packetId = buf.int - val packetNumber = buf.long - if (connection != null) { - if (!connection.isNextPacket(packetNumber)) { - // Skip packet because it's not next - throw IOException( - "Out of order packet received: id $packetId, number $packetNumber, last ${connection.lastPacketNumber}, from $connection", - ) - } - connection.lastPacket = System.currentTimeMillis() - connection.trackers.forEach { (_, tracker) -> - tracker.heartbeat() - } - } - if (packetId == PACKET_BUNDLE) { - bundlePackets.clear() - while (buf.hasRemaining()) { - val bundlePacketLen = Math.min(buf.short.toInt(), buf.remaining()) - if (bundlePacketLen == 0) continue - - val bundlePacketStart = buf.position() - val bundleBuf = buf.slice() - bundleBuf.limit(bundlePacketLen) - val bundlePacketId = bundleBuf.int - val newPacket = getNewPacket(bundlePacketId) - newPacket?.let { - newPacket.readData(bundleBuf) - bundlePackets.add(newPacket) - } - - buf.position(bundlePacketStart + bundlePacketLen) - } - return bundlePackets.toTypedArray() - } else if (packetId == PACKET_BUNDLE_COMPACT) { - bundlePackets.clear() - while (buf.hasRemaining()) { - val bundlePacketLen = Math.min(buf.get().toUByte().toInt(), buf.remaining()) // 1 byte - if (bundlePacketLen == 0) continue - - val bundlePacketStart = buf.position() - val bundleBuf = buf.slice() - bundleBuf.limit(bundlePacketLen) - val bundlePacketId = bundleBuf.get().toUByte().toInt() // 1 byte - val newPacket = getNewPacket(bundlePacketId) - newPacket?.let { - newPacket.readData(bundleBuf) - bundlePackets.add(newPacket) - } - - buf.position(bundlePacketStart + bundlePacketLen) - } - return bundlePackets.toTypedArray() - } - - val newPacket = getNewPacket(packetId) - if (newPacket != null) { - newPacket.readData(buf) - } else { -// LogManager.log.debug( -// "[UDPProtocolParser] Skipped packet id " + -// packetId + " from " + connection -// ) - } - return arrayOf(newPacket) - } - - @Throws(IOException::class) - fun write(buf: ByteBuffer, connection: UDPDevice?, packet: UDPPacket) { - buf.putInt(packet.packetId) - buf.putLong(0) // Packet number is always 0 when sending data to trackers - packet.writeData(buf) - } - - @Throws(IOException::class) - fun writeHandshakeResponse(buf: ByteBuffer, connection: UDPDevice?) { - buf.put(HANDSHAKE_BUFFER) - } - - @Throws(IOException::class) - fun writeSensorInfoResponse( - buf: ByteBuffer, - connection: UDPDevice?, - packet: UDPPacket15SensorInfo, - ) { - buf.putInt(packet.packetId) - buf.put(packet.sensorId.toByte()) - buf.put(packet.sensorStatus.toByte()) - } - - protected fun getNewPacket(packetId: Int): UDPPacket? = when (packetId) { - PACKET_HEARTBEAT -> UDPPacket0Heartbeat - PACKET_ROTATION -> UDPPacket1Rotation() - PACKET_HANDSHAKE -> UDPPacket3Handshake() - PACKET_PING_PONG -> UDPPacket10PingPong() - PACKET_ACCEL -> UDPPacket4Acceleration() - PACKET_SERIAL -> UDPPacket11Serial() - PACKET_BATTERY_LEVEL -> UDPPacket12BatteryLevel() - PACKET_TAP -> UDPPacket13Tap() - PACKET_ERROR -> UDPPacket14Error() - PACKET_SENSOR_INFO -> UDPPacket15SensorInfo() - PACKET_ROTATION_2 -> UDPPacket16Rotation2() - PACKET_ROTATION_DATA -> UDPPacket17RotationData() - PACKET_MAGNETOMETER_ACCURACY -> UDPPacket18MagnetometerAccuracy() - PACKET_SIGNAL_STRENGTH -> UDPPacket19SignalStrength() - PACKET_TEMPERATURE -> UDPPacket20Temperature() - PACKET_USER_ACTION -> UDPPacket21UserAction() - PACKET_FEATURE_FLAGS -> UDPPacket22FeatureFlags() - PACKET_ACK_CONFIG_CHANGE -> UDPPacket24AckConfigChange() - PACKET_FLEX_DATA -> UDPPacket26FlexData() - PACKET_POSITION -> UDPPacket27Position() - PACKET_PROTOCOL_CHANGE -> UDPPacket200ProtocolChange() - else -> null - } - - companion object { - const val PACKET_HEARTBEAT = 0 - const val PACKET_ROTATION = 1 // Deprecated - - // public static final int PACKET_GYRO = 2; // Deprecated - const val PACKET_HANDSHAKE = 3 - const val PACKET_ACCEL = 4 - - // public static final int PACKET_MAG = 5; // Deprecated - // public static final int PACKET_RAW_CALIBRATION_DATA = 6; // Not parsed by - // server - // public static final int PACKET_CALIBRATION_FINISHED = 7; // Not parsed by - // server - // public static final int PACKET_CONFIG = 8; // Not parsed by server - // public static final int PACKET_RAW_MAGNETOMETER = 9 // Deprecated - const val PACKET_PING_PONG = 10 - const val PACKET_SERIAL = 11 - const val PACKET_BATTERY_LEVEL = 12 - const val PACKET_TAP = 13 - const val PACKET_ERROR = 14 - const val PACKET_SENSOR_INFO = 15 - const val PACKET_ROTATION_2 = 16 // Deprecated - const val PACKET_ROTATION_DATA = 17 - const val PACKET_MAGNETOMETER_ACCURACY = 18 - const val PACKET_SIGNAL_STRENGTH = 19 - const val PACKET_TEMPERATURE = 20 - const val PACKET_USER_ACTION = 21 - const val PACKET_FEATURE_FLAGS = 22 - const val PACKET_ROTATION_AND_ACCELERATION = 23 - const val PACKET_ACK_CONFIG_CHANGE = 24 - const val PACKET_SET_CONFIG_FLAG = 25 - const val PACKET_FLEX_DATA = 26 - const val PACKET_POSITION = 27 - const val PACKET_BUNDLE = 100 - const val PACKET_BUNDLE_COMPACT = 101 - const val PACKET_PROTOCOL_CHANGE = 200 - private val HANDSHAKE_BUFFER = ByteArray(64) - private val bundlePackets = ArrayList(128) - - init { - HANDSHAKE_BUFFER[0] = 3 - val str = "Hey OVR =D 5".toByteArray(StandardCharsets.US_ASCII) - System.arraycopy(str, 0, HANDSHAKE_BUFFER, 1, str.size) - } - } -} diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt deleted file mode 100644 index a39ac80a00..0000000000 --- a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt +++ /dev/null @@ -1,353 +0,0 @@ -package dev.slimevr.trackingchecklist - -import dev.slimevr.VRServer -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.config.MountingMethods -import dev.slimevr.games.vrchat.VRCConfigListener -import dev.slimevr.games.vrchat.VRCConfigRecommendedValues -import dev.slimevr.games.vrchat.VRCConfigValidity -import dev.slimevr.games.vrchat.VRCConfigValues -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.TrackerUtils -import dev.slimevr.tracking.trackers.udp.TrackerDataType -import io.github.axisangles.ktmath.Quaternion -import solarxr_protocol.datatypes.DeviceIdT -import solarxr_protocol.datatypes.TrackerIdT -import solarxr_protocol.rpc.* -import java.util.* -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.concurrent.timerTask - -interface TrackingChecklistListener { - fun onStepsUpdate() -} - -class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListener { - - private val listeners: MutableList = CopyOnWriteArrayList() - val steps: MutableList = mutableListOf() - - private val updateTrackingChecklistTimer = Timer("TrackingChecklistTimer") - - // Simple flag set to true if reset mounting was performed at least once. - // This value is only runtime and never saved - var resetMountingCompleted = false - var feetResetMountingCompleted = false - - init { - createSteps() - vrServer.vrcConfigManager.addListener(this) - - updateTrackingChecklistTimer.scheduleAtFixedRate( - timerTask { - updateChecklist() - }, - 0, - 1000, - ) - } - - fun addListener(channel: TrackingChecklistListener) { - listeners.add(channel) - } - - fun removeListener(channel: TrackingChecklistListener) { - listeners.removeIf { channel == it } - } - - fun buildTrackersIds(trackers: List): Array = trackers.map { tracker -> - TrackerIdT().apply { - if (tracker.device != null) { - deviceId = DeviceIdT().apply { id = tracker.device.id } - } - trackerNum = tracker.trackerNum - } - }.toTypedArray() - - private fun createSteps() { - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC - enabled = vrServer.networkProfileChecker.isSupported - optional = false - ignorable = true - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.STEAMVR_DISCONNECTED - enabled = false - optional = false - ignorable = true - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.TRACKER_ERROR - valid = true // Default to valid - enabled = true - optional = false - ignorable = false - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.TRACKERS_REST_CALIBRATION - enabled = true - optional = false - ignorable = true - visibility = TrackingChecklistStepVisibility.ALWAYS - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.FULL_RESET - enabled = true - optional = false - ignorable = false - visibility = TrackingChecklistStepVisibility.ALWAYS - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.MOUNTING_CALIBRATION - valid = false - enabled = vrServer.configManager.vrConfig.resetsConfig.lastMountingMethod == MountingMethods.AUTOMATIC - optional = false - ignorable = true - visibility = TrackingChecklistStepVisibility.ALWAYS - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION - valid = false - enabled = false - optional = false - ignorable = true - visibility = TrackingChecklistStepVisibility.ALWAYS - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.UNASSIGNED_HMD - enabled = true - optional = false - ignorable = false - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED - enabled = true - optional = true - ignorable = true - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - - steps.add( - TrackingChecklistStepT().apply { - id = TrackingChecklistStepId.VRCHAT_SETTINGS - enabled = vrServer.vrcConfigManager.isSupported - valid = true - optional = true - ignorable = true - visibility = TrackingChecklistStepVisibility.WHEN_INVALID - }, - ) - } - - fun updateChecklist() { - val assignedTrackers = - vrServer.allTrackers.filter { it.trackerPosition != null && it.status != TrackerStatus.DISCONNECTED } - val imuTrackers = - assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE } - - val trackersWithError = - imuTrackers.filter { it.status == TrackerStatus.ERROR } - updateValidity( - TrackingChecklistStepId.TRACKER_ERROR, - trackersWithError.isEmpty(), - ) { - if (trackersWithError.isNotEmpty()) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistTrackerError - value = TrackingChecklistTrackerErrorT().apply { - trackersId = buildTrackersIds(trackersWithError) - } - } - } else { - it.extraData = null - } - } - - val trackerRequireReset = imuTrackers.filter { - it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset - } - // We ask for a full reset if you need to do mounting calibration but cant because you haven't done full reset in a while - // or if you have trackers that need reset after re-assigning - val usingSavedCalibration = vrServer.configManager.vrConfig.resetsConfig.saveMountingReset && imuTrackers.all { it.resetsHandler.mountRotFix != Quaternion.IDENTITY } - val needFullReset = (vrServer.configManager.vrConfig.resetsConfig.lastMountingMethod == MountingMethods.AUTOMATIC && !usingSavedCalibration && !resetMountingCompleted && !vrServer.serverGuards.canDoMounting) || trackerRequireReset.isNotEmpty() - updateValidity(TrackingChecklistStepId.FULL_RESET, !needFullReset) { - it.enabled = imuTrackers.isNotEmpty() - if (trackerRequireReset.isNotEmpty()) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistTrackerReset - value = TrackingChecklistTrackerResetT().apply { - trackersId = buildTrackersIds(trackerRequireReset) - } - } - resetMountingCompleted = false - feetResetMountingCompleted = false - } else { - it.extraData = null - } - } - val hmd = - vrServer.allTrackers.firstOrNull { it.status != TrackerStatus.DISCONNECTED && it.isHmd && !it.isInternal && it.status.sendData } - val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null - updateValidity(TrackingChecklistStepId.UNASSIGNED_HMD, assignedHmd) { - if (!assignedHmd) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistUnassignedHMD - value = TrackingChecklistUnassignedHMDT().apply { - trackerId = TrackerIdT().apply { - if (hmd.device != null) { - deviceId = DeviceIdT().apply { id = hmd.device.id } - } - trackerNum = hmd.trackerNum - } - } - } - } else { - it.extraData = null - } - } - - val trackersNeedCalibration = imuTrackers.filter { - it.hasCompletedRestCalibration == false - } - updateValidity( - TrackingChecklistStepId.TRACKERS_REST_CALIBRATION, - trackersNeedCalibration.isEmpty(), - ) { - // Don't show the step if none of the trackers connected support IMU calibration - it.enabled = imuTrackers.any { t -> - t.hasCompletedRestCalibration != null - } - if (trackersNeedCalibration.isNotEmpty()) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistNeedCalibration - value = TrackingChecklistNeedCalibrationT().apply { - trackersId = buildTrackersIds(trackersNeedCalibration) - } - } - } else { - it.extraData = null - } - } - - val steamVRBridge = vrServer.getVRBridge(ISteamVRBridge::class.java) - if (steamVRBridge != null) { - val steamvrConnected = steamVRBridge.isConnected() - updateValidity( - TrackingChecklistStepId.STEAMVR_DISCONNECTED, - steamvrConnected, - ) { - it.enabled = true - if (!steamvrConnected) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistSteamVRDisconnected - value = TrackingChecklistSteamVRDisconnectedT().apply { - bridgeSettingsName = steamVRBridge.getBridgeConfigKey() - } - } - } else { - it.extraData = null - } - } - } - - if (vrServer.networkProfileChecker.isSupported) { - updateValidity(TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC, vrServer.networkProfileChecker.publicNetworks.isEmpty()) { - if (vrServer.networkProfileChecker.publicNetworks.isNotEmpty()) { - it.extraData = TrackingChecklistExtraDataUnion().apply { - type = TrackingChecklistExtraData.TrackingChecklistPublicNetworks - value = TrackingChecklistPublicNetworksT().apply { - adapters = vrServer.networkProfileChecker.publicNetworks.map { it.name }.toTypedArray() - } - } - } else { - it.extraData = null - } - } - } - - updateValidity(TrackingChecklistStepId.MOUNTING_CALIBRATION, resetMountingCompleted) { - it.enabled = vrServer.configManager.vrConfig.resetsConfig.lastMountingMethod == MountingMethods.AUTOMATIC && imuTrackers.isNotEmpty() - } - - updateValidity(TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION, feetResetMountingCompleted) { - it.enabled = - vrServer.configManager.vrConfig.resetsConfig.lastMountingMethod == MountingMethods.AUTOMATIC && - !vrServer.configManager.vrConfig.resetsConfig.resetMountingFeet && - imuTrackers.any { t -> TrackerUtils.feetsBodyParts.contains(t.trackerPosition?.bodyPart) } - } - - updateValidity(TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled) - - listeners.forEach { it.onStepsUpdate() } - } - - private fun updateValidity(id: Int, valid: Boolean, beforeUpdate: ((step: TrackingChecklistStepT) -> Unit)? = null) { - require(id != TrackingChecklistStepId.UNKNOWN) { - "id is unknown" - } - val step = steps.find { it.id == id } ?: error("step does not exists") - step.valid = valid - if (beforeUpdate != null) { - beforeUpdate(step) - } - } - - override fun onChange( - validity: VRCConfigValidity, - values: VRCConfigValues, - recommended: VRCConfigRecommendedValues, - muted: List, - ) { - updateValidity( - TrackingChecklistStepId.VRCHAT_SETTINGS, - VRCConfigValidity::class.java.declaredFields.asSequence().all { p -> - p.isAccessible = true - return@all p.get(validity) == true || muted.contains(p.name) - }, - ) - listeners.forEach { it.onStepsUpdate() } - } - - fun ignoreStep(step: TrackingChecklistStepT, ignore: Boolean) { - if (!step.ignorable) return - val ignoredSteps = vrServer.configManager.vrConfig.trackingChecklist.ignoredStepsIds - if (ignore && !ignoredSteps.contains(step.id)) { - ignoredSteps.add(step.id) - } else if (!ignore) { - ignoredSteps.remove(step.id) - } - vrServer.configManager.saveConfig() - } -} diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/behaviours.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/behaviours.kt new file mode 100644 index 0000000000..4d6da7ef37 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/behaviours.kt @@ -0,0 +1,179 @@ +@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + +package dev.slimevr.trackingchecklist + +import dev.slimevr.VRServer +import dev.slimevr.VRServerState +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.device.DeviceState +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.tracker.TrackerState +import dev.slimevr.vrchat.VRCConfigManager +import dev.slimevr.vrchat.VRCConfigState +import dev.slimevr.vrchat.computeRecommendedValues +import dev.slimevr.vrchat.computeValidity +import dev.slimevr.vrchat.isVRCConfigValid +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.datatypes.DeviceId +import solarxr_protocol.datatypes.TrackerId +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.rpc.TrackingChecklistNeedCalibration +import solarxr_protocol.rpc.TrackingChecklistSteamVRDisconnected +import solarxr_protocol.rpc.TrackingChecklistStep +import solarxr_protocol.rpc.TrackingChecklistStepId +import solarxr_protocol.rpc.TrackingChecklistTrackerError +import solarxr_protocol.rpc.TrackingChecklistUnassignedHMD + +// Flat-maps a server state flow into a combined flow of all context states for a given collection. +// Re-emits whenever any item's state changes or the collection itself changes. +private inline fun allContextStates( + server: VRServer, + crossinline select: (VRServerState) -> Collection, + crossinline stateOf: (C) -> Flow, +): Flow> = server.context.state.flatMapLatest { serverState -> + val items = select(serverState) + if (items.isEmpty()) return@flatMapLatest flowOf(emptyList()) + combine(items.map { item -> stateOf(item) }) { states -> states.toList() } +} + +private fun trackerStatesFlow(server: VRServer): Flow> = allContextStates(server, { state -> state.trackers.values }) { tracker -> tracker.context.state } + +private fun deviceStatesFlow(server: VRServer): Flow> = allContextStates(server, { state -> state.devices.values }) { device -> device.context.state } + +private fun trackerIdOf(tracker: TrackerState): TrackerId = TrackerId(trackerNum = tracker.id.toUByte(), deviceId = DeviceId(tracker.deviceId.toUByte())) + +class SteamVRCheckBehaviour(private val server: VRServer) : TrackingChecklistBehaviourType { + override fun observe(receiver: TrackingChecklist) { + server.context.state + .map { state -> + val connected = state.drivers.isNotEmpty() + TrackingChecklistStep( + valid = connected, + enabled = true, + ignorable = true, + extraData = if (!connected) TrackingChecklistSteamVRDisconnected() else null, + ) + } + .distinctUntilChanged() + .onEach { step -> receiver.context.dispatch(TrackingChecklistActions.UpdateStep(TrackingChecklistStepId.STEAMVR_DISCONNECTED, step)) } + .launchIn(receiver.context.scope) + } +} + +class HMDCheckBehaviour(private val server: VRServer) : TrackingChecklistBehaviourType { + private fun computeStep(trackers: List): TrackingChecklistStep { + // FIXME: Most likely incomplete + val hasSteamVR = trackers.any { tracker -> tracker.origin == DeviceOrigin.DRIVER } + val hmdTracker = trackers.firstOrNull { tracker -> tracker.origin == DeviceOrigin.DRIVER && tracker.position != null } + val isAssigned = hmdTracker?.bodyPart == BodyPart.HEAD + return TrackingChecklistStep( + valid = isAssigned, + enabled = hasSteamVR, + ignorable = true, + extraData = if (!isAssigned) { + TrackingChecklistUnassignedHMD( + trackerId = hmdTracker?.let { tracker -> trackerIdOf(tracker) }, + ) + } else { + null + }, + ) + } + + override fun observe(receiver: TrackingChecklist) { + trackerStatesFlow(server) + .map { trackers -> computeStep(trackers) } + .distinctUntilChanged() + .onEach { step -> receiver.context.dispatch(TrackingChecklistActions.UpdateStep(TrackingChecklistStepId.UNASSIGNED_HMD, step)) } + .launchIn(receiver.context.scope) + } +} + +class TrackerRestCheckBehaviour(private val server: VRServer) : TrackingChecklistBehaviourType { + private fun computeStep(trackers: List): TrackingChecklistStep { + val uncalibratedTrackers = trackers.filter { tracker -> + (tracker.origin == DeviceOrigin.UDP || tracker.origin == DeviceOrigin.HID) && + tracker.status == TrackerStatus.OK && + (tracker.completedRestCalibration != null && !tracker.completedRestCalibration) + } + return TrackingChecklistStep( + valid = uncalibratedTrackers.isEmpty(), + enabled = trackers.isNotEmpty(), + extraData = if (!uncalibratedTrackers.isEmpty()) { + TrackingChecklistNeedCalibration( + trackersId = uncalibratedTrackers.map { tracker -> trackerIdOf(tracker) }, + ) + } else { + null + }, + ) + } + + override fun observe(receiver: TrackingChecklist) { + trackerStatesFlow(server) + .map { trackers -> computeStep(trackers) } + .distinctUntilChanged() + .onEach { step -> receiver.context.dispatch(TrackingChecklistActions.UpdateStep(TrackingChecklistStepId.TRACKERS_REST_CALIBRATION, step)) } + .launchIn(receiver.context.scope) + } +} + +class TrackerErrorCheckBehaviour(private val server: VRServer) : TrackingChecklistBehaviourType { + private fun computeStep(trackers: List): TrackingChecklistStep { + val errorTrackers = trackers + .filter { tracker -> tracker.status == TrackerStatus.ERROR && tracker.bodyPart != null } + .toSet() + return TrackingChecklistStep( + valid = errorTrackers.isEmpty(), + enabled = trackers.isNotEmpty(), + extraData = if (errorTrackers.isNotEmpty()) { + TrackingChecklistTrackerError( + trackersId = errorTrackers.map { tracker -> trackerIdOf(tracker) }, + ) + } else { + null + }, + ) + } + + override fun observe(receiver: TrackingChecklist) { + trackerStatesFlow(server) + .map { trackers -> computeStep(trackers) } + .distinctUntilChanged() + .onEach { step -> receiver.context.dispatch(TrackingChecklistActions.UpdateStep(TrackingChecklistStepId.TRACKER_ERROR, step)) } + .launchIn(receiver.context.scope) + } +} + +class VRChatSettingsCheckBehaviour( + private val server: VRServer, + private val skeleton: Skeleton, + private val vrcConfigManager: VRCConfigManager, +) : TrackingChecklistBehaviourType { + private fun computeStep(vrc: VRCConfigState, userHeight: Double): TrackingChecklistStep { + val values = vrc.currentValues + if (!vrc.isSupported || values == null) return TrackingChecklistStep(valid = true, enabled = false) + val recommended = computeRecommendedValues(server, userHeight) + val validity = computeValidity(values, recommended) + return TrackingChecklistStep(valid = isVRCConfigValid(validity, vrc.mutedWarnings), enabled = true) + } + + override fun observe(receiver: TrackingChecklist) { + combine( + skeleton.context.state.map { state -> state.userHeight }, + vrcConfigManager.context.state, + ) { userHeight, vrc -> computeStep(vrc, userHeight) } + .distinctUntilChanged() + .onEach { step -> receiver.context.dispatch(TrackingChecklistActions.UpdateStep(TrackingChecklistStepId.VRCHAT_SETTINGS, step)) } + .launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/module.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/module.kt new file mode 100644 index 0000000000..a191264dcf --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/module.kt @@ -0,0 +1,54 @@ +package dev.slimevr.trackingchecklist + +import dev.slimevr.AppContextProvider +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import solarxr_protocol.rpc.TrackingChecklistStep +import solarxr_protocol.rpc.TrackingChecklistStepId + +data class TrackingChecklistState( + val steps: Map = emptyMap(), +) + +sealed interface TrackingChecklistActions { + data class UpdateStep(val id: TrackingChecklistStepId, val step: TrackingChecklistStep) : TrackingChecklistActions +} + +typealias TrackingChecklistContext = Context +typealias TrackingChecklistBehaviourType = Behaviour + +object ChecklistBaseBehaviour : TrackingChecklistBehaviourType { + override fun reduce(state: TrackingChecklistState, action: TrackingChecklistActions): TrackingChecklistState = when (action) { + is TrackingChecklistActions.UpdateStep -> state.copy(steps = state.steps + (action.id to action.step)) + } +} + +class TrackingChecklist( + val context: TrackingChecklistContext, +) { + fun startObserving(appContext: AppContextProvider) { + val stepBehaviours: List = buildList { + add(SteamVRCheckBehaviour(appContext.server)) + add(HMDCheckBehaviour(appContext.server)) + add(TrackerRestCheckBehaviour(appContext.server)) + add(TrackerErrorCheckBehaviour(appContext.server)) + appContext.vrcConfigManager?.let { add(VRChatSettingsCheckBehaviour(appContext.server, appContext.skeleton, it)) } + } + context.behaviours.addAll(stepBehaviours) + context.observeAll(this) + } + + companion object { + fun create(scope: CoroutineScope): TrackingChecklist { + val initialBehaviours = listOf(ChecklistBaseBehaviour) + val context = Context.create( + initialState = TrackingChecklistState(), + scope = scope, + behaviours = initialBehaviours, + ) + val checklist = TrackingChecklist(context) + return checklist + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseHandler.kt b/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseHandler.kt deleted file mode 100644 index 833dc25d47..0000000000 --- a/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseHandler.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.slimevr.trackingpause - -import java.util.concurrent.CopyOnWriteArrayList - -class TrackingPauseHandler { - private val listeners: MutableList = CopyOnWriteArrayList() - - fun sendTrackingPauseState(trackingPaused: Boolean) { - listeners.forEach { it.onTrackingPause(trackingPaused) } - } - - fun addListener(listener: TrackingPauseListener) { - listeners.add(listener) - } - - fun removeListener(listener: TrackingPauseListener) { - listeners.removeIf { it == listener } - } -} diff --git a/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseListener.kt b/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseListener.kt deleted file mode 100644 index 26182c04a8..0000000000 --- a/server/core/src/main/java/dev/slimevr/trackingpause/TrackingPauseListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slimevr.trackingpause - -interface TrackingPauseListener { - fun onTrackingPause(trackingPaused: Boolean) -} diff --git a/server/core/src/main/java/dev/slimevr/udp/behaviours.kt b/server/core/src/main/java/dev/slimevr/udp/behaviours.kt new file mode 100644 index 0000000000..9e6609b33a --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/udp/behaviours.kt @@ -0,0 +1,471 @@ +package dev.slimevr.udp + +import dev.slimevr.AppLogger +import dev.slimevr.VRServerActions +import dev.slimevr.device.Device +import dev.slimevr.device.DeviceActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.tracker.Tracker +import dev.slimevr.tracker.TrackerActions +import dev.slimevr.tracker.TrackerIdNum +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import solarxr_protocol.datatypes.MagnetometerStatus +import solarxr_protocol.datatypes.TrackerStatus +import kotlin.random.Random + +internal const val CONNECTION_TIMEOUT_MS = 5000L +internal const val CONNECTION_REMOVAL_MS = 30_000L + +object PacketBehaviour : UDPConnectionBehaviour { + override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) { + is UDPConnectionActions.LastPacket -> { + var newState = state.copy(lastPacket = action.time) + if (action.packetNum != null) newState = newState.copy(lastPacketNum = action.packetNum) + newState + } + + else -> state + } + + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onAny { packet -> + val state = receiver.context.state.value + val now = System.currentTimeMillis() + if (now - state.lastPacket > CONNECTION_TIMEOUT_MS && packet.packetNumber == 0L) { + receiver.context.dispatch(UDPConnectionActions.LastPacket(packetNum = 0, time = now)) + AppLogger.udp.info("[${state.address}] Reconnecting") + } else if (packet.packetNumber != null && packet.packetNumber < state.lastPacketNum) { + // Note: Packet number is nullable for bundled packets, as only the bundle packet itself has the number + // the packets inside of it do not + AppLogger.udp.warn("[${state.address}] WARN: Received packet with wrong packet number") + return@onAny + } else { + receiver.context.dispatch(UDPConnectionActions.LastPacket(packetNum = packet.packetNumber, time = now)) + } + } + } +} + +object PacketLossBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + var totalPacketsReceived = 0L + var acceptedPackets = 0L + var lastPacketCounterReset = System.currentTimeMillis() + var lastPacketNumber = 0L + + receiver.packetEvents.onAny { packet -> + val num = packet.packetNumber ?: return@onAny + val now = System.currentTimeMillis() + + if (now - lastPacketCounterReset >= 10_000L) { + totalPacketsReceived = 0L + acceptedPackets = 0L + lastPacketCounterReset = now + } + + totalPacketsReceived++ + val accepted = num == 0L || num > lastPacketNumber + if (accepted) { + lastPacketNumber = num + acceptedPackets++ + } + + receiver.getDevice()?.context?.dispatch( + DeviceActions.PacketStats(packetsReceived = totalPacketsReceived, packetsLost = totalPacketsReceived - acceptedPackets), + ) + } + } +} + + +object PingBehaviour : UDPConnectionBehaviour { + override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) { + is UDPConnectionActions.StartPing -> state.copy(lastPing = state.lastPing.copy(startTime = action.startTime, id = action.pingId)) + else -> state + } + + override fun observe(receiver: UDPConnection) { + // Send the ping every 1s + receiver.context.scope.launch { + while (isActive) { + val state = receiver.context.state.value + if (state.didHandshake) { + val pingId = Random.nextInt() + receiver.context.dispatch(UDPConnectionActions.StartPing(startTime = System.currentTimeMillis(), pingId = pingId)) + receiver.send(PingPong(pingId)) + } + delay(1000) + } + } + + // listen for the pong + receiver.packetEvents.onPacket { packet -> + val state = receiver.context.state.value + val deviceId = state.deviceId ?: return@onPacket + + if (packet.data.pingId != state.lastPing.id) { + AppLogger.udp.warn("[${state.address}] Ping ID does not match, ignoring ${packet.data.pingId} != ${state.lastPing.id}") + return@onPacket + } + + val ping = (System.currentTimeMillis() - state.lastPing.startTime) / 2 + val device = receiver.appContext.server.getDevice(deviceId) ?: return@onPacket + device.context.dispatch(DeviceActions.Update { copy(ping = ping) }) + } + } +} + +object HandshakeBehaviour : UDPConnectionBehaviour { + override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) { + is UDPConnectionActions.Handshake -> state.copy(didHandshake = true, deviceId = action.deviceId) + is UDPConnectionActions.TimedOut -> state.copy(didHandshake = false) + else -> state + } + + private fun findOrCreateDevice(receiver: UDPConnection, state: UDPConnectionState, data: Handshake): Device { + val devices = receiver.appContext.server.context.state.value.devices.values + val existing = data.macString?.let { mac -> + devices.find { device -> + val ds = device.context.state.value + ds.macAddress == mac && ds.origin == DeviceOrigin.UDP + } + } + if (existing != null) { + receiver.context.dispatch(UDPConnectionActions.Handshake(existing.context.state.value.id)) + return existing + } + val deviceId = receiver.appContext.server.nextHandle() + val newDevice = Device.create( + id = deviceId, + scope = receiver.appContext.server.context.scope, + address = state.address, + macAddress = data.macString, + origin = DeviceOrigin.UDP, + protocolVersion = data.protocolVersion, + ) + receiver.appContext.server.context.dispatch(VRServerActions.NewDevice(deviceId = deviceId, context = newDevice)) + receiver.context.dispatch(UDPConnectionActions.Handshake(deviceId)) + return newDevice + } + + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { packet -> + val state = receiver.context.state.value + + val device = if (state.deviceId == null) { + findOrCreateDevice(receiver, state, packet.data) + } else { + receiver.context.dispatch(UDPConnectionActions.Handshake(state.deviceId)) + receiver.getDevice() ?: run { + AppLogger.udp.warn("[${state.address}] Reconnect handshake but device ${state.deviceId} not found") + receiver.send(Handshake()) + return@onPacket + } + } + + // Apply handshake fields to device, always, for both first connect and reconnect + device.context.dispatch( + DeviceActions.Update { + copy( + macAddress = packet.data.macString ?: macAddress, + boardType = packet.data.boardType, + mcuType = packet.data.mcuType, + firmware = packet.data.firmware ?: firmware, + protocolVersion = packet.data.protocolVersion, + ) + }, + ) + + receiver.send(Handshake()) + } + } +} + +object TimeoutBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.context.scope.launch { + while (isActive) { + val state = receiver.context.state.value + if (!state.didHandshake) { + delay(500) + continue + } + val timeUntilTimeout = CONNECTION_TIMEOUT_MS - (System.currentTimeMillis() - state.lastPacket) + if (timeUntilTimeout <= 0) { + AppLogger.udp.info("[${state.address}] Connection timed out for ${state.id}") + receiver.context.dispatch(UDPConnectionActions.TimedOut) + receiver.getDevice()?.context?.dispatch( + DeviceActions.Update { copy(status = TrackerStatus.TIMED_OUT) }, + ) + state.trackerIds.mapNotNull { receiver.appContext.server.getTracker(it.id) }.forEach { tracker -> + tracker.context.dispatch(TrackerActions.SetStatus(TrackerStatus.TIMED_OUT)) + } + } else { + delay(timeUntilTimeout + 1) + } + } + } + } +} + +object DisconnectBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + var removalJob: Job? = null + receiver.context.state + .distinctUntilChangedBy { it.didHandshake } + .onEach { state -> + if (!state.didHandshake) { + removalJob = receiver.context.scope.launch { + delay(CONNECTION_REMOVAL_MS) + val currentState = receiver.context.state.value + AppLogger.udp.info("[${currentState.address}] Connection removed after extended timeout") + receiver.appContext.udpServer.context.dispatch(UdpServerActions.ConnectionRemoved(currentState.address)) + receiver.getDevice()?.context?.dispatch( + DeviceActions.Update { copy(status = TrackerStatus.DISCONNECTED) }, + ) + currentState.trackerIds.mapNotNull { receiver.appContext.server.getTracker(it.id) }.forEach { tracker -> + tracker.context.dispatch(TrackerActions.SetStatus(TrackerStatus.DISCONNECTED)) + } + } + } else { + removalJob?.cancel() + removalJob = null + } + } + .launchIn(receiver.context.scope) + } +} + +object DeviceStatsBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + val device = receiver.getDevice() ?: return@onPacket + device.context.dispatch( + DeviceActions.Update { + copy(batteryLevel = event.data.level, batteryVoltage = event.data.voltage) + }, + ) + } + + receiver.packetEvents.onPacket { event -> + val device = receiver.getDevice() ?: return@onPacket + device.context.dispatch(DeviceActions.Update { copy(signalStrength = event.data.signal) }) + } + } +} + +object SensorInfoBehaviour : UDPConnectionBehaviour { + private suspend fun assignTracker(receiver: UDPConnection, device: Device, event: PacketEvent): Pair { + val deviceState = device.context.state.value + val mac = deviceState.macAddress ?: run { + AppLogger.udp.warn("[${deviceState.address}] No MAC address available, falling back to IP for hardware ID") + deviceState.address + } + val hardwareId = "$mac:${event.data.sensorId}" + + val existingTracker = receiver.appContext.server.context.state.value.trackers.values + .find { t -> t.context.state.value.deviceId == deviceState.id && t.context.state.value.hardwareId == hardwareId } + + if (existingTracker != null) { + receiver.context.dispatch( + UDPConnectionActions.AssignTracker( + trackerId = TrackerIdNum(id = existingTracker.context.state.value.id, trackerNum = event.data.sensorId), + ), + ) + return existingTracker to false + } + + val trackerId = receiver.appContext.server.nextHandle() + val newTracker = Tracker.create( + id = trackerId, + hardwareId = hardwareId, + sensorType = event.data.imuType, + deviceId = deviceState.id, + origin = DeviceOrigin.UDP, + scope = receiver.appContext.server.context.scope, + server = receiver.appContext.server, + settings = receiver.appContext.config.settings, + ) + receiver.appContext.server.context.dispatch(VRServerActions.NewTracker(trackerId = trackerId, context = newTracker)) + receiver.context.dispatch( + UDPConnectionActions.AssignTracker(trackerId = TrackerIdNum(id = trackerId, trackerNum = event.data.sensorId)), + ) + return newTracker to true + } + + override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) { + is UDPConnectionActions.AssignTracker -> state.copy(trackerIds = state.trackerIds + action.trackerId) + else -> state + } + + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + val device = receiver.getDevice() ?: error("invalid state - a device should exist at this point") + + val existingTracker = receiver.getTracker(event.data.sensorId) + if (existingTracker != null) { + existingTracker.context.dispatchAll( + listOf( + TrackerActions.Update { copy(sensorType = event.data.imuType, completedRestCalibration = event.data.hasCompletedRestCalibration) }, + TrackerActions.SetStatus(event.data.status), + ), + ) + return@onPacket + } + + val (tracker, isNew) = assignTracker(receiver, device, event) + tracker.context.dispatchAll( + listOf( + TrackerActions.Update { copy(sensorType = event.data.imuType, completedRestCalibration = event.data.hasCompletedRestCalibration) }, + TrackerActions.SetStatus(event.data.status), + ), + ) + if (isNew && tracker.context.state.value.magStatus == MagnetometerStatus.NOT_SUPPORTED) { + tracker.context.dispatch( + TrackerActions.SetMagStatus( + if (event.data.sensorConfig?.magSupported == true) MagnetometerStatus.DISABLED else MagnetometerStatus.NOT_SUPPORTED, + ), + ) + } + + val remoteMagStatus = event.data.sensorConfig?.let { + if (it.magSupported) { + if (it.magEnabled) MagnetometerStatus.ENABLED else MagnetometerStatus.DISABLED + } else { + MagnetometerStatus.NOT_SUPPORTED + } + } ?: MagnetometerStatus.NOT_SUPPORTED + + var desiredMagStatus = tracker.context.state.value.magStatus + val globalMagEnabled = receiver.appContext.config.settings.context.state.value.data.globalMagEnabled + if (remoteMagStatus != desiredMagStatus) { + if (globalMagEnabled && remoteMagStatus != MagnetometerStatus.ENABLED && desiredMagStatus != MagnetometerStatus.NOT_SUPPORTED) { + desiredMagStatus = MagnetometerStatus.ENABLED + } + receiver.context.dispatch( + UDPConnectionActions.SetSensorConfig(sensorId = event.data.sensorId, flags = SensorConfigFlags(magStatus = desiredMagStatus)), + ) + } + } + } +} + +object SensorRotationBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = event.data.rotation)) + } + + receiver.packetEvents.onPacket { event -> + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = event.data.rotation, acceleration = event.data.acceleration)) + } + + receiver.packetEvents.onPacket { event -> + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(acceleration = event.data.acceleration)) + } + + receiver.packetEvents.onPacket { event -> + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.SetRotation(rotation = event.data.rotation)) + } + } +} + +object BundledPacketBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + for (packet in event.data.packets) { + if (!receiver.context.state.value.didHandshake && packet !is PreHandshakePacket) continue + // we set the packetNumber to null so we ignore the check + // it should be done by the parent packet + receiver.packetEvents.emit(PacketEvent(packet, packetNumber = null)) + } + } + + receiver.packetEvents.onPacket { event -> + for (packet in event.data.packets) { + if (!receiver.context.state.value.didHandshake && packet !is PreHandshakePacket) continue + // we set the packetNumber to null so we ignore the check + // it should be done by the parent packet + receiver.packetEvents.emit(PacketEvent(packet, packetNumber = null)) + } + } + } +} + +object FlagsBehaviour : UDPConnectionBehaviour { + override fun reduce( + state: UDPConnectionState, + action: UDPConnectionActions, + ): UDPConnectionState = when (action) { + is UDPConnectionActions.FirmwareFeatures -> state.copy(features = action.features) + else -> state + } + + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + receiver.context.dispatch(UDPConnectionActions.FirmwareFeatures(event.data.firmwareFeatures)) + // send back the server features + receiver.send(FeatureFlags()) + } + } +} + +object TemperatureBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + tracker.context.dispatch(TrackerActions.Update { copy(imuTemp = event.data.temp) }) + } + } +} + +object SensorConfigBehaviour : UDPConnectionBehaviour { + override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) { + is UDPConnectionActions.SetSensorConfig -> state.copy( + sensorConfigFlags = state.sensorConfigFlags + (action.sensorId to action.flags), + ) + + else -> state + } + + override fun observe(receiver: UDPConnection) { + receiver.context.state + .distinctUntilChangedBy { it.sensorConfigFlags } + .onEach { state -> + for ((sensorId, flags) in state.sensorConfigFlags) { + receiver.send( + SetConfigFlag( + sensorId = sensorId, + configType = SensorConfigType.MAGNETOMETER, + state = flags.magStatus == MagnetometerStatus.ENABLED, + ), + ) + } + } + .launchIn(receiver.context.scope) + } +} + +object AckConfigBehaviour : UDPConnectionBehaviour { + override fun observe(receiver: UDPConnection) { + receiver.packetEvents.onPacket { event -> + val configType = SensorConfigType.fromId(event.data.configType) ?: return@onPacket + val flags = receiver.context.state.value.sensorConfigFlags[event.data.sensorId] ?: return@onPacket + + val tracker = receiver.getTracker(event.data.sensorId) ?: return@onPacket + if (configType == SensorConfigType.MAGNETOMETER) { + tracker.context.dispatch(TrackerActions.SetMagStatus(flags.magStatus)) + } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/udp/connection.kt b/server/core/src/main/java/dev/slimevr/udp/connection.kt new file mode 100644 index 0000000000..d7b2f81965 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/udp/connection.kt @@ -0,0 +1,164 @@ +package dev.slimevr.udp + +import dev.slimevr.AppContextProvider +import dev.slimevr.EventDispatcher +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.device.Device +import dev.slimevr.tracker.Tracker +import dev.slimevr.tracker.TrackerIdNum +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.io.Buffer +import kotlinx.io.readByteArray +import solarxr_protocol.datatypes.MagnetometerStatus +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress + +data class LastPing( + val id: Int, + val startTime: Long, +) + +data class SensorConfigFlags( + val magStatus: MagnetometerStatus, +) + +data class UDPConnectionState( + val id: String, + val lastPacket: Long, + val lastPacketNum: Long, + val lastPing: LastPing, + val didHandshake: Boolean, + val address: String, + val port: Int, + val deviceId: Int?, + val trackerIds: List, + val features: FirmwareFeatures?, + val sensorConfigFlags: Map, +) + +sealed interface UDPConnectionActions { + data class StartPing(val startTime: Long, val pingId: Int) : UDPConnectionActions + data class Handshake(val deviceId: Int) : UDPConnectionActions + data class LastPacket(val packetNum: Long? = null, val time: Long) : UDPConnectionActions + data class AssignTracker(val trackerId: TrackerIdNum) : UDPConnectionActions + data class FirmwareFeatures(val features: dev.slimevr.udp.FirmwareFeatures) : UDPConnectionActions + data class SetSensorConfig(val sensorId: Int, val flags: SensorConfigFlags) : UDPConnectionActions + data object TimedOut : UDPConnectionActions +} + +typealias UDPConnectionContext = Context +typealias UDPConnectionBehaviour = Behaviour + +class UDPConnection( + val context: UDPConnectionContext, + val appContext: AppContextProvider, + val packetEvents: UDPPacketDispatcher, + val packetChannel: Channel>, + private val socket: DatagramSocket, + private val remoteInetAddress: InetAddress, + private val remotePort: Int, + private val scope: CoroutineScope, +) { + fun send(packet: UDPPacket) { + scope.launch(Dispatchers.IO) { + val buf = Buffer() + writePacket(buf, packet) + val bytes = buf.readByteArray() + socket.send(DatagramPacket(bytes, bytes.size, remoteInetAddress, remotePort)) + } + } + + fun startObserving() = context.observeAll(this) + + fun getDevice(): Device? { + val deviceId = context.state.value.deviceId + return if (deviceId != null) appContext.server.getDevice(deviceId) else null + } + + fun getTracker(id: Int): Tracker? { + val trackerId = context.state.value.trackerIds.find { it.trackerNum == id } + return if (trackerId != null) appContext.server.getTracker(trackerId.id) else null + } + + companion object { + fun create( + id: String, + socket: DatagramSocket, + remoteIp: String, + remotePort: Int, + appContext: AppContextProvider, + scope: CoroutineScope, + ): UDPConnection { + val behaviours = listOf( + PacketBehaviour, + PacketLossBehaviour, + HandshakeBehaviour, + TimeoutBehaviour, + DisconnectBehaviour, + PingBehaviour, + DeviceStatsBehaviour, + SensorInfoBehaviour, + SensorRotationBehaviour, + BundledPacketBehaviour, + FlagsBehaviour, + TemperatureBehaviour, + SensorConfigBehaviour, + AckConfigBehaviour, + ) + + val context = Context.create( + initialState = UDPConnectionState( + id = id, + lastPacket = System.currentTimeMillis(), + lastPacketNum = 0, + lastPing = LastPing(id = 0, startTime = 0), + didHandshake = false, + address = remoteIp, + port = remotePort, + deviceId = null, + trackerIds = listOf(), + features = null, + sensorConfigFlags = emptyMap(), + ), + scope = scope, + behaviours = behaviours, + ) + + val dispatcher = EventDispatcher> { it.data::class } + val packetChannel = Channel>(capacity = 256) + val remoteInetAddress = InetAddress.getByName(remoteIp) + + val conn = UDPConnection( + context = context, + appContext = appContext, + packetEvents = dispatcher, + packetChannel = packetChannel, + socket = socket, + remoteInetAddress = remoteInetAddress, + remotePort = remotePort, + scope = scope, + ) + conn.startObserving() + + // Dedicated coroutine per connection so the receive loop is never blocked by packet processing + scope.launch { + for (event in packetChannel) { + // We skip any packet from the tracker that are not handshake packets + // if we didn't do a handshake with the server + // this prevents from receiving packets if the server does not know about the + // tracker yet. This usually happen when you restart the server with already + // connected trackers + if (!context.state.value.didHandshake && event.data !is PreHandshakePacket) continue + dispatcher.emit(event) + } + } + + return conn + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/udp/packets.kt b/server/core/src/main/java/dev/slimevr/udp/packets.kt new file mode 100644 index 0000000000..a94608d08b --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/udp/packets.kt @@ -0,0 +1,550 @@ +package dev.slimevr.udp + +import dev.slimevr.EventDispatcher +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import io.ktor.utils.io.core.ByteReadPacket +import io.ktor.utils.io.core.remaining +import kotlinx.io.Sink +import kotlinx.io.Source +import kotlinx.io.readByteArray +import kotlinx.io.readFloat +import kotlinx.io.readString +import kotlinx.io.readUByte +import kotlinx.io.readUShort +import kotlinx.io.write +import kotlinx.io.writeUByte +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.datatypes.hardware_info.BoardType +import solarxr_protocol.datatypes.hardware_info.ImuType +import solarxr_protocol.datatypes.hardware_info.McuType +import java.nio.ByteBuffer + +enum class ServerFeatureFlags { + /** Server can parse bundle packets: `PACKET_BUNDLE` = 100 (0x64). */ + PROTOCOL_BUNDLE_SUPPORT, + + /** Server can parse bundle packets with compact headers and packed IMU rotation/acceleration frames: + - `PACKET_BUNDLE_COMPACT` = 101 (0x65), + - `PACKET_ROTATION_AND_ACCELERATION` = 23 (0x17). */ + PROTOCOL_BUNDLE_COMPACT_SUPPORT, + + ; + + companion object { + val flagsEnabled: Set = setOf( + PROTOCOL_BUNDLE_SUPPORT, + PROTOCOL_BUNDLE_COMPACT_SUPPORT, + ) + + val packed: ByteArray by lazy { + val count = entries.size + val byteLength = if (count == 0) 0 else (count - 1) / 8 + 1 + val tempPacked = ByteArray(byteLength) + + for (flag in flagsEnabled) { + val bit = flag.ordinal + val byteIndex = bit / 8 + val bitIndex = bit % 8 + tempPacked[byteIndex] = (tempPacked[byteIndex].toInt() or (1 shl bitIndex)).toByte() + } + tempPacked + } + } +} + +class FirmwareFeatures { + enum class FirmwareFeatureFlags { + REMOTE_COMMAND, + B64_WIFI_SCANNING, + SENSOR_CONFIG, + ; + + companion object { + // "Size + 7" ensures that 1-8 flags = 1 byte, 9-16 flags = 2 bytes, etc. + val byteCount = (entries.size + 7) / 8 + } + } + + private val flags = ByteArray(FirmwareFeatureFlags.byteCount) + + fun has(flag: FirmwareFeatureFlags): Boolean { + val bit = flag.ordinal + val byteIndex = bit / 8 + if (byteIndex >= flags.size) return false + + return (flags[byteIndex].toInt() and (1 shl (bit % 8))) != 0 + } + + companion object { + + fun from(received: ByteBuffer, length: Int): FirmwareFeatures { + val res = FirmwareFeatures() + val bytesToRead = res.flags.size.coerceAtMost(length) + received.get(res.flags, 0, bytesToRead) + return res + } + } +} + +private fun Source.readU8(): Int = readByte().toInt() and 0xFF + +private fun Source.readSafeFloat(): Float = readFloat().let { if (it.isNaN()) 0f else it } + +private fun Source.readSafeQuat(): Quaternion { + val x = readFloat() + val y = readFloat() + val z = readFloat() + val w = readFloat() + return if (x.isNaN() || y.isNaN() || z.isNaN() || w.isNaN() || (x == 0f && y == 0f && z == 0f && w == 0f)) { + Quaternion.IDENTITY + } else { + Quaternion(w, x, y, z) + } +} + +enum class PacketType(val id: Int) { + HEARTBEAT(0), + ROTATION(1), + HANDSHAKE(3), + ACCEL(4), + PING_PONG(10), + SERIAL(11), + BATTERY_LEVEL(12), + TAP(13), + ERROR(14), + SENSOR_INFO(15), + ROTATION_2(16), + ROTATION_DATA(17), + MAGNETOMETER_ACCURACY(18), + SIGNAL_STRENGTH(19), + TEMPERATURE(20), + USER_ACTION(21), + FEATURE_FLAGS(22), + ROTATION_AND_ACCEL(23), + ACK_CONFIG_CHANGE(24), + SET_CONFIG_FLAG(25), + FLEX_DATA(26), + POSITION(27), + PACKET_BUNDLE(100), + PACKET_BUNDLE_COMPACT(101), + PROTOCOL_CHANGE(200), + ; + + companion object { + private val map = entries.associateBy { it.id } + fun fromId(id: Int) = map[id] + } +} + +sealed interface UDPPacket { + fun write(dst: Sink) {} +} + +/** Packets that are processed before the handshake is complete */ +sealed interface PreHandshakePacket : UDPPacket + +sealed interface SensorSpecificPacket : UDPPacket { + val sensorId: Int +} + +data object Heartbeat : UDPPacket + +data class Handshake( + val boardType: BoardType = BoardType.UNKNOWN, + val imuType: Int = 0, + val mcuType: McuType = McuType.Other, + val protocolVersion: Int = 0, + val firmware: String? = null, + val macString: String? = null, +) : PreHandshakePacket { + override fun write(dst: Sink) { + dst.writeByte(PacketType.HANDSHAKE.id.toByte()) + dst.write("Hey OVR =D 5".toByteArray(Charsets.US_ASCII)) + } + + companion object { + fun read(src: Source): Handshake = with(src) { + if (remaining == 0L) return Handshake() + val b = if (remaining >= 4) BoardType.fromValue(readInt().toUShort()) ?: BoardType.UNKNOWN else BoardType.UNKNOWN + val i = if (remaining >= 4) readInt() else 0 + val m = if (remaining >= 4) McuType.fromValue(readInt().toUShort()) ?: McuType.Other else McuType.Other + if (remaining >= 12) { + readInt() + readInt() + readInt() + } + val p = if (remaining >= 4) readInt() else 0 + val f = if (remaining >= 1) readString(readByte().toLong()) else null + val mac = if (remaining >= 6) { + val bytes = readByteArray(6) + bytes.joinToString(":") { "%02X".format(it) }.takeIf { it != "00:00:00:00:00:00" } + } else { + null + } + Handshake(b, i, m, p, f, mac) + } + } +} + +data class PacketBundle( + val packets: List, +) : PreHandshakePacket { + companion object { + fun read(src: Source): PacketBundle = with(src) { + val readPackets = mutableListOf() + while (remaining >= 2) { + val bundlePacketLen = readUShort().toInt() + if (bundlePacketLen <= 0) continue + + val rawBytes = readByteArray(bundlePacketLen) + val subSrc = ByteReadPacket(rawBytes) + + subSrc.use { subSrc -> + if (subSrc.remaining >= 4) { + val packetId = subSrc.readInt() + val type = PacketType.fromId(packetId) + + if (type != null) { + // 4. Pass the isolated sub-source to your existing parser + val packetData = readPacket(type, subSrc) + readPackets.add(packetData) + } + } + } + } + return PacketBundle(packets = readPackets) + } + } +} + +data class PacketBundleCompact( + val packets: List, +) : PreHandshakePacket { + companion object { + fun read(src: Source): PacketBundle = with(src) { + val readPackets = mutableListOf() + while (remaining >= 1) { + val bundlePacketLen = readUByte().toInt() + if (bundlePacketLen <= 0) continue + + val rawBytes = readByteArray(bundlePacketLen) + val subSrc = ByteReadPacket(rawBytes) + + subSrc.use { subSrc -> + if (subSrc.remaining >= 4) { + val packetId = subSrc.readUByte().toInt() + val type = PacketType.fromId(packetId) + + if (type != null) { + // 4. Pass the isolated sub-source to your existing parser + val packetData = readPacket(type, subSrc) + readPackets.add(packetData) + } + } + } + } + return PacketBundle(packets = readPackets) + } + } +} + +data class Rotation(override val sensorId: Int = 0, val rotation: Quaternion = Quaternion.IDENTITY) : SensorSpecificPacket { + companion object { + fun read(src: Source) = Rotation(0, src.readSafeQuat()) + } +} + +data class Accel(val acceleration: Vector3 = Vector3.NULL, override val sensorId: Int = 0) : SensorSpecificPacket { + companion object { + fun read(src: Source) = Accel( + Vector3(src.readSafeFloat(), src.readSafeFloat(), src.readSafeFloat()), + if (src.remaining > 0) src.readU8() else 0, + ) + } +} + +data class PingPong(val pingId: Int = 0) : PreHandshakePacket { + override fun write(dst: Sink) { + dst.writeInt(pingId) + } + companion object { + fun read(src: Source) = PingPong(src.readInt()) + } +} + +data class Serial(val serial: String = "") : UDPPacket { + companion object { + fun read(src: Source) = Serial(src.readString(src.readInt().toLong())) + } +} + +data class BatteryLevel(val voltage: Float = 0f, val level: Float = 0f) : UDPPacket { + companion object { + fun read(src: Source): BatteryLevel { + val f = src.readSafeFloat() + return if (src.remaining >= 4) BatteryLevel(f, src.readSafeFloat()) else BatteryLevel(0f, f) + } + } +} + +data class Tap(override val sensorId: Int = 0, val tap: Int = 0) : SensorSpecificPacket { + companion object { + fun read(src: Source) = Tap(src.readU8(), src.readU8()) + } +} + +data class ErrorPacket(override val sensorId: Int = 0, val errorNumber: Int = 0) : SensorSpecificPacket { + companion object { + fun read(src: Source) = ErrorPacket(src.readU8(), src.readU8()) + } +} + +data class SensorInfo( + override val sensorId: Int = 0, + val status: TrackerStatus = TrackerStatus.DISCONNECTED, + val imuType: ImuType = ImuType.Other, + val sensorConfig: SensorConfig? = null, + val hasCompletedRestCalibration: Boolean? = null, + val trackerPosition: Int? = null, + val trackerDataType: Int? = null, +) : SensorSpecificPacket { + data class SensorConfig( + val magEnabled: Boolean, + val magSupported: Boolean, + val calibrationEnabled: Boolean, + val calibrationSupported: Boolean, + val tempGradientCalibrationEnabled: Boolean, + val tempGradientCalibrationSupported: Boolean, + ) { + companion object { + fun fromUDP(raw: UShort) = with(raw) { + val raw = toInt() + + SensorConfig( + magEnabled = raw and 1 != 0, + magSupported = (raw shr 1) and 1 != 0, + calibrationEnabled = (raw shr 2) and 1 != 0, + calibrationSupported = (raw shr 3) and 1 != 0, + tempGradientCalibrationEnabled = (raw shr 4) and 1 != 0, + tempGradientCalibrationSupported = (raw shr 5) and 1 != 0, + ) + } + } + } + + companion object { + private fun statusFromUDP(raw: UByte): TrackerStatus = when (raw.toInt()) { + 0 -> TrackerStatus.DISCONNECTED + 1 -> TrackerStatus.OK + 2 -> TrackerStatus.ERROR + else -> TrackerStatus.DISCONNECTED + } + + fun read(src: Source) = with(src) { + val id = readU8() + val stat = statusFromUDP(readUByte()) + val imu = if (remaining > 0) ImuType.fromValue(readUByte().toUShort()) ?: ImuType.Other else ImuType.Other + val conf = if (remaining >= 2) SensorConfig.fromUDP(readShort().toUShort()) else null + val calib = if (remaining > 0) readU8() != 0 else null + val pos = if (remaining > 0) readU8() else null + val dt = if (remaining > 0) readU8() else null + SensorInfo(id, stat, imu, conf, calib, pos, dt) + } + } +} + +data class Rotation2(override val sensorId: Int = 1, val rotation: Quaternion = Quaternion.IDENTITY) : SensorSpecificPacket { + companion object { + fun read(src: Source) = Rotation2(1, src.readSafeQuat()) + } +} + +data class RotationData( + override val sensorId: Int = 0, + val dataType: Int = 0, + val rotation: Quaternion = Quaternion.IDENTITY, + val calibrationInfo: Int = 0, +) : SensorSpecificPacket { + companion object { + fun read(src: Source): RotationData = with(src) { + val id = readU8() + val type = readU8() + val rot = readSafeQuat() + val calib = if (remaining > 0) readU8() else 0 + return RotationData(id, type, rot, calib) + } + } +} + +data class MagnetometerAccuracy(override val sensorId: Int = 0, val accuracy: Float = 0f) : SensorSpecificPacket { + companion object { + fun read(src: Source) = MagnetometerAccuracy(src.readU8(), src.readSafeFloat()) + } +} + +data class SignalStrength(override val sensorId: Int = 0, val signal: Int = 0) : SensorSpecificPacket { + companion object { + fun read(src: Source): SignalStrength { + val id = src.readU8() + val sig = src.readByte().toInt() + return SignalStrength(id, sig) + } + } +} + +data class Temperature(override val sensorId: Int = 0, val temp: Float = 0f) : SensorSpecificPacket { + companion object { + fun read(src: Source): Temperature { + val id = src.readU8() + val t = if (src.remaining >= 4) src.readSafeFloat() else 0f + return Temperature(id, t) + } + } +} + +data class UserActionPacket(val type: Int = 0) : UDPPacket { + companion object { + fun read(src: Source) = UserActionPacket(src.readU8()) + } +} + +data class FeatureFlags(val firmwareFeatures: FirmwareFeatures = FirmwareFeatures()) : UDPPacket { + override fun write(dst: Sink) { + dst.write(ServerFeatureFlags.packed) + } + + companion object { + fun read(src: Source) = FeatureFlags( + FirmwareFeatures.from( + length = src.remaining.toInt(), + received = ByteBuffer.wrap(src.readByteArray()), + ), + ) + } +} + +data class RotationAndAccel( + override val sensorId: Int = 0, + val rotation: Quaternion = Quaternion.IDENTITY, + val acceleration: Vector3 = Vector3.NULL, +) : SensorSpecificPacket { + companion object { + fun read(src: Source): RotationAndAccel { + val id = src.readU8() + val scaleR = 1f / 32768f + val x = src.readShort() * scaleR + val y = src.readShort() * scaleR + val z = src.readShort() * scaleR + val w = src.readShort() * scaleR + val scaleA = 1f / 128f + val accel = Vector3(src.readShort() * scaleA, src.readShort() * scaleA, src.readShort() * scaleA) + return RotationAndAccel(id, Quaternion(w, x, y, z).unit(), accel) + } + } +} + +data class AckConfigChange(override val sensorId: Int = 0, val configType: UShort = 0u) : SensorSpecificPacket { + companion object { + fun read(src: Source) = AckConfigChange(src.readU8(), src.readShort().toUShort()) + } +} + +enum class SensorConfigType(val id: UShort) { + MAGNETOMETER(1u), + ; + + companion object { + private val byId = entries.associateBy { it.id } + fun fromId(id: UShort) = byId[id] + } +} + +data class SetConfigFlag(override val sensorId: Int = 255, val configType: SensorConfigType, val state: Boolean = false) : SensorSpecificPacket { + override fun write(dst: Sink) { + dst.writeUByte(sensorId.toUByte()) + dst.writeShort(configType.id.toShort()) + dst.writeUByte(if (state) 1u else 0u) + } +} + +data class FlexData(override val sensorId: Int = 0, val flexData: Float = 0f) : SensorSpecificPacket { + companion object { + fun read(src: Source) = FlexData(src.readU8(), src.readSafeFloat()) + } +} + +data class PositionPacket(override val sensorId: Int = 0, val position: Vector3 = Vector3.NULL) : SensorSpecificPacket { + companion object { + fun read(src: Source) = PositionPacket(src.readU8(), Vector3(src.readSafeFloat(), src.readSafeFloat(), src.readSafeFloat())) + } +} + +data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = 0) : UDPPacket { + override fun write(dst: Sink) { + dst.writeUByte(targetProtocol.toUByte()) + dst.writeUByte(targetVersion.toUByte()) + } + companion object { + fun read(src: Source) = ProtocolChange(src.readU8(), src.readU8()) + } +} + +fun readPacket(type: PacketType, src: Source): UDPPacket = when (type) { + PacketType.HEARTBEAT -> Heartbeat + PacketType.HANDSHAKE -> Handshake.read(src) + PacketType.ROTATION -> Rotation.read(src) + PacketType.ACCEL -> Accel.read(src) + PacketType.PING_PONG -> PingPong.read(src) + PacketType.SERIAL -> Serial.read(src) + PacketType.BATTERY_LEVEL -> BatteryLevel.read(src) + PacketType.TAP -> Tap.read(src) + PacketType.ERROR -> ErrorPacket.read(src) + PacketType.SENSOR_INFO -> SensorInfo.read(src) + PacketType.ROTATION_2 -> Rotation2.read(src) + PacketType.ROTATION_DATA -> RotationData.read(src) + PacketType.MAGNETOMETER_ACCURACY -> MagnetometerAccuracy.read(src) + PacketType.SIGNAL_STRENGTH -> SignalStrength.read(src) + PacketType.TEMPERATURE -> Temperature.read(src) + PacketType.USER_ACTION -> UserActionPacket.read(src) + PacketType.FEATURE_FLAGS -> FeatureFlags.read(src) + PacketType.ROTATION_AND_ACCEL -> RotationAndAccel.read(src) + PacketType.ACK_CONFIG_CHANGE -> AckConfigChange.read(src) + PacketType.FLEX_DATA -> FlexData.read(src) + PacketType.POSITION -> PositionPacket.read(src) + PacketType.PACKET_BUNDLE -> PacketBundle.read(src) + PacketType.PACKET_BUNDLE_COMPACT -> PacketBundleCompact.read(src) + PacketType.PROTOCOL_CHANGE -> ProtocolChange.read(src) + else -> error("Inbound support not implemented for ${type::class.simpleName}") +} + +fun writePacket(dst: Sink, packet: UDPPacket) { + val type = when (packet) { + is Heartbeat -> PacketType.HEARTBEAT + is Handshake -> PacketType.HANDSHAKE + is PingPong -> PacketType.PING_PONG + is SetConfigFlag -> PacketType.SET_CONFIG_FLAG + is ProtocolChange -> PacketType.PROTOCOL_CHANGE + is FeatureFlags -> PacketType.FEATURE_FLAGS + else -> error("Outbound support not implemented for ${packet::class.simpleName}") + } + + if (type != PacketType.HANDSHAKE) { + dst.writeInt(type.id) + dst.writeLong(0) + } + packet.write(dst) +} + +data class PacketEvent( + val data: T, + // Packet number is optional for the inner packets of a bundle packet + val packetNumber: Long?, +) + +typealias UDPPacketDispatcher = EventDispatcher> + +@Suppress("UNCHECKED_CAST") +inline fun UDPPacketDispatcher.onPacket(crossinline callback: suspend (PacketEvent) -> Unit) { + register(T::class) { callback(it as PacketEvent) } +} diff --git a/server/core/src/main/java/dev/slimevr/udp/server.kt b/server/core/src/main/java/dev/slimevr/udp/server.kt new file mode 100644 index 0000000000..be5a747a40 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/udp/server.kt @@ -0,0 +1,105 @@ +package dev.slimevr.udp + +import dev.slimevr.AppContextProvider +import dev.slimevr.AppLogger +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.io.Buffer +import java.net.DatagramPacket +import java.net.DatagramSocket +import kotlin.time.measureTime + +data class UdpServerState( + val connections: Map, +) + +sealed interface UdpServerActions { + data class ConnectionAdded(val ip: String, val conn: UDPConnection) : UdpServerActions + data class ConnectionRemoved(val ip: String) : UdpServerActions +} + +typealias UdpServerContext = Context +typealias UdpServerBehaviour = Behaviour + +object UdpServerBaseBehaviour : UdpServerBehaviour { + override fun reduce(state: UdpServerState, action: UdpServerActions) = when (action) { + is UdpServerActions.ConnectionAdded -> state.copy(connections = state.connections + (action.ip to action.conn)) + is UdpServerActions.ConnectionRemoved -> state.copy(connections = state.connections - action.ip) + } +} + +class UdpServer(val context: UdpServerContext) { + fun startObserving() = context.observeAll(this) + + fun findConnectionForDevice(deviceId: Int): UDPConnection? = context.state.value.connections.values.find { conn -> + conn.context.state.value.deviceId == deviceId + } + + fun startReceiving(appContext: AppContextProvider, scope: CoroutineScope) { + scope.launch { + val port = appContext.config.settings.context.state.value.data.trackerPort + val socket = withContext(Dispatchers.IO) { DatagramSocket(port) } + val recvBuffer = ByteArray(2048) + val recvPacket = DatagramPacket(recvBuffer, recvBuffer.size) + + launch(Dispatchers.IO) { + while (isActive) { + socket.receive(recvPacket) + val took = measureTime { + val src = Buffer() + src.write(recvBuffer, 0, recvPacket.length) + + val packetId = src.readInt() + val packetNumber = src.readLong() + val type = PacketType.fromId(packetId) ?: return@measureTime + val packetData = readPacket(type, src) + + val ip = recvPacket.address.hostAddress + val port = recvPacket.port + val conn = context.state.value.connections[ip] + + val event = PacketEvent(data = packetData, packetNumber = packetNumber) + + if (conn != null) { + conn.packetChannel.trySend(event) + } else { + val newConn = UDPConnection.create( + id = ip, + remoteIp = ip, + remotePort = port, + socket = socket, + appContext = appContext, + scope = scope, + ) + context.dispatch(UdpServerActions.ConnectionAdded(ip, newConn)) + newConn.packetChannel.trySend(event) + } + } + if (took.inWholeMilliseconds > 2) { + AppLogger.udp.warn("Packet processing took too long ${took.inWholeMilliseconds}") + } + } + } + } + } + + companion object { + val INITIAL_STATE = UdpServerState(connections = emptyMap()) + + fun create(scope: CoroutineScope): UdpServer { + val context = Context.create( + initialState = INITIAL_STATE, + scope = scope, + behaviours = listOf(UdpServerBaseBehaviour), + ) + val server = UdpServer(context) + server.startObserving() + return server + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/util/InterpolationHandler.kt b/server/core/src/main/java/dev/slimevr/util/InterpolationHandler.kt deleted file mode 100644 index 1be4995499..0000000000 --- a/server/core/src/main/java/dev/slimevr/util/InterpolationHandler.kt +++ /dev/null @@ -1,102 +0,0 @@ -package dev.slimevr.util - -import io.eiren.math.FloatMath.animateEase -import io.github.axisangles.ktmath.Quaternion - -class InterpolationHandler { - /** - * The starting rotation of this movement. - */ - var start = Quaternion.IDENTITY - private set - - /** - * The ending rotation of this movement. - */ - var end = Quaternion.IDENTITY - private set - - /** - * The time left for the current movement. - */ - var remainingTime = 0f - private set - - /** - * The total interval of interpolation for this movement. - */ - var totalTime = 0f - private set - - /** - * The current interpolated value for this tick. - */ - var curRotation = Quaternion.IDENTITY - private set - - /** - * Starts an interpolation from [start] to [end] over [interval] seconds. - * @param start The starting rotation to interpolate from. - * @param end The ending rotation to interpolate to. - * @param interval The amount of time that the interpolation will take. - * @param shortestDistance Whether the interpolation will take the shortest path or - * take the quaternion space path between [start] and [end]. - */ - @Synchronized - fun interpolate( - start: Quaternion = curRotation, - end: Quaternion, - interval: Float, - shortestDistance: Boolean = true, - ) { - this.totalTime = interval - remainingTime = interval - - this.start = if (shortestDistance) { - start.twinNearest(end) - } else { - start - } - this.end = end - - // The current state is at the start - // TODO: Maybe handle a mid-interval interpolation swap? - curRotation = this.start - } - - /** - * The main ticking function, computes [curRotation] for each tick and reduces - * [remainingTime] by [delta]. - * @param delta The time in seconds between the last time [tick] was run and the - * current tick. - */ - @Synchronized - fun tick(delta: Float) { - if (remainingTime > 0f) { - remainingTime -= delta - - // If we still need to interpolate after the delta change - if (remainingTime > 0f) { - // Remaining time decreases to 0, so the interpolation is reversed - curRotation = end.interpR( - start, - animateEase(remainingTime / totalTime), - ) - } else { - remainingTime = 0f - curRotation = end - } - } - } - - @Synchronized - fun reset() { - remainingTime = 0f - totalTime = 0f - - start = Quaternion.IDENTITY - end = Quaternion.IDENTITY - - curRotation = Quaternion.IDENTITY - } -} diff --git a/server/core/src/main/java/dev/slimevr/util/TickReducer.kt b/server/core/src/main/java/dev/slimevr/util/TickReducer.kt deleted file mode 100644 index b4cb2879d3..0000000000 --- a/server/core/src/main/java/dev/slimevr/util/TickReducer.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.slimevr.util - -class TickReducer( - /** - * The function to run every tick interval. - */ - private val onTick: (delta: Float) -> Unit, - /** - * The tick interval in seconds. - */ - var interval: Float, - /** - * The amount of time in seconds that a tick can fire early. - */ - var resolution: Float = 0f, -) { - /** - * The amount of time in seconds since the last tick. - */ - private var tickDelta = 0f - - /** - * The offset in timing for the next frame to approximate a tick rate with low - * resolution ticking. - */ - private var tickOffset = 0f - - /** - * The main ticking function to be run at a fast tick rate. Runs [onTick] at the - * specified [interval] and [resolution]. - * @param delta The time in seconds between the last time [tick] was run and the - * current tick. - */ - @Synchronized - fun tick(delta: Float) { - // Update tick delta from delta - tickDelta += delta - - // If the next tick time is not within the given resolution - if (tickDelta + tickOffset + resolution < interval) return - - // Run tick, providing the delta time - onTick(tickDelta) - - // Reset tick timing including an offset to compensate for inaccuracy - // Define a maximum for the offset to prevent double ticking - tickOffset = (tickDelta - interval).coerceAtMost(interval / 2f) - tickDelta = 0f - } - - /** - * Resets the tick timing. - */ - @Synchronized - fun reset() { - tickDelta = 0f - tickOffset = 0f - } -} diff --git a/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java b/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java deleted file mode 100644 index 37d6d7180a..0000000000 --- a/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.slimevr.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -@Retention(value = RetentionPolicy.SOURCE) -public @interface VRServerThread { - -} diff --git a/server/core/src/main/java/dev/slimevr/vrchat/behaviours.kt b/server/core/src/main/java/dev/slimevr/vrchat/behaviours.kt new file mode 100644 index 0000000000..848ad692b1 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/vrchat/behaviours.kt @@ -0,0 +1,33 @@ +package dev.slimevr.vrchat + +import dev.slimevr.config.SettingsActions +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +object DefaultVRCConfigBehaviour : VRCConfigBehaviour { + override fun reduce(state: VRCConfigState, action: VRCConfigActions) = when (action) { + is VRCConfigActions.UpdateValues -> state.copy(currentValues = action.values) + + is VRCConfigActions.ToggleMutedWarning -> { + if (action.key !in VRC_VALID_KEYS) { + state + } else if (action.key in state.mutedWarnings) { + state.copy(mutedWarnings = state.mutedWarnings - action.key) + } else { + state.copy(mutedWarnings = state.mutedWarnings + action.key) + } + } + } + + override fun observe(receiver: VRCConfigManager) { + receiver.context.state.map { it.mutedWarnings }.distinctUntilChanged().onEach { warnings -> + receiver.config.settings.context.dispatch( + SettingsActions.Update { + copy(mutedVRCWarnings = warnings) + }, + ) + }.launchIn(receiver.context.scope) + } +} diff --git a/server/core/src/main/java/dev/slimevr/vrchat/module.kt b/server/core/src/main/java/dev/slimevr/vrchat/module.kt new file mode 100644 index 0000000000..6acf69962c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/vrchat/module.kt @@ -0,0 +1,129 @@ +package dev.slimevr.vrchat + +import dev.slimevr.VRServer +import dev.slimevr.config.AppConfig +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import solarxr_protocol.datatypes.BodyPart +import solarxr_protocol.rpc.VRCAvatarMeasurementType +import solarxr_protocol.rpc.VRCConfigRecommendedValues +import solarxr_protocol.rpc.VRCConfigValidity +import solarxr_protocol.rpc.VRCConfigValues +import solarxr_protocol.rpc.VRCSpineMode +import solarxr_protocol.rpc.VRCTrackerModel +import kotlin.math.abs + +val VRC_VALID_KEYS = setOf( + "legacyModeOk", + "shoulderTrackingOk", + "shoulderWidthCompensationOk", + "userHeightOk", + "calibrationRangeOk", + "calibrationVisualsOk", + "trackerModelOk", + "spineModeOk", + "avatarMeasurementTypeOk", +) + +data class VRCConfigState( + val currentValues: VRCConfigValues?, + val isSupported: Boolean, + val mutedWarnings: List, +) + +sealed interface VRCConfigActions { + data class UpdateValues(val values: VRCConfigValues?) : VRCConfigActions + data class ToggleMutedWarning(val key: String) : VRCConfigActions +} + +typealias VRCConfigContext = Context +typealias VRCConfigBehaviour = Behaviour + +class VRCConfigManager( + val context: VRCConfigContext, + val config: AppConfig, +) { + fun startObserving() = context.observeAll(this) + + companion object { + fun create( + config: AppConfig, + scope: CoroutineScope, + isSupported: Boolean, + values: Flow, + ): VRCConfigManager { + val behaviours = listOf(DefaultVRCConfigBehaviour) + + val context = Context.create( + initialState = VRCConfigState( + currentValues = null, + isSupported = isSupported, + mutedWarnings = listOf(), + ), + scope = scope, + behaviours = behaviours, + ) + + scope.launch { + values.collect { context.dispatch(VRCConfigActions.UpdateValues(it)) } + } + + return VRCConfigManager(context = context, config = config) + } + } +} + +fun computeRecommendedValues(server: VRServer, userHeight: Double): VRCConfigRecommendedValues { + val trackers = server.context.state.value.trackers.values + + fun hasTracker(bodyPart: BodyPart) = trackers.any { it.context.state.value.bodyPart == bodyPart } + + val hasLeftHandWithPosition = hasTracker(BodyPart.LEFT_HAND) + val hasRightHandWithPosition = hasTracker(BodyPart.RIGHT_HAND) + + val isMissingAnArmTracker = !hasTracker(BodyPart.LEFT_LOWER_ARM) || + !hasTracker(BodyPart.RIGHT_LOWER_ARM) || + !hasTracker(BodyPart.LEFT_UPPER_ARM) || + !hasTracker(BodyPart.RIGHT_UPPER_ARM) + val isMissingAShoulderTracker = !hasTracker(BodyPart.LEFT_SHOULDER) || + !hasTracker(BodyPart.RIGHT_SHOULDER) + + return VRCConfigRecommendedValues( + legacyMode = false, + shoulderTrackingDisabled = + (!hasLeftHandWithPosition || !hasRightHandWithPosition || isMissingAnArmTracker) && + ((hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker), + userHeight = userHeight.toFloat(), + calibrationRange = 0.2f, + trackerModel = VRCTrackerModel.AXIS, + spineMode = listOf(VRCSpineMode.LOCK_HIP, VRCSpineMode.LOCK_HEAD), + calibrationVisuals = true, + avatarMeasurementType = VRCAvatarMeasurementType.HEIGHT, + shoulderWidthCompensation = true, + ) +} + +fun isVRCConfigValid(validity: VRCConfigValidity, mutedWarnings: List): Boolean = (validity.legacyModeOk || "legacyModeOk" in mutedWarnings) && + (validity.shoulderTrackingOk || "shoulderTrackingOk" in mutedWarnings) && + (validity.shoulderWidthCompensationOk || "shoulderWidthCompensationOk" in mutedWarnings) && + (validity.userHeightOk || "userHeightOk" in mutedWarnings) && + (validity.calibrationRangeOk || "calibrationRangeOk" in mutedWarnings) && + (validity.calibrationVisualsOk || "calibrationVisualsOk" in mutedWarnings) && + (validity.trackerModelOk || "trackerModelOk" in mutedWarnings) && + (validity.spineModeOk || "spineModeOk" in mutedWarnings) && + (validity.avatarMeasurementTypeOk || "avatarMeasurementTypeOk" in mutedWarnings) + +fun computeValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity = VRCConfigValidity( + legacyModeOk = values.legacyMode == recommended.legacyMode, + shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled, + spineModeOk = recommended.spineMode?.contains(values.spineMode) == true, + trackerModelOk = values.trackerModel == recommended.trackerModel, + calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1f, + userHeightOk = abs(recommended.userHeight - values.userHeight) < 0.1f, + calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals, + avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType, + shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation, +) diff --git a/server/core/src/main/java/dev/slimevr/vrserver.kt b/server/core/src/main/java/dev/slimevr/vrserver.kt new file mode 100644 index 0000000000..f067ed509c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/vrserver.kt @@ -0,0 +1,66 @@ +package dev.slimevr + +import dev.slimevr.context.Behaviour +import dev.slimevr.context.Context +import dev.slimevr.device.Device +import dev.slimevr.driver.DriverBridge +import dev.slimevr.feeder.FeederBridge +import dev.slimevr.solarxr.SolarXRBridge +import dev.slimevr.tracker.Tracker +import kotlinx.coroutines.CoroutineScope +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.incrementAndFetch + +data class VRServerState( + val trackers: Map, + val devices: Map, + val drivers: Map, + val feeders: Map, + val solarxr: Map, +) + +sealed interface VRServerActions { + data class NewTracker(val trackerId: Int, val context: Tracker) : VRServerActions + data class NewDevice(val deviceId: Int, val context: Device) : VRServerActions + data class DriverConnected(val bridge: DriverBridge) : VRServerActions + data class DriverDisconnected(val bridgeId: Int) : VRServerActions + data class FeederConnected(val bridge: FeederBridge) : VRServerActions + data class FeederDisconnected(val bridgeId: Int) : VRServerActions + data class SolarXRConnected(val connection: SolarXRBridge) : VRServerActions + data class SolarXRDisconnected(val connectionId: Int) : VRServerActions +} + +typealias VRServerContext = Context +typealias VRServerBehaviour = Behaviour + +@OptIn(ExperimentalAtomicApi::class) +class VRServer( + val context: VRServerContext, +) { + private val handleCounter: AtomicInt = AtomicInt(0) + + fun nextHandle() = handleCounter.incrementAndFetch() + fun getTracker(id: Int) = context.state.value.trackers[id] + fun getDevice(id: Int) = context.state.value.devices[id] + + companion object { + fun create(scope: CoroutineScope): VRServer { + val behaviours = listOf(BaseBehaviour) + val context = Context.create( + initialState = VRServerState( + trackers = emptyMap(), + devices = emptyMap(), + drivers = emptyMap(), + feeders = emptyMap(), + solarxr = emptyMap(), + ), + scope = scope, + behaviours = behaviours, + ) + val server = VRServer(context = context) + behaviours.forEach { it.observe(server) } + return server + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt deleted file mode 100644 index 63ea4eecd7..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt +++ /dev/null @@ -1,239 +0,0 @@ -package dev.slimevr.websocketapi - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import dev.slimevr.VRServer -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.VRServer.Companion.instance -import dev.slimevr.bridge.Bridge -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import org.java_websocket.WebSocket -import org.java_websocket.handshake.ClientHandshake -import java.util.* -import java.util.concurrent.atomic.AtomicBoolean - -class WebSocketVRBridge( - computedTrackers: List, - server: VRServer, -) : WebsocketAPI(server, server.protocolAPI), - Bridge { - private val computedTrackers: List = FastList(computedTrackers) - private val internalTrackers: MutableList = FastList(computedTrackers.size) - private val newHMDData = AtomicBoolean(false) - private val mapper = ObjectMapper() - private val internalHMDTracker = Tracker( - null, - 0, - "internal://HMD", - "internal://HMD", - TrackerPosition.HEAD, - hasPosition = true, - hasRotation = true, - isInternal = true, - isComputed = true, - ) - private var hmdTracker: Tracker? = null - - init { - for (t in computedTrackers) { - val ct = Tracker( - null, - t.id, - "internal://${t.name}", - "internal://${t.name}", - t.trackerPosition, - hasPosition = true, - hasRotation = true, - userEditable = true, - isInternal = true, - ) - ct.status = TrackerStatus.OK - internalTrackers.add(ct) - } - } - - override fun dataRead() { - if (newHMDData.compareAndSet(true, false)) { - if (hmdTracker == null) { - // Create HMD for websocket - val hmdDevice = server.deviceManager - .createDevice("WebSocketVRBridge", null, null) - hmdTracker = Tracker( - null, - getNextLocalTrackerId(), - "WebSocketHMD", - "WebSocketHMD", - TrackerPosition.HEAD, - hasPosition = true, - hasRotation = true, - userEditable = true, - isComputed = true, - ) - hmdTracker!!.status = TrackerStatus.OK - hmdDevice.trackers[0] = hmdTracker!! - server.registerTracker(hmdTracker!!) - } - - hmdTracker!!.position = internalHMDTracker.position - hmdTracker!!.setRotation(internalHMDTracker.getRotation()) - hmdTracker!!.dataTick() - } - } - - override fun dataWrite() { - for (i in computedTrackers.indices) { - val t = computedTrackers[i] - val it = internalTrackers[i] - if (t.hasPosition) it.position = t.position - if (t.hasRotation) it.setRotation(t.getRotation()) - } - } - - override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { - super.onOpen(conn, handshake) - // Register trackers - for (i in internalTrackers.indices) { - val message = mapper.nodeFactory.objectNode() - message.put("type", "config") - message.put("tracker_id", "SlimeVR Tracker " + (i + 1)) - message - .put( - "location", - computedTrackers[i] - .trackerPosition - ?.trackerRole - ?.name - ?.lowercase(Locale.getDefault()), - ) - message.put("tracker_type", message["location"].asText()) - conn.send(message.toString()) - } - } - - override fun onMessage(conn: WebSocket, message: String) { - // LogManager.info(message); - try { - val json = mapper.readTree(message) as ObjectNode - if (json.has("type")) { - when (json["type"].asText()) { - "pos" -> { - parsePosition(json, conn) - return - } - - "action" -> { - parseAction(json, conn) - return - } - - // TODO Ignore it for now, it should only register HMD in our test case with id 0 - "config" -> { - LogManager.info("[WebSocket] Config received: $json") - return - } - } - } - LogManager - .warning( - "[WebSocket] Unrecognized message from " + - conn.remoteSocketAddress.address.hostAddress + - ": " + - message, - ) - } catch (e: Exception) { - LogManager - .severe( - "[WebSocket] Exception parsing message from " + - conn.remoteSocketAddress.address.hostAddress + - ". Message: " + - message, - e, - ) - } - } - - private fun parsePosition(json: ObjectNode, conn: WebSocket) { - if (json["tracker_id"].asInt() == 0) { - // Read HMD information - internalHMDTracker - .position = Vector3( - json["x"].asDouble().toFloat(), - json["y"].asDouble().toFloat() + 0.2f, - json["z"].asDouble().toFloat(), - ) - // TODO Wtf is this hack? VRWorkout issue? - internalHMDTracker - .setRotation( - Quaternion( - json["qw"].asDouble().toFloat(), - json["qx"].asDouble().toFloat(), - json["qy"].asDouble().toFloat(), - json["qz"].asDouble().toFloat(), - ), - ) - internalHMDTracker.dataTick() - newHMDData.set(true) - - // Send tracker info in reply - for (i in internalTrackers.indices) { - val message = mapper.nodeFactory.objectNode() - message.put("type", "pos") - message.put("src", "full") - message.put("tracker_id", "SlimeVR Tracker ${i + 1}") - - val t = internalTrackers[i] - message.put("x", t.position.x) - message.put("y", t.position.y) - message.put("z", t.position.z) - message.put("qx", t.getRotation().x) - message.put("qy", t.getRotation().y) - message.put("qz", t.getRotation().z) - message.put("qw", t.getRotation().w) - - conn.send(message.toString()) - } - } - } - - private fun parseAction(json: ObjectNode, conn: WebSocket) { - when (json["name"].asText()) { - "calibrate" -> instance.resetTrackersYaw(RESET_SOURCE_NAME) - "full_calibrate" -> instance.resetTrackersFull(RESET_SOURCE_NAME) - "mounting_calibrate" -> instance.resetTrackersMounting(RESET_SOURCE_NAME) - "mounting_clear" -> instance.clearTrackersMounting(RESET_SOURCE_NAME) - "toggle_pause_tracking" -> instance.togglePauseTracking(RESET_SOURCE_NAME) - } - } - - override fun onStart() { - LogManager.info("[WebSocket] Web Socket VR Bridge started on port $port") - connectionLostTimeout = 0 - connectionLostTimeout = 1 - // This has to be removed for Android - // (keepalive did not work for me @mgschwan) - } - - override fun addSharedTracker(tracker: Tracker?) { - // TODO Auto-generated method stub - } - - override fun removeSharedTracker(tracker: Tracker?) { - // TODO Auto-generated method stub - } - - override fun startBridge() { - start() - } - - override fun isConnected(): Boolean = super.getConnections().isNotEmpty() - - companion object { - private const val RESET_SOURCE_NAME = "WebSocketVRBridge" - } -} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java deleted file mode 100644 index 0e3cf870e4..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java +++ /dev/null @@ -1,111 +0,0 @@ -package dev.slimevr.websocketapi; - -import dev.slimevr.VRServer; -import dev.slimevr.protocol.GenericConnection; -import dev.slimevr.protocol.ProtocolAPI; -import dev.slimevr.protocol.ProtocolAPIServer; -import io.eiren.util.logging.LogManager; -import org.java_websocket.WebSocket; -import org.java_websocket.drafts.Draft_6455; -import org.java_websocket.handshake.ClientHandshake; -import org.java_websocket.server.WebSocketServer; -import org.jetbrains.annotations.NotNull; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.Objects; -import java.util.stream.Stream; - - -public class WebsocketAPI extends WebSocketServer implements ProtocolAPIServer { - - public final VRServer server; - public final ProtocolAPI protocolAPI; - - public WebsocketAPI(VRServer server, ProtocolAPI protocolAPI) { - super(new InetSocketAddress(21110), Collections.singletonList(new Draft_6455())); - this.server = server; - this.protocolAPI = protocolAPI; - - this.protocolAPI.registerAPIServer(this); - setReuseAddr(true); - } - - @Override - public void onOpen(WebSocket conn, ClientHandshake handshake) { - LogManager - .info( - "[WebSocketAPI] New connection from: " - + conn.getRemoteSocketAddress().getAddress().getHostAddress() - ); - conn.setAttachment(new WebsocketConnection(conn)); - } - - /** - * Helper function to get the string of the `conn` while handling `null` - */ - protected static String connAddr(WebSocket conn) { - if (conn == null) { - return "null"; - } - var remote = conn.getRemoteSocketAddress(); - if (remote == null) { - return conn.toString(); - } - var addr = remote.getAddress(); - if (addr == null) { - return remote.toString(); - } - return addr.getHostAddress(); - } - - @Override - public void onClose(WebSocket conn, int code, String reason, boolean remote) { - LogManager - .info( - "[WebSocketAPI] Disconnected: " - + connAddr(conn) - + ", (" - + code - + ") " - + reason - + ". Remote: " - + remote - ); - } - - @Override - public void onMessage(WebSocket conn, String message) { - } - - @Override - public void onMessage(WebSocket conn, ByteBuffer message) { - var connection = conn.getAttachment(); - if (connection != null) - this.protocolAPI.onMessage(connection, message); - } - - @Override - public void onError(WebSocket conn, Exception ex) { - LogManager - .severe( - "[WebSocket] Exception on connection " + connAddr(conn), - ex - ); - } - - @Override - public void onStart() { - LogManager.info("[WebSocketAPI] Web Socket API started on port " + getPort()); - setConnectionLostTimeout(0); - } - - @Override - public @NotNull Stream getApiConnections() { - return this.getConnections().stream().map(conn -> { - var c = conn.getAttachment(); - return (GenericConnection) c; - }).filter(Objects::nonNull); - } -} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java deleted file mode 100644 index 73340b7443..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.slimevr.websocketapi; - -import dev.slimevr.protocol.ConnectionContext; -import dev.slimevr.protocol.GenericConnection; -import org.java_websocket.WebSocket; -import org.java_websocket.exceptions.WebsocketNotConnectedException; - -import java.nio.ByteBuffer; -import java.util.UUID; - - -public class WebsocketConnection implements GenericConnection { - - public final ConnectionContext context; - public final WebSocket conn; - public UUID id; - - public WebsocketConnection(WebSocket conn) { - this.context = new ConnectionContext(); - this.conn = conn; - this.id = UUID.randomUUID(); - } - - @Override - public ConnectionContext getContext() { - return this.context; - } - - @Override - public void send(ByteBuffer bytes) { - if (this.conn.isOpen()) { - try { - this.conn.send(bytes.slice()); - } catch (WebsocketNotConnectedException ignored) { - // Race condition if it closes between our check and sending - } - } - } - - @Override - public UUID getConnectionId() { - return id; - } -} diff --git a/server/core/src/main/java/io/eiren/math/FloatMath.kt b/server/core/src/main/java/io/eiren/math/FloatMath.kt deleted file mode 100644 index 66e50faabb..0000000000 --- a/server/core/src/main/java/io/eiren/math/FloatMath.kt +++ /dev/null @@ -1,435 +0,0 @@ -package io.eiren.math - -import com.jme3.math.FastMath -import com.jme3.math.FastMath.normalize -import io.github.axisangles.ktmath.Vector3 -import kotlin.math.* - -object FloatMath { - const val PI: Float = Math.PI.toFloat() - const val TWO_PI: Float = (Math.PI * 2).toFloat() - const val ANGLE_EPSILON: Float = 0.028f // in degrees (float - - // epsilon for sin/cos) - val ANGLE_EPSILON_RAD: Float = toRad(ANGLE_EPSILON) - - const val ZERO_TOLERANCE_F: Float = FastMath.ZERO_TOLERANCE - const val ZERO_TOLERANCE_D: Double = 0.0001 - - val SQRT_TWO: Float = kotlin.math.sqrt(2.0).toFloat() - val INV_SQRT_TWO: Float = 1f / SQRT_TWO - val SQRT_THREE: Float = kotlin.math.sqrt(3.0).toFloat() - val INV_SQRT_THREE: Float = 1f / SQRT_THREE - const val TWO_FPI: Float = PI * 2 - - const val SIN_75_DEG: Float = 0.965926f - const val SIN_60_DEG: Float = 0.866025f - const val SIN_45_DEG: Float = 0.707107f - const val SIN_30_DEG: Float = 0.5f - const val SIN_15_DEG: Float = 0.258819f - - const val COS_75_DEG: Float = 0.258819f - const val COS_60_DEG: Float = 0.5f - const val COS_45_DEG: Float = 0.707107f - const val COS_30_DEG: Float = 0.866025f - const val COS_15_DEG: Float = 0.965926f - - const val TEN_BITS: Int = (0.inv() shl 10).inv() - const val TENTH_BIT: Int = 1 shl 10 - const val TEN_BITS_MAX: Int = (0.inv() shl 9).inv() - const val TEN_BITS_MAX_UNSIGNED: Int = (0.inv() shl 10).inv() - const val TWO_BITS: Int = (0.inv() shl 2).inv() - const val SECOND_BIT: Int = 1 shl 2 - const val TWO_BITS_MAX: Int = (0.inv() shl 1).inv() - const val TWO_BITS_MAX_UNSIGNED: Int = (0.inv() shl 2).inv() - - fun roundIfZero(x: Float): Float = if (kotlin.math.abs(x.toDouble()) < ZERO_TOLERANCE_F) 0.0f else x - - fun equalsToZero(x: Float): Boolean = kotlin.math.abs(x.toDouble()) < ZERO_TOLERANCE_F - - fun lessThanZero(x: Float): Boolean = (x < -ZERO_TOLERANCE_F) - - fun lessOrEqualsToZero(x: Float): Boolean = (x < ZERO_TOLERANCE_F) - - fun greaterThanZero(x: Float): Boolean = (x > ZERO_TOLERANCE_F) - - fun greaterOrEqualsToZero(x: Float): Boolean = (x > -ZERO_TOLERANCE_F) - - fun equalsToZero(x: Float, epsilon: Float): Boolean = kotlin.math.abs(x.toDouble()) < epsilon - - fun equalsWithEpsilon(x: Float, y: Float): Boolean = kotlin.math.abs((x - y).toDouble()) < ZERO_TOLERANCE_F - - fun equalsWithEpsilon(x: Float, y: Float, epsilon: Float): Boolean = kotlin.math.abs((x - y).toDouble()) < epsilon - - fun lessWithEpsilon(x: Float, y: Float): Boolean = (x < y - ZERO_TOLERANCE_F) - - fun lessOrEqualsWithEpsilon(x: Float, y: Float): Boolean = (x < y + ZERO_TOLERANCE_F) - - fun lessWithEpsilon(x: Float, y: Float, epsilon: Float): Boolean = (x < y - epsilon) - - fun lessOrEqualsWithEpsilon(x: Float, y: Float, epsilon: Float): Boolean = (x < y + epsilon) - - fun greaterWithEpsilon(x: Float, y: Float): Boolean = (x > y + ZERO_TOLERANCE_F) - - fun greaterOrEqualsWithEpsilon(x: Float, y: Float): Boolean = (x > y - ZERO_TOLERANCE_F) - - fun greaterWithEpsilon(x: Float, y: Float, epsilon: Float): Boolean = (x > y + epsilon) - - fun greaterOrEqualsWithEpsilon(x: Float, y: Float, epsilon: Float): Boolean = (x > y - epsilon) - - fun roundIfZero(x: Double): Double = if (kotlin.math.abs(x) < ZERO_TOLERANCE_D) 0.0 else x - - fun equalsToZero(x: Double): Boolean = kotlin.math.abs(x) < ZERO_TOLERANCE_D - - fun equalsWithEpsilon(x: Double, y: Double): Boolean = kotlin.math.abs(x - y) < ZERO_TOLERANCE_D - - fun lessWithEpsilon(x: Double, y: Double): Boolean = (x < y - ZERO_TOLERANCE_D) - - fun lessOrEqualsWithEpsilon(x: Double, y: Double): Boolean = (x < y + ZERO_TOLERANCE_D) - - fun greaterWithEpsilon(x: Double, y: Double): Boolean = (x > y + ZERO_TOLERANCE_D) - - fun greaterOrEqualsWithEpsilon(x: Double, y: Double): Boolean = (x > y - ZERO_TOLERANCE_D) - - fun toDegrees(angrad: Float): Float = angrad * 180.0f / PI - - fun toRad(deg: Float): Float = deg / 180.0f * PI - - fun radEqual(angle1: Float, angle2: Float): Boolean { - val diff = clampRad(angle1 - angle2) - return kotlin.math.abs(diff.toDouble()) < ANGLE_EPSILON_RAD - } - - fun degreesEqual(angle1: Float, angle2: Float): Boolean { - val diff = clampDegrees(angle1 - angle2) - return kotlin.math.abs(diff.toDouble()) < ANGLE_EPSILON - } - - @Deprecated( - "use {@link #normalizeRad(float)}", - ReplaceWith("normalizeRad(angle)", "io.eiren.math.FloatMath.normalizeRad"), - ) - fun clampRad(angle: Float): Float = normalizeRad(angle) - - fun normalizeRad(angle: Float): Float = normalize(angle, -FastMath.PI, FastMath.PI) - - @Deprecated( - "use {@link #normalizeDegrees(float)}", - ReplaceWith("normalizeDegrees(angle)", "io.eiren.math.FloatMath.normalizeDegrees"), - ) - fun clampDegrees(angle: Float): Float = normalizeDegrees(angle) - - fun normalizeDegrees(angle: Float): Float = normalize(angle, -180f, 180f) - - fun animateEase(t: Float): Float { - // Special case of Bezier interpolation (p0 = p1 = 0, p2 = p3 = 1) - return (3.0f - 2.0f * t) * t * t - } - - fun animateEaseIn(t: Float): Float = t * t - - /** - * Lineary remaps value from the source interval to the target interval. - * [details](https://en.wikipedia.org/wiki/Linear_interpolation) - */ - fun mapValue( - value: Float, - sourceStart: Float, - sourceEnd: Float, - targetStart: Float, - targetEnd: Float, - ): Float = ( - targetStart + - (value - sourceStart) * - (targetEnd - targetStart) / - (sourceEnd - sourceStart) - ) - - /** - * Clamps the given value and remaps to the target interval. - * - * - * Note the source interval values should be sorted. - */ - fun mapValueWithClampBefore( - value: Float, - sourceBottom: Float, - sourceTop: Float, - targetBottom: Float, - targetTop: Float, - ): Float = mapValue( - clamp(value, sourceBottom, sourceTop), - sourceBottom, - sourceTop, - targetBottom, - targetTop, - ) - - /** - * Remaps the given value to the target interval and clamps. - * - * - * Note the target interval values should be sorted. - */ - fun mapValueWithClampAfter( - value: Float, - sourceBottom: Float, - sourceTop: Float, - targetBottom: Float, - targetTop: Float, - ): Float = clamp( - mapValue(value, sourceBottom, sourceTop, targetBottom, targetTop), - targetBottom, - targetTop, - ) - - fun smoothstep(edge0: Float, edge1: Float, x: Float): Float { - // Scale, bias and saturate x to 0..1 range - var x = x - x = FastMath.clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f) - // Evaluate polynomial - return x * x * (3f - 2f * x) - } - - fun smootherstep(edge0: Float, edge1: Float, x: Float): Float { - // Scale, and clamp x to 0..1 range - var x = x - x = FastMath.clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f) - // Evaluate polynomial - return x * x * x * (x * (x * 6f - 15f) + 10f) - } - - /** - * Applies linear contrast (with clamping). - * - * @param t - input value in range (0..1) - * @param k - contrast factor in range (-1..1): - * - * * 1.0 - maximal contrast - * * **0.0** - bypass (returns input value) - * * -1.0 - minimal contrast (returns 0.5f for any input) - * - * @return contrasted value in range (0..1) - */ - fun contrastLinear(t: Float, k: Float): Float { - val x = 2f * t - 1f // -1..1 - val gamma = (1f + k) / (1f - k) - val f = FastMath.clamp(gamma * x, -1f, 1f) // -1..1 - return 0.5f * (f + 1f) // 0..1 - } - - /** - * Applies non-linear contrast by power function. - * - * @param t - input value in range (0..1) - * @param k - contrast factor in range (-1..1) exclusive: - * - * * 0.999 - maximal contrast - * * 0.0 - bypass (returns input value) - * * -0.999 - minimal contrast - * - * @return contrasted value in range (0..1) - */ - fun contrastPower(t: Float, k: Float): Float { - val x = 2f * t - 1f // -1..1 - val gamma = (1f - k) / (1f + k) - val f = sign(x) * abs(x).pow(gamma) // -1..1 - return 0.5f * (f + 1f) // 0..1 - } - - /** - * Applies non-linear contrast by square splines. - * - * @param t - input value in range (0..1) - * @param k - contrast factor in range (-1..1): - * - * * 1.0 - maximal contrast - * * 0.0 - bypass (returns input value) - * * -1.0 - minimal contrast - * - * @return contrasted value in range (0..1) - */ - fun contrastQuadricSpline(t: Float, k: Float): Float { - val x = 2f * t - 1f // -1..1 - val f = x * (1f + k * (1f - abs(x))) // -1..1 - return 0.5f * (f + 1f) // 0..1 - } - - /** - * Applies non-linear contrast by square splines inverted function. - * - * @param t - input value in range (0..1) - * @param k - contrast factor in range (-2..2): - * - * * 2.0 - maximal contrast - * * 0.0 - bypass (returns input value) - * * -2.0 - minimal contrast - * - * @return contrasted value in range (0..1) - */ - fun contrastInvertQuadricSpline(t: Float, k: Float): Float { - val x = 2f * t - 1f // -1..1 - val g = if (k > 0) { - sign(x) * sqrt(abs(x)) - 2f * x - } else { - sign(x) * (sqrt(1f - abs(x)) - 1f) - } - val f = (1f + k) * x + k * g // -1..1 - return 0.5f * (f + 1f) // 0..1 - } - - /** - * Applies non-linear contrast by cubic splines. - * - * @param t - input value in range (0..1) - * @param k - contrast factor in range (-1..1): - * - * * 1.0 - maximal contrast - * * 0.0 - bypass (returns input value) - * * -1.0 - minimal contrast - * - * @return contrasted value in range (0..1) - */ - fun contrastCubicSpline(t: Float, k: Float): Float { - val x = 2f * t - 1f // -1..1 - var f = x * (1f + abs(k) * (x * x - 1f)) - if (k < 0) f -= x * 3f * k * (1f - abs(x)) - return 0.5f * (f + 1f) // 0..1 - } - - fun fraction(f: Float): Float = f - f.toInt() - - fun fraction(d: Double): Double = d - d.toLong() - - fun distance(x0: Float, y0: Float, z0: Float, x1: Float, y1: Float, z1: Float): Float = distance(x1 - x0, y1 - y0, z1 - z0) - - fun distance(x: Float, y: Float, z: Float): Float = sqrt(sqrDistance(x, y, z)) - - fun sqrDistance(x: Float, y: Float, z: Float): Float = x * x + y * y + z * z - - fun distance(x: Float, y: Float): Float = sqrt(sqrDistance(x, y)) - - fun sqrDistance(x: Float, y: Float): Float = x * x + y * y - - fun sqrDistance(v: Vector3, x1: Float, y1: Float, z1: Float): Float = sqrDistance(x1 - v.x, y1 - v.y, z1 - v.z) - - fun sqrDistance(x0: Float, y0: Float, z0: Float, x1: Float, y1: Float, z1: Float): Float = sqrDistance(x1 - x0, y1 - y0, z1 - z0) - - /** - * The same as FastMath.clamp - */ - fun clamp(value: Float, min: Float, max: Float): Float = kotlin.math.max(min.toDouble(), kotlin.math.min(max.toDouble(), value.toDouble())).toFloat() - - fun int2101010RevToFloats(packedValue: Int, source: Vector3): Vector3 { - var x = source.x - var y = source.y - var z = source.z - - x = (packedValue and TEN_BITS_MAX).toFloat() - if ((packedValue and TENTH_BIT) != 0) x *= -1f - y = ((packedValue ushr 10) and TEN_BITS_MAX).toFloat() - if ((packedValue and (TENTH_BIT shl 10)) != 0) y *= -1f - z = ((packedValue ushr 20) and TEN_BITS_MAX).toFloat() - if ((packedValue and (TENTH_BIT shl 20)) != 0) z *= -1f - return Vector3(x, y, z) - } - - fun floatToInt210101Rev(values: Vector3): Int { - var store = 0 - store = store or ((values.x.toInt()) and TEN_BITS_MAX) - if (values.x < 0) store = store or TENTH_BIT - store = store or (((values.y.toInt()) and TEN_BITS_MAX) shl 10) - if (values.y < 0) store = store or (TENTH_BIT shl 10) - store = store or (((values.z.toInt()) and TEN_BITS_MAX) shl 20) - if (values.z < 0) store = store or (TENTH_BIT shl 20) - return store - } - - fun floatToInt210101RevNormalized(values: Vector3): Int { - var store = 0 - store = store or (((values.x * TEN_BITS).toInt()) and TEN_BITS_MAX) - if (values.x < 0) store = store or TENTH_BIT - store = store or ((((values.y * TEN_BITS).toInt()) and TEN_BITS_MAX) shl 10) - if (values.y < 0) store = store or (TENTH_BIT shl 10) - store = store or ((((values.z * TEN_BITS).toInt()) and TEN_BITS_MAX) shl 20) - if (values.z < 0) store = store or (TENTH_BIT shl 20) - return store - } - - fun floatToUnsignedInt210101Rev(values: Vector3): Int { - var store = 0 - store = store or ((values.x.toInt()) and TEN_BITS) - store = store or (((values.y.toInt()) and TEN_BITS) shl 10) - store = store or (((values.z.toInt()) and TEN_BITS) shl 20) - return store - } - - fun floatToUnsignedInt210101RevNormalized(values: Vector3): Int { - var store = 0 - store = store or (((values.x * TEN_BITS).toInt()) and TEN_BITS) - store = store or ((((values.y * TEN_BITS).toInt()) and TEN_BITS) shl 10) - store = store or ((((values.z * TEN_BITS).toInt()) and TEN_BITS) shl 20) - return store - } - - fun floatToInt210101Rev(x: Float, y: Float, z: Float): Int { - var store = 0 - store = store or ((x.toInt()) and TEN_BITS_MAX) - if (x < 0) store = store or TENTH_BIT - store = store or (((y.toInt()) and TEN_BITS_MAX) shl 10) - if (y < 0) store = store or (TENTH_BIT shl 10) - store = store or (((z.toInt()) and TEN_BITS_MAX) shl 20) - if (z < 0) store = store or (TENTH_BIT shl 20) - return store - } - - fun floatToUnsignedInt210101Rev(x: Float, y: Float, z: Float): Int { - var store = 0 - store = store or ((x.toInt()) and TEN_BITS) - store = store or (((y.toInt()) and TEN_BITS) shl 10) - store = store or (((z.toInt()) and TEN_BITS) shl 20) - return store - } - - fun unsignedInt2101010RevToFloats(packedValue: Int, source: Vector3): Vector3 { - var x = source.x - var y = source.y - var z = source.z - - x = (packedValue and TEN_BITS).toFloat() - y = ((packedValue ushr 10) and TEN_BITS).toFloat() - z = ((packedValue ushr 20) and TEN_BITS).toFloat() - - return Vector3(x, y, z) - } - - fun int2101010RevNormalizedToFloats(packedValue: Int, source: Vector3): Vector3 { - var source = source - source = int2101010RevToFloats(packedValue, source) - var x = source.x - var y = source.y - var z = source.z - - x /= TEN_BITS_MAX.toFloat() - y /= TEN_BITS_MAX.toFloat() - z /= TEN_BITS_MAX.toFloat() - return Vector3(x, y, z) - } - - fun unsignedInt2101010RevNormalizedToFloats( - packedValue: Int, - source: Vector3, - ): Vector3 { - var source = source - source = unsignedInt2101010RevToFloats(packedValue, source) - var x = source.x - var y = source.y - var z = source.z - - x /= TEN_BITS.toFloat() - y /= TEN_BITS.toFloat() - z /= TEN_BITS.toFloat() - - return Vector3(x, y, z) - } -} diff --git a/server/core/src/main/java/io/eiren/util/BufferedTimer.java b/server/core/src/main/java/io/eiren/util/BufferedTimer.java deleted file mode 100644 index a254c2a574..0000000000 --- a/server/core/src/main/java/io/eiren/util/BufferedTimer.java +++ /dev/null @@ -1,127 +0,0 @@ -package io.eiren.util; - -import com.jme3.system.NanoTimer; - - -/** - * This timer accumulate measured TPF and returns average/min/max FPS value - */ -public class BufferedTimer extends NanoTimer { - - private final float measureInterval; - private float averageTpf; - private float averageFps; - private float averageFrameRenderTime; - private float sumFrameRenderTime; - private float sumTpf; - private float minFpsCurrent; - private float maxFpsCurrent; - private float maxFps; - private float minFps; - private int count; - private boolean measured = false; - - /** - * Measure average tpf over the provided inverval in seconds - * - * @param measureInterval interval to measure averages over - */ - public BufferedTimer(float measureInterval) { - averageFps = 0; - sumTpf = 0; - count = 0; - this.measureInterval = measureInterval; - } - - public float getAverageFPS() { - return averageFps; - } - - public float getMinFPS() { - return minFps; - } - - public float getMaxFPS() { - return maxFps; - } - - public void addRenderTime(float renderTime) { - sumFrameRenderTime += renderTime; - } - - public float getAverageFrameRenderTime() { - return averageFrameRenderTime; - } - - public boolean isMeasured() { - if (measured) { - measured = false; - return true; - } - return false; - } - - public TimerSample getCurrentData() { - return new TimerSample(getFrameRate(), minFps, maxFps, averageFps); - } - - @Override - public void update() { - super.update(); - // Accumulate instant rate - sumTpf += getTimePerFrame(); - float fps = getFrameRate(); - if (fps < minFpsCurrent) - minFpsCurrent = fps; - if (fps > maxFpsCurrent) - maxFpsCurrent = fps; - ++count; - // Calculate results once per measure interval - if (!measured || sumTpf > measureInterval) { - // Average results - averageTpf = sumTpf / count; - averageFps = 1.0f / averageTpf; - averageFrameRenderTime = sumFrameRenderTime / count; - minFps = minFpsCurrent; - maxFps = maxFpsCurrent; - // Reset counter - sumTpf = 0; - sumFrameRenderTime = 0; - minFpsCurrent = Float.MAX_VALUE; - maxFpsCurrent = 0; - count = 0; - measured = true; - } - } - - public static class TimerSample { - - public float fps; - public float minFps; - public float maxFps; - public float averageFps; - - public TimerSample(float fps, float minFps, float maxFps, float averageFps) { - this.fps = fps; - this.minFps = minFps; - this.maxFps = maxFps; - this.averageFps = averageFps; - } - - public float getFps() { - return fps; - } - - public float getMinFps() { - return minFps; - } - - public float getMaxFps() { - return maxFps; - } - - public float getAverageFps() { - return averageFps; - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/OperatingSystem.kt b/server/core/src/main/java/io/eiren/util/OperatingSystem.kt deleted file mode 100644 index 8a651e95e9..0000000000 --- a/server/core/src/main/java/io/eiren/util/OperatingSystem.kt +++ /dev/null @@ -1,69 +0,0 @@ -package io.eiren.util - -import java.io.File -import java.nio.file.Path -import java.util.* -import kotlin.io.path.Path - -enum class OperatingSystem( - val descriptor: String, - private val aliases: Array, -) { - LINUX("linux", arrayOf("linux", "unix")), - WINDOWS("windows", arrayOf("win")), - OSX("osx", arrayOf("mac")), - UNKNOWN("unknown", arrayOf()), - ; - - companion object { - val currentPlatform: OperatingSystem by lazy { - val osName = System.getProperty("os.name").lowercase(Locale.getDefault()) - entries.find { os -> os.aliases.any { alias -> osName.contains(alias) } }?.let { - return@lazy it - } - UNKNOWN - } - fun getJavaExecutable(forceConsole: Boolean): String { - val separator = System.getProperty("file.separator") - val path = System.getProperty("java.home") + separator + "bin" + separator - return if (currentPlatform == WINDOWS) { - if (!forceConsole && File(path + "javaw.exe").isFile) path + "javaw.exe" else path + "java.exe" - } else { - path + "java" - } - } - - val socketDirectory: String - get() { - var dir = System.getenv("SLIMEVR_SOCKET_DIR") - if (dir != null) return dir - if (currentPlatform == LINUX) { - dir = System.getenv("XDG_RUNTIME_DIR") - if (dir != null) return dir - } - return System.getProperty("java.io.tmpdir") - } - - fun resolveConfigDirectory(identifier: String): Path? = when (currentPlatform) { - LINUX -> System.getenv("XDG_CONFIG_HOME")?.let { Path(it, identifier) } - ?: System.getenv("HOME")?.let { Path(it, ".config", identifier) } - - WINDOWS -> System.getenv("AppData")?.let { Path(it, identifier) } - - OSX -> System.getenv("HOME")?.let { Path(it, "Library", "Application Support", identifier) } - - UNKNOWN -> null - } - - fun resolveLogDirectory(identifier: String): Path? = when (currentPlatform) { - LINUX -> System.getenv("XDG_DATA_HOME")?.let { Path(it, identifier, "logs") } - ?: System.getenv("HOME")?.let { Path(it, ".local", "share", identifier, "logs") } - - WINDOWS -> System.getenv("AppData")?.let { Path(it, identifier, "logs") } - - OSX -> System.getenv("HOME")?.let { Path(it, "Library", "Logs", identifier) } - - UNKNOWN -> null - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/StringUtils.java b/server/core/src/main/java/io/eiren/util/StringUtils.java deleted file mode 100644 index 7ee9369640..0000000000 --- a/server/core/src/main/java/io/eiren/util/StringUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.eiren.util; - -import java.text.DecimalFormatSymbols; -import java.util.Locale; - - -public class StringUtils { - - private static char DECIMAL_SEP; - - public static char getDecimalSeparator() { - if (DECIMAL_SEP == '\u0000') { - final Locale l = Locale.getDefault(Locale.Category.FORMAT); - // Formatter.java always use "." in the Locale.US - DECIMAL_SEP = (l == null || l.equals(Locale.US) - ? '.' - : DecimalFormatSymbols.getInstance(l).getDecimalSeparator()); - } - return DECIMAL_SEP; - } - - public static String prettyNumber(float f) { - return prettyNumber(f, 4); - } - - public static String prettyNumber(float f, int numDigits) { - String str = String.format("%." + numDigits + "f", f); - if (numDigits != 0) - str = org.apache.commons.lang3.StringUtils.stripEnd(str, "0"); - char lastChar = str.charAt(str.length() - 1); - if (lastChar == getDecimalSeparator()) - str = str.substring(0, str.length() - 1); - return str; - } -} diff --git a/server/core/src/main/java/io/eiren/util/Util.java b/server/core/src/main/java/io/eiren/util/Util.java deleted file mode 100644 index a50980bdf2..0000000000 --- a/server/core/src/main/java/io/eiren/util/Util.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.eiren.util; - -import java.io.Closeable; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; - - -public class Util { - - public static void close(Object r) { - try { - if (r != null) { - if (r instanceof Closeable) - ((Closeable) r).close(); - else if (r instanceof AutoCloseable) - ((AutoCloseable) r).close(); - } - } catch (Exception ignored) {} - } - - public static void close(Object r1, Object r2) { - close(r1); - close(r2); - } - - public static void close(Object... r) { - for (Object o : r) - try { - if (o != null) { - if (o instanceof Closeable) - ((Closeable) o).close(); - else if (o instanceof AutoCloseable) - ((AutoCloseable) o).close(); - } - } catch (Exception ignored) {} - } - - public static void close(AutoCloseable... r) { - for (AutoCloseable autoCloseable : r) - try { - if (autoCloseable != null) - autoCloseable.close(); - } catch (Exception ignored) {} - } - - public static void close(Closeable... r) { - for (Closeable closeable : r) - try { - if (closeable != null) - closeable.close(); - } catch (Exception ignored) {} - } - - /** - *

- * Performs a deep toString of provided object. It shows content of arrays, - * collections and maps (trove not supported yet). - *

- *

- * Highly ineffective, use only for debug. - *

- * - * @param object - * @return - */ - public static String toString(Object object) { - if (object == null) - return "null"; - StringBuilder buf = new StringBuilder(); - elementToString(object, buf, new HashSet<>()); - return buf.toString(); - } - - private static void deepToString(Map m, StringBuilder buf, Set dejaVu) { - if (m == null) { - buf.append("null"); - return; - } - if (m.size() == 0) { - buf.append("{}"); - return; - } - dejaVu.add(m); - buf.append('{'); - Iterator> iterator = m.entrySet().iterator(); - boolean has = false; - while (iterator.hasNext()) { - if (has) - buf.append(','); - Entry e = iterator.next(); - elementToString(e.getKey(), buf, dejaVu); - buf.append(':'); - elementToString(e.getValue(), buf, dejaVu); - has = true; - } - buf.append('}'); - dejaVu.remove(m); - } - - private static void deepToString( - Collection list, - StringBuilder buf, - Set dejaVu - ) { - Object[] array = list.toArray(); - deepToString(array, buf, dejaVu); - } - - private static void deepToString(Object[] a, StringBuilder buf, Set dejaVu) { - if (a == null) { - buf.append("null"); - return; - } - if (a.length == 0) { - buf.append("[]"); - return; - } - dejaVu.add(a); - buf.append('['); - for (int i = 0; i < a.length; i++) { - if (i != 0) - buf.append(','); - Object element = a[i]; - elementToString(element, buf, dejaVu); - } - buf.append(']'); - dejaVu.remove(a); - } - - @SuppressWarnings("unchecked") - private static void elementToString(Object element, StringBuilder buf, Set dejaVu) { - if (element == null) { - buf.append("null"); - } else { - Class eClass = element.getClass(); - if (eClass.isArray()) { - if (eClass == byte[].class) - buf.append(Arrays.toString((byte[]) element)); - else if (eClass == short[].class) - buf.append(Arrays.toString((short[]) element)); - else if (eClass == int[].class) - buf.append(Arrays.toString((int[]) element)); - else if (eClass == long[].class) - buf.append(Arrays.toString((long[]) element)); - else if (eClass == char[].class) - buf.append(Arrays.toString((char[]) element)); - else if (eClass == float[].class) - buf.append(Arrays.toString((float[]) element)); - else if (eClass == double[].class) - buf.append(Arrays.toString((double[]) element)); - else if (eClass == boolean[].class) - buf.append(Arrays.toString((boolean[]) element)); - else { // element is an array of object references - if (dejaVu.contains(element)) - buf.append("[...]"); - else - deepToString((Object[]) element, buf, dejaVu); - } - } else { // element is non-null and not an array - if (element instanceof Collection) - deepToString((Collection) element, buf, dejaVu); - else if (element instanceof Map) - deepToString((Map) element, buf, dejaVu); - else if (element instanceof CharSequence) - buf.append('"').append(element).append('"'); - else - buf.append(element); - } - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/ann/AWTThread.java b/server/core/src/main/java/io/eiren/util/ann/AWTThread.java deleted file mode 100644 index a4ca9828c7..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/AWTThread.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -@Retention(value = RetentionPolicy.SOURCE) -public @interface AWTThread { - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/DebugSwitch.java b/server/core/src/main/java/io/eiren/util/ann/DebugSwitch.java deleted file mode 100644 index 9b7827bec4..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/DebugSwitch.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; - - -@Target({ ElementType.FIELD, ElementType.METHOD }) -public @interface DebugSwitch { -} diff --git a/server/core/src/main/java/io/eiren/util/ann/NativeUnsafe.java b/server/core/src/main/java/io/eiren/util/ann/NativeUnsafe.java deleted file mode 100644 index 52052dcb36..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/NativeUnsafe.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -/** - * Marks methods and classes that use unsafe or direct access to memory. Proceed - * with caution. - * - * @author Rena - */ -@Retention(value = RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE }) -public @interface NativeUnsafe { - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/Synchronize.java b/server/core/src/main/java/io/eiren/util/ann/Synchronize.java deleted file mode 100644 index d18d9f8339..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/Synchronize.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -/** - *

- * Означает необходимость обязательной синхронизации этого меcта во внешних - * методах. В аргументах передаётся название поля для синхронизации. - *

- *

- * Методы, помеченные данной аннотацией могут вызывать только Thread-Safe - * методы, либо методы, помеченные такой же аннотацией с тем же полем - * синхронизации. - *

- *

- * Поля, помеченные данной аннотацией должны быть синхронизированны на указанное - * поле при чтении или записи. - *

- * - * @see {@link ThreadSafe}, {@link ThreadSecure}, {@link ThreadSafeSingle} - * @author Rena - */ -@Retention(value = RetentionPolicy.SOURCE) -public @interface Synchronize { - - String[] value(); - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/ThreadSafe.java b/server/core/src/main/java/io/eiren/util/ann/ThreadSafe.java deleted file mode 100644 index c74fcfb39b..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/ThreadSafe.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -/** - *

- * Методы, помеченные этой аннотацией должны быть Thread-Safe. - *

- *

- * Важно: данные методы гарантированно должны обеспечивать потоковую - * безопасность, но не обязаны обеспечивать концессивность (полноту данных или - * точность синхронизации). - *

- *

- * Для полностью потоко-безопасных методов можно использовать аннотацию - * {@link ThreadSecure}. - *

- * - * @see {@link ThreadSecure}, {@link Synchronize}, {@link ThreadSafeSingle} - * @author Rena - */ -@Retention(value = RetentionPolicy.SOURCE) -public @interface ThreadSafe { - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/ThreadSafeSingle.java b/server/core/src/main/java/io/eiren/util/ann/ThreadSafeSingle.java deleted file mode 100644 index 0e918499e9..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/ThreadSafeSingle.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -/** - * Соблюдает те же требования что и {@link ThreadSafe} но при условии, что сам - * метод вызывается только из одного потока одновременно. - * - * @see {@link ThreadSafe}, {@link ThreadSecure}, {@link Synchronize} - * @author Rena - */ -@Retention(value = RetentionPolicy.SOURCE) -public @interface ThreadSafeSingle { - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/ThreadSecure.java b/server/core/src/main/java/io/eiren/util/ann/ThreadSecure.java deleted file mode 100644 index 2b375fe365..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/ThreadSecure.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -/** - *

- * Методы, помеченные этой аннотацией должны быть полностью Thread-Safe. - *

- *

- * Важно: данные методы гарантированно должны обеспечивать потоковую - * безопасность и консистентность (полноту данных и точность синхронизации). - *

- * - * @see {@link ThreadSafe}, {@link Synchronize}, {@link ThreadSafeSingle} - * @author Rena - */ -@Retention(value = RetentionPolicy.SOURCE) -public @interface ThreadSecure { - -} diff --git a/server/core/src/main/java/io/eiren/util/ann/Transient.java b/server/core/src/main/java/io/eiren/util/ann/Transient.java deleted file mode 100644 index 3502dff425..0000000000 --- a/server/core/src/main/java/io/eiren/util/ann/Transient.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.eiren.util.ann; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -/** - *

- * Означает что поле используется для временных или быстро изменяющихся - * переменных. - *

- *

- * Поле помеченное этой аннотацией не влияет на долгосрочное состояние объекта, - * не участвует в сериализации, вычислении equals и hashCode, не определяет - * поведение объекта для внешнего кода. Поэтому такие поля не должны - * использоваться внешним кодом, их состояние имеет смысл только для самого - * объекта в котором они объявлены. - *

- * Примеры: - *
    - *
  • Временный объект, который используется в методах для внутренних - * вычислений. Например векторные и матричные вычисления.
  • - *
  • Внутренний флаг для мультитрединга. Например, флаг апдейта графического - * состояния взводимый из игрового потока.
  • - *
  • Выведенное значение или структура, которое инициализируется самим - * объектом по фиксированному правилу. Например, производное значение от - * переменной параметризующей объект. Инициализируемый в конструкторе lookup - * table.
  • - *
- * - * @author tort32 - */ -@Retention(value = RetentionPolicy.SOURCE) -@Target({ ElementType.FIELD }) -public @interface Transient { - -} diff --git a/server/core/src/main/java/io/eiren/util/collections/FastList.java b/server/core/src/main/java/io/eiren/util/collections/FastList.java deleted file mode 100644 index 4f4ee0749b..0000000000 --- a/server/core/src/main/java/io/eiren/util/collections/FastList.java +++ /dev/null @@ -1,546 +0,0 @@ -package io.eiren.util.collections; - -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; - - -@SuppressWarnings("unchecked") -public class FastList extends AbstractList - implements RandomAccess, Cloneable, RemoveAtSwapList { - - private static final Object[] emptyArray = new Object[0]; - - public static final int MAX_ARRAY_SIZE = 2147483639; - - protected int size = 0; - protected Object[] array; - - public FastList(int capacity) { - array = capacity == 0 ? emptyArray : new Object[capacity]; - } - - public FastList() { - this(5); - } - - public FastList(Collection source) { - this(source.size()); - addAll(source); - } - - public FastList(FastList source) { - this(source.size); - addAllInternal(0, source.array, source.size); - } - - public FastList(E[] source) { - this(source.length); - addAll(source); - } - - public FastList(E source) { - this(); - add(source); - } - - private FastList(Object[] arr, int size) { - this(size); - System.arraycopy(arr, 0, array, 0, size); - this.size = size; - } - - private FastList(boolean f) { - } - - public static FastList reuseArray(E[] source) { - FastList list = new FastList<>(true); - list.array = source; - list.size = source.length; - return list; - } - - private void checkBounds(int index) { - if (index < 0 || index >= size) - throw new ArrayIndexOutOfBoundsException( - new StringBuilder("Index: ") - .append(index) - .append(", size: ") - .append(size) - .toString() - ); - } - - public void ensureCapacity(int numToFit) { - if (array.length < size + numToFit) - grow(numToFit + size); - } - - private void grow(int i) { - int j = array.length; - int k = j + (j >> 1); - if (k - i < 0) - k = i; - if (k - 2147483639 > 0) - k = hugeCapacity(i); - array = Arrays.copyOf(array, k); - } - - private static int hugeCapacity(int i) { - if (i < 0) - throw new OutOfMemoryError("Huge capacity negative: " + i); - else - return i <= MAX_ARRAY_SIZE ? MAX_ARRAY_SIZE : 2147483647; - } - - public void copyInto(Object[] anArray) { - System.arraycopy(array, 0, anArray, 0, size); - } - - @Override - public E get(int index) { - checkBounds(index); - return (E) array[index]; - } - - public E unsafeGet(int index) { - return (E) array[index]; - } - - @Override - public boolean isEmpty() { - return size == 0; - } - - @Override - public int size() { - return size; - } - - @Override - public int indexOf(Object obj) { - for (int j = 0; j < size; j++) - if (obj == array[j]) - return j; - return -1; - } - - @Override - public int lastIndexOf(Object obj) { - for (int j = size - 1; j >= 0; j--) - if (obj == array[j]) - return j; - return -1; - } - - @Override - public boolean contains(Object obj) { - return indexOf(obj) >= 0; - } - - public void trimToSize() { - int i = array.length; - if (size < i) - array = Arrays.copyOf(array, size); - } - - @Override - public Object[] toArray() { - return Arrays.copyOf(array, size); - } - - @Override - public T[] toArray(T[] aobj) { - if (aobj.length < size) - return (T[]) Arrays.copyOf(array, size, aobj.getClass()); - System.arraycopy(array, 0, aobj, 0, size); - if (aobj.length > size) - aobj[size] = null; - return aobj; - } - - @Override - public boolean add(E e) { - ensureCapacity(1); - array[size++] = e; - return true; - } - - @Override - public E remove(int i) { - checkBounds(i); - E obj = (E) array[i]; - removeInternal(i); - return obj; - } - - @Override - public boolean remove(Object obj) { - for (int j = 0; j < size; j++) - if (obj == array[j]) { - removeInternal(j); - return true; - } - return false; - } - - public boolean removeAll(Object[] toRemove) { - boolean removed = false; - for (int i = toRemove.length - 1; i >= 0; --i) { - int index = indexOf(toRemove[i]); - if (index != -1) { - removeInternal(index); - removed = true; - } - } - return removed; - } - - protected void removeInternal(int i) { - int j = size - i - 1; - if (j > 0) - System.arraycopy(array, i + 1, array, i, j); - array[--size] = null; - } - - public void unsafeRemove(int i) { - removeInternal(i); - } - - @Override - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - return batchRemove(c, false); - } - - @Override - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - return batchRemove(c, true); - } - - private boolean batchRemove(Collection c, boolean complement) { - final Object[] elementData = this.array; - int r = 0, w = 0; - boolean modified = false; - try { - for (; r < size; r++) - if (c.contains(elementData[r]) == complement) - elementData[w++] = elementData[r]; - } finally { - // Preserve behavioral compatibility with AbstractCollection, - // even if c.contains() throws. - if (r != size) { - System.arraycopy(elementData, r, elementData, w, size - r); - w += size - r; - } - if (w != size) { - for (int i = w; i < size; i++) - elementData[i] = null; - size = w; - modified = true; - } - } - return modified; - } - - @Override - public void clear() { - for (int i = 0; i < size; i++) - array[i] = null; - size = 0; - } - - public void fakeClear() { - size = 0; - } - - @Override - public boolean addAll(Collection collection) { - return addAll(size, collection); - } - - public void addAll(E[] arr) { - addAllInternal(size, arr, arr.length); - } - - public void addAll(E[] arr, int limit) { - addAllInternal(size, arr, limit); - } - - public void addAll(int index, E[] arr) { - addAllInternal(index, arr, arr.length); - } - - public void addAll(int index, E[] arr, int limit) { - addAllInternal(index, arr, limit); - } - - private void addAllInternal(int index, Object[] arr, int limit) { - if (limit > arr.length) - limit = arr.length; - if (limit == 1) { - add(index, (E) arr[0]); - } else if (limit > 0) { - if (index >= size) { - ensureCapacity(size - index + limit); - System.arraycopy(arr, 0, array, index, limit); - size = index + limit; - } else { - if (array.length < size + limit) { - Object[] newArray = new Object[size + limit]; - System.arraycopy(array, 0, newArray, 0, index); - System.arraycopy(arr, 0, newArray, index, limit); - System.arraycopy(array, index, newArray, index + limit, size - index); - array = newArray; - } else { - System.arraycopy(array, index, array, index + 1, size - index); - System.arraycopy(arr, 0, array, index, limit); - } - size += limit; - } - } - } - - @Override - public boolean addAll(int index, Collection collection) { - if (collection.size() > 0) { - if (collection instanceof FastList) { - addAllInternal( - index, - ((FastList) collection).array, - collection.size() - ); - } else if (collection instanceof RandomAccess) { - Object[] arr = collection.toArray(new Object[collection.size()]); - addAllInternal(index, arr, arr.length); - } else { - if (index >= size) { - ensureCapacity(size - index + collection.size()); - Iterator iterator = collection.iterator(); - int i = index; - while (iterator.hasNext()) - array[i++] = iterator.next(); - size = index + collection.size(); - } else { - if (array.length < size + collection.size()) { - Object[] newArray = new Object[size + collection.size()]; - System.arraycopy(array, 0, newArray, 0, index); - Iterator iterator = collection.iterator(); - int i = index; - while (iterator.hasNext()) - newArray[i++] = iterator.next(); - System - .arraycopy( - array, - index, - newArray, - index + collection.size(), - size - index - ); - array = newArray; - } else { - System.arraycopy(array, index, array, index + 1, size - index); - for (E e : collection) - array[index++] = e; - } - size += collection.size(); - } - } - return true; - } - return false; - } - - @Override - public void add(int index, E element) { - if (index >= size) { - ensureCapacity(size - index + 1); - size = index + 1; - array[index] = element; - } else { - if (array.length < size + 1) { - Object[] newArray = new Object[size + 1]; - System.arraycopy(array, 0, newArray, 0, index); - newArray[index] = element; - System.arraycopy(array, index, newArray, index + 1, size - index); - array = newArray; - } else { - System.arraycopy(array, index, array, index + 1, size - index); - array[index] = element; - } - size++; - } - } - - @Override - public E set(int index, E element) { - checkBounds(index); - E oldValue = (E) array[index]; - array[index] = element; - return oldValue; - } - - @Override - public FastList clone() { - return new FastList<>(array, size); - } - - @Override - public void forEach(Consumer action) { - Objects.requireNonNull(action); - final int expectedModCount = modCount; - final E[] elementData = (E[]) this.array; - final int size = this.size; - for (int i = 0; modCount == expectedModCount && i < size; i++) { - action.accept(elementData[i]); - } - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - } - - @Override - public E removeAtSwap(int i) { - checkBounds(i); - E obj = (E) array[i]; - removeAtSwapInternal(i); - return obj; - } - - @Override - public boolean removeAtSwap(Object obj) { - for (int j = 0; j < size; j++) - if (obj == array[j]) { - removeAtSwapInternal(j); - return true; - } - return false; - } - - protected void removeAtSwapInternal(int i) { - int j = size - i - 1; - if (j > 0) - array[i] = array[size - 1]; - array[--size] = null; - } - - @Override - public void removeRange(int i, int toIndex) { - checkBounds(i); - checkBounds(toIndex); - int j = size - toIndex - 1; - if (j > 0) - System.arraycopy(array, toIndex + 1, array, i, j); - size -= (toIndex - i + 1); - Arrays.fill(array, i, toIndex, null); - } - - @Override - public void replaceAll(UnaryOperator operator) { - Objects.requireNonNull(operator); - for (int i = 0; i < size; ++i) - set(i, operator.apply(get(i))); - } - - @Override - public void sort(Comparator c) { - Arrays.sort((E[]) array, 0, size, c); - } - - @Override - public int hashCode() { - int hashCode = 1; - for (int i = 0; i < size; ++i) { - Object o = array[i]; - hashCode = 31 * hashCode + (o == null ? 0 : o.hashCode()); - } - return hashCode; - } - - @Override - public Spliterator spliterator() { - return Spliterators.spliterator(array, 0, size, Spliterator.ORDERED); - } - - /** - * Special comodification iterator. Use with caution. - *

- * To get element type correctly assign result to reference type - * {@code FastList.SkipFastListIterator} - * - * @return skip iterator to iterate this list in thread-safe manner - */ - public SkipFastListIterator skipIterator() { - return new SkipFastListIterator(); - } - - @Override - public boolean removeIf(Predicate filter) { - Objects.requireNonNull(filter); - // figure out which elements are to be removed - // any exception thrown from the filter predicate at this stage - // will leave the collection unmodified - int removeCount = 0; - final BitSet removeSet = new BitSet(size); - final int expectedModCount = modCount; - final int size = this.size; - for (int i = 0; modCount == expectedModCount && i < size; i++) { - final E element = (E) array[i]; - if (filter.test(element)) { - removeSet.set(i); - removeCount++; - } - } - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - - // shift surviving elements left over the spaces left by removed - // elements - final boolean anyToRemove = removeCount > 0; - if (anyToRemove) { - final int newSize = size - removeCount; - for (int i = 0, j = 0; (i < size) && (j < newSize); i++, j++) { - i = removeSet.nextClearBit(i); - array[j] = array[i]; - } - for (int k = newSize; k < size; k++) { - array[k] = null; // Let gc do its work - } - this.size = newSize; - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - modCount++; - } - - return anyToRemove; - } - - public class SkipFastListIterator implements ResettableIterator, SkipIterator { - - public int position; - - @Override - public boolean hasNext() { - return position < size; - } - - @Override - public E next() { - Object[] arr = array; - if (arr.length > position) { - return (E) arr[position++]; - } - position++; // Increase position so hasNext() never loops infinitely - return null; - } - - @Override - public void reset() { - position = 0; - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapFastList.java b/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapFastList.java deleted file mode 100644 index 1c00001e1e..0000000000 --- a/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapFastList.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.eiren.util.collections; - -import java.util.Collection; - - -/** - * FastList that performs Remove-At-Swap on stanard remove() operations. - * - *

- * Remove operations breaks ordering of this list - * - * @author Rena - * - * @param - */ -public class RemoveAtSwapFastList extends FastList { - - public RemoveAtSwapFastList(int capacity) { - super(capacity); - } - - public RemoveAtSwapFastList() { - } - - public RemoveAtSwapFastList(Collection source) { - super(source); - } - - public RemoveAtSwapFastList(E[] source) { - super(source); - } - - public RemoveAtSwapFastList(E source) { - super(source); - } - - @Override - protected void removeInternal(int i) { - super.removeAtSwapInternal(i); - } -} diff --git a/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapList.java b/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapList.java deleted file mode 100644 index cd77323bb0..0000000000 --- a/server/core/src/main/java/io/eiren/util/collections/RemoveAtSwapList.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.eiren.util.collections; - -import java.util.List; - - -public interface RemoveAtSwapList extends List { - - E removeAtSwap(int i); - - boolean removeAtSwap(Object object); -} diff --git a/server/core/src/main/java/io/eiren/util/collections/ResettableIterator.java b/server/core/src/main/java/io/eiren/util/collections/ResettableIterator.java deleted file mode 100644 index 9ef8b9ee17..0000000000 --- a/server/core/src/main/java/io/eiren/util/collections/ResettableIterator.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.eiren.util.collections; - -import java.util.Iterator; - - -/** - * {@link Iterator} that can be reset and iterated from the start by using - * {@link #reset()} - * - * @author Rena - * - * @param - */ -public interface ResettableIterator extends Iterator { - - void reset(); -} diff --git a/server/core/src/main/java/io/eiren/util/collections/SkipIterator.java b/server/core/src/main/java/io/eiren/util/collections/SkipIterator.java deleted file mode 100644 index 9c79579adc..0000000000 --- a/server/core/src/main/java/io/eiren/util/collections/SkipIterator.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.eiren.util.collections; - -import java.util.Iterator; - - -/** - * {@link Iterator} that can return null on {@link #next()} or can lie on - * {@link #hasNext()}. It is not thread-secure! - * - * @param the type of elements returned by this iterator - */ -public interface SkipIterator extends Iterator { - - @Override - E next(); -} diff --git a/server/core/src/main/java/io/eiren/util/logging/DefaultGLog.java b/server/core/src/main/java/io/eiren/util/logging/DefaultGLog.java deleted file mode 100644 index 26f7ab7081..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/DefaultGLog.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.eiren.util.logging; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.logging.Level; -import java.util.logging.Logger; - - -public class DefaultGLog extends Thread implements IGLog { - - private final Logger logger; - - public static class LogEntry { - - private Level level; - private String message; - private Throwable t; - - public LogEntry(Level level, String message, Throwable t) { - this(level, message); - this.t = t; - } - - public LogEntry(Level level, String message) { - this.level = level; - this.message = message; - this.t = null; - } - - public Level getLevel() { - return level; - } - - public String getMessage() { - return message; - } - - public Throwable getException() { - return t; - } - } - - private final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(50000); - private volatile LoggerRecorder recorder; - - @Override - public void info(String message) { - add(new LogEntry(Level.INFO, message)); - } - - @Override - public void info(String message, Throwable t) { - add(new LogEntry(Level.INFO, message, t)); - } - - @Override - public void severe(String message) { - add(new LogEntry(Level.SEVERE, message)); - } - - @Override - public void severe(String message, Throwable t) { - add(new LogEntry(Level.SEVERE, message, t)); - } - - @Override - public void warning(String message) { - add(new LogEntry(Level.WARNING, message)); - } - - @Override - public void warning(String message, Throwable t) { - add(new LogEntry(Level.WARNING, message, t)); - } - - @Override - public void debug(String message) { - add(new LogEntry(Level.INFO, "[DBG] " + message)); - } - - @Override - public void debug(String message, Throwable t) { - add(new LogEntry(Level.INFO, "[DBG] " + message, t)); - } - - @Override - public void log(Level level, String message) { - add(new LogEntry(level, message)); - } - - @Override - public void log(Level level, String message, Throwable t) { - add(new LogEntry(level, message, t)); - } - - private void add(LogEntry entry) { - try { - queue.put(entry); - } catch (InterruptedException ignored) {} - try { - if (recorder != null) - recorder.addEntry(entry); - } catch (NullPointerException ignored) {} - } - - @Override - public void setRecorder(LoggerRecorder recorder) { - this.recorder = recorder; - } - - @Override - public LoggerRecorder removeRecorder() { - LoggerRecorder lr = this.recorder; - this.recorder = null; - return lr; - } - - public DefaultGLog(Logger logger) { - super("Logger"); - this.logger = logger; - this.setDaemon(true); - this.setPriority(7); - this.start(); - } - - @Override - public void run() { - while (true) { - try { - LogEntry log = queue.take(); - if (log.t != null) - logger.log(log.level, log.message, log.t); - else - logger.log(log.level, log.message); - } catch (InterruptedException ignored) {} - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/FileLogFormatter.java b/server/core/src/main/java/io/eiren/util/logging/FileLogFormatter.java deleted file mode 100644 index d0be04b0ec..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/FileLogFormatter.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.eiren.util.logging; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.text.SimpleDateFormat; -import java.util.logging.Formatter; -import java.util.logging.Level; -import java.util.logging.LogRecord; - - -public class FileLogFormatter extends Formatter { - - private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - @Override - public String format(LogRecord record) { - StringBuilder sb = new StringBuilder(); - sb.append(dateFormat.format(record.getMillis())); - Level localLevel = record.getLevel(); - if (localLevel == Level.FINEST) - sb.append(" [FINEST] "); - else if (localLevel == Level.FINER) - sb.append(" [FINER] "); - else if (localLevel == Level.FINE) - sb.append(" [FINE] "); - else if (localLevel == Level.INFO) - sb.append(" [INFO] "); - else if (localLevel == Level.WARNING) - sb.append(" [WARNING] "); - else if (localLevel == Level.SEVERE) - sb.append(" [SEVERE] "); - else - sb.append(" [").append(localLevel.getLocalizedName()).append("] "); - - sb.append(record.getMessage()); - sb.append('\n'); - - Throwable localThrowable = record.getThrown(); - if (localThrowable != null) { - StringWriter localStringWriter = new StringWriter(); - localThrowable.printStackTrace(new PrintWriter(localStringWriter)); - sb.append(localStringWriter); - } - - String message = sb.toString(); - Object[] parameters = record.getParameters(); - if (parameters == null || parameters.length == 0) - return message; - if ( - message.contains("{0") - || message.contains("{1") - || message.contains("{2") - || message.contains("{3") - ) - return java.text.MessageFormat.format(message, parameters); - return message; - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.kt b/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.kt deleted file mode 100644 index dda319cc0f..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.kt +++ /dev/null @@ -1,308 +0,0 @@ -package io.eiren.util.logging - -import java.io.BufferedOutputStream -import java.io.DataOutputStream -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.nio.file.Path -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.logging.ErrorManager -import java.util.logging.LogRecord -import java.util.logging.StreamHandler - -/** - * A log handler that manages a set of log files, deleting the oldest log(s) when limits - * are reached. - * @param path The directory to store and discover the log files in. - * @param logTag The identifier tag for log files (ex. "slimevr-server"). - * @param dateFormat The format to use for the date and time in the log file names. - * @param limit The independent log file size limit in bytes. - * @param maxCount The collective log file count limit. - * @param collectiveLimit The collective log file size limit in bytes. - */ -class FileLogHandler @JvmOverloads constructor( - private val path: Path, - private val logTag: String, - private val dateFormat: DateTimeFormatter, - private val limit: Int, - private val maxCount: Int, - private val collectiveLimit: Long = -1, -) : StreamHandler() { - - /** - * A log [File] with a [LocalDateTime] and [count] associated to it. - */ - inner class DatedLogFile(val file: File, val dateTime: LocalDateTime, val count: Int) : Comparable { - override fun compareTo(o: DatedLogFile): Int { - val dtCompare = dateTime.compareTo(o.dateTime) - return if (dtCompare != 0) dtCompare else count.compareTo(o.count) - } - } - - /** - * The symbol to use for separating file name sections. - */ - private val sectionSeparator = '_' - - /** - * The file suffix to use for logs. - */ - private val logSuffix = ".log" - - /** - * The list of log files being managed by this [FileLogHandler]. - */ - private val logFiles: MutableList - - /** - * The [LocalDateTime] to use for log files created by this [FileLogHandler] - * instance. - */ - private val dateTime: LocalDateTime = LocalDateTime.now() - - /** - * The formatted string representing [dateTime]. - */ - private val date: String = dateTime.format(dateFormat) - - /** - * The current file [DataOutputStream]. - */ - private var curStream: DataOutputStream? = null - - /** - * The current file count for logs based on the same [dateTime] made by this - * [FileLogHandler] instance. - */ - private var fileCount = 0 - - /** - * The current collective size of all log files in bytes. - */ - private var collectiveSize: Long = 0 - - init { - // Find old logs to manage - logFiles = findLogs(path) - if (collectiveLimit > 0) { - collectiveSize = sumFileSizes(logFiles) - } - - // Create new log and delete over the count - newFile() - } - - /** - * Parses [file]'s name and returns a parsed [DatedLogFile]. - * @param file The file to parse the name of. - * @return The parsed [DatedLogFile]. - */ - private fun parseFileName(file: File): DatedLogFile? { - val name = file.getName() - - // Log name should have at least two separators, one integer, and at - // least one char for the datetime (4 chars) - if (!name.startsWith(logTag) || !name.endsWith(logSuffix) || name.length < (logTag.length + logSuffix.length + 4)) { - // Ignore non-matching files - return null - } - - val dateEnd = name.lastIndexOf(sectionSeparator) - if (dateEnd < 0) { - // Ignore non-matching files - return null - } - - try { - // Move past the tag, then between the two separators - val dateTimeStr = name.substring(logTag.length + 1, dateEnd) - val dateTime = LocalDateTime.parse(dateTimeStr, dateFormat) - - // Move past the date separator and behind the suffix - - val logNum = name.substring(dateEnd + 1, name.length - logSuffix.length).toInt() - - return DatedLogFile(file, dateTime, logNum) - } catch (_: Exception) { - // Unable to parse log file, probably not valid - return null - } - } - - /** - * Finds all the log files parseable by [parseFileName] in the provided path, - * [path]. - * @param path The path to check for log files. - * @return A list containing all parsed [DatedLogFile]s from [path]. - */ - private fun findLogs(path: Path): MutableList { - val logFiles = mutableListOf() - - val files = path.toFile().listFiles() - if (files == null) return logFiles - - // Find all parseable log files - for (log in files) { - val parsedFile = parseFileName(log) - if (parsedFile != null) { - logFiles.add(parsedFile) - } - } - - return logFiles - } - - /** - * Computes the sum of the file sizes from the provided [logFiles]. - * @param logFiles The list of [DatedLogFile]s to compute the size of. - * @return The size of all the provided [DatedLogFile]s in bytes. - */ - private fun sumFileSizes(logFiles: List): Long { - var size: Long = 0 - for (log in logFiles) { - size += log.file.length() - } - return size - } - - /** - * Tries to delete the provided [file]. - * @param file The [File] to delete. - */ - private fun deleteFile(file: File) { - if (file.delete()) return - - file.deleteOnExit() - reportError( - "Failed to delete file, deleting on exit.", - null, - ErrorManager.GENERIC_FAILURE, - ) - } - - /** - * Returns the earliest [DatedLogFile] from the provided list, [logFiles]. - * @param logFiles The [DatedLogFile]s to find the earliest file from. - * @return The earliest [DatedLogFile] or null if none could be found. - */ - private fun getEarliestFile(logFiles: List): DatedLogFile? { - var earliest: DatedLogFile? = null - - for (log in logFiles) { - if (earliest == null || log < earliest) { - earliest = log - } - } - - return earliest - } - - /** - * Deletes the earliest log file from [logFiles]. - */ - @Synchronized - private fun deleteEarliestFile() { - val earliest = getEarliestFile(logFiles) - if (earliest == null) return - - // If we have a collective limit, update the current size and clamp - if (collectiveLimit > 0) { - collectiveSize = ( - collectiveSize - earliest.file.length() - ).coerceAtLeast(0) - } - - logFiles.remove(earliest) - deleteFile(earliest.file) - } - - /** - * Deletes the earliest log files until the [collectiveSize] plus [curFileSize] is - * less than [collectiveLimit]. - * @param curFileSize The size of the current log file in bytes. - */ - @Synchronized - private fun deleteOverCollectiveLimit(curFileSize: Int = 0) { - if (collectiveLimit <= 0) return - - // Delete files over the collective size limit, including the current stream - while (!logFiles.isEmpty() && collectiveSize + curFileSize >= collectiveLimit) { - deleteEarliestFile() - } - } - - /** - * Creates a new log file, closing [curStream] automatically and incrementing - * [fileCount]. This method also handles deleting old log files based on the - * specified limits: [limit], [maxCount], and [collectiveLimit]. - */ - @Synchronized - private fun newFile() { - // Clear the last log file - val lastStream = curStream - if (lastStream != null) { - // Flush the log first - close() - // Then accumulate the amount written after - collectiveSize += lastStream.size() - } - - if (maxCount > 0) { - // Delete files over the count - while (logFiles.size >= maxCount) { - deleteEarliestFile() - } - } - - // Handle collective limit - deleteOverCollectiveLimit() - - try { - val newFile = path.resolve( - logTag + sectionSeparator + date + sectionSeparator + fileCount + logSuffix, - ).toFile() - - // Use DataOutputStream to count bytes written - val newStream = DataOutputStream( - BufferedOutputStream(FileOutputStream(newFile)), - ) - // Closes the last stream automatically if not already done - setOutputStream(newStream) - curStream = newStream - - // Add log to the tracking list to be deleted if needed - logFiles.add(DatedLogFile(newFile, dateTime, fileCount)) - fileCount += 1 - } catch (e: FileNotFoundException) { - reportError(null, e, ErrorManager.OPEN_FAILURE) - } - } - - /** - * Publishes the provided [record]. This automatically manages deleting old log - * files based on [collectiveLimit] as the new log is written and creating new log - * files after the file size limit, [limit], is reached. - */ - @Synchronized - override fun publish(record: LogRecord?) { - if (!isLoggable(record)) return - - // Push the log - super.publish(record) - // Then flush, so we always have the latest output - flush() - - // The number of bytes written to the current log file - val curFileSize = curStream!!.size() - - // Handle the collective limit as we write to the new log file - deleteOverCollectiveLimit(curFileSize) - - // If written above the log limit, make a new file - if (limit > 0 && curFileSize >= limit) { - newFile() - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/IGLog.java b/server/core/src/main/java/io/eiren/util/logging/IGLog.java deleted file mode 100644 index db4d328f87..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/IGLog.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.eiren.util.logging; - -import java.util.logging.Level; - - -public interface IGLog { - - void info(String message); - - void severe(String message); - - void warning(String message); - - void debug(String message); - - default void info(String message, Throwable t) { - log(Level.INFO, message, t); - } - - default void severe(String message, Throwable t) { - log(Level.SEVERE, message, t); - } - - default void warning(String message, Throwable t) { - log(Level.WARNING, message, t); - } - - default void debug(String message, Throwable t) { - log(Level.INFO, "[DBG] " + message, t); - } - - void log(Level level, String message); - - void log(Level level, String message, Throwable t); - - void setRecorder(LoggerRecorder recorder); - - LoggerRecorder removeRecorder(); - - class GLevel extends Level { - - private static final long serialVersionUID = -539856764608026895L; - - private GLevel(String s, int i) { - super(s, i); - } - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/LogManager.java b/server/core/src/main/java/io/eiren/util/logging/LogManager.java deleted file mode 100644 index bd1fe0b76d..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/LogManager.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.eiren.util.logging; - -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.time.format.DateTimeFormatter; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.Logger; - - -public class LogManager { - - private static AtomicBoolean initialized = new AtomicBoolean(false); - - public static Logger global = Logger.getLogger(""); - public static final IGLog log = new DefaultGLog(global); - public static ConsoleHandler handler; - - public static void initialize(File mainLogDir) - throws SecurityException, IOException { - if (initialized.getAndSet(true)) - return; - FileLogFormatter loc = new FileLogFormatter(); - if (mainLogDir != null) { - // Ensure the log directory exists - if (!mainLogDir.exists()) - mainLogDir.mkdirs(); - - FileLogHandler fileHandler = new FileLogHandler( - mainLogDir.toPath(), - "slimevr-server", - DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"), - 25 * 1000000, // 25 MB max file size - 8, // 8 max log files - 100 * 1000000 // 100 MB max collective size - ); - fileHandler.setFormatter(loc); - global.addHandler(fileHandler); - } - } - - public static void replaceMainHandler(ConsoleHandler newHandler) { - handler.close(); - global.removeHandler(handler); - handler = newHandler; - global.addHandler(newHandler); - } - - public static void addHandler(Handler add) { - global.addHandler(add); - } - - public static void removeHandler(Handler remove) { - global.removeHandler(remove); - } - - public static void enablePreciseTimestamp() { - handler.setFormatter(new PreciseConsoleLogFormatter()); - } - - public static void info(String message) { - log.info(message); - } - - public static void severe(String message) { - log.severe(message); - } - - public static void warning(String message) { - log.warning(message); - } - - public static void debug(String message) { - log.debug(message); - } - - public static void info(String message, Throwable t) { - log.info(message, t); - } - - public static void severe(String message, Throwable t) { - log.severe(message, t); - } - - public static void warning(String message, Throwable t) { - log.warning(message, t); - } - - public static void debug(String message, Throwable t) { - log.debug(message, t); - } - - public static void log(Level level, String message) { - log.log(level, message); - } - - public static void log(Level level, String message, Throwable t) { - log.log(level, message, t); - } - - public static void closeLogger() { - for (Handler handler : global.getHandlers()) { - handler.close(); - removeHandler(handler); - } - } - - static { - boolean hasConsoleHandler = false; - for (Handler h : global.getHandlers()) { - if (h instanceof ConsoleHandler) { - handler = (ConsoleHandler) h; - hasConsoleHandler = true; - } - } - if (!hasConsoleHandler) { - handler = new ConsoleHandler(); - global.addHandler(handler); - } - handler.setFormatter(new ShortConsoleLogFormatter()); - - System.setOut(new PrintStream(new LoggerOutputStream(log, Level.INFO), true)); - System.setErr(new PrintStream(new LoggerOutputStream(log, Level.SEVERE), true)); - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/LoggerOutputStream.java b/server/core/src/main/java/io/eiren/util/logging/LoggerOutputStream.java deleted file mode 100644 index 8527e637bd..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/LoggerOutputStream.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.eiren.util.logging; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.logging.Level; - - -public class LoggerOutputStream extends ByteArrayOutputStream { - - private static final String separator = System.getProperty("line.separator"); - - private final IGLog logger; - private final Level level; - private final String prefix; - private final StringBuilder buffer = new StringBuilder(); - - public LoggerOutputStream(IGLog logger, Level level) { - this(logger, level, ""); - } - - public LoggerOutputStream(IGLog logger, Level level, String prefix) { - super(); - this.logger = logger; - this.level = level; - this.prefix = prefix; - } - - @Override - public void flush() throws IOException { - synchronized (this) { - super.flush(); - String record = this.toString(); - super.reset(); - if (record.length() > 0) { - buffer.append(record); - if (record.contains(separator)) { - String s = buffer.toString(); - String[] split = s.split(separator); - for (String value : split) - logger.log(level, prefix + value); - buffer.setLength(0); - // buffer.append(split[split.length - 1]); - } - } - } - } - - @Override - public void close() throws IOException { - flush(); - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/LoggerRecorder.java b/server/core/src/main/java/io/eiren/util/logging/LoggerRecorder.java deleted file mode 100644 index 589eca2365..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/LoggerRecorder.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.eiren.util.logging; - -import java.util.List; - -import io.eiren.util.collections.FastList; -import io.eiren.util.logging.DefaultGLog.LogEntry; - - -public class LoggerRecorder { - - private final List recorded = new FastList<>(); - - public LoggerRecorder() { - } - - public synchronized void addEntry(LogEntry e) { - recorded.add(e); - } - - public List getEntries() { - return recorded; - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/PreciseConsoleLogFormatter.java b/server/core/src/main/java/io/eiren/util/logging/PreciseConsoleLogFormatter.java deleted file mode 100644 index 69abe91ddd..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/PreciseConsoleLogFormatter.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.eiren.util.logging; - -import java.text.SimpleDateFormat; -import java.util.logging.LogRecord; - - -/** - * Format message timestamp as time passed from the start with milliseconds. - */ -public class PreciseConsoleLogFormatter extends ShortConsoleLogFormatter { - - private final long startMills; - - public PreciseConsoleLogFormatter() { - startMills = System.currentTimeMillis(); - } - - @Override - protected SimpleDateFormat createDateFormat() { - return new SimpleDateFormat("mm:ss.SSS"); - } - - @Override - protected void buildMessage(StringBuilder builder, LogRecord record) { - builder.append(date.format(record.getMillis() - startMills)); - builder.append(" ["); - builder.append(record.getLevel().getLocalizedName().toUpperCase()); - builder.append("] "); - builder.append(record.getMessage()); - builder.append('\n'); - } -} diff --git a/server/core/src/main/java/io/eiren/util/logging/ShortConsoleLogFormatter.java b/server/core/src/main/java/io/eiren/util/logging/ShortConsoleLogFormatter.java deleted file mode 100644 index 566e4722b7..0000000000 --- a/server/core/src/main/java/io/eiren/util/logging/ShortConsoleLogFormatter.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.eiren.util.logging; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.text.SimpleDateFormat; -import java.util.logging.Formatter; -import java.util.logging.LogRecord; - - -public class ShortConsoleLogFormatter extends Formatter { - - protected final SimpleDateFormat date; - - public ShortConsoleLogFormatter() { - this.date = createDateFormat(); - } - - protected SimpleDateFormat createDateFormat() { - return new SimpleDateFormat("HH:mm:ss"); - } - - protected void buildMessage(StringBuilder builder, LogRecord record) { - builder.append(date.format(record.getMillis())); - builder.append(" ["); - builder.append(record.getLevel().getLocalizedName().toUpperCase()); - builder.append("] "); - builder.append(record.getMessage()); - builder.append('\n'); - } - - @Override - public String format(LogRecord record) { - StringBuilder builder = new StringBuilder(); - Throwable ex = record.getThrown(); - - buildMessage(builder, record); - - if (ex != null) { - StringWriter writer = new StringWriter(); - ex.printStackTrace(new PrintWriter(writer)); - builder.append(writer); - } - - String message = builder.toString(); - Object[] parameters = record.getParameters(); - if (parameters == null || parameters.length == 0) - return message; - if ( - message.contains("{0") - || message.contains("{1") - || message.contains("{2") - || message.contains("{3") - ) - return java.text.MessageFormat.format(message, parameters); - return message; - } - -} diff --git a/server/core/src/main/java/io/github/axisangles/ktmath/EulerAngles.kt b/server/core/src/main/java/io/github/axisangles/ktmath/EulerAngles.kt index 98dfac676b..1d52bd01dd 100644 --- a/server/core/src/main/java/io/github/axisangles/ktmath/EulerAngles.kt +++ b/server/core/src/main/java/io/github/axisangles/ktmath/EulerAngles.kt @@ -8,9 +8,8 @@ import kotlin.math.sin enum class EulerOrder { XYZ, YZX, ZXY, ZYX, YXZ, XZY } -@JvmInline @Serializable -value class EulerAngles(val order: EulerOrder, val x: Float, val y: Float, val z: Float) { +class EulerAngles(val order: EulerOrder, val x: Float, val y: Float, val z: Float) { operator fun component1(): EulerOrder = order operator fun component2(): Float = x operator fun component3(): Float = y @@ -73,6 +72,22 @@ value class EulerAngles(val order: EulerOrder, val x: Float, val y: Float, val z } } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EulerAngles) return false + return order == other.order && x == other.x && y == other.y && z == other.z + } + + override fun hashCode(): Int { + var result = order.hashCode() + result = 31 * result + x.hashCode() + result = 31 * result + y.hashCode() + result = 31 * result + z.hashCode() + return result + } + + override fun toString(): String = "EulerAngles(order=$order, x=$x, y=$y, z=$z)" + // temp, replace with direct conversion later // fun toMatrix(): Matrix3 = this.toQuaternion().toMatrix() diff --git a/server/core/src/main/java/io/github/axisangles/ktmath/Matrix3.kt b/server/core/src/main/java/io/github/axisangles/ktmath/Matrix3.kt index 5d787fc3b9..ea015ea223 100644 --- a/server/core/src/main/java/io/github/axisangles/ktmath/Matrix3.kt +++ b/server/core/src/main/java/io/github/axisangles/ktmath/Matrix3.kt @@ -5,9 +5,8 @@ package io.github.axisangles.ktmath import kotlinx.serialization.Serializable import kotlin.math.* -@JvmInline @Serializable -value class Matrix3 +class Matrix3 @Suppress("ktlint") constructor( val xx: Float, val yx: Float, val zx: Float, val xy: Float, val yy: Float, val zy: Float, @@ -462,6 +461,29 @@ value class Matrix3 orthonormalize().toEulerAnglesAssumingOrthonormal(order) fun toObject() = ObjectMatrix3(xx, yx, zx, xy, yy, zy, xz, yz, zz) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Matrix3) return false + return xx == other.xx && yx == other.yx && zx == other.zx && + xy == other.xy && yy == other.yy && zy == other.zy && + xz == other.xz && yz == other.yz && zz == other.zz + } + + override fun hashCode(): Int { + var result = xx.hashCode() + result = 31 * result + yx.hashCode() + result = 31 * result + zx.hashCode() + result = 31 * result + xy.hashCode() + result = 31 * result + yy.hashCode() + result = 31 * result + zy.hashCode() + result = 31 * result + xz.hashCode() + result = 31 * result + yz.hashCode() + result = 31 * result + zz.hashCode() + return result + } + + override fun toString(): String = "Matrix3([$xx, $yx, $zx], [$xy, $yy, $zy], [$xz, $yz, $zz])" } data class ObjectMatrix3( @@ -474,4 +496,4 @@ data class ObjectMatrix3( operator fun Float.times(that: Matrix3): Matrix3 = that * this -operator fun Float.div(that: Matrix3): Matrix3 = that.inv() * this \ No newline at end of file +operator fun Float.div(that: Matrix3): Matrix3 = that.inv() * this diff --git a/server/core/src/main/java/io/github/axisangles/ktmath/Quaternion.kt b/server/core/src/main/java/io/github/axisangles/ktmath/Quaternion.kt index 1f4cccb398..9c498fc60b 100644 --- a/server/core/src/main/java/io/github/axisangles/ktmath/Quaternion.kt +++ b/server/core/src/main/java/io/github/axisangles/ktmath/Quaternion.kt @@ -5,9 +5,8 @@ package io.github.axisangles.ktmath import kotlinx.serialization.Serializable import kotlin.math.* -@JvmInline @Serializable -value class Quaternion(val w: Float, val x: Float, val y: Float, val z: Float) { +class Quaternion(val w: Float, val x: Float, val y: Float, val z: Float) { companion object { val NULL = Quaternion(0f, 0f, 0f, 0f) val IDENTITY = Quaternion(1f, 0f, 0f, 0f) @@ -493,6 +492,22 @@ value class Quaternion(val w: Float, val x: Float, val y: Float, val z: Float) { fun toEulerAngles(order: EulerOrder): EulerAngles = this.toMatrix().toEulerAnglesAssumingOrthonormal(order) fun toObject() = ObjectQuaternion(w, x, y, z) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Quaternion) return false + return w == other.w && x == other.x && y == other.y && z == other.z + } + + override fun hashCode(): Int { + var result = w.hashCode() + result = 31 * result + x.hashCode() + result = 31 * result + y.hashCode() + result = 31 * result + z.hashCode() + return result + } + + override fun toString(): String = "Quaternion(w=$w, x=$x, y=$y, z=$z)" } data class ObjectQuaternion(val w: Float, val x: Float, val y: Float, val z: Float) { diff --git a/server/core/src/main/java/io/github/axisangles/ktmath/Vector3.kt b/server/core/src/main/java/io/github/axisangles/ktmath/Vector3.kt index 1b1d8481f3..32d2750612 100644 --- a/server/core/src/main/java/io/github/axisangles/ktmath/Vector3.kt +++ b/server/core/src/main/java/io/github/axisangles/ktmath/Vector3.kt @@ -5,9 +5,8 @@ package io.github.axisangles.ktmath import kotlinx.serialization.Serializable import kotlin.math.* -@JvmInline @Serializable -value class Vector3(val x: Float, val y: Float, val z: Float) { +class Vector3(val x: Float, val y: Float, val z: Float) { companion object { val NULL = Vector3(0f, 0f, 0f) val POS_X = Vector3(1f, 0f, 0f) @@ -101,6 +100,21 @@ value class Vector3(val x: Float, val y: Float, val z: Float) { fun angleTo(that: Vector3): Float = atan2(this.cross(that).len(), this.dot(that)) fun isNear(other: Vector3, maxError: Float = 1e-6f) = abs(x - other.x) <= maxError && abs(y - other.y) <= maxError && abs(z - other.z) <= maxError + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Vector3) return false + return x == other.x && y == other.y && z == other.z + } + + override fun hashCode(): Int { + var result = x.hashCode() + result = 31 * result + y.hashCode() + result = 31 * result + z.hashCode() + return result + } + + override fun toString(): String = "Vector3(x=$x, y=$y, z=$z)" } operator fun Float.times(that: Vector3): Vector3 = that * this diff --git a/server/core/src/test/java/dev/slimevr/TestServer.kt b/server/core/src/test/java/dev/slimevr/TestServer.kt new file mode 100644 index 0000000000..abbd03e79b --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/TestServer.kt @@ -0,0 +1,81 @@ +package dev.slimevr + +import dev.llelievr.espflashkotlin.FlasherSerialInterface +import dev.slimevr.config.AppConfig +import dev.slimevr.config.DefaultUserBehaviour +import dev.slimevr.config.UserConfig +import dev.slimevr.config.UserConfigData +import dev.slimevr.config.UserConfigState +import dev.slimevr.context.Context +import dev.slimevr.firmware.FirmwareManager +import dev.slimevr.heightcalibration.HeightCalibrationManager +import dev.slimevr.provisioning.ProvisioningManager +import dev.slimevr.serial.SerialPortHandle +import dev.slimevr.serial.SerialServer +import dev.slimevr.skeleton.DEFAULT_SKELETON_STATE +import dev.slimevr.skeleton.ProportionsBehaviour +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.skeleton.buildBones +import dev.slimevr.trackingchecklist.TrackingChecklist +import dev.slimevr.udp.UdpServer +import dev.slimevr.vrchat.VRCConfigManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import java.nio.file.Files + +fun buildTestSerialServer(scope: CoroutineScope) = SerialServer.create( + openPort = { loc, _, _ -> SerialPortHandle(loc, "Fake $loc", {}, {}) }, + openFlashingPort = { + object : FlasherSerialInterface { + override fun openSerial(port: Any) = Unit + override fun closeSerial() = Unit + override fun write(data: ByteArray) = Unit + override fun read(length: Int) = ByteArray(length) + override fun setDTR(value: Boolean) = Unit + override fun setRTS(value: Boolean) = Unit + override fun changeBaud(baud: Int) = Unit + override fun setReadTimeout(timeout: Long) = Unit + override fun availableBytes() = 0 + override fun flushIOBuffers() = Unit + } + }, + scope = scope, +) + +fun buildTestVrServer(scope: CoroutineScope): VRServer = VRServer.create(scope) + +fun buildTestUserConfig(scope: CoroutineScope): UserConfig { + val tempDir = Files.createTempDirectory("slimevr-test").toFile() + tempDir.deleteOnExit() + val context = Context.create( + initialState = UserConfigState(data = UserConfigData(), name = "test"), + scope = scope, + behaviours = listOf(DefaultUserBehaviour), + ) + val userConfig = UserConfig(context, scope = scope, userConfigDir = tempDir) + context.observeAll(userConfig) + return userConfig +} + +fun buildTestSkeleton(scope: CoroutineScope): Skeleton { + val context = Context.create( + initialState = DEFAULT_SKELETON_STATE, + scope = scope, + behaviours = listOf(ProportionsBehaviour()), + ) + val skeleton = Skeleton(context, MutableStateFlow(buildBones(context.state.value))) + skeleton.startObserving() + return skeleton +} + +abstract class TestAppContext : AppContextProvider { + override val config: AppConfig get() = error("not used in test") + override val serialServer: SerialServer get() = error("not used in test") + override val firmwareManager: FirmwareManager get() = error("not used in test") + override val vrcConfigManager: VRCConfigManager? = null + override val provisioningManager: ProvisioningManager get() = error("not used in test") + override val heightCalibrationManager: HeightCalibrationManager get() = error("not used in test") + override val trackingChecklist: TrackingChecklist get() = error("not used in test") + override val udpServer: UdpServer get() = error("not used in test") + override fun startObserving() {} +} diff --git a/server/core/src/test/java/dev/slimevr/firmware/DoSerialFlashTest.kt b/server/core/src/test/java/dev/slimevr/firmware/DoSerialFlashTest.kt new file mode 100644 index 0000000000..faaaa95c8e --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/firmware/DoSerialFlashTest.kt @@ -0,0 +1,314 @@ +package dev.slimevr.firmware + +import dev.llelievr.espflashkotlin.FlasherSerialInterface +import dev.slimevr.VRServer +import dev.slimevr.VRServerActions +import dev.slimevr.device.Device +import dev.slimevr.device.DeviceActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.serial.SerialPortHandle +import dev.slimevr.serial.SerialPortInfo +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.datatypes.hardware_info.BoardType +import solarxr_protocol.datatypes.hardware_info.McuType +import solarxr_protocol.rpc.FirmwareUpdateStatus +import kotlin.test.Test +import kotlin.test.assertEquals + +private fun fakePortHandle(loc: String) = SerialPortHandle( + portLocation = loc, + descriptivePortName = "Fake $loc", + writeCommand = {}, + close = {}, +) + +private fun fakePort(loc: String = "COM1") = SerialPortInfo(loc, "Fake $loc", 0x1A86, 0x7523) + +/** Fails immediately at openSerial so the Flasher throws with no IO delays */ +private fun failingFlashHandler() = object : FlasherSerialInterface { + override fun openSerial(port: Any) = error("simulated flash failure") + override fun closeSerial() {} + override fun write(data: ByteArray) {} + override fun read(length: Int) = ByteArray(length) + override fun setDTR(value: Boolean) {} + override fun setRTS(value: Boolean) {} + override fun changeBaud(baud: Int) {} + override fun setReadTimeout(timeout: Long) {} + override fun availableBytes() = 0 + override fun flushIOBuffers() {} +} + +private fun buildSerialServer( + scope: kotlinx.coroutines.CoroutineScope, + flashHandler: () -> FlasherSerialInterface = ::failingFlashHandler, +) = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = flashHandler, + scope = scope, +) + +// VRServer's BaseBehaviour sets up infinite StateFlow collectors via launchIn(scope). +// backgroundScope lets those run on the test scheduler but doesn't cause +// UncompletedCoroutinesError when the test ends. +private fun buildVrServer( + backgroundScope: kotlinx.coroutines.CoroutineScope, +): VRServer = VRServer.create(backgroundScope) + +class DoSerialFlashTest { + + @Test + fun `emits ERROR_DEVICE_NOT_FOUND when port is not available`() = runTest { + val server = buildSerialServer(this) + val statuses = mutableListOf() + + doSerialFlash( + portLocation = "COM1", + parts = emptyList(), + needManualReboot = false, + ssid = null, + password = null, + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + scope = this, + ) + + assertEquals(FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, statuses.last()) + } + + @Test + fun `emits ERROR_DEVICE_NOT_FOUND when port already has a connection`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val statuses = mutableListOf() + + doSerialFlash( + portLocation = "COM1", + parts = emptyList(), + needManualReboot = false, + ssid = null, + password = null, + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + scope = this, + ) + + assertEquals(FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, statuses.last()) + } + + @Test + fun `emits ERROR_UPLOAD_FAILED when flash throws`() = runTest { + val server = buildSerialServer(this, ::failingFlashHandler) + server.onPortDetected(fakePort()) + val statuses = mutableListOf() + + doSerialFlash( + portLocation = "COM1", + parts = emptyList(), + needManualReboot = false, + ssid = null, + password = null, + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + scope = this, + ) + + assertEquals(FirmwareUpdateStatus.ERROR_UPLOAD_FAILED, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits ERROR_DEVICE_NOT_FOUND when device has not reconnected after flash`() = runTest { + // Port not back in availablePorts yet, openConnection inside doSerialFlashPostFlash is a no-op + val server = buildSerialServer(this) + val statuses = mutableListOf() + + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = "wifi", + password = "pass", + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + ) + + assertEquals(FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits ERROR_PROVISIONING_FAILED when MAC not received within timeout`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val statuses = mutableListOf() + + val job = launch { + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = "wifi", + password = "pass", + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + ) + } + + advanceTimeBy(10_001) + job.join() + + assertEquals(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits ERROR_PROVISIONING_FAILED when ssid or password is null`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val statuses = mutableListOf() + + launch { + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = null, + password = null, + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + ) + } + + launch { + delay(100) + server.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + } + + advanceUntilIdle() + + assertEquals(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits ERROR_PROVISIONING_FAILED when wifi does not connect within timeout`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val statuses = mutableListOf() + + val job = launch { + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = "wifi", + password = "pass", + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + ) + } + + launch { + delay(100) + server.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + } + + // MAC arrives at 100ms; wifi timeout fires 30s later + advanceTimeBy(30_101) + job.join() + + assertEquals(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits ERROR_TIMEOUT when tracker does not appear within timeout`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val statuses = mutableListOf() + + val job = launch { + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = "wifi", + password = "pass", + serialServer = server, + server = buildVrServer(backgroundScope), + onStatus = { s, _ -> statuses += s }, + ) + } + + launch { + delay(100) + server.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(200) + server.onDataReceived("COM1", "looking for the server") + } + + // MAC at 100ms, wifi log at 200ms; tracker timeout fires 60s after wifi confirmed + advanceTimeBy(60_201) + job.join() + + assertEquals(FirmwareUpdateStatus.ERROR_TIMEOUT, statuses.last()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `emits DONE when everything succeeds`() = runTest { + val server = buildSerialServer(this) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + val vrServer = buildVrServer(backgroundScope) + val statuses = mutableListOf() + + launch { + doSerialFlashPostFlash( + portLocation = "COM1", + needManualReboot = false, + ssid = "wifi", + password = "pass", + serialServer = server, + server = vrServer, + onStatus = { s, _ -> statuses += s }, + ) + } + + launch { + delay(100) + server.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(200) + server.onDataReceived("COM1", "looking for the server") + delay(300) + val device = Device.create( + backgroundScope, + id = vrServer.nextHandle(), + address = "192.168.1.100", + macAddress = "AA:BB:CC:DD:EE:FF", + origin = DeviceOrigin.UDP, + protocolVersion = 0, + ) + vrServer.context.dispatch(VRServerActions.NewDevice(device.context.state.value.id, device)) + device.context.dispatch(DeviceActions.Update { copy(status = TrackerStatus.OK) }) + } + + advanceUntilIdle() + + assertEquals(FirmwareUpdateStatus.DONE, statuses.last()) + } +} diff --git a/server/core/src/test/java/dev/slimevr/firmware/reducers/FirmwareManagerReducerTest.kt b/server/core/src/test/java/dev/slimevr/firmware/reducers/FirmwareManagerReducerTest.kt new file mode 100644 index 0000000000..4cfd216712 --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/firmware/reducers/FirmwareManagerReducerTest.kt @@ -0,0 +1,91 @@ +package dev.slimevr.firmware.reducers + +import dev.slimevr.context.Context +import dev.slimevr.firmware.FirmwareJobStatus +import dev.slimevr.firmware.FirmwareManagerActions +import dev.slimevr.firmware.FirmwareManagerBaseBehaviour +import dev.slimevr.firmware.FirmwareManagerState +import kotlinx.coroutines.test.runTest +import solarxr_protocol.rpc.FirmwareUpdateStatus +import solarxr_protocol.rpc.SerialDevicePort +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +private fun serialJob(port: String, status: FirmwareUpdateStatus, progress: Int = 0) = FirmwareManagerActions.UpdateJob( + FirmwareJobStatus( + portLocation = port, + firmwareDeviceId = SerialDevicePort(port = port), + status = status, + progress = progress, + ), +) + +class FirmwareManagerReducerTest { + private fun makeContext(scope: kotlinx.coroutines.CoroutineScope) = Context.create( + initialState = FirmwareManagerState(jobs = mapOf()), + behaviours = listOf(FirmwareManagerBaseBehaviour), + scope = scope, + ) + + @Test + fun `UpdateJob adds a new job`() = runTest { + val context = makeContext(this) + + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.UPLOADING, 42)) + + val job = context.state.value.jobs["COM1"] + assertNotNull(job) + assertEquals(FirmwareUpdateStatus.UPLOADING, job.status) + assertEquals(42, job.progress) + } + + @Test + fun `UpdateJob replaces an existing job`() = runTest { + val context = makeContext(this) + + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.DOWNLOADING)) + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.UPLOADING, 75)) + + val job = context.state.value.jobs["COM1"] + assertNotNull(job) + assertEquals(FirmwareUpdateStatus.UPLOADING, job.status) + assertEquals(75, job.progress) + assertEquals(1, context.state.value.jobs.size) + } + + @Test + fun `UpdateJob tracks multiple ports independently`() = runTest { + val context = makeContext(this) + + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.UPLOADING, 10)) + context.dispatch(serialJob("COM2", FirmwareUpdateStatus.DOWNLOADING)) + + assertEquals(2, context.state.value.jobs.size) + assertEquals(FirmwareUpdateStatus.UPLOADING, context.state.value.jobs["COM1"]?.status) + assertEquals(FirmwareUpdateStatus.DOWNLOADING, context.state.value.jobs["COM2"]?.status) + } + + @Test + fun `RemoveJob removes an existing job`() = runTest { + val context = makeContext(this) + + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.UPLOADING, 50)) + context.dispatch(FirmwareManagerActions.RemoveJob("COM1")) + + assertNull(context.state.value.jobs["COM1"]) + assertTrue(context.state.value.jobs.isEmpty()) + } + + @Test + fun `RemoveJob on unknown port is a no-op`() = runTest { + val context = makeContext(this) + + context.dispatch(serialJob("COM1", FirmwareUpdateStatus.UPLOADING, 50)) + context.dispatch(FirmwareManagerActions.RemoveJob("COM2")) + + assertEquals(1, context.state.value.jobs.size) + } +} diff --git a/server/core/src/test/java/dev/slimevr/heightcalibration/HeightCalibrationTest.kt b/server/core/src/test/java/dev/slimevr/heightcalibration/HeightCalibrationTest.kt new file mode 100644 index 0000000000..810b1bcb04 --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/heightcalibration/HeightCalibrationTest.kt @@ -0,0 +1,360 @@ +@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class, kotlinx.coroutines.FlowPreview::class) + +package dev.slimevr.heightcalibration + +import dev.slimevr.buildTestUserConfig +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import solarxr_protocol.rpc.UserHeightCalibrationStatus +import kotlin.test.Test +import kotlin.test.assertEquals + +// Stability durations in milliseconds (converted from nanosecond constants) +private const val FLOOR_STABILITY_MS = CONTROLLER_STABILITY_DURATION / 1_000_000L +private const val HEIGHT_STABILITY_MS = HEAD_STABILITY_DURATION / 1_000_000L + +// Rotation that maps controller forward (-Z) to down (-Y), satisfying the pointing-down check +private val POINTING_DOWN = Quaternion.fromTo(Vector3.NEG_Z, Vector3.NEG_Y) + +// Identity: controller forward is -Z, not pointing down +private val POINTING_FORWARD = Quaternion.IDENTITY + +// Identity: HMD up is +Y, within 15° leveled threshold +private val HMD_LEVEL = Quaternion.IDENTITY + +// 90° around Z: HMD up maps to +X, failing the leveled check +private val HMD_TILTED = Quaternion.fromTo(Vector3.POS_Y, Vector3.POS_X) + +// Position just below the floor threshold (0.10m) +private val FLOOR_POSITION = Vector3(0f, 0.05f, 0f) + +// Position comfortably above rise threshold (1.2m) with zero floor level +private val STANDING_POSITION = Vector3(0f, 1.7f, 0f) + +private fun makeContext(scope: kotlinx.coroutines.CoroutineScope) = HeightCalibrationContext.create( + initialState = INITIAL_HEIGHT_CALIBRATION_STATE, + scope = scope, + behaviours = listOf(CalibrationBehaviour), +) + +// Launches a calibration session with the virtual-time clock, so that advancing virtual time +// drives both the sample() operator and the stability duration checks in one unified timeline. +private fun TestScope.launchSession( + context: HeightCalibrationContext, + hmdFlow: MutableSharedFlow, + controllerFlow: MutableSharedFlow, +): Job { + val scope = this + val userConfig = buildTestUserConfig(backgroundScope) + return launch { + runCalibrationSession(context, userConfig, hmdFlow, controllerFlow, clock = { scope.currentTime * 1_000_000L }) + } +} + +// Emits snapshot at SAMPLE_INTERVAL_MS rate for the given duration of virtual time, +// then does one extra advance to let any pending coroutine resumptions finish. +private suspend fun TestScope.emitFor( + flow: MutableSharedFlow, + snapshot: TrackerSnapshot, + durationMs: Long, +) { + val end = currentTime + durationMs + while (currentTime < end) { + flow.emit(snapshot) + advanceTimeBy(SAMPLE_INTERVAL_MS) + } + advanceTimeBy(SAMPLE_INTERVAL_MS) +} + +// Holds the controller steady on the floor long enough for the floor phase to complete. +private suspend fun TestScope.completeFloorPhase( + controllerFlow: MutableSharedFlow, + floorSnapshot: TrackerSnapshot, +) { + emitFor(controllerFlow, floorSnapshot, FLOOR_STABILITY_MS + SAMPLE_INTERVAL_MS * 5) +} + +// Holds the HMD steady at standing height long enough for the height phase to complete. +private suspend fun TestScope.completeHeightPhase( + hmdFlow: MutableSharedFlow, + hmdSnapshot: TrackerSnapshot, +) { + emitFor(hmdFlow, hmdSnapshot, HEIGHT_STABILITY_MS + SAMPLE_INTERVAL_MS * 5) +} + +class HeightCalibrationReducerTest { + @Test + fun `Update changes status and height`() = runTest { + val context = makeContext(this) + + context.dispatch(HeightCalibrationActions.Update(UserHeightCalibrationStatus.RECORDING_HEIGHT, 1.65f)) + + assertEquals(UserHeightCalibrationStatus.RECORDING_HEIGHT, context.state.value.status) + assertEquals(1.65f, context.state.value.currentHeight) + } +} + +class HeightCalibrationSessionTest { + @Test + fun `session starts in RECORDING_FLOOR`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + assertEquals(UserHeightCalibrationStatus.RECORDING_FLOOR, context.state.value.status) + job.cancel() + } + + @Test + fun `controller too high does not change status`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + emitFor(controllerFlow, TrackerSnapshot(Vector3(0f, 0.5f, 0f), POINTING_DOWN), SAMPLE_INTERVAL_MS) + + assertEquals(UserHeightCalibrationStatus.RECORDING_FLOOR, context.state.value.status) + job.cancel() + } + + @Test + fun `controller not pointing down transitions to WAITING_FOR_CONTROLLER_PITCH`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + emitFor(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_FORWARD), SAMPLE_INTERVAL_MS) + + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_CONTROLLER_PITCH, context.state.value.status) + job.cancel() + } + + @Test + fun `stable floor transitions to WAITING_FOR_RISE`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_RISE, context.state.value.status) + job.cancel() + } + + @Test + fun `HMD below rise threshold stays WAITING_FOR_RISE`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + emitFor(hmdFlow, TrackerSnapshot(Vector3(0f, 0.5f, 0f), HMD_LEVEL), SAMPLE_INTERVAL_MS) + + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_RISE, context.state.value.status) + job.cancel() + } + + @Test + fun `HMD not leveled transitions to WAITING_FOR_FW_LOOK`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + emitFor(hmdFlow, TrackerSnapshot(STANDING_POSITION, HMD_TILTED), SAMPLE_INTERVAL_MS) + + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK, context.state.value.status) + job.cancel() + } + + @Test + fun `stable HMD at valid height transitions to DONE`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + completeHeightPhase(hmdFlow, TrackerSnapshot(STANDING_POSITION, HMD_LEVEL)) + + assertEquals(UserHeightCalibrationStatus.DONE, context.state.value.status) + assertEquals(STANDING_POSITION.y - FLOOR_POSITION.y, context.state.value.currentHeight) + job.cancel() + } + + @Test + fun `stable HMD below HEIGHT_MIN transitions to ERROR_TOO_SMALL`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(Vector3(0f, 0f, 0f), POINTING_DOWN)) + completeHeightPhase(hmdFlow, TrackerSnapshot(Vector3(0f, 1.3f, 0f), HMD_LEVEL)) + + assertEquals(UserHeightCalibrationStatus.ERROR_TOO_SMALL, context.state.value.status) + job.cancel() + } + + @Test + fun `stable HMD above HEIGHT_MAX transitions to ERROR_TOO_HIGH`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(Vector3(0f, 0f, 0f), POINTING_DOWN)) + completeHeightPhase(hmdFlow, TrackerSnapshot(Vector3(0f, 2.0f, 0f), HMD_LEVEL)) + + assertEquals(UserHeightCalibrationStatus.ERROR_TOO_HIGH, context.state.value.status) + job.cancel() + } + + @Test + fun `unstable floor sample resets controller stability window`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + val stableSnapshot = TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN) + // X offset of 1m pushes energy well above CONTROLLER_STABILITY_THRESHOLD + val unstableSnapshot = TrackerSnapshot(Vector3(1f, FLOOR_POSITION.y, 0f), POINTING_DOWN) + + // Build up stability but not long enough to complete + emitFor(controllerFlow, stableSnapshot, FLOOR_STABILITY_MS - SAMPLE_INTERVAL_MS * 5) + assertEquals(UserHeightCalibrationStatus.RECORDING_FLOOR, context.state.value.status) + + // Unstable sample resets the stability window + emitFor(controllerFlow, unstableSnapshot, SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.RECORDING_FLOOR, context.state.value.status) + + // Must hold stable for the full duration again from the reset point + completeFloorPhase(controllerFlow, stableSnapshot) + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_RISE, context.state.value.status) + job.cancel() + } + + @Test + fun `out-of-threshold HMD sample resets height stability window`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + val stableSnapshot = TrackerSnapshot(STANDING_POSITION, HMD_LEVEL) + val unstableSnapshot = TrackerSnapshot(Vector3(0f, 1.9f, 0f), HMD_LEVEL) + + // Build up stability but not long enough to complete + emitFor(hmdFlow, stableSnapshot, HEIGHT_STABILITY_MS - SAMPLE_INTERVAL_MS * 5) + assertEquals(UserHeightCalibrationStatus.RECORDING_HEIGHT, context.state.value.status) + + // Unstable sample resets the stability window + emitFor(hmdFlow, unstableSnapshot, SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.RECORDING_HEIGHT, context.state.value.status) + + // Must hold stable for the full duration again from the reset point + completeHeightPhase(hmdFlow, stableSnapshot) + assertEquals(UserHeightCalibrationStatus.DONE, context.state.value.status) + job.cancel() + } + + @Test + fun `timeout fires ERROR_TIMEOUT`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launch { runCalibrationSession(context, buildTestUserConfig(backgroundScope), hmdFlow, controllerFlow) } + + advanceTimeBy(TIMEOUT_MS + 1) + + assertEquals(UserHeightCalibrationStatus.ERROR_TIMEOUT, context.state.value.status) + job.cancel() + } + + @Test + fun `controller pitch recovery leads to WAITING_FOR_RISE`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + // Bad pitch triggers WAITING_FOR_CONTROLLER_PITCH + emitFor(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_FORWARD), SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_CONTROLLER_PITCH, context.state.value.status) + + // Recovery: hold steady on floor for the required duration + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_RISE, context.state.value.status) + job.cancel() + } + + @Test + fun `leveling HMD after WAITING_FOR_FW_LOOK transitions to RECORDING_HEIGHT`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + // Tilted HMD + emitFor(hmdFlow, TrackerSnapshot(STANDING_POSITION, HMD_TILTED), SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_FW_LOOK, context.state.value.status) + + // Recovery: level the HMD + emitFor(hmdFlow, TrackerSnapshot(STANDING_POSITION, HMD_LEVEL), SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.RECORDING_HEIGHT, context.state.value.status) + job.cancel() + } + + @Test + fun `HMD rising above threshold transitions to RECORDING_HEIGHT`() = runTest { + val context = makeContext(this) + val controllerFlow = MutableSharedFlow(extraBufferCapacity = 1) + val hmdFlow = MutableSharedFlow(extraBufferCapacity = 1) + val job = launchSession(context, hmdFlow, controllerFlow) + advanceTimeBy(SAMPLE_INTERVAL_MS) + + completeFloorPhase(controllerFlow, TrackerSnapshot(FLOOR_POSITION, POINTING_DOWN)) + + // HMD below threshold + emitFor(hmdFlow, TrackerSnapshot(Vector3(0f, 0.5f, 0f), HMD_LEVEL), SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.WAITING_FOR_RISE, context.state.value.status) + + // HMD rises above threshold + emitFor(hmdFlow, TrackerSnapshot(STANDING_POSITION, HMD_LEVEL), SAMPLE_INTERVAL_MS) + assertEquals(UserHeightCalibrationStatus.RECORDING_HEIGHT, context.state.value.status) + job.cancel() + } +} diff --git a/server/core/src/test/java/dev/slimevr/provisioning/ProvisioningManagerTest.kt b/server/core/src/test/java/dev/slimevr/provisioning/ProvisioningManagerTest.kt new file mode 100644 index 0000000000..7fa4a4c803 --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/provisioning/ProvisioningManagerTest.kt @@ -0,0 +1,303 @@ +package dev.slimevr.provisioning + +import dev.slimevr.VRServer +import dev.slimevr.VRServerActions +import dev.slimevr.buildTestSerialServer +import dev.slimevr.buildTestVrServer +import dev.slimevr.context.Context +import dev.slimevr.device.Device +import dev.slimevr.device.DeviceActions +import dev.slimevr.device.DeviceOrigin +import dev.slimevr.serial.SerialPortInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import solarxr_protocol.datatypes.TrackerStatus +import solarxr_protocol.rpc.WifiProvisioningStatus +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +private fun fakePort(loc: String = "COM1") = SerialPortInfo(loc, "Fake $loc", 0x1A86, 0x7523) + +private fun buildManager(serialServer: dev.slimevr.serial.SerialServer, scope: CoroutineScope): ProvisioningManager { + val context = Context.create( + initialState = ProvisioningManager.INITIAL_STATE, + scope = scope, + behaviours = listOf(ProvisioningManagerBaseBehaviour), + ) + return ProvisioningManager(context = context, serialServer = serialServer, scope = scope).also { it.startObserving() } +} + +// Injects a fully connected device into the VRServer, simulating a tracker appearing on the network. +private fun connectDevice(vrServer: VRServer, mac: String, scope: CoroutineScope) { + val device = Device.create( + scope, + id = vrServer.nextHandle(), + address = "192.168.1.100", + macAddress = mac, + origin = DeviceOrigin.UDP, + protocolVersion = 0, + ) + vrServer.context.dispatch(VRServerActions.NewDevice(device.context.state.value.id, device)) + device.context.dispatch(DeviceActions.Update { copy(status = TrackerStatus.OK) }) +} + +@OptIn(ExperimentalCoroutinesApi::class) +class ProvisioningManagerTest { + + @Test + fun `NO_SERIAL_DEVICE_FOUND when no port appears within 15 seconds`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + + advanceTimeBy(15_001) + + assertEquals(WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND, manager.context.state.value.status) + } + + @Test + fun `OBTAINING_MAC_ADDRESS after port is detected and 2 second reboot delay elapses`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + advanceTimeBy(2_001) + + assertEquals(WifiProvisioningStatus.OBTAINING_MAC_ADDRESS, manager.context.state.value.status) + } + + @Test + fun `NO_SERIAL_LOGS_ERROR when tracker produces no serial output after 5 second timeout`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + // reboot delay (2s) + MAC acquisition timeout (5s) + advanceTimeBy(7_001) + + assertEquals(WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR, manager.context.state.value.status) + } + + @Test + fun `recovers from NO_SERIAL_LOGS_ERROR and sets macAddress when logs appear containing the MAC`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + advanceTimeBy(7_001) + assertEquals(WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR, manager.context.state.value.status) + + launch { + delay(100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + } + + advanceTimeBy(200) + + assertEquals("AA:BB:CC:DD:EE:FF", manager.context.state.value.macAddress) + } + + @Test + fun `CONNECTION_ERROR when logs are present but no MAC received within 5 second timeout`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(500) + serialServer.onDataReceived("COM1", "some log line without a mac address") + } + + // reboot delay (2s) + MAC timeout (5s) + error delay (3s) + advanceTimeBy(10_001) + + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + } + + @Test + fun `CONNECTION_ERROR when WiFi credentials not acknowledged within 5 second timeout`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + // No credential acknowledgement sent + } + + // reboot delay (2s) + MAC at 2.1s + credential ack timeout (5s) + error delay (3s) + advanceTimeBy(10_200) + + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + } + + @Test + fun `CONNECTION_ERROR when WiFi does not connect within 15 second timeout`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(100) + serialServer.onDataReceived("COM1", "new wifi credentials set") + // No WiFi connection log + } + + // reboot delay (2s) + MAC + creds ack + WiFi connect timeout (15s) + error delay (3s) + advanceTimeBy(20_500) + + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + } + + @Test + fun `CONNECTION_ERROR after exhausting all retries on can't connect`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(100) + serialServer.onDataReceived("COM1", "new wifi credentials set") + // One initial attempt + MAX_CONNECTION_RETRIES retries; each retry needs a fresh "can't connect" + repeat(MAX_CONNECTION_RETRIES + 1) { + delay(100) + serialServer.onDataReceived("COM1", "can't connect from any credentials") + delay(3_000) // wait out the per-retry delay before the next window opens + } + } + + // 2200ms (MAC + creds) + 4 × (100ms "can't connect" + 3000ms retry delay) + 3000ms final delay + advanceTimeBy(18_000) + + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + } + + @Test + fun `COULD_NOT_FIND_SERVER when tracker does not appear on the network within 30 second timeout`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(100) + serialServer.onDataReceived("COM1", "new wifi credentials set") + delay(100) + serialServer.onDataReceived("COM1", "looking for the server") + // No device connects to the server + } + + // reboot delay (2s) + MAC + creds + WiFi connect + server connect timeout (30s) + error delay (3s) + advanceTimeBy(35_500) + + assertEquals(WifiProvisioningStatus.COULD_NOT_FIND_SERVER, manager.context.state.value.status) + } + + @Test + fun `DONE when full provisioning succeeds`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val vrServer = buildTestVrServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(vrServer, "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(100) + serialServer.onDataReceived("COM1", "new wifi credentials set") + delay(100) + serialServer.onDataReceived("COM1", "looking for the server") + delay(100) + connectDevice(vrServer, "AA:BB:CC:DD:EE:FF", backgroundScope) + } + + // Advance past the device connection but well before the 30s device connection timeout + advanceTimeBy(5_000) + + assertEquals(WifiProvisioningStatus.DONE, manager.context.state.value.status) + } + + @Test + fun `resets to NONE and clears port after USB disconnect following DONE`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val vrServer = buildTestVrServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(vrServer, "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(2_100) + serialServer.onDataReceived("COM1", "mac: AA:BB:CC:DD:EE:FF") + delay(100) + serialServer.onDataReceived("COM1", "new wifi credentials set") + delay(100) + serialServer.onDataReceived("COM1", "looking for the server") + delay(100) + connectDevice(vrServer, "AA:BB:CC:DD:EE:FF", backgroundScope) + } + + // Advance past the device connection but well before the 30s server-connect timeout + advanceTimeBy(5_000) + assertEquals(WifiProvisioningStatus.DONE, manager.context.state.value.status) + + serialServer.onPortLost("COM1") + advanceTimeBy(1) + + assertEquals(WifiProvisioningStatus.NONE, manager.context.state.value.status) + assertNull(manager.context.state.value.portLocation) + } + + @Test + fun `does not retry the same port after failure - waits for USB disconnect`() = runTest { + val serialServer = buildTestSerialServer(backgroundScope) + val manager = buildManager(serialServer, backgroundScope) + + manager.startProvisioning(buildTestVrServer(backgroundScope), "wifi", "pass", null) + serialServer.onPortDetected(fakePort()) + + launch { + delay(500) + serialServer.onDataReceived("COM1", "some log without mac") + } + + // Reach CONNECTION_ERROR: reboot delay (2s) + MAC timeout (5s) + error delay (3s) + advanceTimeBy(10_001) + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + + // Port is still connected, advance significant time and confirm provisioning + // is blocked waiting for disconnect, not looping back to select a new port + advanceTimeBy(30_000) + assertEquals(WifiProvisioningStatus.CONNECTION_ERROR, manager.context.state.value.status) + } +} diff --git a/server/core/src/test/java/dev/slimevr/serial/SerialServerTest.kt b/server/core/src/test/java/dev/slimevr/serial/SerialServerTest.kt new file mode 100644 index 0000000000..1197d9f9ff --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/serial/SerialServerTest.kt @@ -0,0 +1,199 @@ +package dev.slimevr.serial + +import dev.llelievr.espflashkotlin.FlasherSerialInterface +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +private fun fakePortHandle(portLocation: String) = SerialPortHandle( + portLocation = portLocation, + descriptivePortName = "Fake $portLocation", + writeCommand = {}, + close = {}, +) + +private fun fakeFlashingHandler() = object : FlasherSerialInterface { + override fun openSerial(port: Any) {} + override fun closeSerial() {} + override fun write(data: ByteArray) {} + override fun read(length: Int) = ByteArray(length) + override fun setDTR(value: Boolean) {} + override fun setRTS(value: Boolean) {} + override fun changeBaud(baud: Int) {} + override fun setReadTimeout(timeout: Long) {} + override fun availableBytes() = 0 + override fun flushIOBuffers() {} +} + +private fun fakePort() = SerialPortInfo("COM1", "Fake COM1", 0x1A86, 0x7523) + +class SerialServerTest { + @Test + fun `openForFlashing registers Flashing connection`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + + val handler = server.openForFlashing("COM1") + + assertNotNull(handler) + assertIs(server.context.state.value.connections["COM1"]) + } + + @Test + fun `openForFlashing returns null when port has an existing connection`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + + val handler = server.openForFlashing("COM1") + + assertNull(handler) + } + + @Test + fun `openForFlashing returns null for unknown port`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + + // No onPortDetected call, port is not in availablePorts + val handler = server.openForFlashing("COM1") + + assertNull(handler) + } + + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + @Test + fun `closeSerial removes Flashing connection asynchronously`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + val handler = server.openForFlashing("COM1")!! + + handler.closeSerial() + + // The scope.launch inside closeSerial has not run yet + assertIs(server.context.state.value.connections["COM1"]) + + advanceUntilIdle() + + // Now the dispatched RemoveConnection has run + assertNull(server.context.state.value.connections["COM1"]) + } + + @Test + fun `openConnection registers Console connection`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + + server.openConnection("COM1") + + assertIs(server.context.state.value.connections["COM1"]) + } + + @Test + fun `onPortLost closes Console and removes connection`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + server.openConnection("COM1") + + server.onPortLost("COM1") + + assertNull(server.context.state.value.connections["COM1"]) + assertNull(server.context.state.value.availablePorts["COM1"]) + } + + @Test + fun `openConnection while flashing is a no-op`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + server.openForFlashing("COM1") + + server.openConnection("COM1") + + // Still Flashing, openConnection must not have replaced it + assertIs(server.context.state.value.connections["COM1"]) + } + + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + @Test + fun `port can be flashed again after previous flash completes`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + val firstHandler = server.openForFlashing("COM1")!! + firstHandler.closeSerial() + advanceUntilIdle() + + // Connection is gone, port is still available, can flash again + val secondHandler = server.openForFlashing("COM1") + + assertNotNull(secondHandler) + assertIs(server.context.state.value.connections["COM1"]) + } + + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + @Test + fun `openConnection succeeds after flash completes`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + val handler = server.openForFlashing("COM1")!! + handler.closeSerial() + advanceUntilIdle() + + server.openConnection("COM1") + + assertIs(server.context.state.value.connections["COM1"]) + } + + @Test + fun `onPortLost during flash removes Flashing connection`() = runTest { + val server = SerialServer.create( + openPort = { loc, _, _ -> fakePortHandle(loc) }, + openFlashingPort = ::fakeFlashingHandler, + scope = this, + ) + server.onPortDetected(fakePort()) + server.openForFlashing("COM1") + + server.onPortLost("COM1") + + assertNull(server.context.state.value.connections["COM1"]) + assertNull(server.context.state.value.availablePorts["COM1"]) + } +} diff --git a/server/core/src/test/java/dev/slimevr/serial/reducers/SerialConnectionReducerTest.kt b/server/core/src/test/java/dev/slimevr/serial/reducers/SerialConnectionReducerTest.kt new file mode 100644 index 0000000000..1bd3b9ccba --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/serial/reducers/SerialConnectionReducerTest.kt @@ -0,0 +1,52 @@ +package dev.slimevr.serial.reducers + +import dev.slimevr.serial.SerialConnectionActions +import dev.slimevr.serial.SerialConnectionState +import dev.slimevr.serial.SerialLogBehaviour +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class SerialConnectionReducerTest { + private fun state(lines: List = emptyList(), connected: Boolean = true) = SerialConnectionState( + portLocation = "COM1", + descriptivePortName = "Test Port", + connected = connected, + logLines = lines, + ) + + @Test + fun `LogLine appends to empty log`() { + val result = SerialLogBehaviour.reduce(state(), SerialConnectionActions.LogLine("hello")) + assertEquals(listOf("hello"), result.logLines) + } + + @Test + fun `LogLine appends to existing log`() { + val result = SerialLogBehaviour.reduce(state(listOf("a", "b")), SerialConnectionActions.LogLine("c")) + assertEquals(listOf("a", "b", "c"), result.logLines) + } + + @Test + fun `LogLine drops oldest line when at capacity`() { + val full = state(lines = List(500) { "line $it" }) + val result = SerialLogBehaviour.reduce(full, SerialConnectionActions.LogLine("new")) + assertEquals(500, result.logLines.size) + assertEquals("line 1", result.logLines.first()) + assertEquals("new", result.logLines.last()) + } + + @Test + fun `LogLine does not drop below capacity`() { + val almostFull = state(lines = List(499) { "line $it" }) + val result = SerialLogBehaviour.reduce(almostFull, SerialConnectionActions.LogLine("new")) + assertEquals(500, result.logLines.size) + assertEquals("line 0", result.logLines.first()) + } + + @Test + fun `Disconnected sets connected to false`() { + val result = SerialLogBehaviour.reduce(state(connected = true), SerialConnectionActions.Disconnected) + assertFalse(result.connected) + } +} diff --git a/server/core/src/test/java/dev/slimevr/solarxr/DataFeedTest.kt b/server/core/src/test/java/dev/slimevr/solarxr/DataFeedTest.kt new file mode 100644 index 0000000000..65d0019a5b --- /dev/null +++ b/server/core/src/test/java/dev/slimevr/solarxr/DataFeedTest.kt @@ -0,0 +1,116 @@ +package dev.slimevr.solarxr + +import dev.slimevr.EventDispatcher +import dev.slimevr.TestAppContext +import dev.slimevr.buildTestSkeleton +import dev.slimevr.buildTestVrServer +import dev.slimevr.context.Context +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import solarxr_protocol.MessageBundle +import solarxr_protocol.data_feed.DataFeedConfig +import solarxr_protocol.data_feed.PollDataFeed +import solarxr_protocol.data_feed.StartDataFeed +import kotlin.test.Test +import kotlin.test.assertEquals + +private fun testConn(backgroundScope: kotlinx.coroutines.CoroutineScope, onSend: suspend (ByteArray) -> Unit): SolarXRBridge { + val server = buildTestVrServer(backgroundScope) + val skeleton = buildTestSkeleton(backgroundScope) + val appContext = object : TestAppContext() { + override val server = server + override val skeleton = skeleton + } + val context = Context.create( + initialState = SolarXRBridgeState(dataFeedConfigs = listOf(), datafeedTimers = listOf()), + scope = backgroundScope, + behaviours = listOf(DataFeedInitBehaviour(server, skeleton)), + ) + val bridge = SolarXRBridge( + id = 1, + context = context, + appContext = appContext, + dataFeedDispatcher = EventDispatcher(), + rpcDispatcher = EventDispatcher(), + ) + bridge.startObserving() + bridge.outbound.on { _ -> onSend(ByteArray(0)) } + return bridge +} + +private fun config(intervalMs: Int) = DataFeedConfig(minimumTimeSinceLast = intervalMs.toUShort()) + +@OptIn(ExperimentalCoroutinesApi::class) +class DataFeedTest { + + @Test + fun `StartDataFeed sends frames at the configured interval`() = runTest { + var sendCount = 0 + val conn = testConn(backgroundScope) { sendCount++ } + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = listOf(config(100)))) + + // fires at t=0, t=100, t=200 + advanceTimeBy(250) + assertEquals(3, sendCount) + } + + @Test + fun `StartDataFeed with multiple configs runs each at its own frequency`() = runTest { + var sendCount = 0 + val conn = testConn(backgroundScope) { sendCount++ } + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = listOf(config(100), config(200)))) + + // 100ms feed: t=0, t=100, t=200 -> 3 sends + // 200ms feed: t=0, t=200 -> 2 sends + advanceTimeBy(250) + assertEquals(5, sendCount) + } + + @Test + fun `PollDataFeed sends exactly one frame without starting a repeating timer`() = runTest { + var sendCount = 0 + val conn = testConn(backgroundScope) { sendCount++ } + + conn.dataFeedDispatcher.emit(PollDataFeed(config = config(100))) + + advanceTimeBy(500) + assertEquals(1, sendCount) + } + + @Test + fun `StartDataFeed cancels old timers when called a second time`() = runTest { + var sendCount = 0 + val conn = testConn(backgroundScope) { sendCount++ } + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = listOf(config(100)))) + advanceTimeBy(250) + assertEquals(3, sendCount) + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = listOf(config(100)))) + sendCount = 0 + + advanceTimeBy(250) + assertEquals(3, sendCount) + } + + @Test + fun `StartDataFeed with empty list stops all existing timers`() = runTest { + var sendCount = 0 + val conn = testConn(backgroundScope) { sendCount++ } + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = listOf(config(100)))) + advanceTimeBy(250) + assertEquals(3, sendCount) + + conn.dataFeedDispatcher.emit(StartDataFeed(dataFeeds = emptyList())) + sendCount = 0 + + advanceTimeBy(500) + assertEquals(0, sendCount) + } + + // TODO: need more tests for the content of a datafeed + check if the masks work +} diff --git a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt b/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt deleted file mode 100644 index 830a60f9db..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt +++ /dev/null @@ -1,72 +0,0 @@ -package dev.slimevr.unit - -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.processor.config.SkeletonConfigToggles -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerRole -import dev.slimevr.tracking.trackers.TrackerStatus -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.QuaternionTest -import io.github.axisangles.ktmath.Vector3 -import org.junit.jupiter.api.Test -import kotlin.test.assertFails - -class LegTweaksTests { - - @Test - fun toeSnap() { - val hmd = Tracker( - null, - 0, - "test:headset", - "Headset", - TrackerPosition.HEAD, - hasPosition = true, - hasRotation = true, - isComputed = true, - imuType = null, - allowReset = false, - allowMounting = false, - isHmd = true, - trackRotDirection = false, - ) - hmd.status = TrackerStatus.OK - - val hpm = HumanPoseManager(listOf(hmd)) - val height = hpm.userHeightFromConfig - val lFoot = hpm.getComputedTracker(TrackerRole.LEFT_FOOT) - - assert(height > 0f) { - "Skeleton was not populated with default proportions (height = $height)" - } - val lFootLen = hpm.skeleton.leftFootBone.length - assert(lFootLen > 0f) { - "Skeleton's left foot has no length (length = $lFootLen)" - } - - // Skeleton setup - hpm.skeleton.hasKneeTrackers = true - hpm.setToggle(SkeletonConfigToggles.TOE_SNAP, true) - - // Set the floor height - hmd.position = Vector3(0f, height, 0f) - hpm.update() - - // Validate initial state - QuaternionTest.assertEquals(Quaternion.IDENTITY, lFoot.getRotation()) - - // Ensure `leftToeTouched` and `rightToeTouched` are true - hmd.position = Vector3(0f, height - 0.02f, 0f) - hpm.update() - - // Lift skeleton within toe snap range - hmd.position = Vector3(0f, height + 0.02f, 0f) - hpm.update() - - // This should fail now that the toes are snapped - assertFails { - QuaternionTest.assertEquals(Quaternion.IDENTITY, lFoot.getRotation()) - } - } -} diff --git a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt b/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt deleted file mode 100644 index 79f0718a9a..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt +++ /dev/null @@ -1,163 +0,0 @@ -package dev.slimevr.unit - -import com.jme3.math.FastMath -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.udp.IMUType -import dev.slimevr.unit.TrackerTestUtils.assertAnglesApproxEqual -import dev.slimevr.unit.TrackerTestUtils.deg -import dev.slimevr.unit.TrackerTestUtils.yaw -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestFactory - -/** - * Tests [TrackerResetsHandler.resetMounting] - * - * Head rotation does not get reset. - * Tracker yaw is set to head yaw on reset. - */ -class MountingResetTests { - - @TestFactory - fun testResetAndMounting(): List = TrackerTestUtils.directions.flatMap { e -> - TrackerTestUtils.directions.map { m -> - DynamicTest.dynamicTest( - "Full and Mounting Reset Test of Tracker (Expected: ${deg(e)}, reference: ${deg(m)})", - ) { - checkResetMounting(e, m) - } - } - } - - private fun checkResetMounting(expected: Quaternion, reference: Quaternion) { - // Compute the pitch/roll for the expected mounting - val trackerRot = expected * TrackerTestUtils.frontRot / expected - - val tracker = Tracker( - null, - getNextLocalTrackerId(), - "test", - "test", - null, - hasRotation = true, - imuType = IMUType.UNKNOWN, - allowReset = true, - allowMounting = true, - trackRotDirection = false, - ) - - // Apply full reset and mounting - tracker.setRotation(Quaternion.IDENTITY) - tracker.resetsHandler.resetFull(Quaternion.IDENTITY) - tracker.setRotation(trackerRot) - tracker.resetsHandler.resetMounting(Quaternion.IDENTITY) - - val expectedYaw = yaw(expected) - val resultYaw = yaw(tracker.resetsHandler.mountRotFix) - assertAnglesApproxEqual( - expectedYaw, - resultYaw, - "Resulting mounting yaw after full reset is not equal to reference yaw (${deg(expectedYaw)} vs ${deg(resultYaw)})", - ) - - // Apply full reset and mounting plus offset - tracker.setRotation(Quaternion.IDENTITY) - tracker.resetsHandler.resetFull(reference) - // Apply an offset of reference to the rotation - tracker.setRotation(reference * trackerRot) - // Since reference is the offset from quat identity (reset) and the rotation, - // it needs to be applied twice - tracker.resetsHandler.resetMounting(reference * reference) - - val expectedYaw2 = yaw(expected) - val resultYaw2 = yaw(tracker.resetsHandler.mountRotFix) - assertAnglesApproxEqual( - expectedYaw2, - resultYaw2, - "Resulting mounting yaw after full reset with offset is not equal to reference yaw (${deg(expectedYaw2)} vs ${deg(resultYaw2)})", - ) - - // Apply yaw reset and mounting - tracker.setRotation(Quaternion.IDENTITY) - tracker.resetsHandler.resetFull(reference) - tracker.resetsHandler.resetYaw(Quaternion.IDENTITY) - tracker.setRotation(trackerRot) - tracker.resetsHandler.resetMounting(Quaternion.IDENTITY) - - val expectedYaw3 = yaw(expected) - val resultYaw3 = yaw(tracker.resetsHandler.mountRotFix) - assertAnglesApproxEqual( - expectedYaw3, - resultYaw3, - "Resulting mounting yaw after yaw reset is not equal to reference yaw (${deg(expectedYaw3)} vs ${deg(resultYaw3)})", - ) - - // Apply yaw reset and mounting plus offset - tracker.setRotation(Quaternion.IDENTITY) - tracker.resetsHandler.resetFull(Quaternion.IDENTITY) - tracker.resetsHandler.resetYaw(reference) - // Apply an offset of reference to the rotation - tracker.setRotation(reference * trackerRot) - // Since reference is the offset from quat identity (reset) and the rotation, - // it needs to be applied twice - tracker.resetsHandler.resetMounting(reference * reference) - - val expectedYaw4 = yaw(expected) - val resultYaw4 = yaw(tracker.resetsHandler.mountRotFix) - assertAnglesApproxEqual( - expectedYaw3, - resultYaw3, - "Resulting mounting yaw after yaw reset with offset is not equal to reference yaw (${deg(expectedYaw4)} vs ${deg(resultYaw4)})", - ) - } - - @Test - fun testYawAfter() { - val expected = Quaternion.SLIMEVR.RIGHT - val reference = EulerAngles(EulerOrder.YZX, FastMath.PI / 8f, FastMath.HALF_PI, 0f).toQuaternion() - // Compute the pitch/roll for the expected mounting - val trackerRot = expected * TrackerTestUtils.frontRot / expected - - val tracker = Tracker( - null, - getNextLocalTrackerId(), - "test", - "test", - null, - hasRotation = true, - imuType = IMUType.UNKNOWN, - allowReset = true, - allowMounting = true, - trackRotDirection = false, - ) - - // Apply full reset and mounting - tracker.setRotation(Quaternion.IDENTITY) - tracker.resetsHandler.resetFull(Quaternion.IDENTITY) - tracker.setRotation(trackerRot) - tracker.resetsHandler.resetMounting(Quaternion.IDENTITY) - - val expectedYaw = yaw(expected) - val resultYaw = yaw(tracker.resetsHandler.mountRotFix) - assertAnglesApproxEqual( - expectedYaw, - resultYaw, - "Resulting mounting yaw after full reset is not equal to reference yaw (${deg(expectedYaw)} vs ${deg(resultYaw)})", - ) - - tracker.setRotation(reference * reference) - tracker.resetsHandler.resetYaw(reference) - - val expectedYaw2 = yaw(reference) - val resultYaw2 = yaw(tracker.getRotation()) - assertAnglesApproxEqual( - expectedYaw2, - resultYaw2, - "Resulting rotation after yaw reset is not equal to reference yaw (${deg(expectedYaw2)} vs ${deg(resultYaw2)})", - ) - } -} diff --git a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt b/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt deleted file mode 100644 index 5a90b1ddb9..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt +++ /dev/null @@ -1,337 +0,0 @@ -package dev.slimevr.unit - -import com.jme3.math.FastMath -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.tracking.processor.TransformNode -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.udp.IMUType -import io.eiren.math.FloatMath -import io.eiren.util.StringUtils.prettyNumber -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3.Companion.POS_Y -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory -import java.util.stream.Stream -import kotlin.streams.asStream - -/** - * Tests [TrackerResetsHandler.resetFull] - */ -class ReferenceAdjustmentsTests { - @get:TestFactory - val testsYaw: Stream - get() = anglesSet - .map { p: AnglesSet -> - DynamicTest.dynamicTest( - "Adjustment Yaw Test of Tracker(${p.pitch},${p.yaw},${p.roll})", - ) { - yaws.forEach { - checkReferenceAdjustmentYaw( - q(p.pitch.toFloat(), p.yaw.toFloat(), p.roll.toFloat()), - 0, - it, - 0, - ) - } - } - } - - @get:TestFactory - val testsFull: Stream - get() = anglesSet - .map { p: AnglesSet -> - DynamicTest.dynamicTest( - "Adjustment Full Test of Tracker(${p.pitch},${p.yaw},${p.roll})", - ) { - anglesSet - .forEach { - checkReferenceAdjustmentFull( - q(p.pitch.toFloat(), p.yaw.toFloat(), p.roll.toFloat()), - it.pitch, - it.yaw, - it.roll, - ) - } - } - } - - // TODO : Test is not passing because the test is wrong - // See issue https://github.com/SlimeVR/SlimeVR-Server/issues/55 - // @TestFactory - val testsForRotation: Stream - get() = anglesSet - .flatMap { p: AnglesSet -> - yaws.asSequence().map { - DynamicTest.dynamicTest( - "Adjustment Rotation Test of Tracker(${p.pitch},${p.yaw},${p.roll}), Ref $it", - ) { - testAdjustedTrackerRotation( - q(p.pitch.toFloat(), p.yaw.toFloat(), p.roll.toFloat()), - 0, - it, - 0, - ) - } - }.asStream() - } - - fun checkReferenceAdjustmentFull( - trackerQuat: Quaternion, - refPitch: Int, - refYaw: Int, - refRoll: Int, - ) { - var referenceQuat = q(refPitch.toFloat(), refYaw.toFloat(), refRoll.toFloat()) - val tracker = Tracker( - null, - getNextLocalTrackerId(), - "test", - "test", - null, - hasRotation = true, - imuType = IMUType.UNKNOWN, - allowReset = true, - ) - tracker.setRotation(trackerQuat) - tracker.resetsHandler.resetFull(referenceQuat) - val read = tracker.getRotation() - Assertions.assertNotNull(read, "Adjusted tracker didn't return rotation") - - // Use only yaw HMD rotation - referenceQuat = referenceQuat.project(POS_Y).unit() - Assertions.assertEquals( - QuatEqualFullWithEpsilon(referenceQuat), - QuatEqualFullWithEpsilon(read), - "Adjusted quat is not equal to reference quat (${toDegs(referenceQuat)} vs ${toDegs(read)})", - ) - } - - fun checkReferenceAdjustmentYaw( - trackerQuat: Quaternion, - refPitch: Int, - refYaw: Int, - refRoll: Int, - ) { - // FIXME - val referenceQuat = q(refPitch.toFloat(), refYaw.toFloat(), refRoll.toFloat()) - val tracker = Tracker( - null, - getNextLocalTrackerId(), - "test", - "test", - null, - hasRotation = true, - imuType = IMUType.UNKNOWN, - allowReset = true, - ) - tracker.setRotation(trackerQuat) - tracker.resetsHandler.resetYaw(referenceQuat) - val read = tracker.getRotation() - Assertions.assertNotNull(read, "Adjusted tracker didn't return rotation") - Assertions.assertEquals( - QuatEqualYawWithEpsilon(referenceQuat), - QuatEqualYawWithEpsilon(read), - "Adjusted quat is not equal to reference quat (${toDegs(referenceQuat)} vs ${toDegs(read)})", - ) - } - - private fun testAdjustedTrackerRotation( - trackerQuat: Quaternion, - refPitch: Int, - refYaw: Int, - refRoll: Int, - ) { - val referenceQuat = q(refPitch.toFloat(), refYaw.toFloat(), refRoll.toFloat()) - val tracker = Tracker( - null, - getNextLocalTrackerId(), - "test", - "test", - null, - hasRotation = true, - imuType = IMUType.UNKNOWN, - allowReset = true, - ) - tracker.setRotation(trackerQuat) - tracker.resetsHandler.resetFull(referenceQuat) - - // Use only yaw HMD rotation - referenceQuat.project(POS_Y).unit() - val trackerNode = TransformNode(true) - val rotationNode = TransformNode(true) - rotationNode.attachChild(trackerNode) - trackerNode.localTransform.rotation = tracker.getRawRotation() - var yaw = 0 - while (yaw <= 360) { - var pitch = -90 - while (pitch <= 90) { - var roll = -90 - while (roll <= 90) { - val rotation = EulerAngles( - EulerOrder.YZX, - pitch * FastMath.DEG_TO_RAD, - yaw * FastMath.DEG_TO_RAD, - roll * FastMath.DEG_TO_RAD, - ).toQuaternion() - val rotationCompare = EulerAngles( - EulerOrder.YZX, - pitch * FastMath.DEG_TO_RAD, - (yaw + refYaw) * FastMath.DEG_TO_RAD, - roll * FastMath.DEG_TO_RAD, - ).toQuaternion() - rotationNode.localTransform.rotation = rotation - rotationNode.update() - tracker.setRotation(trackerNode.worldTransform.rotation) - val angles = tracker.getRawRotation().toEulerAngles(EulerOrder.YZX) - val anglesAdj = tracker.getRotation().toEulerAngles(EulerOrder.YZX) - val anglesDiff = tracker - .getRotation() - .inv() - .times(rotationCompare) - .toEulerAngles(EulerOrder.YZX) - if (!PRINT_TEST_RESULTS) { - Assertions.assertTrue( - FloatMath.equalsToZero(anglesDiff.x) && - FloatMath.equalsToZero(anglesDiff.y) && - FloatMath.equalsToZero(anglesDiff.z), - name(yaw, pitch, roll, angles, anglesAdj, anglesDiff), - ) - } else { - if (FloatMath.equalsToZero(anglesDiff.x) && - FloatMath.equalsToZero(anglesDiff.y) && - FloatMath.equalsToZero(anglesDiff.z) - ) { - successes++ - } else { - errors++ - } - println(name(yaw, pitch, roll, angles, anglesAdj, anglesDiff)) - } - roll += 15 - } - pitch += 15 - } - yaw += 30 - } - if (PRINT_TEST_RESULTS) println("Errors: $errors, successes: $successes") - } - - private data class QuatEqualYawWithEpsilon(val q: Quaternion) { - override fun equals(other: Any?): Boolean { - if (other == null) return false - val q2: Quaternion = when (other) { - is Quaternion -> other - is QuatEqualYawWithEpsilon -> other.q - else -> return false - } - var degs1 = q.toEulerAngles(EulerOrder.YZX) - var degs2 = q2.toEulerAngles(EulerOrder.YZX) - if (degs1.y < -FloatMath.ANGLE_EPSILON_RAD) { - degs1 = EulerAngles( - EulerOrder.YZX, - degs1.x, - degs1.y + FastMath.TWO_PI, - degs1.z, - ) - } - if (degs2.y < -FloatMath.ANGLE_EPSILON_RAD) { - degs2 = EulerAngles( - EulerOrder.YZX, - degs2.x, - degs2.y + FastMath.TWO_PI, - degs2.z, - ) - } - return FloatMath.equalsWithEpsilon(degs1.y, degs2.y) - } - - override fun hashCode(): Int = q.hashCode() - } - - data class QuatEqualFullWithEpsilon(val q: Quaternion) { - override fun hashCode(): Int = q.hashCode() - - override fun equals(other: Any?): Boolean { - if (other == null) return false - val q2: Quaternion = when (other) { - is Quaternion -> other - is QuatEqualFullWithEpsilon -> other.q - else -> return false - } - var degs1 = q.toEulerAngles(EulerOrder.YZX) - var degs2 = q2.toEulerAngles(EulerOrder.YZX) - if (degs1.y < -FloatMath.ANGLE_EPSILON_RAD) { - degs1 = EulerAngles( - EulerOrder.YZX, - degs1.x, - degs1.y + FastMath.TWO_PI, - degs1.z, - ) - } - if (degs2.y < -FloatMath.ANGLE_EPSILON_RAD) { - degs2 = EulerAngles( - EulerOrder.YZX, - degs2.x, - degs2.y + FastMath.TWO_PI, - degs2.z, - ) - } - return ( - FloatMath.equalsWithEpsilon(degs1.x, degs2.x) && - FloatMath.equalsWithEpsilon(degs1.y, degs2.y) && - FloatMath.equalsWithEpsilon(degs1.z, degs2.z) - ) - } - } - - companion object { - private val yaws = intArrayOf(0, 45, 90, 180, 270) - private val pitches = intArrayOf(0, 15, 35, -15, -35) - private val rolls = intArrayOf(0, 15, 35, -15, -35) - private const val PRINT_TEST_RESULTS = false - private var errors = 0 - private var successes = 0 - - val anglesSet: Stream - get() = yaws.asSequence() - .zip(pitches.asSequence()) - .zip(rolls.asSequence()) { (yaw, pitch), roll -> - AnglesSet(pitch, yaw, roll) - }.asStream() - - private fun name( - yaw: Int, - pitch: Int, - roll: Int, - angles: EulerAngles, - anglesAdj: EulerAngles, - anglesDiff: EulerAngles, - ): String = """Rot: $yaw/$pitch/$roll. - Angles: ${prettyNumber(angles.x * FastMath.RAD_TO_DEG, 1)}/${prettyNumber(anglesAdj.x * FastMath.RAD_TO_DEG, 1)}, - ${prettyNumber(angles.y * FastMath.RAD_TO_DEG, 1)}/${prettyNumber(anglesAdj.y * FastMath.RAD_TO_DEG, 1)}, - ${prettyNumber(angles.z * FastMath.RAD_TO_DEG, 1)}/${prettyNumber(anglesAdj.z * FastMath.RAD_TO_DEG, 1)}. - Diff: ${prettyNumber(anglesDiff.x * FastMath.RAD_TO_DEG, 1)}, - ${prettyNumber(anglesDiff.y * FastMath.RAD_TO_DEG, 1)}, - ${prettyNumber(anglesDiff.z * FastMath.RAD_TO_DEG, 1)} - """.replace('\n', ' ') - - fun q(pitch: Float, yaw: Float, roll: Float): Quaternion = EulerAngles( - EulerOrder.YZX, - pitch * FastMath.DEG_TO_RAD, - yaw * FastMath.DEG_TO_RAD, - roll * FastMath.DEG_TO_RAD, - ).toQuaternion() - - fun toDegs(q: Quaternion): String { - val (_, x, y, z) = q.toEulerAngles(EulerOrder.YZX) - return "${prettyNumber(x * FastMath.RAD_TO_DEG, 0)}, " + - "${prettyNumber(y * FastMath.RAD_TO_DEG, 0)}, " + - prettyNumber(z * FastMath.RAD_TO_DEG, 0) - } - } -} - -data class AnglesSet(val pitch: Int, val yaw: Int, val roll: Int) diff --git a/server/core/src/test/java/dev/slimevr/unit/SkeletonResetTests.kt b/server/core/src/test/java/dev/slimevr/unit/SkeletonResetTests.kt deleted file mode 100644 index cbbc3f734f..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/SkeletonResetTests.kt +++ /dev/null @@ -1,122 +0,0 @@ -package dev.slimevr.unit - -import com.jme3.math.FastMath -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.unit.TrackerTestUtils.assertAnglesApproxEqual -import dev.slimevr.unit.TrackerTestUtils.quatApproxEqual -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import org.junit.jupiter.api.Test - -class SkeletonResetTests { - - val resetSource = "Unit Test" - - @Test - fun testSkeletonFullReset() { - val trackers = TestTrackerSet() - - // Initialize skeleton and everything - val hpm = HumanPoseManager(trackers.allL) - - val headRot1 = EulerAngles(EulerOrder.YZX, 0f, FastMath.HALF_PI, FastMath.QUARTER_PI).toQuaternion() - val expectRot1 = EulerAngles(EulerOrder.YZX, 0f, FastMath.HALF_PI, 0f).toQuaternion() - - // Randomize tracker orientations, these should be zeroed and matched to the - // headset yaw by full reset - for ((i, tracker) in trackers.set.withIndex()) { - tracker.setRotation(TrackerTestUtils.testRotFromIndex(i)) - } - trackers.head.setRotation(headRot1) - hpm.resetTrackersFull(resetSource) - - for (tracker in trackers.set) { - val actual = tracker.getRotation() - assert(quatApproxEqual(expectRot1, actual)) { - "\"${tracker.name}\" did not reset to the reference rotation. Expected <$expectRot1>, actual <$actual>." - } - } - } - - @Test - fun testSkeletonYawReset() { - val trackers = TestTrackerSet() - - // Initialize skeleton and everything - val hpm = HumanPoseManager(trackers.allL) - - // Randomize full tracker orientations, these should match the headset yaw but - // retain orientation otherwise - for ((i, tracker) in trackers.set.withIndex()) { - // Offset index so it's different from last reset - tracker.setRotation(TrackerTestUtils.testRotFromIndex(i)) - } - trackers.head.setRotation(Quaternion.IDENTITY) - hpm.resetTrackersYaw(resetSource) - - for (tracker in trackers.set) { - val yaw = TrackerTestUtils.yaw(tracker.getRotation()) - assertAnglesApproxEqual(0f, yaw, "\"${tracker.name}\" did not reset to the reference rotation.") - } - } - - @Test - fun testSkeletonMountReset() { - val trackers = TestTrackerSet() - - // Initialize skeleton and everything - val hpm = HumanPoseManager(trackers.allL) - - // Just a bunch of random mounting orientations - val expected = arrayOf( - Pair(trackers.chest, Quaternion.SLIMEVR.FRONT), - Pair(trackers.hip, Quaternion.SLIMEVR.RIGHT), - Pair(trackers.leftThigh, Quaternion.SLIMEVR.BACK), - Pair(trackers.leftCalf, Quaternion.SLIMEVR.LEFT), - Pair(trackers.rightThigh, Quaternion.SLIMEVR.FRONT), - Pair(trackers.rightCalf, Quaternion.SLIMEVR.RIGHT), - ) - // Rotate the tracker to fit the expected mounting orientation - for ((tracker, mountRot) in expected) { - tracker.setRotation(mkTrackMount(mountRot)) - } - // Then perform a mounting reset - hpm.resetTrackersMounting(resetSource) - - for ((tracker, mountRot) in expected) { - // Some mounting needs to be inverted (when in a specific pose) - // TODO: Make this less hardcoded, accept alternative poses - val expectedMounting = when (tracker.trackerPosition) { - TrackerPosition.CHEST, - TrackerPosition.HIP, - TrackerPosition.LEFT_LOWER_LEG, - TrackerPosition.RIGHT_LOWER_LEG, - -> mountRot * Quaternion.SLIMEVR.FRONT - - TrackerPosition.LEFT_UPPER_LEG, - TrackerPosition.RIGHT_UPPER_LEG, - -> mountRot - - else -> mountRot - } - val actualMounting = tracker.resetsHandler.mountRotFix - - // Make sure yaw matches - val expectedY = TrackerTestUtils.yaw(expectedMounting) - val actualY = TrackerTestUtils.yaw(actualMounting) - assertAnglesApproxEqual(expectedY, actualY, "\"${tracker.name}\" did not reset to the reference rotation.") - - // X and Z components should be zero for mounting - assert(FastMath.isApproxZero(actualMounting.x)) { - "\"${tracker.name}\" did not reset to the reference rotation. Expected <0.0>, actual <${actualMounting.x}>." - } - assert(FastMath.isApproxZero(actualMounting.z)) { - "\"${tracker.name}\" did not reset to the reference rotation. Expected <0.0>, actual <${actualMounting.z}>." - } - } - } - - fun mkTrackMount(rot: Quaternion): Quaternion = rot * TrackerTestUtils.frontRot / rot -} diff --git a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt b/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt deleted file mode 100644 index cac3f38514..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt +++ /dev/null @@ -1,69 +0,0 @@ -package dev.slimevr.unit - -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus - -class TestTrackerSet( - val computed: Boolean = false, - val positional: Boolean = false, - val resetHead: Boolean = false, -) { - - val head = mkTrack(0, TrackerPosition.HEAD, true) - - val chest = mkTrack(1, TrackerPosition.CHEST) - val hip = mkTrack(2, TrackerPosition.HIP) - - val leftThigh = mkTrack(3, TrackerPosition.LEFT_UPPER_LEG) - val leftCalf = mkTrack(4, TrackerPosition.LEFT_LOWER_LEG) - - val rightThigh = mkTrack(5, TrackerPosition.RIGHT_UPPER_LEG) - val rightCalf = mkTrack(6, TrackerPosition.RIGHT_LOWER_LEG) - - /** - * All the trackers in the set. - */ - val set = arrayOf( - chest, - hip, - leftThigh, - leftCalf, - rightThigh, - rightCalf, - ) - - /** - * All the trackers in the set as a list. - */ - val setL = set.asList() - - /** - * All the trackers in the set plus the headset. - */ - val all = set + head - - /** - * All the trackers in the set plus the headset as a list. - */ - val allL = all.asList() - - fun mkTrack(id: Int, pos: TrackerPosition, isHmd: Boolean = false): Tracker { - val tracker = Tracker( - device = null, - id = id, - name = pos.name, - trackerPosition = pos, - trackerNum = 0, - hasPosition = positional || isHmd, - hasRotation = true, - isComputed = computed || isHmd, - allowReset = resetHead || !isHmd, - allowMounting = resetHead || !isHmd, - isHmd = isHmd, - trackRotDirection = false, - ) - tracker.status = TrackerStatus.OK - return tracker - } -} diff --git a/server/core/src/test/java/dev/slimevr/unit/TrackerTestUtils.kt b/server/core/src/test/java/dev/slimevr/unit/TrackerTestUtils.kt deleted file mode 100644 index 8668e2d225..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/TrackerTestUtils.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.slimevr.unit - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import org.junit.jupiter.api.AssertionFailureBuilder -import kotlin.math.abs - -object TrackerTestUtils { - val directions = arrayOf( - Quaternion.SLIMEVR.FRONT, - Quaternion.SLIMEVR.FRONT_LEFT, - Quaternion.SLIMEVR.LEFT, - Quaternion.SLIMEVR.BACK_LEFT, - Quaternion.SLIMEVR.FRONT_RIGHT, - Quaternion.SLIMEVR.RIGHT, - Quaternion.SLIMEVR.BACK_RIGHT, - Quaternion.SLIMEVR.BACK, - ) - - val frontRot = Quaternion(0.707f, 0.707f, 0f, 0f) - - // A diverse range of quaternions to be used for testing where specific rotations - // do not matter - val testRots = arrayOf( - // Various rotations - frontRot, - Quaternion(0.707f, 0f, 0f, 0.707f), - Quaternion(0.854f, 0.354f, 0.146f, 0.354f), - // Pure yaw rotations - Quaternion.SLIMEVR.FRONT, - Quaternion.SLIMEVR.LEFT, - Quaternion.SLIMEVR.RIGHT, - // Axes - Quaternion.I, - Quaternion.K, - // Negative axes (same rotations, different sign) - -Quaternion.I, - -Quaternion.K, - // Identity - Quaternion.IDENTITY, - ) - - fun testRotFromIndex(index: Int): Quaternion = testRots[abs(index) % testRots.size] - - /** - * Makes a radian angle positive - */ - fun posRad(rot: Float): Float { - // Reduce the rotation to the smallest form - val redRot = rot % FastMath.TWO_PI - return abs(if (rot < 0f) FastMath.TWO_PI + redRot else redRot) - } - - /** - * Gets the yaw of a rotation in radians - */ - fun yaw(rot: Quaternion): Float = posRad(rot.toEulerAngles(EulerOrder.YZX).y) - - /** - * Converts radians to degrees - */ - fun deg(rot: Float): Float = rot * FastMath.RAD_TO_DEG - - fun deg(rot: Quaternion): Float = deg(yaw(rot)) - - private fun anglesApproxEqual(a: Float, b: Float): Boolean = FastMath.isApproxEqual(a, b) || - FastMath.isApproxEqual(a - FastMath.TWO_PI, b) || - FastMath.isApproxEqual(a, b - FastMath.TWO_PI) - - fun assertAnglesApproxEqual(expected: Float, actual: Float, message: String?) { - if (!anglesApproxEqual(expected, actual)) { - AssertionFailureBuilder.assertionFailure().message(message) - .expected(expected).actual(actual).buildAndThrow() - } - } - - fun quatApproxEqual(q1: Quaternion, q2: Quaternion, tolerance: Float = FastMath.ZERO_TOLERANCE): Boolean = FastMath.isApproxEqual(q1.w, q2.w, tolerance) && - FastMath.isApproxEqual(q1.x, q2.x, tolerance) && - FastMath.isApproxEqual(q1.y, q2.y, tolerance) && - FastMath.isApproxEqual(q1.z, q2.z, tolerance) - - fun vectorApproxEqual(v1: Vector3, v2: Vector3, tolerance: Float = FastMath.ZERO_TOLERANCE): Boolean = FastMath.isApproxEqual(v1.x, v2.x, tolerance) && - FastMath.isApproxEqual(v1.y, v2.y, tolerance) && - FastMath.isApproxEqual(v1.z, v2.z, tolerance) -} diff --git a/server/core/src/test/java/dev/slimevr/unit/TrackingPauseTests.kt b/server/core/src/test/java/dev/slimevr/unit/TrackingPauseTests.kt deleted file mode 100644 index f87dbbbbf6..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/TrackingPauseTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package dev.slimevr.unit - -import dev.slimevr.tracking.processor.HumanPoseManager -import dev.slimevr.unit.TrackerTestUtils.quatApproxEqual -import dev.slimevr.unit.TrackerTestUtils.vectorApproxEqual -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import org.junit.jupiter.api.Test -import kotlin.test.assertNotNull - -class TrackingPauseTests { - - val resetSource = "Unit Test" - - @Test - fun testTrackingPause() { - val trackers = TestTrackerSet() - - // Initialize skeleton and everything - val hpm = HumanPoseManager(trackers.allL) - // TODO: This being enabled makes waist position infinity initially but not - // later? Something weird is going on here... - hpm.setLegTweaksEnabled(false) - hpm.update() - - // Store expected tracker data - val expected = mutableMapOf>() - for (tracker in hpm.computedTrackers) { - expected.put(tracker.id, Pair(tracker.getRotation(), tracker.position)) - } - - // Pause tracking, nothing should move after this - hpm.setPauseTracking(true, resetSource) - - // Randomize tracker orientations, this should not affect the skeleton! - for ((i, tracker) in trackers.set.withIndex()) { - tracker.setRotation(TrackerTestUtils.testRotFromIndex(i)) - } - - // Tick the skeleton with random tracker rotations - hpm.update() - - // Since we paused before moving anything, the output should still be identity - for (tracker in hpm.computedTrackers) { - val trackerExpected = expected[tracker.id] - assertNotNull(trackerExpected) - - val expectedRot = trackerExpected.first - val expectedPos = trackerExpected.second - - val actualRot = tracker.getRotation() - val actualPos = tracker.position - - assert(quatApproxEqual(expectedRot, actualRot)) { - "\"${tracker.name}\" moved after being paused. Expected <$expectedRot>, actual <$actualRot>." - } - assert(vectorApproxEqual(expectedPos, actualPos)) { - "\"${tracker.name}\" moved after being paused. Expected <$expectedPos>, actual <$actualPos>." - } - } - } -} diff --git a/server/core/src/test/java/dev/slimevr/unit/TwinExtendedBackTests.kt b/server/core/src/test/java/dev/slimevr/unit/TwinExtendedBackTests.kt deleted file mode 100644 index 1d500db423..0000000000 --- a/server/core/src/test/java/dev/slimevr/unit/TwinExtendedBackTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.slimevr.unit - -import com.jme3.math.FastMath -import io.github.axisangles.ktmath.EulerAngles -import io.github.axisangles.ktmath.EulerOrder -import io.github.axisangles.ktmath.Quaternion -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory -import kotlin.math.sign -import kotlin.test.assertEquals - -class TwinExtendedBackTests { - - // Bottom back quarter - val negRange = -269..-180 - - // Front two quarters and top back quarter - val posRange = -179..90 - - val pitchRange = negRange.map { - AngleTest(it.toFloat(), -1f) - }.plus( - posRange.map { - AngleTest(it.toFloat(), 1f) - }, - ) - - @get:TestFactory - val pitchTests: List - get() = pitchRange - .map { a: AngleTest -> - DynamicTest.dynamicTest( - "Dot product test for ${if (a.sign > 0f) "positive" else "negative"} signs <$a>", - ) { - testSign( - Quaternion.IDENTITY, - EulerAngles( - EulerOrder.YZX, - a.pitch * FastMath.DEG_TO_RAD, - 0f, - 0f, - ).toQuaternion(), - a.sign, - ) - } - } - - fun testSign(ref: Quaternion, extended: Quaternion, expectedSign: Float) { - val result = extended.twinExtendedBack(ref) - val dot = ref.dot(result) - assertEquals(expectedSign.sign, dot.sign, "Resulting dot ($dot) does not match the expected sign ($expectedSign)") - } - - data class AngleTest(val pitch: Float, val sign: Float) -} diff --git a/server/desktop/.gitignore b/server/desktop/.gitignore index 796b96d1c4..d18cf6eed8 100644 --- a/server/desktop/.gitignore +++ b/server/desktop/.gitignore @@ -1 +1,3 @@ /build +config/* +!config/README.md diff --git a/server/desktop/build.gradle.kts b/server/desktop/build.gradle.kts index 1950bdbcc3..968abedbdb 100644 --- a/server/desktop/build.gradle.kts +++ b/server/desktop/build.gradle.kts @@ -14,21 +14,22 @@ plugins { id("com.gradleup.shadow") id("com.github.gmazzo.buildconfig") id("org.ajoberstar.grgit") + id("com.squareup.wire") } kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(24)) } } tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_24) freeCompilerArgs.set(listOf("-Xvalue-classes")) } } @@ -54,19 +55,40 @@ allprojects { } } +val downloadDriverProto by tasks.registering { + val protoFile = layout.buildDirectory.file("proto/ProtobufMessages.proto") + outputs.file(protoFile) + doLast { + val url = "https://raw.githubusercontent.com/SlimeVR/SlimeVR-OpenVR-Driver/main/src/bridge/ProtobufMessages.proto" + protoFile.get().asFile.parentFile.mkdirs() + uri(url).toURL().openStream().use { it.copyTo(protoFile.get().asFile.outputStream()) } + } +} + +wire { + sourcePath { + srcDir(downloadDriverProto.map { layout.buildDirectory.dir("proto") }) + } + kotlin { } +} + dependencies { implementation(project(":server:core")) implementation(project(":solarxr-protocol")) - implementation("commons-cli:commons-cli:1.11.0") - implementation("org.apache.commons:commons-lang3:3.20.0") - implementation("com.google.protobuf:protobuf-java:4.31.1") implementation("net.java.dev.jna:jna:5.+") implementation("net.java.dev.jna:jna-platform:5.+") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("com.fazecast:jSerialComm:2.11.3") { exclude(group = "com.fazecast", module = "android") } implementation("org.hid4java:hid4java:0.8.0") + implementation("io.klogging:klogging:0.11.7") + + val ktor_version = "3.4.1" + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") } tasks.shadowJar { @@ -83,6 +105,7 @@ tasks.shadowJar { } application { mainClass.set("dev.slimevr.desktop.Main") + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") } buildConfig { @@ -99,5 +122,6 @@ buildConfig { tasks.run { standardInput = System.`in` // this is not working + jvmArgs("--enable-native-access=ALL-UNNAMED") args = listOf("run") } diff --git a/server/desktop/protobuf_update.bat b/server/desktop/protobuf_update.bat deleted file mode 100644 index 0daebc314c..0000000000 --- a/server/desktop/protobuf_update.bat +++ /dev/null @@ -1 +0,0 @@ -protoc --proto_path=../../../SlimeVR-OpenVR-Driver/src/bridge --java_out=./src/main/java ProtobufMessages.proto diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt deleted file mode 100644 index 3fbf796673..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt +++ /dev/null @@ -1,285 +0,0 @@ -package dev.slimevr.desktop - -import com.sun.jna.Pointer -import com.sun.jna.platform.win32.COM.COMException -import com.sun.jna.platform.win32.COM.COMUtils -import com.sun.jna.platform.win32.COM.Dispatch -import com.sun.jna.platform.win32.Guid.CLSID -import com.sun.jna.platform.win32.Guid.IID -import com.sun.jna.platform.win32.OaIdl.VARIANT_BOOLByReference -import com.sun.jna.platform.win32.Ole32 -import com.sun.jna.platform.win32.OleAuto -import com.sun.jna.platform.win32.WTypes -import com.sun.jna.platform.win32.WinNT.HRESULT -import com.sun.jna.ptr.IntByReference -import com.sun.jna.ptr.PointerByReference -import dev.slimevr.ConnectivityFlags -import dev.slimevr.NetworkCategory -import dev.slimevr.NetworkInfo -import dev.slimevr.NetworkProfileChecker -import dev.slimevr.VRServer -import io.eiren.util.OperatingSystem -import java.util.* -import kotlin.concurrent.scheduleAtFixedRate - -/** - * @see INetworkConnection interface (netlistmgr.h) - */ -@Suppress("ktlint:standard:backing-property-naming", "ktlint:standard:function-naming") -class INetworkConnection(instance: Pointer?) : - Dispatch(instance), - AutoCloseable { - override fun close() { - Release() - } - companion object VTable { - // IUnknown +3 - // IDispatch +4 - // IEnumNetworkConnections - const val GetNetwork = 7 - const val IsConnectedToInternet = 8 - const val IsConnected = 9 - const val GetConnectivity = 10 - const val GetConnectionId = 11 - const val GetAdapterId = 12 - const val GetDomainType = 13 - } - - @Throws(COMException::class) - fun GetNetwork(): INetwork { - val pNetwork = PointerByReference() - val hr = _invokeNativeInt(VTable.GetNetwork, arrayOf(pointer, pNetwork)) - COMUtils.checkRC(HRESULT(hr)) - return INetwork(pNetwork.value) - } -} - -/** - * @see IEnumNetworkConnections interface (netlistmgr.h) - */ -@Suppress("ktlint:standard:backing-property-naming", "ktlint:standard:function-naming") -class IEnumNetworkConnections(instance: Pointer?) : - Dispatch(instance), - AutoCloseable { - override fun close() { - Release() - } - companion object VTable { - // IUnknown +3 - // IDispatch +4 - // IEnumNetworkConnections - const val _NewEnum = 7 - const val Next = 8 - const val Skip = 9 - const val Reset = 10 - const val Clone = 11 - } - - @Throws(COMException::class) - fun Next(): INetworkConnection? { - val ppNetworkConnections = PointerByReference() - val nFetched = IntByReference() - val hr = _invokeNativeInt(VTable.Next, arrayOf(pointer, 1, ppNetworkConnections, nFetched)) - COMUtils.checkRC(HRESULT(hr)) - if (nFetched.value.equals(0)) return null - return INetworkConnection(ppNetworkConnections.value) - } -} - -/** - * @see INetworkListManager interface (netlistmgr.h) - */ -@Suppress("ktlint:standard:backing-property-naming", "ktlint:standard:function-naming") -class INetworkListManager(instance: Pointer?) : - Dispatch(instance), - AutoCloseable { - override fun close() { - Release() - } - companion object VTable { - // IUnknown +3 - // IDispatch +4 - // INetworkListManager - const val GetNetworks = 7 - const val GetNetwork = 8 - const val GetNetworkConnections = 9 - const val GetNetworkConnection = 10 - const val IsConnectedToInternet = 11 - const val IsConnected = 12 - const val GetConnectivity = 13 - } - - @Throws(COMException::class) - fun GetNetworkConnections(): IEnumNetworkConnections { - val pEnumNetworkConnections = PointerByReference() - val hr = _invokeNativeInt(VTable.GetNetworkConnections, arrayOf(pointer, pEnumNetworkConnections)) - COMUtils.checkRC(HRESULT(hr)) - return IEnumNetworkConnections(pEnumNetworkConnections.value) - } -} - -/** - * @see INetwork interface (netlistmgr.h) - */ -@Suppress("ktlint:standard:backing-property-naming", "ktlint:standard:function-naming") -class INetwork(instance: Pointer?) : - Dispatch(instance), - AutoCloseable { - override fun close() { - Release() - } - companion object VTable { - // IUnknown +3 - // IDispatch +4 - // INetworkListManager - const val GetName = 7 - const val SetName = 8 - const val GetDescription = 9 - const val SetDescription = 10 - const val GetNetworkId = 11 - const val GetDomainType = 12 - const val GetNetworkConnections = 13 - const val GetTimeCreatedAndConnected = 14 - const val IsConnectedToInternet = 15 - const val IsConnected = 16 - const val GetConnectivity = 17 - const val GetCategory = 18 - const val SetCategory = 19 - } - - @Throws(COMException::class) - private fun getNativeString(vtableId: Int): String? { - val pStr = PointerByReference() - val hr = _invokeNativeInt(vtableId, arrayOf(pointer, pStr)) - COMUtils.checkRC(HRESULT(hr)) - val bstr = WTypes.BSTR(pStr.value) - val stringValue = bstr.value - OleAuto.INSTANCE.SysFreeString(bstr) - return stringValue - } - - @Throws(COMException::class) - fun GetName(): String? = getNativeString(VTable.GetName) - - @Throws(COMException::class) - fun GetDescription(): String? = getNativeString(VTable.GetDescription) - - @Throws(COMException::class) - fun IsConnected(): Boolean { - val bool = VARIANT_BOOLByReference() - val hr = _invokeNativeInt(VTable.IsConnected, arrayOf(pointer, bool)) - COMUtils.checkRC(HRESULT(hr)) - return bool.value.booleanValue() - } - - @Throws(COMException::class) - fun GetConnectivity(): Set { - val connectivity = IntByReference() - val hr = _invokeNativeInt(VTable.GetConnectivity, arrayOf(pointer, connectivity)) - COMUtils.checkRC(HRESULT(hr)) - return ConnectivityFlags.fromInt(connectivity.value) - } - - @Throws(COMException::class) - fun GetCategory(): NetworkCategory? { - val category = IntByReference() - val hr = _invokeNativeInt(VTable.GetCategory, arrayOf(pointer, category)) - COMUtils.checkRC(HRESULT(hr)) - return NetworkCategory.fromInt(category.value) - } -} - -/** - * Network List Manager API wrapper - * @see Network List Manager API - * @throws COMException - */ -class COMNetworkManager -@Throws(COMException::class) -constructor() : AutoCloseable { - var instance: INetworkListManager - private var shouldUninitialize = false - - init { - shouldUninitialize = COMUtils.SUCCEEDED(Ole32.INSTANCE.CoInitialize(null)) - - val CLSID_NetworkListManager = CLSID("dcb00c01-570f-4a9b-8d69-199fdba5723b") - val IID_INetworkListManager = IID("dcb00000-570f-4a9b-8d69-199fdba5723b") - - val ptr = PointerByReference() - val hr = Ole32.INSTANCE.CoCreateInstance( - CLSID_NetworkListManager, - null, - WTypes.CLSCTX_ALL, - IID_INetworkListManager, - ptr, - ) - try { - COMUtils.checkRC(hr) - } catch (err: Exception) { - if (shouldUninitialize) { - Ole32.INSTANCE.CoUninitialize() - } - throw err - } - - instance = INetworkListManager(ptr.value) - } - - override fun close() { - instance.close() - if (shouldUninitialize) { - Ole32.INSTANCE.CoUninitialize() - } - } -} - -fun enumerateNetworks(): List? { - if (OperatingSystem.currentPlatform != OperatingSystem.WINDOWS) { - return null - } - try { - COMNetworkManager().use { netmgr -> - netmgr.instance.GetNetworkConnections().use { conns -> - return generateSequence { conns.Next() } - .map { conn -> - conn.use { - conn.GetNetwork().use { network -> - NetworkInfo( - network.GetName(), - network.GetDescription(), - network.GetCategory(), - network.GetConnectivity(), - network.IsConnected(), - ) - } - } - }.toList() - } - } - } catch (err: Exception) { - println(err.stackTraceToString()) - } - return null -} - -class DesktopNetworkProfileChecker(private val server: VRServer) : NetworkProfileChecker() { - private val updateTickTimer = Timer("NetworkProfileCheck") - private var publicNetworksLocal: List = listOf() - - override val isSupported: Boolean - get() = OperatingSystem.currentPlatform == OperatingSystem.WINDOWS - - override val publicNetworks: List - get() = publicNetworksLocal - - init { - if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) { - this.updateTickTimer.scheduleAtFixedRate(0, 3000) { - publicNetworksLocal = enumerateNetworks()?.filter { net -> - net.connected == true && net.category == NetworkCategory.PUBLIC - } ?: listOf() - } - } - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index 22267c260a..7fb4ba79dd 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -2,259 +2,59 @@ package dev.slimevr.desktop -import dev.slimevr.Keybinding -import dev.slimevr.SLIMEVR_IDENTIFIER +import dev.slimevr.AppContext +import dev.slimevr.Phase1Context import dev.slimevr.VRServer -import dev.slimevr.bridge.Bridge -import dev.slimevr.config.ConfigManager -import dev.slimevr.desktop.firmware.DesktopSerialFlashingHandler -import dev.slimevr.desktop.games.vrchat.DesktopVRCConfigHandler -import dev.slimevr.desktop.platform.SteamVRBridge -import dev.slimevr.desktop.platform.linux.UnixSocketBridge -import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge -import dev.slimevr.desktop.platform.windows.WindowsNamedPipeBridge -import dev.slimevr.desktop.serial.DesktopSerialHandler -import dev.slimevr.desktop.tracking.trackers.hid.DesktopHIDManager -import dev.slimevr.tracking.trackers.Tracker -import io.eiren.util.OperatingSystem -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import org.apache.commons.cli.CommandLine -import org.apache.commons.cli.CommandLineParser -import org.apache.commons.cli.DefaultParser -import org.apache.commons.cli.HelpFormatter -import org.apache.commons.cli.Options -import org.apache.commons.lang3.SystemUtils -import java.io.File -import java.io.IOException -import java.lang.System -import java.net.DatagramSocket -import java.net.ServerSocket -import java.nio.file.Files -import java.nio.file.Paths -import javax.swing.JOptionPane -import kotlin.concurrent.thread -import kotlin.io.path.Path -import kotlin.io.path.exists -import kotlin.io.path.pathString -import kotlin.system.exitProcess - -val VERSION = - (GIT_VERSION_TAG.ifEmpty { GIT_COMMIT_HASH }) + - if (GIT_CLEAN) "" else "-dirty" - -fun main(args: Array) { - System.setProperty("awt.useSystemAAFontSettings", "on") - System.setProperty("swing.aatext", "true") - - val parser: CommandLineParser = DefaultParser() - val formatter = HelpFormatter() - val options = Options() - options.addOption("h", "help", false, "Show help") - options.addOption("V", "version", false, "Show version") - val cmd: CommandLine = try { - parser.parse(options, args, true) - } catch (e: org.apache.commons.cli.ParseException) { - formatter.printHelp("slimevr.jar", options) - exitProcess(1) - } - - if (cmd.hasOption("help")) { - formatter.printHelp("slimevr.jar", options) - exitProcess(0) - } - if (cmd.hasOption("version")) { - println("SlimeVR Server $VERSION") - exitProcess(0) - } - - if (cmd.args.isEmpty()) { - System.err.println("No command specified, expected 'run'") - exitProcess(1) - } - if (!cmd.args[0].equals("run", true)) { - System.err.println("Unknown command: ${cmd.args[0]}, expected 'run'") - exitProcess(1) - } - - val dir = OperatingSystem.resolveLogDirectory(SLIMEVR_IDENTIFIER)?.toFile()?.absoluteFile - ?: File("").absoluteFile - try { - LogManager.initialize(dir) - } catch (e1: java.lang.Exception) { - e1.printStackTrace() - } - LogManager.info("Using log folder: $dir") - LogManager.info("Running version $VERSION") - if (!SystemUtils.isJavaVersionAtLeast(org.apache.commons.lang3.JavaVersion.JAVA_17)) { - LogManager.severe("SlimeVR start-up error! A minimum of Java 17 is required.") - JOptionPane - .showMessageDialog( - null, - "SlimeVR start-up error! A minimum of Java 17 is required.", - "SlimeVR: Java Runtime Mismatch", - JOptionPane.ERROR_MESSAGE, - ) - LogManager.closeLogger() - return - } - - val configDir = resolveConfig() - LogManager.info("Using config dir: $configDir") - - val configManager = ConfigManager(configDir) - configManager.loadConfig() - - try { - DatagramSocket(configManager.vrConfig.server.trackerPort).close() - ServerSocket(21110).close() - } catch (e: IOException) { - val message = "SlimeVR start-up error! A required port (${configManager.vrConfig.server.trackerPort} and 21110) is busy. " + - "Make sure there is no other instance of SlimeVR Server running." - LogManager - .severe(message) - JOptionPane - .showMessageDialog( - null, - message, - "SlimeVR: Ports are busy", - JOptionPane.ERROR_MESSAGE, - ) - LogManager.closeLogger() - return - } - try { - val vrServer = VRServer( - ::provideBridges, - { _ -> DesktopSerialHandler() }, - { _ -> DesktopSerialFlashingHandler() }, - { _ -> DesktopVRCConfigHandler() }, - { server -> DesktopNetworkProfileChecker(server) }, - configManager = configManager, - ) - vrServer.start() - - // Start service for USB HID trackers - DesktopHIDManager( - "Sensors HID service", - ) { tracker: Tracker -> vrServer.registerTracker(tracker) } - - Keybinding(vrServer) - val scanner = thread { - while (true) { - if (readln() == "exit") { - vrServer.interrupt() - break - } - } - } - - vrServer.join() - scanner.join() - LogManager.closeLogger() - exitProcess(0) - } catch (e: Throwable) { - e.printStackTrace() - exitProcess(1) - } -} - -fun provideBridges( - server: VRServer, - computedTrackers: List, -): Sequence = sequence { - when (OperatingSystem.currentPlatform) { - OperatingSystem.WINDOWS -> { - // Create named pipe bridge for SteamVR driver - yield( - WindowsNamedPipeBridge( - server, - "steamvr", - "SteamVR Driver Bridge", - """\\.\pipe\SlimeVRDriver""", - computedTrackers, - ), - ) - - // Create named pipe bridge for SteamVR input - yield( - WindowsNamedPipeBridge( - server, - "steamvr_feeder", - "SteamVR Feeder Bridge", - """\\.\pipe\SlimeVRInput""", - FastList(), - ), - ) - } - - OperatingSystem.LINUX -> { - var linuxBridge: SteamVRBridge? = null - try { - linuxBridge = UnixSocketBridge( - server, - "steamvr", - "SteamVR Driver Bridge", - Paths.get(OperatingSystem.socketDirectory, "SlimeVRDriver") - .toString(), - computedTrackers, - ) - } catch (ex: Exception) { - LogManager.severe( - "Failed to initiate Unix socket, disabling driver bridge...", - ex, - ) - } - if (linuxBridge != null) { - // Close the named socket on shutdown, or otherwise it's not going to get removed - Runtime.getRuntime().addShutdownHook( - Thread { - try { - (linuxBridge as? UnixSocketBridge)?.close() - } catch (e: Exception) { - throw RuntimeException(e) - } - }, - ) - yield(linuxBridge) - } - - yield( - UnixSocketBridge( - server, - "steamvr_feeder", - "SteamVR Feeder Bridge", - Paths.get(OperatingSystem.socketDirectory, "SlimeVRInput") - .toString(), - FastList(), - ), - ) - - yield( - UnixSocketRpcBridge( - server, - Paths.get(OperatingSystem.socketDirectory, "SlimeVRRpc") - .toString(), - computedTrackers, - ), - ) - } - - else -> {} - } -} - -const val CONFIG_FILENAME = "vrconfig.yml" -fun resolveConfig(): String { - // If config folder exists, then save config on relative path - if (Path("config/").exists()) { - return CONFIG_FILENAME - } - - val configFile = OperatingSystem.resolveConfigDirectory(SLIMEVR_IDENTIFIER)?.resolve(CONFIG_FILENAME) ?: return CONFIG_FILENAME - if (!configFile.exists() && Path(CONFIG_FILENAME).exists()) { - LogManager.info("Moved local config file to appdata folder") - Files.move(Path(CONFIG_FILENAME), configFile) - } - return configFile.pathString +import dev.slimevr.config.AppConfig +import dev.slimevr.desktop.hid.createDesktopHIDManager +import dev.slimevr.desktop.ipc.createIpcServers +import dev.slimevr.desktop.ipc.createSolarXRWebsocketServer +import dev.slimevr.desktop.serial.createDesktopSerialServer +import dev.slimevr.desktop.vrchat.createDesktopVRCConfigManager +import dev.slimevr.firmware.FirmwareManager +import dev.slimevr.heightcalibration.HeightCalibrationManager +import dev.slimevr.provisioning.ProvisioningManager +import dev.slimevr.resolveConfigDirectory +import dev.slimevr.skeleton.Skeleton +import dev.slimevr.trackingchecklist.TrackingChecklist +import dev.slimevr.udp.UdpServer +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +fun main(args: Array) = runBlocking { + val configFolder = resolveConfigDirectory() ?: error("Unable to resolve config folder") + val config = AppConfig.create(this, configFolder = configFolder.toFile()) + val server = VRServer.create(this) + val serialServer = createDesktopSerialServer(this) + + val phase1 = Phase1Context(server = server, config = config, serialServer = serialServer) + + val firmwareManager = FirmwareManager.create(ctx = phase1, scope = this) + val vrcConfigManager = createDesktopVRCConfigManager(ctx = phase1, scope = this) + val skeleton = Skeleton.create(scope = this, ctx = phase1) + val provisioningManager = ProvisioningManager.create(ctx = phase1, scope = this) + val heightCalibrationManager = HeightCalibrationManager.create(ctx = phase1, scope = this) + val trackingChecklist = TrackingChecklist.create(scope = this) + val udpServer = UdpServer.create(scope = this) + + val appContext = AppContext( + server = server, + config = config, + serialServer = serialServer, + skeleton = skeleton, + firmwareManager = firmwareManager, + vrcConfigManager = vrcConfigManager, + provisioningManager = provisioningManager, + heightCalibrationManager = heightCalibrationManager, + trackingChecklist = trackingChecklist, + udpServer = udpServer, + ) + + appContext.startObserving() + + launch { createDesktopHIDManager(appContext, this) } + launch { createSolarXRWebsocketServer(appContext) } + launch { createIpcServers(appContext) } + + Unit } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/firmware/DesktopSerialFlashingHandler.kt b/server/desktop/src/main/java/dev/slimevr/desktop/firmware/DesktopSerialFlashingHandler.kt deleted file mode 100644 index 1d626bf6e4..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/firmware/DesktopSerialFlashingHandler.kt +++ /dev/null @@ -1,88 +0,0 @@ -package dev.slimevr.desktop.firmware - -import com.fazecast.jSerialComm.SerialPort -import dev.slimevr.firmware.SerialFlashingHandler -import io.eiren.util.logging.LogManager -import dev.slimevr.serial.SerialPort as SerialPortWrapper - -class DesktopSerialFlashingHandler : SerialFlashingHandler { - private var port: SerialPort? = null - - override fun openSerial(port: Any) { - if (port !is SerialPortWrapper) { - error("Not a serial port") - } - val ports = SerialPort.getCommPorts() - val comPort = ports.find { it.portLocation == port.portLocation } - ?: error("Unable to find port ${port.portLocation}") - if (comPort.isOpen) { - comPort.closePort() - } - if (!comPort.openPort(1000)) { - error("unable to open port") - } - this.port = comPort - } - - override fun closeSerial() { - val p = port ?: error("no port to close") - try { - p.closePort() - LogManager.info("Port closed") - } catch (e: Exception) { - error("unable to close port") - } - } - - override fun setDTR(value: Boolean) { - val p = port ?: error("no port to set DTR") - if (value) { - p.setDTR() - } else { - p.clearDTR() - } - } - - override fun setRTS(value: Boolean) { - val p = port ?: error("no port to set RTS") - if (value) { - p.setRTS() - } else { - p.clearRTS() - } - } - - override fun write(data: ByteArray) { - val p = port ?: error("no port to write") - p.writeBytes(data, data.size) - } - - override fun read(length: Int): ByteArray { - val p = port ?: error("no port to read") - val data = ByteArray(length) - p.readBytes(data, length) - return data - } - - override fun changeBaud(baud: Int) { - val p = port ?: error("no port to set the baud") - if (!p.setBaudRate(baud)) { - error("Unable to change baudrate") - } - } - - override fun setReadTimeout(timeout: Long) { - val p = port ?: error("no port to set the timeout") - p.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, timeout.toInt(), 0) - } - - override fun availableBytes(): Int { - val p = port ?: error("no port to check available bytes") - return p.bytesAvailable() - } - - override fun flushIOBuffers() { - val p = port ?: error("no port to flush") - p.flushIOBuffers() - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/DesktopVRCConfigHandler.kt b/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/DesktopVRCConfigHandler.kt deleted file mode 100644 index e780fa852b..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/DesktopVRCConfigHandler.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.slimevr.desktop.games.vrchat - -import dev.slimevr.games.vrchat.VRCAvatarMeasurementType -import dev.slimevr.games.vrchat.VRCConfigHandler -import dev.slimevr.games.vrchat.VRCConfigValues -import dev.slimevr.games.vrchat.VRCSpineMode -import dev.slimevr.games.vrchat.VRCTrackerModel -import io.eiren.util.OperatingSystem -import java.util.Timer -import kotlin.concurrent.timerTask - -const val VRC_REG_PATH = "Software\\VRChat\\VRChat" - -class DesktopVRCConfigHandler : VRCConfigHandler() { - - private val getDevicesTimer = Timer("FetchVRCConfigTimer") - private val regEdit: AbstractRegEdit = - if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) { - RegEditWindows() - } else { - RegEditLinux() - } - - private var configState: VRCConfigValues? = null - private var vrcConfigKeys: Map - lateinit var onChange: (config: VRCConfigValues) -> Unit - - private fun intValue(key: String): Int? { - val realKey = vrcConfigKeys[key] ?: return null - return regEdit.getDwordValue(VRC_REG_PATH, realKey) - } - - private fun doubleValue(key: String): Double? { - val realKey = vrcConfigKeys[key] ?: return null - return regEdit.getQwordValue(VRC_REG_PATH, realKey) - } - - init { - vrcConfigKeys = if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS || - OperatingSystem.currentPlatform == OperatingSystem.LINUX - ) { - regEdit.getVRChatKeys(VRC_REG_PATH) - } else { - mapOf() - } - } - - private fun updateCurrentState() { - vrcConfigKeys = regEdit.getVRChatKeys(VRC_REG_PATH) - val newConfig = VRCConfigValues( - legacyMode = intValue("VRC_IK_LEGACY") == 1, - shoulderTrackingDisabled = intValue("VRC_IK_DISABLE_SHOULDER_TRACKING") == 1, - userHeight = doubleValue("PlayerHeight") ?: -1.0, - calibrationRange = doubleValue("VRC_IK_CALIBRATION_RANGE") ?: -1.0, - trackerModel = VRCTrackerModel.getByValue(intValue("VRC_IK_TRACKER_MODEL") ?: -1) ?: VRCTrackerModel.UNKNOWN, - spineMode = VRCSpineMode.getByValue(intValue("VRC_IK_FBT_SPINE_MODE") ?: -1) ?: VRCSpineMode.UNKNOWN, - calibrationVisuals = intValue("VRC_IK_CALIBRATION_VIS") == 1, - avatarMeasurementType = VRCAvatarMeasurementType.getByValue(intValue("VRC_IK_AVATAR_MEASUREMENT_TYPE") ?: -1) ?: VRCAvatarMeasurementType.UNKNOWN, - shoulderWidthCompensation = intValue("VRC_IK_SHOULDER_WIDTH_COMPENSATION") == 1, - ) - if (newConfig != configState) { - configState = newConfig - onChange(newConfig) - } - } - - override val isSupported: Boolean - get() = ( - OperatingSystem.currentPlatform == OperatingSystem.WINDOWS || - OperatingSystem.currentPlatform == OperatingSystem.LINUX - ) && - vrcConfigKeys.isNotEmpty() - - override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) { - this.onChange = onChange - if (isSupported) { - updateCurrentState() - getDevicesTimer.scheduleAtFixedRate( - timerTask { - updateCurrentState() - }, - 0, - 3000, - ) - } - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt b/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt deleted file mode 100644 index 3bbf88acc2..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt +++ /dev/null @@ -1,159 +0,0 @@ -package dev.slimevr.desktop.games.vrchat - -import com.sun.jna.Memory -import com.sun.jna.platform.win32.Advapi32 -import com.sun.jna.platform.win32.Advapi32Util -import com.sun.jna.platform.win32.WinNT -import com.sun.jna.platform.win32.WinReg -import com.sun.jna.ptr.IntByReference -import io.eiren.util.logging.LogManager -import java.io.BufferedReader -import java.io.FileReader -import java.io.InvalidObjectException -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.io.path.Path -import kotlin.io.path.exists - -abstract class AbstractRegEdit { - abstract fun getQwordValue(path: String, key: String): Double? - abstract fun getDwordValue(path: String, key: String): Int? - abstract fun getVRChatKeys(path: String): Map -} - -class RegEditWindows : AbstractRegEdit() { - // Vrchat is dumb and write 64 bit doubles in the registry as DWORD instead of QWORD. - // so we have to be creative - override fun getQwordValue(path: String, key: String): Double? { - val hKey = WinReg.HKEY_CURRENT_USER - val phkResult = WinReg.HKEYByReference() - - // Open the registry key - if (Advapi32.INSTANCE.RegOpenKeyEx(hKey, path, 0, WinNT.KEY_READ, phkResult) != 0) { - LogManager.severe("[VRChatRegEdit] Error: Cannot open registry key") - return null - } - - val lpData = Memory(8) - val lpcbData = IntByReference(8) - - val result = Advapi32.INSTANCE.RegQueryValueEx( - phkResult.value, - key, - 0, - null, - lpData, - lpcbData, - ) - Advapi32.INSTANCE.RegCloseKey(phkResult.value) - - if (result != 0) { - LogManager.severe("[VRChatRegEdit] Error: Cannot read registry key") - return null - } - return lpData.getDouble(0) - } - - override fun getDwordValue(path: String, key: String): Int? = try { - val data = Advapi32Util.registryGetIntValue(WinReg.HKEY_CURRENT_USER, path, key) - data - } catch (e: Exception) { - LogManager.severe("[VRChatRegEdit] Error reading DWORD: ${e.message}") - null - } - - override fun getVRChatKeys(path: String): Map { - val keysMap = mutableMapOf() - - try { - Advapi32Util.registryGetValues(WinReg.HKEY_CURRENT_USER, path).forEach { - keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.key - } - } catch (e: Exception) { - LogManager.severe("[VRChatRegEdit] Error reading Values from VRC registry: ${e.message}") - } - return keysMap - } -} - -class RegEditLinux : AbstractRegEdit() { - init { - if (USER_REG_PATH == null) { - LogManager.info("[VRChatRegEdit] Couldn't find any VRChat registry file") - } else { - LogManager.info("[VRChatRegEdit] Using VRChat registry file: $USER_REG_PATH") - } - } - lateinit var registry: Map - - override fun getQwordValue(path: String, key: String): Double? { - val value = registry[key] ?: return null - if (!value.startsWith("hex(4):")) { - LogManager.severe("[VRChatRegEdit] Couldn't find value with the expected type") - return null - } - return ByteBuffer.wrap(value.substring(7).hexToByteArray(HEX_FORMAT)) - .order(ByteOrder.LITTLE_ENDIAN) - .double - } - - override fun getDwordValue(path: String, key: String): Int? = try { - val value = registry[key] ?: return null - if (value.startsWith("dword:")) { - value.substring(6).toInt() - } else { - throw InvalidObjectException("The requested key is not a DWORD but it is instead a $value") - } - } catch (e: Exception) { - LogManager.severe("[VRChatRegEdit] Error reading DWORD: ${e.message}") - null - } - - override fun getVRChatKeys(path: String): Map { - val keysMap = mutableMapOf() - val map = mutableMapOf() - - try { - BufferedReader(FileReader(USER_REG_PATH?.toFile() ?: return keysMap)).use { reader -> - // The reg file uses double backward-slash for paths - val actualPath = "[${path.replace("\\", """\\""")}]" - while (reader.ready()) { - val line = reader.readLine() - if (!line.startsWith(actualPath)) continue - // Skip the `#time` line - reader.readLine() - while (reader.ready()) { - val keyValue = reader.readLine() - if (keyValue == "") break - - KEY_VALUE_PATTERN.matchEntire(keyValue)?.let { - map[it.groupValues[1]] = it.groupValues[2] - keysMap[it.groupValues[1].replace("""_h\d+$""".toRegex(), "")] = it.groupValues[1] - } - } - break - } - } - } catch (e: Exception) { - LogManager.severe("[VRChatRegEdit] Error reading Values from VRC registry: ${e.message}") - } - registry = map - return keysMap - } - - companion object { - const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg" - val USER_REG_PATH = - System.getenv("HOME")?.let { - Path(it, ".steam", "root", USER_REG_SUBPATH).let { if (it.exists()) it else null } - ?: Path(it, ".steam", "debian-installation", USER_REG_SUBPATH).let { if (it.exists()) it else null } - ?: Path(it, ".var", "app", "com.valvesoftware.Steam", "data", "Steam", USER_REG_SUBPATH).let { if (it.exists()) it else null } - } - val KEY_VALUE_PATTERN = Regex(""""(.+)"=(.+)""") - - val HEX_FORMAT = HexFormat { - upperCase = false - bytes.byteSeparator = "," - } - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/hid/hid.kt b/server/desktop/src/main/java/dev/slimevr/desktop/hid/hid.kt new file mode 100644 index 0000000000..45021a4957 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/hid/hid.kt @@ -0,0 +1,137 @@ +package dev.slimevr.desktop.hid + +import dev.slimevr.AppContextProvider +import dev.slimevr.AppLogger +import dev.slimevr.device.DeviceActions +import dev.slimevr.hid.HIDReceiver +import dev.slimevr.hid.parseHIDPackets +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.hid4java.HidDevice +import org.hid4java.HidManager +import org.hid4java.HidServicesSpecification +import org.hid4java.jna.HidApi +import org.hid4java.jna.HidDeviceInfoStructure +import solarxr_protocol.datatypes.TrackerStatus + +private const val HID_POLL_INTERVAL_MS = 3000L + +private const val HID_TRACKER_RECEIVER_VID = 0x1209 +private const val HID_TRACKER_RECEIVER_PID = 0x7690 +private const val HID_TRACKER_PID = 0x7692 + +private fun isCompatibleDevice(vid: Int, pid: Int) = vid == HID_TRACKER_RECEIVER_VID && (pid == HID_TRACKER_RECEIVER_PID || pid == HID_TRACKER_PID) + +private val hidSpec = HidServicesSpecification().apply { isAutoStart = false } + +// Initialize the native HID library. Must be called before enumerateDevices. +private val hidServices by lazy { HidManager.getHidServices(hidSpec) } + +private fun enumerateCompatibleDevices(): Map { + hidServices // ensure native lib is loaded + val root = HidApi.enumerateDevices(0, 0) ?: return emptyMap() + val result = mutableMapOf() + var info: HidDeviceInfoStructure? = root + while (info != null) { + if (isCompatibleDevice(info.vendor_id.toInt(), info.product_id.toInt())) { + val device = HidDevice(info, null, hidSpec) + // Use path as key, unique per physical device, available without opening + result[info.path] = device + } + info = info.next() + } + HidApi.freeEnumeration(root) + return result +} + +private data class ActiveReceiver(val job: Job, val receiver: HIDReceiver) + +fun createDesktopHIDManager(appContext: AppContextProvider, scope: CoroutineScope) { + val active = mutableMapOf() + + scope.launch { + while (isActive) { + val found = withContext(Dispatchers.IO) { + try { + enumerateCompatibleDevices() + } catch (_: Exception) { + emptyMap() + } + } + + // Devices no longer present + jobs that exited on their own (read error) + val toRemove = (active.keys - found.keys) + + active.entries.filter { !it.value.job.isActive }.map { it.key } + for (path in toRemove) { + val entry = active.remove(path) ?: continue + entry.job.cancel() + entry.job.join() + AppLogger.hid.info("HID device removed: $path") + } + + // Open newly detected devices + for ((path, hidDevice) in found) { + if (path in active) continue + + if (!hidDevice.open()) { + AppLogger.hid.warn("Failed to open HID device: $path") + continue + } + + val serial = hidDevice.serialNumber ?: path + AppLogger.hid.info("HID device detected: $serial") + + val deviceJob = Job(scope.coroutineContext[Job]) + val deviceScope = CoroutineScope(scope.coroutineContext + deviceJob) + + val receiver = HIDReceiver.create( + serialNumber = serial, + appContext = appContext, + scope = deviceScope, + ) + + deviceScope.launch { + try { + while (isActive) { + val data = withContext(Dispatchers.IO) { + try { + hidDevice.readAll(0) + } catch (_: Exception) { + null + } + } + when { + data == null -> return@launch + + // read error, device gone + data.isNotEmpty() -> parseHIDPackets(data).forEach { receiver.packetEvents.emit(it) } + + else -> delay(1) // no data yet, yield without busy-spinning + } + } + } finally { + withContext(NonCancellable + Dispatchers.IO) { hidDevice.close() } + withContext(NonCancellable) { + for (record in receiver.context.state.value.trackers.values) { + appContext.server.getDevice(record.deviceId)?.context?.dispatch( + DeviceActions.Update { copy(status = TrackerStatus.DISCONNECTED) }, + ) + } + } + } + } + deviceJob.complete() + + active[path] = ActiveReceiver(deviceJob, receiver) + } + + delay(HID_POLL_INTERVAL_MS) + } + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ipc.kt b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ipc.kt new file mode 100644 index 0000000000..4bd23d7eec --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ipc.kt @@ -0,0 +1,33 @@ +package dev.slimevr.desktop.ipc + +import dev.slimevr.AppContextProvider +import dev.slimevr.CURRENT_PLATFORM +import dev.slimevr.Platform +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +const val DRIVER_SOCKET_NAME = "SlimeVRDriver" +const val FEEDER_SOCKET_NAME = "SlimeVRInput" +const val SOLARXR_SOCKET_NAME = "SlimeVRRpc" + +const val DRIVER_PIPE = "\\\\.\\pipe\\SlimeVRDriver" +const val FEEDER_PIPE = "\\\\.\\pipe\\SlimeVRInput" +const val SOLARXR_PIPE = "\\\\.\\pipe\\SlimeVRRpc" + +suspend fun createIpcServers(appContext: AppContextProvider) = coroutineScope { + when (CURRENT_PLATFORM) { + Platform.LINUX, Platform.OSX -> { + launch { createUnixDriverSocket(appContext) } + launch { createUnixFeederSocket(appContext) } + launch { createUnixSolarXRSocket(appContext) } + } + + Platform.WINDOWS -> { + launch { createWindowsDriverPipe(appContext) } + launch { createWindowsFeederPipe(appContext) } + launch { createWindowsSolarXRPipe(appContext) } + } + + else -> Unit + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/ipc/linux.kt b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/linux.kt new file mode 100644 index 0000000000..fc99b6cd15 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/linux.kt @@ -0,0 +1,95 @@ +package dev.slimevr.desktop.ipc + +import dev.slimevr.AppContextProvider +import dev.slimevr.getSocketDirectory +import dev.slimevr.solarxr.handleSolarXRBridge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.StandardProtocolFamily +import java.net.UnixDomainSocketAddress +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel +import kotlin.io.path.Path + +suspend fun createUnixDriverSocket(appContext: AppContextProvider) = acceptUnixClients(DRIVER_SOCKET_NAME) { channel -> + handleDriverConnection( + appContext = appContext, + messages = readFramedMessages(channel), + send = { bytes -> withContext(Dispatchers.IO) { writeFramed(channel, bytes) } }, + ) +} + +suspend fun createUnixFeederSocket(appContext: AppContextProvider) = acceptUnixClients(FEEDER_SOCKET_NAME) { channel -> + handleFeederConnection( + appContext = appContext, + messages = readFramedMessages(channel), + send = { bytes -> withContext(Dispatchers.IO) { writeFramed(channel, bytes) } }, + ) +} + +suspend fun createUnixSolarXRSocket(appContext: AppContextProvider) = acceptUnixClients(SOLARXR_SOCKET_NAME) { channel -> + handleSolarXRBridge( + appContext = appContext, + messages = readFramedMessages(channel), + send = { bytes -> withContext(Dispatchers.IO) { writeFramed(channel, bytes) } }, + ) +} + +private fun isSocketInUse(socketPath: String): Boolean = try { + SocketChannel.open(StandardProtocolFamily.UNIX).use { + it.connect(UnixDomainSocketAddress.of(socketPath)) + true + } +} catch (_: Exception) { + false +} + +// Length field is LE u32 and includes the 4-byte header itself +private fun readFramedMessages(channel: SocketChannel) = flow { + val lenBuf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) + while (true) { + lenBuf.clear() + if (channel.read(lenBuf) == -1) break + lenBuf.flip() + + val dataBuf = ByteBuffer.allocate(lenBuf.int - 4) + while (dataBuf.hasRemaining()) { + if (channel.read(dataBuf) == -1) break + } + emit(dataBuf.array()) + } +}.flowOn(Dispatchers.IO) + +private fun writeFramed(channel: SocketChannel, bytes: ByteArray) { + val header = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(bytes.size + 4).flip() + channel.write(arrayOf(header, ByteBuffer.wrap(bytes))) +} + +private suspend fun acceptUnixClients( + name: String, + handle: suspend (SocketChannel) -> Unit, +) = withContext(Dispatchers.IO) { + val path = Path(getSocketDirectory(), name) + val file = path.toFile() + if (file.exists()) { + check(!isSocketInUse(path.toString())) { + "$name socket is already in use by another process" + } + file.delete() + } + file.deleteOnExit() + + ServerSocketChannel.open(StandardProtocolFamily.UNIX).use { server -> + server.bind(UnixDomainSocketAddress.of(path)) + while (isActive) { + val client = server.accept() + launch { handle(client) } + } + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/ipc/protocol.kt b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/protocol.kt new file mode 100644 index 0000000000..8460288dd8 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/protocol.kt @@ -0,0 +1,83 @@ +package dev.slimevr.desktop.ipc + +import dev.slimevr.AppContextProvider +import dev.slimevr.desktop.platform.Position +import dev.slimevr.desktop.platform.ProtobufMessage +import dev.slimevr.desktop.platform.TrackerAdded +import dev.slimevr.desktop.platform.Version +import dev.slimevr.driver.DriverBridge +import dev.slimevr.driver.DriverBridgeInbound +import dev.slimevr.driver.DriverBridgeOutbound +import dev.slimevr.feeder.FeederBridge +import dev.slimevr.feeder.FeederBridgeInbound +import io.github.axisangles.ktmath.Quaternion +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +const val PROTOCOL_VERSION = 5 + +suspend fun handleDriverConnection( + appContext: AppContextProvider, + messages: Flow, + send: suspend (ByteArray) -> Unit, +) = coroutineScope { + val sendMutex = Mutex() + + suspend fun sendMsg(msg: ProtobufMessage) = sendMutex.withLock { + send(ProtobufMessage.ADAPTER.encode(msg)) + } + + val bridge = DriverBridge.create(id = appContext.server.nextHandle(), appContext = appContext, scope = this) + + bridge.outbound.on { event -> + sendMsg(ProtobufMessage(tracker_added = TrackerAdded(tracker_id = event.trackerId, tracker_serial = event.serial, tracker_name = event.name))) + } + bridge.outbound.on { event -> + sendMsg(ProtobufMessage(position = Position(tracker_id = event.trackerId, qx = event.rotation.x, qy = event.rotation.y, qz = event.rotation.z, qw = event.rotation.w))) + } + + sendMsg(ProtobufMessage(version = Version(protocol_version = PROTOCOL_VERSION))) + + try { + messages.collect { bytes -> + val msg = ProtobufMessage.ADAPTER.decode(bytes) + msg.version?.let { ver -> + bridge.inbound.emit(DriverBridgeInbound.Version(ver.protocol_version)) + } + msg.position?.let { pos -> + bridge.inbound.emit(DriverBridgeInbound.TrackerPosition(trackerId = pos.tracker_id, rotation = Quaternion(w = pos.qw, x = pos.qx, y = pos.qy, z = pos.qz), position = null)) + } + } + } finally { + bridge.disconnect() + } +} + +suspend fun handleFeederConnection( + appContext: AppContextProvider, + messages: Flow, + send: suspend (ByteArray) -> Unit, +) = coroutineScope { + val bridge = FeederBridge.create(id = appContext.server.nextHandle(), appContext = appContext, scope = this) + + send(ProtobufMessage.ADAPTER.encode(ProtobufMessage(version = Version(protocol_version = PROTOCOL_VERSION)))) + + try { + messages.collect { bytes -> + val msg = ProtobufMessage.ADAPTER.decode(bytes) + msg.version?.let { ver -> + bridge.inbound.emit(FeederBridgeInbound.Version(protocolVersion = ver.protocol_version, firmware = ver.toString())) + } + msg.tracker_added?.let { ta -> + bridge.inbound.emit(FeederBridgeInbound.TrackerAdded(serial = ta.tracker_serial)) + } + msg.position?.let { pos -> + bridge.inbound.emit(FeederBridgeInbound.TrackerPosition(trackerId = pos.tracker_id, rotation = Quaternion(w = pos.qw, x = pos.qx, y = pos.qy, z = pos.qz), position = null)) + } + } + } finally { + bridge.disconnect() + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/ipc/windows.kt b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/windows.kt new file mode 100644 index 0000000000..8a17499d7c --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/windows.kt @@ -0,0 +1,127 @@ +package dev.slimevr.desktop.ipc + +import com.sun.jna.platform.win32.Advapi32 +import com.sun.jna.platform.win32.Kernel32 +import com.sun.jna.platform.win32.WinBase +import com.sun.jna.platform.win32.WinError +import com.sun.jna.platform.win32.WinNT +import com.sun.jna.ptr.IntByReference +import dev.slimevr.AppContextProvider +import dev.slimevr.solarxr.handleSolarXRBridge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.nio.ByteBuffer +import java.nio.ByteOrder + +private val k32 = Kernel32.INSTANCE +private val adv32 = Advapi32.INSTANCE + +suspend fun createWindowsDriverPipe(appContext: AppContextProvider) = acceptWindowsClients(DRIVER_PIPE) { handle -> + handleDriverConnection( + appContext = appContext, + messages = readFramedMessages(handle), + send = { bytes -> withContext(Dispatchers.IO) { writeFramedPipe(handle, bytes) } }, + ) +} + +suspend fun createWindowsFeederPipe(appContext: AppContextProvider) = acceptWindowsClients(FEEDER_PIPE) { handle -> + handleFeederConnection( + appContext = appContext, + messages = readFramedMessages(handle), + send = { bytes -> withContext(Dispatchers.IO) { writeFramedPipe(handle, bytes) } }, + ) +} + +suspend fun createWindowsSolarXRPipe(appContext: AppContextProvider) = acceptWindowsClients(SOLARXR_PIPE) { handle -> + handleSolarXRBridge( + appContext = appContext, + messages = readFramedMessages(handle), + send = { bytes -> withContext(Dispatchers.IO) { writeFramedPipe(handle, bytes) } }, + ) +} + +// Length field is LE u32 and includes the 4-byte header itself +private fun readFramedMessages(handle: WinNT.HANDLE) = flow { + val lenBuf = ByteArray(4) + while (true) { + if (!readExact(handle, lenBuf, 4)) break + val totalLen = ByteBuffer.wrap(lenBuf).order(ByteOrder.LITTLE_ENDIAN).int + + val dataBuf = ByteArray(totalLen - 4) + if (!readExact(handle, dataBuf, totalLen - 4)) break + emit(dataBuf) + } +}.flowOn(Dispatchers.IO) + +private fun readExact(handle: WinNT.HANDLE, buf: ByteArray, len: Int): Boolean { + var offset = 0 + val bytesRead = IntByReference() + while (offset < len) { + val ok = k32.ReadFile(handle, buf, len - offset, bytesRead, null) + if (!ok || bytesRead.value == 0) return false + offset += bytesRead.value + } + return true +} + +private fun writeFramedPipe(handle: WinNT.HANDLE, bytes: ByteArray) { + val buf = ByteArray(bytes.size + 4) + ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN).putInt(bytes.size + 4) + bytes.copyInto(buf, destinationOffset = 4) + k32.WriteFile(handle, buf, buf.size, IntByReference(), null) +} + +private fun createSecurePipe(pipeName: String): WinNT.HANDLE { + // Null DACL allows any process (including SteamVR driver) to connect + val descriptor = WinNT.SECURITY_DESCRIPTOR(64 * 1024) + adv32.InitializeSecurityDescriptor(descriptor, WinNT.SECURITY_DESCRIPTOR_REVISION) + adv32.SetSecurityDescriptorDacl(descriptor, true, null, false) + + val attributes = WinBase.SECURITY_ATTRIBUTES() + attributes.lpSecurityDescriptor = descriptor.pointer + attributes.bInheritHandle = false + + val pipe = k32.CreateNamedPipe( + pipeName, + WinBase.PIPE_ACCESS_DUPLEX, + WinBase.PIPE_TYPE_BYTE or WinBase.PIPE_READMODE_BYTE or WinBase.PIPE_WAIT, + WinBase.PIPE_UNLIMITED_INSTANCES, + 65536, + 65536, + 0, + attributes, + ) + check(pipe != WinNT.INVALID_HANDLE_VALUE) { + "CreateNamedPipe failed for $pipeName: ${k32.GetLastError()}" + } + return pipe +} + +private suspend fun acceptWindowsClients( + pipeName: String, + handle: suspend (WinNT.HANDLE) -> Unit, +) = withContext(Dispatchers.IO) { + while (isActive) { + val pipe = createSecurePipe(pipeName) + + val ok = k32.ConnectNamedPipe(pipe, null) + val err = k32.GetLastError() + if (!ok && err != WinError.ERROR_PIPE_CONNECTED) { + k32.CloseHandle(pipe) + continue + } + + launch { + try { + handle(pipe) + } finally { + k32.DisconnectNamedPipe(pipe) + k32.CloseHandle(pipe) + } + } + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ws-server.kt b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ws-server.kt new file mode 100644 index 0000000000..f022389356 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/ipc/ws-server.kt @@ -0,0 +1,47 @@ +package dev.slimevr.desktop.ipc + +import dev.slimevr.AppContextProvider +import dev.slimevr.AppLogger +import dev.slimevr.solarxr.handleSolarXRBridge +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.routing.routing +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.webSocket +import io.ktor.websocket.Frame +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.flow + +const val SOLARXR_PORT = 21110 + +suspend fun createSolarXRWebsocketServer(appContext: AppContextProvider) { + val engine = embeddedServer(Netty, port = SOLARXR_PORT) { + install(WebSockets) + + routing { + webSocket { + AppLogger.solarxr.info("[WS] New connection") + handleSolarXRBridge( + appContext = appContext, + messages = flow { + for (frame in incoming) { + when (frame) { + is Frame.Binary -> emit(frame.data) + is Frame.Close -> AppLogger.solarxr.info("[WS] Connection closed") + else -> {} + } + } + }, + send = { bytes -> send(Frame.Binary(fin = true, data = bytes)) }, + ) + } + } + } + engine.start(wait = false) + try { + awaitCancellation() + } finally { + engine.stop() + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt deleted file mode 100644 index fdbee06725..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt +++ /dev/null @@ -1,300 +0,0 @@ -package dev.slimevr.desktop.platform - -import dev.slimevr.VRServer.Companion.instance -import dev.slimevr.bridge.BridgeThread -import dev.slimevr.bridge.ISteamVRBridge -import dev.slimevr.desktop.platform.ProtobufMessages.* -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.TrackerStatus.Companion.getById -import dev.slimevr.tracking.trackers.TrackerUtils -import dev.slimevr.util.ann.VRServerThread -import io.eiren.util.ann.Synchronize -import io.eiren.util.ann.ThreadSafe -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import java.util.Queue -import java.util.concurrent.LinkedBlockingQueue -import kotlin.collections.HashMap - -abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISteamVRBridge { - @JvmField - @VRServerThread - protected val sharedTrackers: MutableList = FastList() - - @ThreadSafe - private val inputQueue: Queue = LinkedBlockingQueue() - - @ThreadSafe - private val outputQueue: Queue = LinkedBlockingQueue() - - @Synchronize("self") - private val remoteTrackersBySerial: MutableMap = HashMap() - - @Synchronize("self") - private val remoteTrackersByTrackerId: MutableMap = HashMap() - private var hadNewData = false - - /** - * Wakes the bridge thread, implementation is platform-specific. - */ - @ThreadSafe - protected abstract fun signalSend() - - @BridgeThread - protected abstract fun sendMessageReal(message: ProtobufMessage?): Boolean - - private var remoteProtocolVersion: Int = 0 - - @BridgeThread - protected fun messageReceived(message: ProtobufMessage) { - inputQueue.add(message) - } - - @ThreadSafe - protected fun sendMessage(message: ProtobufMessage) { - outputQueue.add(message) - signalSend() - } - - @BridgeThread - protected fun updateMessageQueue() { - var message: ProtobufMessage? - while ((outputQueue.poll().also { message = it }) != null) { - if (!sendMessageReal(message)) return - } - } - - @VRServerThread - override fun dataRead() { - hadNewData = false - var message: ProtobufMessage? - while ((inputQueue.poll().also { message = it }) != null) { - processMessageReceived(message) - hadNewData = true - } - } - - @VRServerThread - protected fun trackerOverrideUpdate(source: Tracker, target: Tracker) { - target.position = source.position - target.setRotation(source.getRotation()) - target.status = source.status - target.batteryLevel = source.batteryLevel - target.batteryVoltage = source.batteryVoltage - target.dataTick() - } - - @VRServerThread - override fun dataWrite() { - if (!hadNewData) { - // Don't write anything if no message were received, we - // always process at the - // speed of the other side - return - } - for (tracker in sharedTrackers) { - writeTrackerUpdate(tracker) - writeBatteryUpdate(tracker) - } - } - - @VRServerThread - protected fun writeTrackerUpdate(localTracker: Tracker?) { - val builder = ProtobufMessages.Position.newBuilder().setTrackerId( - localTracker!!.id, - ) - if (localTracker.hasPosition) { - val pos = localTracker.position - builder.setX(pos.x) - builder.setY(pos.y) - builder.setZ(pos.z) - } - if (localTracker.hasRotation) { - val rot = localTracker.getRotation() - builder.setQx(rot.x) - builder.setQy(rot.y) - builder.setQz(rot.z) - builder.setQw(rot.w) - } - sendMessage(ProtobufMessage.newBuilder().setPosition(builder).build()) - } - - @VRServerThread - protected open fun writeBatteryUpdate(localTracker: Tracker) { - return - } - - @VRServerThread - protected fun processMessageReceived(message: ProtobufMessage?) { - // if(!message.hasPosition()) - // LogManager.log.info("[" + bridgeName + "] MSG: " + message); - if (message!!.hasPosition()) { - positionReceived(message.position) - } else if (message.hasUserAction()) { - userActionReceived(message.userAction) - } else if (message.hasTrackerStatus()) { - trackerStatusReceived(message.trackerStatus) - } else if (message.hasTrackerAdded()) { - trackerAddedReceived(message.trackerAdded) - } else if (message.hasBattery()) { - batteryReceived(message.battery) - } else if (message.hasVersion()) { - versionReceived(message.version) - } - } - - @VRServerThread - protected fun positionReceived(positionMessage: ProtobufMessages.Position) { - val tracker = getInternalRemoteTrackerById(positionMessage.trackerId) - if (tracker != null) { - if (positionMessage.hasX()) { - tracker - .position = Vector3( - positionMessage.x, - positionMessage.y, - positionMessage.z, - ) - } - - tracker - .setRotation( - Quaternion( - positionMessage.qw, - positionMessage.qx, - positionMessage.qy, - positionMessage.qz, - ), - - ) - tracker.dataTick() - } - } - - @VRServerThread - protected open fun versionReceived(versionMessage: Version) { - remoteProtocolVersion = versionMessage.protocolVersion - if (remoteProtocolVersion == PROTOCOL_VERSION) { - LogManager.info("[ProtobufBridge] Driver protocol version matches the server protocol version: $remoteProtocolVersion") - } else { - LogManager.warning("[ProtobufBridge] Driver protocol version ($remoteProtocolVersion) doesn't match the server protocol version ($PROTOCOL_VERSION)") - } - } - - @VRServerThread - protected open fun batteryReceived(batteryMessage: Battery) { - return - } - - @VRServerThread - protected abstract fun createNewTracker(trackerAdded: TrackerAdded): Tracker - - @VRServerThread - protected fun trackerAddedReceived(trackerAdded: TrackerAdded) { - var tracker = getInternalRemoteTrackerById(trackerAdded.trackerId) - if (tracker != null) { - // TODO reinit? - return - } - tracker = createNewTracker(trackerAdded) - synchronized(remoteTrackersBySerial) { - remoteTrackersBySerial.put(tracker!!.name, tracker) - } - synchronized(remoteTrackersByTrackerId) { - remoteTrackersByTrackerId.put(tracker!!.trackerNum, tracker) - } - instance.registerTracker(tracker!!) - } - - @VRServerThread - protected fun userActionReceived(userAction: ProtobufMessages.UserAction) { - val resetSourceName = String.format("%s: %s", resetSourceNamePrefix, bridgeName) - when (userAction.name) { - "reset" -> // TODO : Check pose field - instance.resetTrackersFull(resetSourceName) - - "fast_reset" -> instance.resetTrackersYaw(resetSourceName) - - "mounting_reset" -> instance.resetTrackersMounting(resetSourceName) - - "feet_mounting_reset" -> instance.resetTrackersMounting( - resetSourceName, - TrackerUtils.feetsBodyParts, - ) - - "pause_tracking" -> - instance - .togglePauseTracking(resetSourceName) - } - } - - @VRServerThread - protected fun trackerStatusReceived(trackerStatus: ProtobufMessages.TrackerStatus) { - val tracker = getInternalRemoteTrackerById(trackerStatus.trackerId) - if (tracker != null) { - tracker.status = getById(trackerStatus.statusValue)!! - } - } - - @ThreadSafe - protected fun getInternalRemoteTrackerById(trackerId: Int): Tracker? { - synchronized(remoteTrackersByTrackerId) { - return remoteTrackersByTrackerId[trackerId] - } - } - - @VRServerThread - protected fun reconnected() { - for (tracker in sharedTrackers) { - val builder = TrackerAdded - .newBuilder() - .setTrackerId(tracker.id) - .setTrackerName(tracker.name) - .setTrackerSerial(tracker.name) - .setTrackerRole(tracker.trackerPosition!!.trackerRole!!.id) - sendMessage(ProtobufMessage.newBuilder().setTrackerAdded(builder).build()) - } - } - - @VRServerThread - protected fun disconnected() { - synchronized(remoteTrackersByTrackerId) { - for ((_, value) in remoteTrackersByTrackerId) { - value.status = TrackerStatus.DISCONNECTED - } - } - } - - @VRServerThread - override fun addSharedTracker(tracker: Tracker?) { - if (sharedTrackers.contains(tracker) || tracker == null) return - sharedTrackers.add(tracker) - val builder = TrackerAdded - .newBuilder() - .setTrackerId(tracker.id) - .setTrackerName(tracker.name) - .setTrackerSerial(tracker.name) - .setTrackerRole(tracker.trackerPosition!!.trackerRole!!.id) - sendMessage(ProtobufMessage.newBuilder().setTrackerAdded(builder).build()) - } - - @VRServerThread - override fun removeSharedTracker(tracker: Tracker?) { - // Remove shared tracker - sharedTrackers.remove(tracker) - - // Set the tracker's status as disconnected - val statusBuilder = ProtobufMessages.TrackerStatus - .newBuilder() - .setTrackerId(tracker!!.id) - statusBuilder.setStatus(ProtobufMessages.TrackerStatus.Status.DISCONNECTED) - sendMessage(ProtobufMessage.newBuilder().setTrackerStatus(statusBuilder).build()) - } - - companion object { - private const val resetSourceNamePrefix = "ProtobufBridge" - private const val PROTOCOL_VERSION = 1 - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java deleted file mode 100644 index ebb8e0f03c..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufMessages.java +++ /dev/null @@ -1,9167 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: ProtobufMessages.proto -// Protobuf Java Version: 4.31.1 - -package dev.slimevr.desktop.platform; - -@com.google.protobuf.Generated -public final class ProtobufMessages { - private ProtobufMessages() { - } - - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - ProtobufMessages.class.getName() - ); - } - - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistryLite registry - ) { - } - - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistry registry - ) { - registerAllExtensions( - (com.google.protobuf.ExtensionRegistryLite) registry - ); - } - - public interface PingPongOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.PingPong) - com.google.protobuf.MessageOrBuilder { - } - - /** - * Protobuf type {@code messages.PingPong} - */ - public static final class PingPong extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.PingPong) - PingPongOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - PingPong.class.getName() - ); - } - - // Use PingPong.newBuilder() to construct. - private PingPong(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private PingPong() { - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_PingPong_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_PingPong_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.PingPong.class, - dev.slimevr.desktop.platform.ProtobufMessages.PingPong.Builder.class - ); - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.PingPong)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.PingPong other = (dev.slimevr.desktop.platform.ProtobufMessages.PingPong) obj; - - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.PingPong prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.PingPong} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.PingPong) - dev.slimevr.desktop.platform.ProtobufMessages.PingPongOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_PingPong_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_PingPong_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.PingPong.class, - dev.slimevr.desktop.platform.ProtobufMessages.PingPong.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.PingPong.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_PingPong_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PingPong getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.PingPong.getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PingPong build() { - dev.slimevr.desktop.platform.ProtobufMessages.PingPong result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PingPong buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.PingPong result = new dev.slimevr.desktop.platform.ProtobufMessages.PingPong( - this - ); - onBuilt(); - return result; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.PingPong) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.PingPong) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(dev.slimevr.desktop.platform.ProtobufMessages.PingPong other) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.PingPong - .getDefaultInstance() - ) - return this; - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.PingPong) - } - - // @@protoc_insertion_point(class_scope:messages.PingPong) - private static final dev.slimevr.desktop.platform.ProtobufMessages.PingPong DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.PingPong(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.PingPong getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public PingPong parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PingPong getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface VersionOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.Version) - com.google.protobuf.MessageOrBuilder { - - /** - * int32 protocol_version = 1; - * - * @return The protocolVersion. - */ - int getProtocolVersion(); - } - - /** - * Protobuf type {@code messages.Version} - */ - public static final class Version extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.Version) - VersionOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - Version.class.getName() - ); - } - - // Use Version.newBuilder() to construct. - private Version(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private Version() { - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Version_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Version_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Version.class, - dev.slimevr.desktop.platform.ProtobufMessages.Version.Builder.class - ); - } - - public static final int PROTOCOL_VERSION_FIELD_NUMBER = 1; - private int protocolVersion_ = 0; - - /** - * int32 protocol_version = 1; - * - * @return The protocolVersion. - */ - @java.lang.Override - public int getProtocolVersion() { - return protocolVersion_; - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (protocolVersion_ != 0) { - output.writeInt32(1, protocolVersion_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (protocolVersion_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(1, protocolVersion_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.Version)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.Version other = (dev.slimevr.desktop.platform.ProtobufMessages.Version) obj; - - if ( - getProtocolVersion() - != other.getProtocolVersion() - ) - return false; - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + PROTOCOL_VERSION_FIELD_NUMBER; - hash = (53 * hash) + getProtocolVersion(); - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.Version prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.Version} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.Version) - dev.slimevr.desktop.platform.ProtobufMessages.VersionOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Version_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Version_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Version.class, - dev.slimevr.desktop.platform.ProtobufMessages.Version.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.Version.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - protocolVersion_ = 0; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Version_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.Version.getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version build() { - dev.slimevr.desktop.platform.ProtobufMessages.Version result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.Version result = new dev.slimevr.desktop.platform.ProtobufMessages.Version( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.Version result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.protocolVersion_ = protocolVersion_; - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.Version) { - return mergeFrom((dev.slimevr.desktop.platform.ProtobufMessages.Version) other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(dev.slimevr.desktop.platform.ProtobufMessages.Version other) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance() - ) - return this; - if (other.getProtocolVersion() != 0) { - setProtocolVersion(other.getProtocolVersion()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - protocolVersion_ = input.readInt32(); - bitField0_ |= 0x00000001; - break; - } // case 8 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private int protocolVersion_; - - /** - * int32 protocol_version = 1; - * - * @return The protocolVersion. - */ - @java.lang.Override - public int getProtocolVersion() { - return protocolVersion_; - } - - /** - * int32 protocol_version = 1; - * - * @param value The protocolVersion to set. - * @return This builder for chaining. - */ - public Builder setProtocolVersion(int value) { - - protocolVersion_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * int32 protocol_version = 1; - * - * @return This builder for chaining. - */ - public Builder clearProtocolVersion() { - bitField0_ = (bitField0_ & ~0x00000001); - protocolVersion_ = 0; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.Version) - } - - // @@protoc_insertion_point(class_scope:messages.Version) - private static final dev.slimevr.desktop.platform.ProtobufMessages.Version DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.Version(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Version getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public Version parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface PositionOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.Position) - com.google.protobuf.MessageOrBuilder { - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - int getTrackerId(); - - /** - * optional float x = 2; - * - * @return Whether the x field is set. - */ - boolean hasX(); - - /** - * optional float x = 2; - * - * @return The x. - */ - float getX(); - - /** - * optional float y = 3; - * - * @return Whether the y field is set. - */ - boolean hasY(); - - /** - * optional float y = 3; - * - * @return The y. - */ - float getY(); - - /** - * optional float z = 4; - * - * @return Whether the z field is set. - */ - boolean hasZ(); - - /** - * optional float z = 4; - * - * @return The z. - */ - float getZ(); - - /** - * float qx = 5; - * - * @return The qx. - */ - float getQx(); - - /** - * float qy = 6; - * - * @return The qy. - */ - float getQy(); - - /** - * float qz = 7; - * - * @return The qz. - */ - float getQz(); - - /** - * float qw = 8; - * - * @return The qw. - */ - float getQw(); - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return Whether the dataSource field is set. - */ - boolean hasDataSource(); - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The enum numeric value on the wire for dataSource. - */ - int getDataSourceValue(); - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The dataSource. - */ - dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource getDataSource(); - } - - /** - * Protobuf type {@code messages.Position} - */ - public static final class Position extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.Position) - PositionOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - Position.class.getName() - ); - } - - // Use Position.newBuilder() to construct. - private Position(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private Position() { - dataSource_ = 0; - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Position_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Position_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Position.class, - dev.slimevr.desktop.platform.ProtobufMessages.Position.Builder.class - ); - } - - /** - * Protobuf enum {@code messages.Position.DataSource} - */ - public enum DataSource - implements com.google.protobuf.ProtocolMessageEnum { - /** - * NONE = 0; - */ - NONE(0), - /** - * IMU = 1; - */ - IMU(1), - /** - * PRECISION = 2; - */ - PRECISION(2), - /** - * FULL = 3; - */ - FULL(3), - UNRECOGNIZED(-1), - ; - - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - DataSource.class.getName() - ); - } - /** - * NONE = 0; - */ - public static final int NONE_VALUE = 0; - /** - * IMU = 1; - */ - public static final int IMU_VALUE = 1; - /** - * PRECISION = 2; - */ - public static final int PRECISION_VALUE = 2; - /** - * FULL = 3; - */ - public static final int FULL_VALUE = 3; - - - public final int getNumber() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalArgumentException( - "Can't get the number of an unknown enum value." - ); - } - return value; - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static DataSource valueOf(int value) { - return forNumber(value); - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - */ - public static DataSource forNumber(int value) { - switch (value) { - case 0: - return NONE; - case 1: - return IMU; - case 2: - return PRECISION; - case 3: - return FULL; - default: - return null; - } - } - - public static com.google.protobuf.Internal.EnumLiteMap internalGetValueMap() { - return internalValueMap; - } - - private static final com.google.protobuf.Internal.EnumLiteMap internalValueMap = new com.google.protobuf.Internal.EnumLiteMap() { - public DataSource findValueByNumber(int number) { - return DataSource.forNumber(number); - } - }; - - public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalStateException( - "Can't get the descriptor of an unrecognized enum value." - ); - } - return getDescriptor().getValues().get(ordinal()); - } - - public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { - return getDescriptor(); - } - - public static com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDescriptor() - .getEnumTypes() - .get(0); - } - - private static final DataSource[] VALUES = values(); - - public static DataSource valueOf( - com.google.protobuf.Descriptors.EnumValueDescriptor desc - ) { - if (desc.getType() != getDescriptor()) { - throw new java.lang.IllegalArgumentException( - "EnumValueDescriptor is not for this type." - ); - } - if (desc.getIndex() == -1) { - return UNRECOGNIZED; - } - return VALUES[desc.getIndex()]; - } - - private final int value; - - private DataSource(int value) { - this.value = value; - } - - // @@protoc_insertion_point(enum_scope:messages.Position.DataSource) - } - - private int bitField0_; - public static final int TRACKER_ID_FIELD_NUMBER = 1; - private int trackerId_ = 0; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - public static final int X_FIELD_NUMBER = 2; - private float x_ = 0F; - - /** - * optional float x = 2; - * - * @return Whether the x field is set. - */ - @java.lang.Override - public boolean hasX() { - return ((bitField0_ & 0x00000001) != 0); - } - - /** - * optional float x = 2; - * - * @return The x. - */ - @java.lang.Override - public float getX() { - return x_; - } - - public static final int Y_FIELD_NUMBER = 3; - private float y_ = 0F; - - /** - * optional float y = 3; - * - * @return Whether the y field is set. - */ - @java.lang.Override - public boolean hasY() { - return ((bitField0_ & 0x00000002) != 0); - } - - /** - * optional float y = 3; - * - * @return The y. - */ - @java.lang.Override - public float getY() { - return y_; - } - - public static final int Z_FIELD_NUMBER = 4; - private float z_ = 0F; - - /** - * optional float z = 4; - * - * @return Whether the z field is set. - */ - @java.lang.Override - public boolean hasZ() { - return ((bitField0_ & 0x00000004) != 0); - } - - /** - * optional float z = 4; - * - * @return The z. - */ - @java.lang.Override - public float getZ() { - return z_; - } - - public static final int QX_FIELD_NUMBER = 5; - private float qx_ = 0F; - - /** - * float qx = 5; - * - * @return The qx. - */ - @java.lang.Override - public float getQx() { - return qx_; - } - - public static final int QY_FIELD_NUMBER = 6; - private float qy_ = 0F; - - /** - * float qy = 6; - * - * @return The qy. - */ - @java.lang.Override - public float getQy() { - return qy_; - } - - public static final int QZ_FIELD_NUMBER = 7; - private float qz_ = 0F; - - /** - * float qz = 7; - * - * @return The qz. - */ - @java.lang.Override - public float getQz() { - return qz_; - } - - public static final int QW_FIELD_NUMBER = 8; - private float qw_ = 0F; - - /** - * float qw = 8; - * - * @return The qw. - */ - @java.lang.Override - public float getQw() { - return qw_; - } - - public static final int DATA_SOURCE_FIELD_NUMBER = 9; - private int dataSource_ = 0; - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return Whether the dataSource field is set. - */ - @java.lang.Override - public boolean hasDataSource() { - return ((bitField0_ & 0x00000008) != 0); - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The enum numeric value on the wire for dataSource. - */ - @java.lang.Override - public int getDataSourceValue() { - return dataSource_; - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The dataSource. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource getDataSource() { - dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource result = dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource - .forNumber(dataSource_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource.UNRECOGNIZED - : result; - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (trackerId_ != 0) { - output.writeInt32(1, trackerId_); - } - if (((bitField0_ & 0x00000001) != 0)) { - output.writeFloat(2, x_); - } - if (((bitField0_ & 0x00000002) != 0)) { - output.writeFloat(3, y_); - } - if (((bitField0_ & 0x00000004) != 0)) { - output.writeFloat(4, z_); - } - if (java.lang.Float.floatToRawIntBits(qx_) != 0) { - output.writeFloat(5, qx_); - } - if (java.lang.Float.floatToRawIntBits(qy_) != 0) { - output.writeFloat(6, qy_); - } - if (java.lang.Float.floatToRawIntBits(qz_) != 0) { - output.writeFloat(7, qz_); - } - if (java.lang.Float.floatToRawIntBits(qw_) != 0) { - output.writeFloat(8, qw_); - } - if (((bitField0_ & 0x00000008) != 0)) { - output.writeEnum(9, dataSource_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (trackerId_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(1, trackerId_); - } - if (((bitField0_ & 0x00000001) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, x_); - } - if (((bitField0_ & 0x00000002) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, y_); - } - if (((bitField0_ & 0x00000004) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(4, z_); - } - if (java.lang.Float.floatToRawIntBits(qx_) != 0) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(5, qx_); - } - if (java.lang.Float.floatToRawIntBits(qy_) != 0) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(6, qy_); - } - if (java.lang.Float.floatToRawIntBits(qz_) != 0) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(7, qz_); - } - if (java.lang.Float.floatToRawIntBits(qw_) != 0) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(8, qw_); - } - if (((bitField0_ & 0x00000008) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeEnumSize(9, dataSource_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.Position)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.Position other = (dev.slimevr.desktop.platform.ProtobufMessages.Position) obj; - - if ( - getTrackerId() - != other.getTrackerId() - ) - return false; - if (hasX() != other.hasX()) - return false; - if (hasX()) { - if ( - java.lang.Float.floatToIntBits(getX()) - != java.lang.Float - .floatToIntBits( - other.getX() - ) - ) - return false; - } - if (hasY() != other.hasY()) - return false; - if (hasY()) { - if ( - java.lang.Float.floatToIntBits(getY()) - != java.lang.Float - .floatToIntBits( - other.getY() - ) - ) - return false; - } - if (hasZ() != other.hasZ()) - return false; - if (hasZ()) { - if ( - java.lang.Float.floatToIntBits(getZ()) - != java.lang.Float - .floatToIntBits( - other.getZ() - ) - ) - return false; - } - if ( - java.lang.Float.floatToIntBits(getQx()) - != java.lang.Float - .floatToIntBits( - other.getQx() - ) - ) - return false; - if ( - java.lang.Float.floatToIntBits(getQy()) - != java.lang.Float - .floatToIntBits( - other.getQy() - ) - ) - return false; - if ( - java.lang.Float.floatToIntBits(getQz()) - != java.lang.Float - .floatToIntBits( - other.getQz() - ) - ) - return false; - if ( - java.lang.Float.floatToIntBits(getQw()) - != java.lang.Float - .floatToIntBits( - other.getQw() - ) - ) - return false; - if (hasDataSource() != other.hasDataSource()) - return false; - if (hasDataSource()) { - if (dataSource_ != other.dataSource_) - return false; - } - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + TRACKER_ID_FIELD_NUMBER; - hash = (53 * hash) + getTrackerId(); - if (hasX()) { - hash = (37 * hash) + X_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getX() - ); - } - if (hasY()) { - hash = (37 * hash) + Y_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getY() - ); - } - if (hasZ()) { - hash = (37 * hash) + Z_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getZ() - ); - } - hash = (37 * hash) + QX_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getQx() - ); - hash = (37 * hash) + QY_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getQy() - ); - hash = (37 * hash) + QZ_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getQz() - ); - hash = (37 * hash) + QW_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getQw() - ); - if (hasDataSource()) { - hash = (37 * hash) + DATA_SOURCE_FIELD_NUMBER; - hash = (53 * hash) + dataSource_; - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.Position prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.Position} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.Position) - dev.slimevr.desktop.platform.ProtobufMessages.PositionOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Position_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Position_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Position.class, - dev.slimevr.desktop.platform.ProtobufMessages.Position.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.Position.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - trackerId_ = 0; - x_ = 0F; - y_ = 0F; - z_ = 0F; - qx_ = 0F; - qy_ = 0F; - qz_ = 0F; - qw_ = 0F; - dataSource_ = 0; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Position_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.Position.getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position build() { - dev.slimevr.desktop.platform.ProtobufMessages.Position result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.Position result = new dev.slimevr.desktop.platform.ProtobufMessages.Position( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.Position result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.trackerId_ = trackerId_; - } - int to_bitField0_ = 0; - if (((from_bitField0_ & 0x00000002) != 0)) { - result.x_ = x_; - to_bitField0_ |= 0x00000001; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.y_ = y_; - to_bitField0_ |= 0x00000002; - } - if (((from_bitField0_ & 0x00000008) != 0)) { - result.z_ = z_; - to_bitField0_ |= 0x00000004; - } - if (((from_bitField0_ & 0x00000010) != 0)) { - result.qx_ = qx_; - } - if (((from_bitField0_ & 0x00000020) != 0)) { - result.qy_ = qy_; - } - if (((from_bitField0_ & 0x00000040) != 0)) { - result.qz_ = qz_; - } - if (((from_bitField0_ & 0x00000080) != 0)) { - result.qw_ = qw_; - } - if (((from_bitField0_ & 0x00000100) != 0)) { - result.dataSource_ = dataSource_; - to_bitField0_ |= 0x00000008; - } - result.bitField0_ |= to_bitField0_; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.Position) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.Position) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(dev.slimevr.desktop.platform.ProtobufMessages.Position other) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance() - ) - return this; - if (other.getTrackerId() != 0) { - setTrackerId(other.getTrackerId()); - } - if (other.hasX()) { - setX(other.getX()); - } - if (other.hasY()) { - setY(other.getY()); - } - if (other.hasZ()) { - setZ(other.getZ()); - } - if (java.lang.Float.floatToRawIntBits(other.getQx()) != 0) { - setQx(other.getQx()); - } - if (java.lang.Float.floatToRawIntBits(other.getQy()) != 0) { - setQy(other.getQy()); - } - if (java.lang.Float.floatToRawIntBits(other.getQz()) != 0) { - setQz(other.getQz()); - } - if (java.lang.Float.floatToRawIntBits(other.getQw()) != 0) { - setQw(other.getQw()); - } - if (other.hasDataSource()) { - setDataSourceValue(other.getDataSourceValue()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - trackerId_ = input.readInt32(); - bitField0_ |= 0x00000001; - break; - } // case 8 - case 21: { - x_ = input.readFloat(); - bitField0_ |= 0x00000002; - break; - } // case 21 - case 29: { - y_ = input.readFloat(); - bitField0_ |= 0x00000004; - break; - } // case 29 - case 37: { - z_ = input.readFloat(); - bitField0_ |= 0x00000008; - break; - } // case 37 - case 45: { - qx_ = input.readFloat(); - bitField0_ |= 0x00000010; - break; - } // case 45 - case 53: { - qy_ = input.readFloat(); - bitField0_ |= 0x00000020; - break; - } // case 53 - case 61: { - qz_ = input.readFloat(); - bitField0_ |= 0x00000040; - break; - } // case 61 - case 69: { - qw_ = input.readFloat(); - bitField0_ |= 0x00000080; - break; - } // case 69 - case 72: { - dataSource_ = input.readEnum(); - bitField0_ |= 0x00000100; - break; - } // case 72 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private int trackerId_; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - /** - * int32 tracker_id = 1; - * - * @param value The trackerId to set. - * @return This builder for chaining. - */ - public Builder setTrackerId(int value) { - - trackerId_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * int32 tracker_id = 1; - * - * @return This builder for chaining. - */ - public Builder clearTrackerId() { - bitField0_ = (bitField0_ & ~0x00000001); - trackerId_ = 0; - onChanged(); - return this; - } - - private float x_; - - /** - * optional float x = 2; - * - * @return Whether the x field is set. - */ - @java.lang.Override - public boolean hasX() { - return ((bitField0_ & 0x00000002) != 0); - } - - /** - * optional float x = 2; - * - * @return The x. - */ - @java.lang.Override - public float getX() { - return x_; - } - - /** - * optional float x = 2; - * - * @param value The x to set. - * @return This builder for chaining. - */ - public Builder setX(float value) { - - x_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - /** - * optional float x = 2; - * - * @return This builder for chaining. - */ - public Builder clearX() { - bitField0_ = (bitField0_ & ~0x00000002); - x_ = 0F; - onChanged(); - return this; - } - - private float y_; - - /** - * optional float y = 3; - * - * @return Whether the y field is set. - */ - @java.lang.Override - public boolean hasY() { - return ((bitField0_ & 0x00000004) != 0); - } - - /** - * optional float y = 3; - * - * @return The y. - */ - @java.lang.Override - public float getY() { - return y_; - } - - /** - * optional float y = 3; - * - * @param value The y to set. - * @return This builder for chaining. - */ - public Builder setY(float value) { - - y_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - - /** - * optional float y = 3; - * - * @return This builder for chaining. - */ - public Builder clearY() { - bitField0_ = (bitField0_ & ~0x00000004); - y_ = 0F; - onChanged(); - return this; - } - - private float z_; - - /** - * optional float z = 4; - * - * @return Whether the z field is set. - */ - @java.lang.Override - public boolean hasZ() { - return ((bitField0_ & 0x00000008) != 0); - } - - /** - * optional float z = 4; - * - * @return The z. - */ - @java.lang.Override - public float getZ() { - return z_; - } - - /** - * optional float z = 4; - * - * @param value The z to set. - * @return This builder for chaining. - */ - public Builder setZ(float value) { - - z_ = value; - bitField0_ |= 0x00000008; - onChanged(); - return this; - } - - /** - * optional float z = 4; - * - * @return This builder for chaining. - */ - public Builder clearZ() { - bitField0_ = (bitField0_ & ~0x00000008); - z_ = 0F; - onChanged(); - return this; - } - - private float qx_; - - /** - * float qx = 5; - * - * @return The qx. - */ - @java.lang.Override - public float getQx() { - return qx_; - } - - /** - * float qx = 5; - * - * @param value The qx to set. - * @return This builder for chaining. - */ - public Builder setQx(float value) { - - qx_ = value; - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - - /** - * float qx = 5; - * - * @return This builder for chaining. - */ - public Builder clearQx() { - bitField0_ = (bitField0_ & ~0x00000010); - qx_ = 0F; - onChanged(); - return this; - } - - private float qy_; - - /** - * float qy = 6; - * - * @return The qy. - */ - @java.lang.Override - public float getQy() { - return qy_; - } - - /** - * float qy = 6; - * - * @param value The qy to set. - * @return This builder for chaining. - */ - public Builder setQy(float value) { - - qy_ = value; - bitField0_ |= 0x00000020; - onChanged(); - return this; - } - - /** - * float qy = 6; - * - * @return This builder for chaining. - */ - public Builder clearQy() { - bitField0_ = (bitField0_ & ~0x00000020); - qy_ = 0F; - onChanged(); - return this; - } - - private float qz_; - - /** - * float qz = 7; - * - * @return The qz. - */ - @java.lang.Override - public float getQz() { - return qz_; - } - - /** - * float qz = 7; - * - * @param value The qz to set. - * @return This builder for chaining. - */ - public Builder setQz(float value) { - - qz_ = value; - bitField0_ |= 0x00000040; - onChanged(); - return this; - } - - /** - * float qz = 7; - * - * @return This builder for chaining. - */ - public Builder clearQz() { - bitField0_ = (bitField0_ & ~0x00000040); - qz_ = 0F; - onChanged(); - return this; - } - - private float qw_; - - /** - * float qw = 8; - * - * @return The qw. - */ - @java.lang.Override - public float getQw() { - return qw_; - } - - /** - * float qw = 8; - * - * @param value The qw to set. - * @return This builder for chaining. - */ - public Builder setQw(float value) { - - qw_ = value; - bitField0_ |= 0x00000080; - onChanged(); - return this; - } - - /** - * float qw = 8; - * - * @return This builder for chaining. - */ - public Builder clearQw() { - bitField0_ = (bitField0_ & ~0x00000080); - qw_ = 0F; - onChanged(); - return this; - } - - private int dataSource_ = 0; - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return Whether the dataSource field is set. - */ - @java.lang.Override - public boolean hasDataSource() { - return ((bitField0_ & 0x00000100) != 0); - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The enum numeric value on the wire for dataSource. - */ - @java.lang.Override - public int getDataSourceValue() { - return dataSource_; - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @param value The enum numeric value on the wire for dataSource to - * set. - * @return This builder for chaining. - */ - public Builder setDataSourceValue(int value) { - dataSource_ = value; - bitField0_ |= 0x00000100; - onChanged(); - return this; - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return The dataSource. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource getDataSource() { - dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource result = dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource - .forNumber(dataSource_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource.UNRECOGNIZED - : result; - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @param value The dataSource to set. - * @return This builder for chaining. - */ - public Builder setDataSource( - dev.slimevr.desktop.platform.ProtobufMessages.Position.DataSource value - ) { - if (value == null) { - throw new NullPointerException(); - } - bitField0_ |= 0x00000100; - dataSource_ = value.getNumber(); - onChanged(); - return this; - } - - /** - * optional .messages.Position.DataSource data_source = 9; - * - * @return This builder for chaining. - */ - public Builder clearDataSource() { - bitField0_ = (bitField0_ & ~0x00000100); - dataSource_ = 0; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.Position) - } - - // @@protoc_insertion_point(class_scope:messages.Position) - private static final dev.slimevr.desktop.platform.ProtobufMessages.Position DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.Position(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Position getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public Position parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface UserActionOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.UserAction) - com.google.protobuf.MessageOrBuilder { - - /** - * string name = 1; - * - * @return The name. - */ - java.lang.String getName(); - - /** - * string name = 1; - * - * @return The bytes for name. - */ - com.google.protobuf.ByteString getNameBytes(); - - /** - * map<string, string> action_arguments = 2; - */ - int getActionArgumentsCount(); - - /** - * map<string, string> action_arguments = 2; - */ - boolean containsActionArguments( - java.lang.String key - ); - - /** - * Use {@link #getActionArgumentsMap()} instead. - */ - @java.lang.Deprecated - java.util.Map getActionArguments(); - - /** - * map<string, string> action_arguments = 2; - */ - java.util.Map getActionArgumentsMap(); - - /** - * map<string, string> action_arguments = 2; - */ - /* nullable */ - java.lang.String getActionArgumentsOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ); - - /** - * map<string, string> action_arguments = 2; - */ - java.lang.String getActionArgumentsOrThrow( - java.lang.String key - ); - } - - /** - * Protobuf type {@code messages.UserAction} - */ - public static final class UserAction extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.UserAction) - UserActionOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - UserAction.class.getName() - ); - } - - // Use UserAction.newBuilder() to construct. - private UserAction(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private UserAction() { - name_ = ""; - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_descriptor; - } - - @SuppressWarnings({ "rawtypes" }) - @java.lang.Override - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMapFieldReflection( - int number - ) { - switch (number) { - case 2: - return internalGetActionArguments(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction.class, - dev.slimevr.desktop.platform.ProtobufMessages.UserAction.Builder.class - ); - } - - public static final int NAME_FIELD_NUMBER = 1; - @SuppressWarnings("serial") - private volatile java.lang.Object name_ = ""; - - /** - * string name = 1; - * - * @return The name. - */ - @java.lang.Override - public java.lang.String getName() { - java.lang.Object ref = name_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - name_ = s; - return s; - } - } - - /** - * string name = 1; - * - * @return The bytes for name. - */ - @java.lang.Override - public com.google.protobuf.ByteString getNameBytes() { - java.lang.Object ref = name_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int ACTION_ARGUMENTS_FIELD_NUMBER = 2; - - private static final class ActionArgumentsDefaultEntryHolder { - static final com.google.protobuf.MapEntry defaultEntry = com.google.protobuf.MapEntry.newDefaultInstance( - dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_ActionArgumentsEntry_descriptor, - com.google.protobuf.WireFormat.FieldType.STRING, - "", - com.google.protobuf.WireFormat.FieldType.STRING, - "" - ); - } - - @SuppressWarnings("serial") - private com.google.protobuf.MapField actionArguments_; - - private com.google.protobuf.MapField internalGetActionArguments() { - if (actionArguments_ == null) { - return com.google.protobuf.MapField - .emptyMapField( - ActionArgumentsDefaultEntryHolder.defaultEntry - ); - } - return actionArguments_; - } - - public int getActionArgumentsCount() { - return internalGetActionArguments().getMap().size(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public boolean containsActionArguments( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - return internalGetActionArguments().getMap().containsKey(key); - } - - /** - * Use {@link #getActionArgumentsMap()} instead. - */ - @java.lang.Override - @java.lang.Deprecated - public java.util.Map getActionArguments() { - return getActionArgumentsMap(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public java.util.Map getActionArgumentsMap() { - return internalGetActionArguments().getMap(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public /* nullable */ - java.lang.String getActionArgumentsOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetActionArguments() - .getMap(); - return map.containsKey(key) ? map.get(key) : defaultValue; - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public java.lang.String getActionArgumentsOrThrow( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetActionArguments() - .getMap(); - if (!map.containsKey(key)) { - throw new java.lang.IllegalArgumentException(); - } - return map.get(key); - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(name_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 1, name_); - } - com.google.protobuf.GeneratedMessage - .serializeStringMapTo( - output, - internalGetActionArguments(), - ActionArgumentsDefaultEntryHolder.defaultEntry, - 2 - ); - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(name_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(1, name_); - } - for ( - java.util.Map.Entry entry : internalGetActionArguments() - .getMap() - .entrySet() - ) { - com.google.protobuf.MapEntry actionArguments__ = ActionArgumentsDefaultEntryHolder.defaultEntry - .newBuilderForType() - .setKey(entry.getKey()) - .setValue(entry.getValue()) - .build(); - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, actionArguments__); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.UserAction)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.UserAction other = (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) obj; - - if ( - !getName() - .equals(other.getName()) - ) - return false; - if ( - !internalGetActionArguments() - .equals( - other.internalGetActionArguments() - ) - ) - return false; - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + NAME_FIELD_NUMBER; - hash = (53 * hash) + getName().hashCode(); - if (!internalGetActionArguments().getMap().isEmpty()) { - hash = (37 * hash) + ACTION_ARGUMENTS_FIELD_NUMBER; - hash = (53 * hash) + internalGetActionArguments().hashCode(); - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - byte[] data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.UserAction} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.UserAction) - dev.slimevr.desktop.platform.ProtobufMessages.UserActionOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_descriptor; - } - - @SuppressWarnings({ "rawtypes" }) - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMapFieldReflection( - int number - ) { - switch (number) { - case 2: - return internalGetActionArguments(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @SuppressWarnings({ "rawtypes" }) - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMutableMapFieldReflection( - int number - ) { - switch (number) { - case 2: - return internalGetMutableActionArguments(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction.class, - dev.slimevr.desktop.platform.ProtobufMessages.UserAction.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.UserAction.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - name_ = ""; - internalGetMutableActionArguments().clear(); - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_UserAction_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction build() { - dev.slimevr.desktop.platform.ProtobufMessages.UserAction result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.UserAction result = new dev.slimevr.desktop.platform.ProtobufMessages.UserAction( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.name_ = name_; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.actionArguments_ = internalGetActionArguments(); - result.actionArguments_.makeImmutable(); - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.UserAction) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction other - ) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance() - ) - return this; - if (!other.getName().isEmpty()) { - name_ = other.name_; - bitField0_ |= 0x00000001; - onChanged(); - } - internalGetMutableActionArguments() - .mergeFrom( - other.internalGetActionArguments() - ); - bitField0_ |= 0x00000002; - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 10: { - name_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000001; - break; - } // case 10 - case 18: { - com.google.protobuf.MapEntry actionArguments__ = input - .readMessage( - ActionArgumentsDefaultEntryHolder.defaultEntry - .getParserForType(), - extensionRegistry - ); - internalGetMutableActionArguments() - .getMutableMap() - .put( - actionArguments__.getKey(), - actionArguments__.getValue() - ); - bitField0_ |= 0x00000002; - break; - } // case 18 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private java.lang.Object name_ = ""; - - /** - * string name = 1; - * - * @return The name. - */ - public java.lang.String getName() { - java.lang.Object ref = name_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - name_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - - /** - * string name = 1; - * - * @return The bytes for name. - */ - public com.google.protobuf.ByteString getNameBytes() { - java.lang.Object ref = name_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - /** - * string name = 1; - * - * @param value The name to set. - * @return This builder for chaining. - */ - public Builder setName( - java.lang.String value - ) { - if (value == null) { - throw new NullPointerException(); - } - name_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * string name = 1; - * - * @return This builder for chaining. - */ - public Builder clearName() { - name_ = getDefaultInstance().getName(); - bitField0_ = (bitField0_ & ~0x00000001); - onChanged(); - return this; - } - - /** - * string name = 1; - * - * @param value The bytes for name to set. - * @return This builder for chaining. - */ - public Builder setNameBytes( - com.google.protobuf.ByteString value - ) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - name_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - private com.google.protobuf.MapField actionArguments_; - - private com.google.protobuf.MapField internalGetActionArguments() { - if (actionArguments_ == null) { - return com.google.protobuf.MapField - .emptyMapField( - ActionArgumentsDefaultEntryHolder.defaultEntry - ); - } - return actionArguments_; - } - - private com.google.protobuf.MapField internalGetMutableActionArguments() { - if (actionArguments_ == null) { - actionArguments_ = com.google.protobuf.MapField - .newMapField( - ActionArgumentsDefaultEntryHolder.defaultEntry - ); - } - if (!actionArguments_.isMutable()) { - actionArguments_ = actionArguments_.copy(); - } - bitField0_ |= 0x00000002; - onChanged(); - return actionArguments_; - } - - public int getActionArgumentsCount() { - return internalGetActionArguments().getMap().size(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public boolean containsActionArguments( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - return internalGetActionArguments().getMap().containsKey(key); - } - - /** - * Use {@link #getActionArgumentsMap()} instead. - */ - @java.lang.Override - @java.lang.Deprecated - public java.util.Map getActionArguments() { - return getActionArgumentsMap(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public java.util.Map getActionArgumentsMap() { - return internalGetActionArguments().getMap(); - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public /* nullable */ - java.lang.String getActionArgumentsOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetActionArguments() - .getMap(); - return map.containsKey(key) ? map.get(key) : defaultValue; - } - - /** - * map<string, string> action_arguments = 2; - */ - @java.lang.Override - public java.lang.String getActionArgumentsOrThrow( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetActionArguments() - .getMap(); - if (!map.containsKey(key)) { - throw new java.lang.IllegalArgumentException(); - } - return map.get(key); - } - - public Builder clearActionArguments() { - bitField0_ = (bitField0_ & ~0x00000002); - internalGetMutableActionArguments() - .getMutableMap() - .clear(); - return this; - } - - /** - * map<string, string> action_arguments = 2; - */ - public Builder removeActionArguments( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - internalGetMutableActionArguments() - .getMutableMap() - .remove(key); - return this; - } - - /** - * Use alternate mutation accessors instead. - */ - @java.lang.Deprecated - public java.util.Map getMutableActionArguments() { - bitField0_ |= 0x00000002; - return internalGetMutableActionArguments().getMutableMap(); - } - - /** - * map<string, string> action_arguments = 2; - */ - public Builder putActionArguments( - java.lang.String key, - java.lang.String value - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - if (value == null) { - throw new NullPointerException("map value"); - } - internalGetMutableActionArguments() - .getMutableMap() - .put(key, value); - bitField0_ |= 0x00000002; - return this; - } - - /** - * map<string, string> action_arguments = 2; - */ - public Builder putAllActionArguments( - java.util.Map values - ) { - internalGetMutableActionArguments() - .getMutableMap() - .putAll(values); - bitField0_ |= 0x00000002; - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.UserAction) - } - - // @@protoc_insertion_point(class_scope:messages.UserAction) - private static final dev.slimevr.desktop.platform.ProtobufMessages.UserAction DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.UserAction(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.UserAction getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public UserAction parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface TrackerAddedOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.TrackerAdded) - com.google.protobuf.MessageOrBuilder { - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - int getTrackerId(); - - /** - * string tracker_serial = 2; - * - * @return The trackerSerial. - */ - java.lang.String getTrackerSerial(); - - /** - * string tracker_serial = 2; - * - * @return The bytes for trackerSerial. - */ - com.google.protobuf.ByteString getTrackerSerialBytes(); - - /** - * string tracker_name = 3; - * - * @return The trackerName. - */ - java.lang.String getTrackerName(); - - /** - * string tracker_name = 3; - * - * @return The bytes for trackerName. - */ - com.google.protobuf.ByteString getTrackerNameBytes(); - - /** - * int32 tracker_role = 4; - * - * @return The trackerRole. - */ - int getTrackerRole(); - - /** - * string manufacturer = 5; - * - * @return The manufacturer. - */ - java.lang.String getManufacturer(); - - /** - * string manufacturer = 5; - * - * @return The bytes for manufacturer. - */ - com.google.protobuf.ByteString getManufacturerBytes(); - } - - /** - * Protobuf type {@code messages.TrackerAdded} - */ - public static final class TrackerAdded extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.TrackerAdded) - TrackerAddedOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - TrackerAdded.class.getName() - ); - } - - // Use TrackerAdded.newBuilder() to construct. - private TrackerAdded(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private TrackerAdded() { - trackerSerial_ = ""; - trackerName_ = ""; - manufacturer_ = ""; - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerAdded_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerAdded_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.class, - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.Builder.class - ); - } - - public static final int TRACKER_ID_FIELD_NUMBER = 1; - private int trackerId_ = 0; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - public static final int TRACKER_SERIAL_FIELD_NUMBER = 2; - @SuppressWarnings("serial") - private volatile java.lang.Object trackerSerial_ = ""; - - /** - * string tracker_serial = 2; - * - * @return The trackerSerial. - */ - @java.lang.Override - public java.lang.String getTrackerSerial() { - java.lang.Object ref = trackerSerial_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - trackerSerial_ = s; - return s; - } - } - - /** - * string tracker_serial = 2; - * - * @return The bytes for trackerSerial. - */ - @java.lang.Override - public com.google.protobuf.ByteString getTrackerSerialBytes() { - java.lang.Object ref = trackerSerial_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - trackerSerial_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int TRACKER_NAME_FIELD_NUMBER = 3; - @SuppressWarnings("serial") - private volatile java.lang.Object trackerName_ = ""; - - /** - * string tracker_name = 3; - * - * @return The trackerName. - */ - @java.lang.Override - public java.lang.String getTrackerName() { - java.lang.Object ref = trackerName_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - trackerName_ = s; - return s; - } - } - - /** - * string tracker_name = 3; - * - * @return The bytes for trackerName. - */ - @java.lang.Override - public com.google.protobuf.ByteString getTrackerNameBytes() { - java.lang.Object ref = trackerName_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - trackerName_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int TRACKER_ROLE_FIELD_NUMBER = 4; - private int trackerRole_ = 0; - - /** - * int32 tracker_role = 4; - * - * @return The trackerRole. - */ - @java.lang.Override - public int getTrackerRole() { - return trackerRole_; - } - - public static final int MANUFACTURER_FIELD_NUMBER = 5; - @SuppressWarnings("serial") - private volatile java.lang.Object manufacturer_ = ""; - - /** - * string manufacturer = 5; - * - * @return The manufacturer. - */ - @java.lang.Override - public java.lang.String getManufacturer() { - java.lang.Object ref = manufacturer_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - manufacturer_ = s; - return s; - } - } - - /** - * string manufacturer = 5; - * - * @return The bytes for manufacturer. - */ - @java.lang.Override - public com.google.protobuf.ByteString getManufacturerBytes() { - java.lang.Object ref = manufacturer_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - manufacturer_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (trackerId_ != 0) { - output.writeInt32(1, trackerId_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(trackerSerial_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 2, trackerSerial_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(trackerName_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 3, trackerName_); - } - if (trackerRole_ != 0) { - output.writeInt32(4, trackerRole_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(manufacturer_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 5, manufacturer_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (trackerId_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(1, trackerId_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(trackerSerial_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(2, trackerSerial_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(trackerName_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(3, trackerName_); - } - if (trackerRole_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(4, trackerRole_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(manufacturer_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(5, manufacturer_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded other = (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) obj; - - if ( - getTrackerId() - != other.getTrackerId() - ) - return false; - if ( - !getTrackerSerial() - .equals(other.getTrackerSerial()) - ) - return false; - if ( - !getTrackerName() - .equals(other.getTrackerName()) - ) - return false; - if ( - getTrackerRole() - != other.getTrackerRole() - ) - return false; - if ( - !getManufacturer() - .equals(other.getManufacturer()) - ) - return false; - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + TRACKER_ID_FIELD_NUMBER; - hash = (53 * hash) + getTrackerId(); - hash = (37 * hash) + TRACKER_SERIAL_FIELD_NUMBER; - hash = (53 * hash) + getTrackerSerial().hashCode(); - hash = (37 * hash) + TRACKER_NAME_FIELD_NUMBER; - hash = (53 * hash) + getTrackerName().hashCode(); - hash = (37 * hash) + TRACKER_ROLE_FIELD_NUMBER; - hash = (53 * hash) + getTrackerRole(); - hash = (37 * hash) + MANUFACTURER_FIELD_NUMBER; - hash = (53 * hash) + getManufacturer().hashCode(); - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - byte[] data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.TrackerAdded} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.TrackerAdded) - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAddedOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerAdded_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerAdded_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.class, - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - trackerId_ = 0; - trackerSerial_ = ""; - trackerName_ = ""; - trackerRole_ = 0; - manufacturer_ = ""; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerAdded_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded build() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded result = new dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.trackerId_ = trackerId_; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.trackerSerial_ = trackerSerial_; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.trackerName_ = trackerName_; - } - if (((from_bitField0_ & 0x00000008) != 0)) { - result.trackerRole_ = trackerRole_; - } - if (((from_bitField0_ & 0x00000010) != 0)) { - result.manufacturer_ = manufacturer_; - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded other - ) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance() - ) - return this; - if (other.getTrackerId() != 0) { - setTrackerId(other.getTrackerId()); - } - if (!other.getTrackerSerial().isEmpty()) { - trackerSerial_ = other.trackerSerial_; - bitField0_ |= 0x00000002; - onChanged(); - } - if (!other.getTrackerName().isEmpty()) { - trackerName_ = other.trackerName_; - bitField0_ |= 0x00000004; - onChanged(); - } - if (other.getTrackerRole() != 0) { - setTrackerRole(other.getTrackerRole()); - } - if (!other.getManufacturer().isEmpty()) { - manufacturer_ = other.manufacturer_; - bitField0_ |= 0x00000010; - onChanged(); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - trackerId_ = input.readInt32(); - bitField0_ |= 0x00000001; - break; - } // case 8 - case 18: { - trackerSerial_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000002; - break; - } // case 18 - case 26: { - trackerName_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000004; - break; - } // case 26 - case 32: { - trackerRole_ = input.readInt32(); - bitField0_ |= 0x00000008; - break; - } // case 32 - case 42: { - manufacturer_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000010; - break; - } // case 42 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private int trackerId_; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - /** - * int32 tracker_id = 1; - * - * @param value The trackerId to set. - * @return This builder for chaining. - */ - public Builder setTrackerId(int value) { - - trackerId_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * int32 tracker_id = 1; - * - * @return This builder for chaining. - */ - public Builder clearTrackerId() { - bitField0_ = (bitField0_ & ~0x00000001); - trackerId_ = 0; - onChanged(); - return this; - } - - private java.lang.Object trackerSerial_ = ""; - - /** - * string tracker_serial = 2; - * - * @return The trackerSerial. - */ - public java.lang.String getTrackerSerial() { - java.lang.Object ref = trackerSerial_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - trackerSerial_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - - /** - * string tracker_serial = 2; - * - * @return The bytes for trackerSerial. - */ - public com.google.protobuf.ByteString getTrackerSerialBytes() { - java.lang.Object ref = trackerSerial_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - trackerSerial_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - /** - * string tracker_serial = 2; - * - * @param value The trackerSerial to set. - * @return This builder for chaining. - */ - public Builder setTrackerSerial( - java.lang.String value - ) { - if (value == null) { - throw new NullPointerException(); - } - trackerSerial_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - /** - * string tracker_serial = 2; - * - * @return This builder for chaining. - */ - public Builder clearTrackerSerial() { - trackerSerial_ = getDefaultInstance().getTrackerSerial(); - bitField0_ = (bitField0_ & ~0x00000002); - onChanged(); - return this; - } - - /** - * string tracker_serial = 2; - * - * @param value The bytes for trackerSerial to set. - * @return This builder for chaining. - */ - public Builder setTrackerSerialBytes( - com.google.protobuf.ByteString value - ) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - trackerSerial_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - private java.lang.Object trackerName_ = ""; - - /** - * string tracker_name = 3; - * - * @return The trackerName. - */ - public java.lang.String getTrackerName() { - java.lang.Object ref = trackerName_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - trackerName_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - - /** - * string tracker_name = 3; - * - * @return The bytes for trackerName. - */ - public com.google.protobuf.ByteString getTrackerNameBytes() { - java.lang.Object ref = trackerName_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - trackerName_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - /** - * string tracker_name = 3; - * - * @param value The trackerName to set. - * @return This builder for chaining. - */ - public Builder setTrackerName( - java.lang.String value - ) { - if (value == null) { - throw new NullPointerException(); - } - trackerName_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - - /** - * string tracker_name = 3; - * - * @return This builder for chaining. - */ - public Builder clearTrackerName() { - trackerName_ = getDefaultInstance().getTrackerName(); - bitField0_ = (bitField0_ & ~0x00000004); - onChanged(); - return this; - } - - /** - * string tracker_name = 3; - * - * @param value The bytes for trackerName to set. - * @return This builder for chaining. - */ - public Builder setTrackerNameBytes( - com.google.protobuf.ByteString value - ) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - trackerName_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - - private int trackerRole_; - - /** - * int32 tracker_role = 4; - * - * @return The trackerRole. - */ - @java.lang.Override - public int getTrackerRole() { - return trackerRole_; - } - - /** - * int32 tracker_role = 4; - * - * @param value The trackerRole to set. - * @return This builder for chaining. - */ - public Builder setTrackerRole(int value) { - - trackerRole_ = value; - bitField0_ |= 0x00000008; - onChanged(); - return this; - } - - /** - * int32 tracker_role = 4; - * - * @return This builder for chaining. - */ - public Builder clearTrackerRole() { - bitField0_ = (bitField0_ & ~0x00000008); - trackerRole_ = 0; - onChanged(); - return this; - } - - private java.lang.Object manufacturer_ = ""; - - /** - * string manufacturer = 5; - * - * @return The manufacturer. - */ - public java.lang.String getManufacturer() { - java.lang.Object ref = manufacturer_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - manufacturer_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - - /** - * string manufacturer = 5; - * - * @return The bytes for manufacturer. - */ - public com.google.protobuf.ByteString getManufacturerBytes() { - java.lang.Object ref = manufacturer_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = com.google.protobuf.ByteString - .copyFromUtf8( - (java.lang.String) ref - ); - manufacturer_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - /** - * string manufacturer = 5; - * - * @param value The manufacturer to set. - * @return This builder for chaining. - */ - public Builder setManufacturer( - java.lang.String value - ) { - if (value == null) { - throw new NullPointerException(); - } - manufacturer_ = value; - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - - /** - * string manufacturer = 5; - * - * @return This builder for chaining. - */ - public Builder clearManufacturer() { - manufacturer_ = getDefaultInstance().getManufacturer(); - bitField0_ = (bitField0_ & ~0x00000010); - onChanged(); - return this; - } - - /** - * string manufacturer = 5; - * - * @param value The bytes for manufacturer to set. - * @return This builder for chaining. - */ - public Builder setManufacturerBytes( - com.google.protobuf.ByteString value - ) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - manufacturer_ = value; - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.TrackerAdded) - } - - // @@protoc_insertion_point(class_scope:messages.TrackerAdded) - private static final dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public TrackerAdded parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface TrackerStatusOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.TrackerStatus) - com.google.protobuf.MessageOrBuilder { - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - int getTrackerId(); - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The enum numeric value on the wire for status. - */ - int getStatusValue(); - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The status. - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status getStatus(); - - /** - * map<string, string> extra = 3; - */ - int getExtraCount(); - - /** - * map<string, string> extra = 3; - */ - boolean containsExtra( - java.lang.String key - ); - - /** - * Use {@link #getExtraMap()} instead. - */ - @java.lang.Deprecated - java.util.Map getExtra(); - - /** - * map<string, string> extra = 3; - */ - java.util.Map getExtraMap(); - - /** - * map<string, string> extra = 3; - */ - /* nullable */ - java.lang.String getExtraOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ); - - /** - * map<string, string> extra = 3; - */ - java.lang.String getExtraOrThrow( - java.lang.String key - ); - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return Whether the confidence field is set. - */ - boolean hasConfidence(); - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The enum numeric value on the wire for confidence. - */ - int getConfidenceValue(); - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The confidence. - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence getConfidence(); - } - - /** - * Protobuf type {@code messages.TrackerStatus} - */ - public static final class TrackerStatus extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.TrackerStatus) - TrackerStatusOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - TrackerStatus.class.getName() - ); - } - - // Use TrackerStatus.newBuilder() to construct. - private TrackerStatus(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private TrackerStatus() { - status_ = 0; - confidence_ = 0; - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_descriptor; - } - - @SuppressWarnings({ "rawtypes" }) - @java.lang.Override - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMapFieldReflection( - int number - ) { - switch (number) { - case 3: - return internalGetExtra(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.class, - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Builder.class - ); - } - - /** - * Protobuf enum {@code messages.TrackerStatus.Status} - */ - public enum Status - implements com.google.protobuf.ProtocolMessageEnum { - /** - * DISCONNECTED = 0; - */ - DISCONNECTED(0), - /** - * OK = 1; - */ - OK(1), - /** - * BUSY = 2; - */ - BUSY(2), - /** - * ERROR = 3; - */ - ERROR(3), - /** - * OCCLUDED = 4; - */ - OCCLUDED(4), - UNRECOGNIZED(-1), - ; - - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - Status.class.getName() - ); - } - /** - * DISCONNECTED = 0; - */ - public static final int DISCONNECTED_VALUE = 0; - /** - * OK = 1; - */ - public static final int OK_VALUE = 1; - /** - * BUSY = 2; - */ - public static final int BUSY_VALUE = 2; - /** - * ERROR = 3; - */ - public static final int ERROR_VALUE = 3; - /** - * OCCLUDED = 4; - */ - public static final int OCCLUDED_VALUE = 4; - - - public final int getNumber() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalArgumentException( - "Can't get the number of an unknown enum value." - ); - } - return value; - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static Status valueOf(int value) { - return forNumber(value); - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - */ - public static Status forNumber(int value) { - switch (value) { - case 0: - return DISCONNECTED; - case 1: - return OK; - case 2: - return BUSY; - case 3: - return ERROR; - case 4: - return OCCLUDED; - default: - return null; - } - } - - public static com.google.protobuf.Internal.EnumLiteMap internalGetValueMap() { - return internalValueMap; - } - - private static final com.google.protobuf.Internal.EnumLiteMap internalValueMap = new com.google.protobuf.Internal.EnumLiteMap() { - public Status findValueByNumber(int number) { - return Status.forNumber(number); - } - }; - - public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalStateException( - "Can't get the descriptor of an unrecognized enum value." - ); - } - return getDescriptor().getValues().get(ordinal()); - } - - public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { - return getDescriptor(); - } - - public static com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDescriptor() - .getEnumTypes() - .get(0); - } - - private static final Status[] VALUES = values(); - - public static Status valueOf( - com.google.protobuf.Descriptors.EnumValueDescriptor desc - ) { - if (desc.getType() != getDescriptor()) { - throw new java.lang.IllegalArgumentException( - "EnumValueDescriptor is not for this type." - ); - } - if (desc.getIndex() == -1) { - return UNRECOGNIZED; - } - return VALUES[desc.getIndex()]; - } - - private final int value; - - private Status(int value) { - this.value = value; - } - - // @@protoc_insertion_point(enum_scope:messages.TrackerStatus.Status) - } - - /** - * Protobuf enum {@code messages.TrackerStatus.Confidence} - */ - public enum Confidence - implements com.google.protobuf.ProtocolMessageEnum { - /** - * NO = 0; - */ - NO(0), - /** - * LOW = 1; - */ - LOW(1), - /** - * MEDIUM = 5; - */ - MEDIUM(5), - /** - * HIGH = 10; - */ - HIGH(10), - UNRECOGNIZED(-1), - ; - - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - Confidence.class.getName() - ); - } - /** - * NO = 0; - */ - public static final int NO_VALUE = 0; - /** - * LOW = 1; - */ - public static final int LOW_VALUE = 1; - /** - * MEDIUM = 5; - */ - public static final int MEDIUM_VALUE = 5; - /** - * HIGH = 10; - */ - public static final int HIGH_VALUE = 10; - - - public final int getNumber() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalArgumentException( - "Can't get the number of an unknown enum value." - ); - } - return value; - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static Confidence valueOf(int value) { - return forNumber(value); - } - - /** - * @param value The numeric wire value of the corresponding enum - * entry. - * @return The enum associated with the given numeric wire value. - */ - public static Confidence forNumber(int value) { - switch (value) { - case 0: - return NO; - case 1: - return LOW; - case 5: - return MEDIUM; - case 10: - return HIGH; - default: - return null; - } - } - - public static com.google.protobuf.Internal.EnumLiteMap internalGetValueMap() { - return internalValueMap; - } - - private static final com.google.protobuf.Internal.EnumLiteMap internalValueMap = new com.google.protobuf.Internal.EnumLiteMap() { - public Confidence findValueByNumber(int number) { - return Confidence.forNumber(number); - } - }; - - public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalStateException( - "Can't get the descriptor of an unrecognized enum value." - ); - } - return getDescriptor().getValues().get(ordinal()); - } - - public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { - return getDescriptor(); - } - - public static com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDescriptor() - .getEnumTypes() - .get(1); - } - - private static final Confidence[] VALUES = values(); - - public static Confidence valueOf( - com.google.protobuf.Descriptors.EnumValueDescriptor desc - ) { - if (desc.getType() != getDescriptor()) { - throw new java.lang.IllegalArgumentException( - "EnumValueDescriptor is not for this type." - ); - } - if (desc.getIndex() == -1) { - return UNRECOGNIZED; - } - return VALUES[desc.getIndex()]; - } - - private final int value; - - private Confidence(int value) { - this.value = value; - } - - // @@protoc_insertion_point(enum_scope:messages.TrackerStatus.Confidence) - } - - private int bitField0_; - public static final int TRACKER_ID_FIELD_NUMBER = 1; - private int trackerId_ = 0; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - public static final int STATUS_FIELD_NUMBER = 2; - private int status_ = 0; - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The enum numeric value on the wire for status. - */ - @java.lang.Override - public int getStatusValue() { - return status_; - } - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The status. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status getStatus() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status result = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status - .forNumber(status_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status.UNRECOGNIZED - : result; - } - - public static final int EXTRA_FIELD_NUMBER = 3; - - private static final class ExtraDefaultEntryHolder { - static final com.google.protobuf.MapEntry defaultEntry = com.google.protobuf.MapEntry.newDefaultInstance( - dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_ExtraEntry_descriptor, - com.google.protobuf.WireFormat.FieldType.STRING, - "", - com.google.protobuf.WireFormat.FieldType.STRING, - "" - ); - } - - @SuppressWarnings("serial") - private com.google.protobuf.MapField extra_; - - private com.google.protobuf.MapField internalGetExtra() { - if (extra_ == null) { - return com.google.protobuf.MapField - .emptyMapField( - ExtraDefaultEntryHolder.defaultEntry - ); - } - return extra_; - } - - public int getExtraCount() { - return internalGetExtra().getMap().size(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public boolean containsExtra( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - return internalGetExtra().getMap().containsKey(key); - } - - /** - * Use {@link #getExtraMap()} instead. - */ - @java.lang.Override - @java.lang.Deprecated - public java.util.Map getExtra() { - return getExtraMap(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public java.util.Map getExtraMap() { - return internalGetExtra().getMap(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public /* nullable */ - java.lang.String getExtraOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetExtra().getMap(); - return map.containsKey(key) ? map.get(key) : defaultValue; - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public java.lang.String getExtraOrThrow( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetExtra().getMap(); - if (!map.containsKey(key)) { - throw new java.lang.IllegalArgumentException(); - } - return map.get(key); - } - - public static final int CONFIDENCE_FIELD_NUMBER = 4; - private int confidence_ = 0; - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return Whether the confidence field is set. - */ - @java.lang.Override - public boolean hasConfidence() { - return ((bitField0_ & 0x00000001) != 0); - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The enum numeric value on the wire for confidence. - */ - @java.lang.Override - public int getConfidenceValue() { - return confidence_; - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The confidence. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence getConfidence() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence result = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence - .forNumber(confidence_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence.UNRECOGNIZED - : result; - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (trackerId_ != 0) { - output.writeInt32(1, trackerId_); - } - if ( - status_ - != dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status.DISCONNECTED - .getNumber() - ) { - output.writeEnum(2, status_); - } - com.google.protobuf.GeneratedMessage - .serializeStringMapTo( - output, - internalGetExtra(), - ExtraDefaultEntryHolder.defaultEntry, - 3 - ); - if (((bitField0_ & 0x00000001) != 0)) { - output.writeEnum(4, confidence_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (trackerId_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(1, trackerId_); - } - if ( - status_ - != dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status.DISCONNECTED - .getNumber() - ) { - size += com.google.protobuf.CodedOutputStream - .computeEnumSize(2, status_); - } - for ( - java.util.Map.Entry entry : internalGetExtra() - .getMap() - .entrySet() - ) { - com.google.protobuf.MapEntry extra__ = ExtraDefaultEntryHolder.defaultEntry - .newBuilderForType() - .setKey(entry.getKey()) - .setValue(entry.getValue()) - .build(); - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(3, extra__); - } - if (((bitField0_ & 0x00000001) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeEnumSize(4, confidence_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus other = (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) obj; - - if ( - getTrackerId() - != other.getTrackerId() - ) - return false; - if (status_ != other.status_) - return false; - if ( - !internalGetExtra() - .equals( - other.internalGetExtra() - ) - ) - return false; - if (hasConfidence() != other.hasConfidence()) - return false; - if (hasConfidence()) { - if (confidence_ != other.confidence_) - return false; - } - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + TRACKER_ID_FIELD_NUMBER; - hash = (53 * hash) + getTrackerId(); - hash = (37 * hash) + STATUS_FIELD_NUMBER; - hash = (53 * hash) + status_; - if (!internalGetExtra().getMap().isEmpty()) { - hash = (37 * hash) + EXTRA_FIELD_NUMBER; - hash = (53 * hash) + internalGetExtra().hashCode(); - } - if (hasConfidence()) { - hash = (37 * hash) + CONFIDENCE_FIELD_NUMBER; - hash = (53 * hash) + confidence_; - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - byte[] data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.TrackerStatus} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.TrackerStatus) - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatusOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_descriptor; - } - - @SuppressWarnings({ "rawtypes" }) - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMapFieldReflection( - int number - ) { - switch (number) { - case 3: - return internalGetExtra(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @SuppressWarnings({ "rawtypes" }) - protected com.google.protobuf.MapFieldReflectionAccessor internalGetMutableMapFieldReflection( - int number - ) { - switch (number) { - case 3: - return internalGetMutableExtra(); - default: - throw new RuntimeException( - "Invalid map field number: " + number - ); - } - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.class, - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - trackerId_ = 0; - status_ = 0; - internalGetMutableExtra().clear(); - confidence_ = 0; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_TrackerStatus_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus build() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus result = new dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.trackerId_ = trackerId_; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.status_ = status_; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.extra_ = internalGetExtra(); - result.extra_.makeImmutable(); - } - int to_bitField0_ = 0; - if (((from_bitField0_ & 0x00000008) != 0)) { - result.confidence_ = confidence_; - to_bitField0_ |= 0x00000001; - } - result.bitField0_ |= to_bitField0_; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus other - ) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance() - ) - return this; - if (other.getTrackerId() != 0) { - setTrackerId(other.getTrackerId()); - } - if (other.status_ != 0) { - setStatusValue(other.getStatusValue()); - } - internalGetMutableExtra() - .mergeFrom( - other.internalGetExtra() - ); - bitField0_ |= 0x00000004; - if (other.hasConfidence()) { - setConfidenceValue(other.getConfidenceValue()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - trackerId_ = input.readInt32(); - bitField0_ |= 0x00000001; - break; - } // case 8 - case 16: { - status_ = input.readEnum(); - bitField0_ |= 0x00000002; - break; - } // case 16 - case 26: { - com.google.protobuf.MapEntry extra__ = input - .readMessage( - ExtraDefaultEntryHolder.defaultEntry.getParserForType(), - extensionRegistry - ); - internalGetMutableExtra() - .getMutableMap() - .put( - extra__.getKey(), - extra__.getValue() - ); - bitField0_ |= 0x00000004; - break; - } // case 26 - case 32: { - confidence_ = input.readEnum(); - bitField0_ |= 0x00000008; - break; - } // case 32 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private int trackerId_; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - /** - * int32 tracker_id = 1; - * - * @param value The trackerId to set. - * @return This builder for chaining. - */ - public Builder setTrackerId(int value) { - - trackerId_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * int32 tracker_id = 1; - * - * @return This builder for chaining. - */ - public Builder clearTrackerId() { - bitField0_ = (bitField0_ & ~0x00000001); - trackerId_ = 0; - onChanged(); - return this; - } - - private int status_ = 0; - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The enum numeric value on the wire for status. - */ - @java.lang.Override - public int getStatusValue() { - return status_; - } - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @param value The enum numeric value on the wire for status to - * set. - * @return This builder for chaining. - */ - public Builder setStatusValue(int value) { - status_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return The status. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status getStatus() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status result = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status - .forNumber(status_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status.UNRECOGNIZED - : result; - } - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @param value The status to set. - * @return This builder for chaining. - */ - public Builder setStatus( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Status value - ) { - if (value == null) { - throw new NullPointerException(); - } - bitField0_ |= 0x00000002; - status_ = value.getNumber(); - onChanged(); - return this; - } - - /** - * .messages.TrackerStatus.Status status = 2; - * - * @return This builder for chaining. - */ - public Builder clearStatus() { - bitField0_ = (bitField0_ & ~0x00000002); - status_ = 0; - onChanged(); - return this; - } - - private com.google.protobuf.MapField extra_; - - private com.google.protobuf.MapField internalGetExtra() { - if (extra_ == null) { - return com.google.protobuf.MapField - .emptyMapField( - ExtraDefaultEntryHolder.defaultEntry - ); - } - return extra_; - } - - private com.google.protobuf.MapField internalGetMutableExtra() { - if (extra_ == null) { - extra_ = com.google.protobuf.MapField - .newMapField( - ExtraDefaultEntryHolder.defaultEntry - ); - } - if (!extra_.isMutable()) { - extra_ = extra_.copy(); - } - bitField0_ |= 0x00000004; - onChanged(); - return extra_; - } - - public int getExtraCount() { - return internalGetExtra().getMap().size(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public boolean containsExtra( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - return internalGetExtra().getMap().containsKey(key); - } - - /** - * Use {@link #getExtraMap()} instead. - */ - @java.lang.Override - @java.lang.Deprecated - public java.util.Map getExtra() { - return getExtraMap(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public java.util.Map getExtraMap() { - return internalGetExtra().getMap(); - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public /* nullable */ - java.lang.String getExtraOrDefault( - java.lang.String key, - /* nullable */ - java.lang.String defaultValue - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetExtra().getMap(); - return map.containsKey(key) ? map.get(key) : defaultValue; - } - - /** - * map<string, string> extra = 3; - */ - @java.lang.Override - public java.lang.String getExtraOrThrow( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - java.util.Map map = internalGetExtra().getMap(); - if (!map.containsKey(key)) { - throw new java.lang.IllegalArgumentException(); - } - return map.get(key); - } - - public Builder clearExtra() { - bitField0_ = (bitField0_ & ~0x00000004); - internalGetMutableExtra() - .getMutableMap() - .clear(); - return this; - } - - /** - * map<string, string> extra = 3; - */ - public Builder removeExtra( - java.lang.String key - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - internalGetMutableExtra() - .getMutableMap() - .remove(key); - return this; - } - - /** - * Use alternate mutation accessors instead. - */ - @java.lang.Deprecated - public java.util.Map getMutableExtra() { - bitField0_ |= 0x00000004; - return internalGetMutableExtra().getMutableMap(); - } - - /** - * map<string, string> extra = 3; - */ - public Builder putExtra( - java.lang.String key, - java.lang.String value - ) { - if (key == null) { - throw new NullPointerException("map key"); - } - if (value == null) { - throw new NullPointerException("map value"); - } - internalGetMutableExtra() - .getMutableMap() - .put(key, value); - bitField0_ |= 0x00000004; - return this; - } - - /** - * map<string, string> extra = 3; - */ - public Builder putAllExtra( - java.util.Map values - ) { - internalGetMutableExtra() - .getMutableMap() - .putAll(values); - bitField0_ |= 0x00000004; - return this; - } - - private int confidence_ = 0; - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return Whether the confidence field is set. - */ - @java.lang.Override - public boolean hasConfidence() { - return ((bitField0_ & 0x00000008) != 0); - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The enum numeric value on the wire for confidence. - */ - @java.lang.Override - public int getConfidenceValue() { - return confidence_; - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @param value The enum numeric value on the wire for confidence to - * set. - * @return This builder for chaining. - */ - public Builder setConfidenceValue(int value) { - confidence_ = value; - bitField0_ |= 0x00000008; - onChanged(); - return this; - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return The confidence. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence getConfidence() { - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence result = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence - .forNumber(confidence_); - return result == null - ? dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence.UNRECOGNIZED - : result; - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @param value The confidence to set. - * @return This builder for chaining. - */ - public Builder setConfidence( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Confidence value - ) { - if (value == null) { - throw new NullPointerException(); - } - bitField0_ |= 0x00000008; - confidence_ = value.getNumber(); - onChanged(); - return this; - } - - /** - * optional .messages.TrackerStatus.Confidence confidence = 4; - * - * @return This builder for chaining. - */ - public Builder clearConfidence() { - bitField0_ = (bitField0_ & ~0x00000008); - confidence_ = 0; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.TrackerStatus) - } - - // @@protoc_insertion_point(class_scope:messages.TrackerStatus) - private static final dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public TrackerStatus parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface BatteryOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.Battery) - com.google.protobuf.MessageOrBuilder { - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - int getTrackerId(); - - /** - * float battery_level = 2; - * - * @return The batteryLevel. - */ - float getBatteryLevel(); - - /** - * bool is_charging = 3; - * - * @return The isCharging. - */ - boolean getIsCharging(); - } - - /** - * Protobuf type {@code messages.Battery} - */ - public static final class Battery extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.Battery) - BatteryOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - Battery.class.getName() - ); - } - - // Use Battery.newBuilder() to construct. - private Battery(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private Battery() { - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Battery_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Battery_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Battery.class, - dev.slimevr.desktop.platform.ProtobufMessages.Battery.Builder.class - ); - } - - public static final int TRACKER_ID_FIELD_NUMBER = 1; - private int trackerId_ = 0; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - public static final int BATTERY_LEVEL_FIELD_NUMBER = 2; - private float batteryLevel_ = 0F; - - /** - * float battery_level = 2; - * - * @return The batteryLevel. - */ - @java.lang.Override - public float getBatteryLevel() { - return batteryLevel_; - } - - public static final int IS_CHARGING_FIELD_NUMBER = 3; - private boolean isCharging_ = false; - - /** - * bool is_charging = 3; - * - * @return The isCharging. - */ - @java.lang.Override - public boolean getIsCharging() { - return isCharging_; - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (trackerId_ != 0) { - output.writeInt32(1, trackerId_); - } - if (java.lang.Float.floatToRawIntBits(batteryLevel_) != 0) { - output.writeFloat(2, batteryLevel_); - } - if (isCharging_ != false) { - output.writeBool(3, isCharging_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (trackerId_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(1, trackerId_); - } - if (java.lang.Float.floatToRawIntBits(batteryLevel_) != 0) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, batteryLevel_); - } - if (isCharging_ != false) { - size += com.google.protobuf.CodedOutputStream - .computeBoolSize(3, isCharging_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.Battery)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.Battery other = (dev.slimevr.desktop.platform.ProtobufMessages.Battery) obj; - - if ( - getTrackerId() - != other.getTrackerId() - ) - return false; - if ( - java.lang.Float.floatToIntBits(getBatteryLevel()) - != java.lang.Float - .floatToIntBits( - other.getBatteryLevel() - ) - ) - return false; - if ( - getIsCharging() - != other.getIsCharging() - ) - return false; - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + TRACKER_ID_FIELD_NUMBER; - hash = (53 * hash) + getTrackerId(); - hash = (37 * hash) + BATTERY_LEVEL_FIELD_NUMBER; - hash = (53 * hash) - + java.lang.Float - .floatToIntBits( - getBatteryLevel() - ); - hash = (37 * hash) + IS_CHARGING_FIELD_NUMBER; - hash = (53 * hash) - + com.google.protobuf.Internal - .hashBoolean( - getIsCharging() - ); - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.Battery prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.Battery} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.Battery) - dev.slimevr.desktop.platform.ProtobufMessages.BatteryOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Battery_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Battery_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.Battery.class, - dev.slimevr.desktop.platform.ProtobufMessages.Battery.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.Battery.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - trackerId_ = 0; - batteryLevel_ = 0F; - isCharging_ = false; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_Battery_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.Battery.getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery build() { - dev.slimevr.desktop.platform.ProtobufMessages.Battery result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.Battery result = new dev.slimevr.desktop.platform.ProtobufMessages.Battery( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.Battery result - ) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.trackerId_ = trackerId_; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.batteryLevel_ = batteryLevel_; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.isCharging_ = isCharging_; - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof dev.slimevr.desktop.platform.ProtobufMessages.Battery) { - return mergeFrom((dev.slimevr.desktop.platform.ProtobufMessages.Battery) other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(dev.slimevr.desktop.platform.ProtobufMessages.Battery other) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance() - ) - return this; - if (other.getTrackerId() != 0) { - setTrackerId(other.getTrackerId()); - } - if (java.lang.Float.floatToRawIntBits(other.getBatteryLevel()) != 0) { - setBatteryLevel(other.getBatteryLevel()); - } - if (other.getIsCharging() != false) { - setIsCharging(other.getIsCharging()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - trackerId_ = input.readInt32(); - bitField0_ |= 0x00000001; - break; - } // case 8 - case 21: { - batteryLevel_ = input.readFloat(); - bitField0_ |= 0x00000002; - break; - } // case 21 - case 24: { - isCharging_ = input.readBool(); - bitField0_ |= 0x00000004; - break; - } // case 24 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int bitField0_; - - private int trackerId_; - - /** - * int32 tracker_id = 1; - * - * @return The trackerId. - */ - @java.lang.Override - public int getTrackerId() { - return trackerId_; - } - - /** - * int32 tracker_id = 1; - * - * @param value The trackerId to set. - * @return This builder for chaining. - */ - public Builder setTrackerId(int value) { - - trackerId_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - /** - * int32 tracker_id = 1; - * - * @return This builder for chaining. - */ - public Builder clearTrackerId() { - bitField0_ = (bitField0_ & ~0x00000001); - trackerId_ = 0; - onChanged(); - return this; - } - - private float batteryLevel_; - - /** - * float battery_level = 2; - * - * @return The batteryLevel. - */ - @java.lang.Override - public float getBatteryLevel() { - return batteryLevel_; - } - - /** - * float battery_level = 2; - * - * @param value The batteryLevel to set. - * @return This builder for chaining. - */ - public Builder setBatteryLevel(float value) { - - batteryLevel_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - /** - * float battery_level = 2; - * - * @return This builder for chaining. - */ - public Builder clearBatteryLevel() { - bitField0_ = (bitField0_ & ~0x00000002); - batteryLevel_ = 0F; - onChanged(); - return this; - } - - private boolean isCharging_; - - /** - * bool is_charging = 3; - * - * @return The isCharging. - */ - @java.lang.Override - public boolean getIsCharging() { - return isCharging_; - } - - /** - * bool is_charging = 3; - * - * @param value The isCharging to set. - * @return This builder for chaining. - */ - public Builder setIsCharging(boolean value) { - - isCharging_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - - /** - * bool is_charging = 3; - * - * @return This builder for chaining. - */ - public Builder clearIsCharging() { - bitField0_ = (bitField0_ & ~0x00000004); - isCharging_ = false; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:messages.Battery) - } - - // @@protoc_insertion_point(class_scope:messages.Battery) - private static final dev.slimevr.desktop.platform.ProtobufMessages.Battery DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.Battery(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.Battery getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public Battery parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface ProtobufMessageOrBuilder extends - // @@protoc_insertion_point(interface_extends:messages.ProtobufMessage) - com.google.protobuf.MessageOrBuilder { - - /** - * .messages.Position position = 1; - * - * @return Whether the position field is set. - */ - boolean hasPosition(); - - /** - * .messages.Position position = 1; - * - * @return The position. - */ - dev.slimevr.desktop.platform.ProtobufMessages.Position getPosition(); - - /** - * .messages.Position position = 1; - */ - dev.slimevr.desktop.platform.ProtobufMessages.PositionOrBuilder getPositionOrBuilder(); - - /** - * .messages.UserAction user_action = 2; - * - * @return Whether the userAction field is set. - */ - boolean hasUserAction(); - - /** - * .messages.UserAction user_action = 2; - * - * @return The userAction. - */ - dev.slimevr.desktop.platform.ProtobufMessages.UserAction getUserAction(); - - /** - * .messages.UserAction user_action = 2; - */ - dev.slimevr.desktop.platform.ProtobufMessages.UserActionOrBuilder getUserActionOrBuilder(); - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return Whether the trackerAdded field is set. - */ - boolean hasTrackerAdded(); - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return The trackerAdded. - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getTrackerAdded(); - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAddedOrBuilder getTrackerAddedOrBuilder(); - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return Whether the trackerStatus field is set. - */ - boolean hasTrackerStatus(); - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return The trackerStatus. - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getTrackerStatus(); - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatusOrBuilder getTrackerStatusOrBuilder(); - - /** - * .messages.Battery battery = 5; - * - * @return Whether the battery field is set. - */ - boolean hasBattery(); - - /** - * .messages.Battery battery = 5; - * - * @return The battery. - */ - dev.slimevr.desktop.platform.ProtobufMessages.Battery getBattery(); - - /** - * .messages.Battery battery = 5; - */ - dev.slimevr.desktop.platform.ProtobufMessages.BatteryOrBuilder getBatteryOrBuilder(); - - /** - * .messages.Version version = 6; - * - * @return Whether the version field is set. - */ - boolean hasVersion(); - - /** - * .messages.Version version = 6; - * - * @return The version. - */ - dev.slimevr.desktop.platform.ProtobufMessages.Version getVersion(); - - /** - * .messages.Version version = 6; - */ - dev.slimevr.desktop.platform.ProtobufMessages.VersionOrBuilder getVersionOrBuilder(); - - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.MessageCase getMessageCase(); - } - - /** - * Protobuf type {@code messages.ProtobufMessage} - */ - public static final class ProtobufMessage extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:messages.ProtobufMessage) - ProtobufMessageOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion - .validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 31, - /* patch= */ 1, - /* suffix= */ "", - ProtobufMessage.class.getName() - ); - } - - // Use ProtobufMessage.newBuilder() to construct. - private ProtobufMessage(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - - private ProtobufMessage() { - } - - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_ProtobufMessage_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_ProtobufMessage_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.class, - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.Builder.class - ); - } - - private int messageCase_ = 0; - @SuppressWarnings("serial") - private java.lang.Object message_; - - public enum MessageCase - implements com.google.protobuf.Internal.EnumLite, - com.google.protobuf.AbstractMessage.InternalOneOfEnum { - POSITION(1), - USER_ACTION(2), - TRACKER_ADDED(3), - TRACKER_STATUS(4), - BATTERY(5), - VERSION(6), - MESSAGE_NOT_SET(0); - - private final int value; - - private MessageCase(int value) { - this.value = value; - } - - /** - * @param value The number of the enum to look for. - * @return The enum associated with the given number. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static MessageCase valueOf(int value) { - return forNumber(value); - } - - public static MessageCase forNumber(int value) { - switch (value) { - case 1: - return POSITION; - case 2: - return USER_ACTION; - case 3: - return TRACKER_ADDED; - case 4: - return TRACKER_STATUS; - case 5: - return BATTERY; - case 6: - return VERSION; - case 0: - return MESSAGE_NOT_SET; - default: - return null; - } - } - - public int getNumber() { - return this.value; - } - }; - - public MessageCase getMessageCase() { - return MessageCase - .forNumber( - messageCase_ - ); - } - - public static final int POSITION_FIELD_NUMBER = 1; - - /** - * .messages.Position position = 1; - * - * @return Whether the position field is set. - */ - @java.lang.Override - public boolean hasPosition() { - return messageCase_ == 1; - } - - /** - * .messages.Position position = 1; - * - * @return The position. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position getPosition() { - if (messageCase_ == 1) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Position.getDefaultInstance(); - } - - /** - * .messages.Position position = 1; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PositionOrBuilder getPositionOrBuilder() { - if (messageCase_ == 1) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Position.getDefaultInstance(); - } - - public static final int USER_ACTION_FIELD_NUMBER = 2; - - /** - * .messages.UserAction user_action = 2; - * - * @return Whether the userAction field is set. - */ - @java.lang.Override - public boolean hasUserAction() { - return messageCase_ == 2; - } - - /** - * .messages.UserAction user_action = 2; - * - * @return The userAction. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction getUserAction() { - if (messageCase_ == 2) { - return (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction.getDefaultInstance(); - } - - /** - * .messages.UserAction user_action = 2; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserActionOrBuilder getUserActionOrBuilder() { - if (messageCase_ == 2) { - return (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction.getDefaultInstance(); - } - - public static final int TRACKER_ADDED_FIELD_NUMBER = 3; - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return Whether the trackerAdded field is set. - */ - @java.lang.Override - public boolean hasTrackerAdded() { - return messageCase_ == 3; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return The trackerAdded. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getTrackerAdded() { - if (messageCase_ == 3) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.getDefaultInstance(); - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAddedOrBuilder getTrackerAddedOrBuilder() { - if (messageCase_ == 3) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.getDefaultInstance(); - } - - public static final int TRACKER_STATUS_FIELD_NUMBER = 4; - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return Whether the trackerStatus field is set. - */ - @java.lang.Override - public boolean hasTrackerStatus() { - return messageCase_ == 4; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return The trackerStatus. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getTrackerStatus() { - if (messageCase_ == 4) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.getDefaultInstance(); - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatusOrBuilder getTrackerStatusOrBuilder() { - if (messageCase_ == 4) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.getDefaultInstance(); - } - - public static final int BATTERY_FIELD_NUMBER = 5; - - /** - * .messages.Battery battery = 5; - * - * @return Whether the battery field is set. - */ - @java.lang.Override - public boolean hasBattery() { - return messageCase_ == 5; - } - - /** - * .messages.Battery battery = 5; - * - * @return The battery. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery getBattery() { - if (messageCase_ == 5) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Battery.getDefaultInstance(); - } - - /** - * .messages.Battery battery = 5; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.BatteryOrBuilder getBatteryOrBuilder() { - if (messageCase_ == 5) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Battery.getDefaultInstance(); - } - - public static final int VERSION_FIELD_NUMBER = 6; - - /** - * .messages.Version version = 6; - * - * @return Whether the version field is set. - */ - @java.lang.Override - public boolean hasVersion() { - return messageCase_ == 6; - } - - /** - * .messages.Version version = 6; - * - * @return The version. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version getVersion() { - if (messageCase_ == 6) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Version.getDefaultInstance(); - } - - /** - * .messages.Version version = 6; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.VersionOrBuilder getVersionOrBuilder() { - if (messageCase_ == 6) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Version.getDefaultInstance(); - } - - private byte memoizedIsInitialized = -1; - - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) - return true; - if (isInitialized == 0) - return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (messageCase_ == 1) { - output - .writeMessage( - 1, - (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_ - ); - } - if (messageCase_ == 2) { - output - .writeMessage( - 2, - (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_ - ); - } - if (messageCase_ == 3) { - output - .writeMessage( - 3, - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_ - ); - } - if (messageCase_ == 4) { - output - .writeMessage( - 4, - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_ - ); - } - if (messageCase_ == 5) { - output - .writeMessage( - 5, - (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_ - ); - } - if (messageCase_ == 6) { - output - .writeMessage( - 6, - (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_ - ); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) - return size; - - size = 0; - if (messageCase_ == 1) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 1, - (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_ - ); - } - if (messageCase_ == 2) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 2, - (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_ - ); - } - if (messageCase_ == 3) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 3, - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_ - ); - } - if (messageCase_ == 4) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 4, - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_ - ); - } - if (messageCase_ == 5) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 5, - (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_ - ); - } - if (messageCase_ == 6) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize( - 6, - (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_ - ); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage)) { - return super.equals(obj); - } - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage other = (dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage) obj; - - if (!getMessageCase().equals(other.getMessageCase())) - return false; - switch (messageCase_) { - case 1: - if ( - !getPosition() - .equals(other.getPosition()) - ) - return false; - break; - case 2: - if ( - !getUserAction() - .equals(other.getUserAction()) - ) - return false; - break; - case 3: - if ( - !getTrackerAdded() - .equals(other.getTrackerAdded()) - ) - return false; - break; - case 4: - if ( - !getTrackerStatus() - .equals(other.getTrackerStatus()) - ) - return false; - break; - case 5: - if ( - !getBattery() - .equals(other.getBattery()) - ) - return false; - break; - case 6: - if ( - !getVersion() - .equals(other.getVersion()) - ) - return false; - break; - case 0: - default: - } - if (!getUnknownFields().equals(other.getUnknownFields())) - return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - switch (messageCase_) { - case 1: - hash = (37 * hash) + POSITION_FIELD_NUMBER; - hash = (53 * hash) + getPosition().hashCode(); - break; - case 2: - hash = (37 * hash) + USER_ACTION_FIELD_NUMBER; - hash = (53 * hash) + getUserAction().hashCode(); - break; - case 3: - hash = (37 * hash) + TRACKER_ADDED_FIELD_NUMBER; - hash = (53 * hash) + getTrackerAdded().hashCode(); - break; - case 4: - hash = (37 * hash) + TRACKER_STATUS_FIELD_NUMBER; - hash = (53 * hash) + getTrackerStatus().hashCode(); - break; - case 5: - hash = (37 * hash) + BATTERY_FIELD_NUMBER; - hash = (53 * hash) + getBattery().hashCode(); - break; - case 6: - hash = (37 * hash) + VERSION_FIELD_NUMBER; - hash = (53 * hash) + getVersion().hashCode(); - break; - case 0: - default: - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - java.nio.ByteBuffer data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - com.google.protobuf.ByteString data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - byte[] data - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseDelimitedFrom( - java.io.InputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - com.google.protobuf.CodedInputStream input - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { - return newBuilder(); - } - - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - - public static Builder newBuilder( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage prototype - ) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() - : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - Builder builder = new Builder(parent); - return builder; - } - - /** - * Protobuf type {@code messages.ProtobufMessage} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:messages.ProtobufMessage) - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessageOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_ProtobufMessage_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_ProtobufMessage_fieldAccessorTable - .ensureFieldAccessorsInitialized( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.class, - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.Builder.class - ); - } - - // Construct using - // dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent - ) { - super(parent); - - } - - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - if (positionBuilder_ != null) { - positionBuilder_.clear(); - } - if (userActionBuilder_ != null) { - userActionBuilder_.clear(); - } - if (trackerAddedBuilder_ != null) { - trackerAddedBuilder_.clear(); - } - if (trackerStatusBuilder_ != null) { - trackerStatusBuilder_.clear(); - } - if (batteryBuilder_ != null) { - batteryBuilder_.clear(); - } - if (versionBuilder_ != null) { - versionBuilder_.clear(); - } - messageCase_ = 0; - message_ = null; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.internal_static_messages_ProtobufMessage_descriptor; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage getDefaultInstanceForType() { - return dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage - .getDefaultInstance(); - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage build() { - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage buildPartial() { - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage result = new dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage( - this - ); - if (bitField0_ != 0) { - buildPartial0(result); - } - buildPartialOneofs(result); - onBuilt(); - return result; - } - - private void buildPartial0( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage result - ) { - int from_bitField0_ = bitField0_; - } - - private void buildPartialOneofs( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage result - ) { - result.messageCase_ = messageCase_; - result.message_ = this.message_; - if ( - messageCase_ == 1 - && - positionBuilder_ != null - ) { - result.message_ = positionBuilder_.build(); - } - if ( - messageCase_ == 2 - && - userActionBuilder_ != null - ) { - result.message_ = userActionBuilder_.build(); - } - if ( - messageCase_ == 3 - && - trackerAddedBuilder_ != null - ) { - result.message_ = trackerAddedBuilder_.build(); - } - if ( - messageCase_ == 4 - && - trackerStatusBuilder_ != null - ) { - result.message_ = trackerStatusBuilder_.build(); - } - if ( - messageCase_ == 5 - && - batteryBuilder_ != null - ) { - result.message_ = batteryBuilder_.build(); - } - if ( - messageCase_ == 6 - && - versionBuilder_ != null - ) { - result.message_ = versionBuilder_.build(); - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if ( - other instanceof dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage - ) { - return mergeFrom( - (dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage) other - ); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom( - dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage other - ) { - if ( - other - == dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage - .getDefaultInstance() - ) - return this; - switch (other.getMessageCase()) { - case POSITION: { - mergePosition(other.getPosition()); - break; - } - case USER_ACTION: { - mergeUserAction(other.getUserAction()); - break; - } - case TRACKER_ADDED: { - mergeTrackerAdded(other.getTrackerAdded()); - break; - } - case TRACKER_STATUS: { - mergeTrackerStatus(other.getTrackerStatus()); - break; - } - case BATTERY: { - mergeBattery(other.getBattery()); - break; - } - case VERSION: { - mergeVersion(other.getVersion()); - break; - } - case MESSAGE_NOT_SET: { - break; - } - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 10: { - input - .readMessage( - internalGetPositionFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 1; - break; - } // case 10 - case 18: { - input - .readMessage( - internalGetUserActionFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 2; - break; - } // case 18 - case 26: { - input - .readMessage( - internalGetTrackerAddedFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 3; - break; - } // case 26 - case 34: { - input - .readMessage( - internalGetTrackerStatusFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 4; - break; - } // case 34 - case 42: { - input - .readMessage( - internalGetBatteryFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 5; - break; - } // case 42 - case 50: { - input - .readMessage( - internalGetVersionFieldBuilder().getBuilder(), - extensionRegistry - ); - messageCase_ = 6; - break; - } // case 50 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - - private int messageCase_ = 0; - private java.lang.Object message_; - - public MessageCase getMessageCase() { - return MessageCase - .forNumber( - messageCase_ - ); - } - - public Builder clearMessage() { - messageCase_ = 0; - message_ = null; - onChanged(); - return this; - } - - private int bitField0_; - - private com.google.protobuf.SingleFieldBuilder positionBuilder_; - - /** - * .messages.Position position = 1; - * - * @return Whether the position field is set. - */ - @java.lang.Override - public boolean hasPosition() { - return messageCase_ == 1; - } - - /** - * .messages.Position position = 1; - * - * @return The position. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Position getPosition() { - if (positionBuilder_ == null) { - if (messageCase_ == 1) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance(); - } else { - if (messageCase_ == 1) { - return positionBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance(); - } - } - - /** - * .messages.Position position = 1; - */ - public Builder setPosition( - dev.slimevr.desktop.platform.ProtobufMessages.Position value - ) { - if (positionBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - positionBuilder_.setMessage(value); - } - messageCase_ = 1; - return this; - } - - /** - * .messages.Position position = 1; - */ - public Builder setPosition( - dev.slimevr.desktop.platform.ProtobufMessages.Position.Builder builderForValue - ) { - if (positionBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - positionBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 1; - return this; - } - - /** - * .messages.Position position = 1; - */ - public Builder mergePosition( - dev.slimevr.desktop.platform.ProtobufMessages.Position value - ) { - if (positionBuilder_ == null) { - if ( - messageCase_ == 1 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Position - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 1) { - positionBuilder_.mergeFrom(value); - } else { - positionBuilder_.setMessage(value); - } - } - messageCase_ = 1; - return this; - } - - /** - * .messages.Position position = 1; - */ - public Builder clearPosition() { - if (positionBuilder_ == null) { - if (messageCase_ == 1) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 1) { - messageCase_ = 0; - message_ = null; - } - positionBuilder_.clear(); - } - return this; - } - - /** - * .messages.Position position = 1; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.Position.Builder getPositionBuilder() { - return internalGetPositionFieldBuilder().getBuilder(); - } - - /** - * .messages.Position position = 1; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.PositionOrBuilder getPositionOrBuilder() { - if ((messageCase_ == 1) && (positionBuilder_ != null)) { - return positionBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 1) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance(); - } - } - - /** - * .messages.Position position = 1; - */ - private com.google.protobuf.SingleFieldBuilder internalGetPositionFieldBuilder() { - if (positionBuilder_ == null) { - if (!(messageCase_ == 1)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Position - .getDefaultInstance(); - } - positionBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Position) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 1; - onChanged(); - return positionBuilder_; - } - - private com.google.protobuf.SingleFieldBuilder userActionBuilder_; - - /** - * .messages.UserAction user_action = 2; - * - * @return Whether the userAction field is set. - */ - @java.lang.Override - public boolean hasUserAction() { - return messageCase_ == 2; - } - - /** - * .messages.UserAction user_action = 2; - * - * @return The userAction. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction getUserAction() { - if (userActionBuilder_ == null) { - if (messageCase_ == 2) { - return (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance(); - } else { - if (messageCase_ == 2) { - return userActionBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance(); - } - } - - /** - * .messages.UserAction user_action = 2; - */ - public Builder setUserAction( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction value - ) { - if (userActionBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - userActionBuilder_.setMessage(value); - } - messageCase_ = 2; - return this; - } - - /** - * .messages.UserAction user_action = 2; - */ - public Builder setUserAction( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction.Builder builderForValue - ) { - if (userActionBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - userActionBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 2; - return this; - } - - /** - * .messages.UserAction user_action = 2; - */ - public Builder mergeUserAction( - dev.slimevr.desktop.platform.ProtobufMessages.UserAction value - ) { - if (userActionBuilder_ == null) { - if ( - messageCase_ == 2 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 2) { - userActionBuilder_.mergeFrom(value); - } else { - userActionBuilder_.setMessage(value); - } - } - messageCase_ = 2; - return this; - } - - /** - * .messages.UserAction user_action = 2; - */ - public Builder clearUserAction() { - if (userActionBuilder_ == null) { - if (messageCase_ == 2) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 2) { - messageCase_ = 0; - message_ = null; - } - userActionBuilder_.clear(); - } - return this; - } - - /** - * .messages.UserAction user_action = 2; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.UserAction.Builder getUserActionBuilder() { - return internalGetUserActionFieldBuilder().getBuilder(); - } - - /** - * .messages.UserAction user_action = 2; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.UserActionOrBuilder getUserActionOrBuilder() { - if ((messageCase_ == 2) && (userActionBuilder_ != null)) { - return userActionBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 2) { - return (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance(); - } - } - - /** - * .messages.UserAction user_action = 2; - */ - private com.google.protobuf.SingleFieldBuilder internalGetUserActionFieldBuilder() { - if (userActionBuilder_ == null) { - if (!(messageCase_ == 2)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.UserAction - .getDefaultInstance(); - } - userActionBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.UserAction) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 2; - onChanged(); - return userActionBuilder_; - } - - private com.google.protobuf.SingleFieldBuilder trackerAddedBuilder_; - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return Whether the trackerAdded field is set. - */ - @java.lang.Override - public boolean hasTrackerAdded() { - return messageCase_ == 3; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - * - * @return The trackerAdded. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded getTrackerAdded() { - if (trackerAddedBuilder_ == null) { - if (messageCase_ == 3) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance(); - } else { - if (messageCase_ == 3) { - return trackerAddedBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance(); - } - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - public Builder setTrackerAdded( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded value - ) { - if (trackerAddedBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - trackerAddedBuilder_.setMessage(value); - } - messageCase_ = 3; - return this; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - public Builder setTrackerAdded( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.Builder builderForValue - ) { - if (trackerAddedBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - trackerAddedBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 3; - return this; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - public Builder mergeTrackerAdded( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded value - ) { - if (trackerAddedBuilder_ == null) { - if ( - messageCase_ == 3 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 3) { - trackerAddedBuilder_.mergeFrom(value); - } else { - trackerAddedBuilder_.setMessage(value); - } - } - messageCase_ = 3; - return this; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - public Builder clearTrackerAdded() { - if (trackerAddedBuilder_ == null) { - if (messageCase_ == 3) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 3) { - messageCase_ = 0; - message_ = null; - } - trackerAddedBuilder_.clear(); - } - return this; - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded.Builder getTrackerAddedBuilder() { - return internalGetTrackerAddedFieldBuilder().getBuilder(); - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerAddedOrBuilder getTrackerAddedOrBuilder() { - if ((messageCase_ == 3) && (trackerAddedBuilder_ != null)) { - return trackerAddedBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 3) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance(); - } - } - - /** - * .messages.TrackerAdded tracker_added = 3; - */ - private com.google.protobuf.SingleFieldBuilder internalGetTrackerAddedFieldBuilder() { - if (trackerAddedBuilder_ == null) { - if (!(messageCase_ == 3)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded - .getDefaultInstance(); - } - trackerAddedBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerAdded) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 3; - onChanged(); - return trackerAddedBuilder_; - } - - private com.google.protobuf.SingleFieldBuilder trackerStatusBuilder_; - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return Whether the trackerStatus field is set. - */ - @java.lang.Override - public boolean hasTrackerStatus() { - return messageCase_ == 4; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - * - * @return The trackerStatus. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus getTrackerStatus() { - if (trackerStatusBuilder_ == null) { - if (messageCase_ == 4) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance(); - } else { - if (messageCase_ == 4) { - return trackerStatusBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance(); - } - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - public Builder setTrackerStatus( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus value - ) { - if (trackerStatusBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - trackerStatusBuilder_.setMessage(value); - } - messageCase_ = 4; - return this; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - public Builder setTrackerStatus( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Builder builderForValue - ) { - if (trackerStatusBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - trackerStatusBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 4; - return this; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - public Builder mergeTrackerStatus( - dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus value - ) { - if (trackerStatusBuilder_ == null) { - if ( - messageCase_ == 4 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 4) { - trackerStatusBuilder_.mergeFrom(value); - } else { - trackerStatusBuilder_.setMessage(value); - } - } - messageCase_ = 4; - return this; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - public Builder clearTrackerStatus() { - if (trackerStatusBuilder_ == null) { - if (messageCase_ == 4) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 4) { - messageCase_ = 0; - message_ = null; - } - trackerStatusBuilder_.clear(); - } - return this; - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus.Builder getTrackerStatusBuilder() { - return internalGetTrackerStatusFieldBuilder().getBuilder(); - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatusOrBuilder getTrackerStatusOrBuilder() { - if ((messageCase_ == 4) && (trackerStatusBuilder_ != null)) { - return trackerStatusBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 4) { - return (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance(); - } - } - - /** - * .messages.TrackerStatus tracker_status = 4; - */ - private com.google.protobuf.SingleFieldBuilder internalGetTrackerStatusFieldBuilder() { - if (trackerStatusBuilder_ == null) { - if (!(messageCase_ == 4)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus - .getDefaultInstance(); - } - trackerStatusBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.TrackerStatus) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 4; - onChanged(); - return trackerStatusBuilder_; - } - - private com.google.protobuf.SingleFieldBuilder batteryBuilder_; - - /** - * .messages.Battery battery = 5; - * - * @return Whether the battery field is set. - */ - @java.lang.Override - public boolean hasBattery() { - return messageCase_ == 5; - } - - /** - * .messages.Battery battery = 5; - * - * @return The battery. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Battery getBattery() { - if (batteryBuilder_ == null) { - if (messageCase_ == 5) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance(); - } else { - if (messageCase_ == 5) { - return batteryBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance(); - } - } - - /** - * .messages.Battery battery = 5; - */ - public Builder setBattery(dev.slimevr.desktop.platform.ProtobufMessages.Battery value) { - if (batteryBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - batteryBuilder_.setMessage(value); - } - messageCase_ = 5; - return this; - } - - /** - * .messages.Battery battery = 5; - */ - public Builder setBattery( - dev.slimevr.desktop.platform.ProtobufMessages.Battery.Builder builderForValue - ) { - if (batteryBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - batteryBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 5; - return this; - } - - /** - * .messages.Battery battery = 5; - */ - public Builder mergeBattery( - dev.slimevr.desktop.platform.ProtobufMessages.Battery value - ) { - if (batteryBuilder_ == null) { - if ( - messageCase_ == 5 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Battery - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 5) { - batteryBuilder_.mergeFrom(value); - } else { - batteryBuilder_.setMessage(value); - } - } - messageCase_ = 5; - return this; - } - - /** - * .messages.Battery battery = 5; - */ - public Builder clearBattery() { - if (batteryBuilder_ == null) { - if (messageCase_ == 5) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 5) { - messageCase_ = 0; - message_ = null; - } - batteryBuilder_.clear(); - } - return this; - } - - /** - * .messages.Battery battery = 5; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.Battery.Builder getBatteryBuilder() { - return internalGetBatteryFieldBuilder().getBuilder(); - } - - /** - * .messages.Battery battery = 5; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.BatteryOrBuilder getBatteryOrBuilder() { - if ((messageCase_ == 5) && (batteryBuilder_ != null)) { - return batteryBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 5) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance(); - } - } - - /** - * .messages.Battery battery = 5; - */ - private com.google.protobuf.SingleFieldBuilder internalGetBatteryFieldBuilder() { - if (batteryBuilder_ == null) { - if (!(messageCase_ == 5)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Battery - .getDefaultInstance(); - } - batteryBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Battery) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 5; - onChanged(); - return batteryBuilder_; - } - - private com.google.protobuf.SingleFieldBuilder versionBuilder_; - - /** - * .messages.Version version = 6; - * - * @return Whether the version field is set. - */ - @java.lang.Override - public boolean hasVersion() { - return messageCase_ == 6; - } - - /** - * .messages.Version version = 6; - * - * @return The version. - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.Version getVersion() { - if (versionBuilder_ == null) { - if (messageCase_ == 6) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance(); - } else { - if (messageCase_ == 6) { - return versionBuilder_.getMessage(); - } - return dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance(); - } - } - - /** - * .messages.Version version = 6; - */ - public Builder setVersion(dev.slimevr.desktop.platform.ProtobufMessages.Version value) { - if (versionBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - message_ = value; - onChanged(); - } else { - versionBuilder_.setMessage(value); - } - messageCase_ = 6; - return this; - } - - /** - * .messages.Version version = 6; - */ - public Builder setVersion( - dev.slimevr.desktop.platform.ProtobufMessages.Version.Builder builderForValue - ) { - if (versionBuilder_ == null) { - message_ = builderForValue.build(); - onChanged(); - } else { - versionBuilder_.setMessage(builderForValue.build()); - } - messageCase_ = 6; - return this; - } - - /** - * .messages.Version version = 6; - */ - public Builder mergeVersion( - dev.slimevr.desktop.platform.ProtobufMessages.Version value - ) { - if (versionBuilder_ == null) { - if ( - messageCase_ == 6 - && - message_ - != dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance() - ) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Version - .newBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_ - ) - .mergeFrom(value) - .buildPartial(); - } else { - message_ = value; - } - onChanged(); - } else { - if (messageCase_ == 6) { - versionBuilder_.mergeFrom(value); - } else { - versionBuilder_.setMessage(value); - } - } - messageCase_ = 6; - return this; - } - - /** - * .messages.Version version = 6; - */ - public Builder clearVersion() { - if (versionBuilder_ == null) { - if (messageCase_ == 6) { - messageCase_ = 0; - message_ = null; - onChanged(); - } - } else { - if (messageCase_ == 6) { - messageCase_ = 0; - message_ = null; - } - versionBuilder_.clear(); - } - return this; - } - - /** - * .messages.Version version = 6; - */ - public dev.slimevr.desktop.platform.ProtobufMessages.Version.Builder getVersionBuilder() { - return internalGetVersionFieldBuilder().getBuilder(); - } - - /** - * .messages.Version version = 6; - */ - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.VersionOrBuilder getVersionOrBuilder() { - if ((messageCase_ == 6) && (versionBuilder_ != null)) { - return versionBuilder_.getMessageOrBuilder(); - } else { - if (messageCase_ == 6) { - return (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_; - } - return dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance(); - } - } - - /** - * .messages.Version version = 6; - */ - private com.google.protobuf.SingleFieldBuilder internalGetVersionFieldBuilder() { - if (versionBuilder_ == null) { - if (!(messageCase_ == 6)) { - message_ = dev.slimevr.desktop.platform.ProtobufMessages.Version - .getDefaultInstance(); - } - versionBuilder_ = new com.google.protobuf.SingleFieldBuilder( - (dev.slimevr.desktop.platform.ProtobufMessages.Version) message_, - getParentForChildren(), - isClean() - ); - message_ = null; - } - messageCase_ = 6; - onChanged(); - return versionBuilder_; - } - - // @@protoc_insertion_point(builder_scope:messages.ProtobufMessage) - } - - // @@protoc_insertion_point(class_scope:messages.ProtobufMessage) - private static final dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage(); - } - - public static dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public ProtobufMessage parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry - ) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e - .asInvalidProtocolBufferException() - .setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_PingPong_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_PingPong_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_Version_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_Version_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_Position_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_Position_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_UserAction_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_UserAction_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_UserAction_ActionArgumentsEntry_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_UserAction_ActionArgumentsEntry_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_TrackerAdded_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_TrackerAdded_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_TrackerStatus_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_TrackerStatus_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_TrackerStatus_ExtraEntry_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_TrackerStatus_ExtraEntry_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_Battery_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_Battery_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor internal_static_messages_ProtobufMessage_descriptor; - private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_messages_ProtobufMessage_fieldAccessorTable; - - public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { - return descriptor; - } - - private static com.google.protobuf.Descriptors.FileDescriptor descriptor; - static { - java.lang.String[] descriptorData = { - "\n\026ProtobufMessages.proto\022\010messages\"\n\n\010Pi" - + - "ngPong\"#\n\007Version\022\030\n\020protocol_version\030\001 " - + - "\001(\005\"\223\002\n\010Position\022\022\n\ntracker_id\030\001 \001(\005\022\016\n\001" - + - "x\030\002 \001(\002H\000\210\001\001\022\016\n\001y\030\003 \001(\002H\001\210\001\001\022\016\n\001z\030\004 \001(\002H" - + - "\002\210\001\001\022\n\n\002qx\030\005 \001(\002\022\n\n\002qy\030\006 \001(\002\022\n\n\002qz\030\007 \001(\002" - + - "\022\n\n\002qw\030\010 \001(\002\0227\n\013data_source\030\t \001(\0162\035.mess" - + - "ages.Position.DataSourceH\003\210\001\001\"8\n\nDataSou" - + - "rce\022\010\n\004NONE\020\000\022\007\n\003IMU\020\001\022\r\n\tPRECISION\020\002\022\010\n" - + - "\004FULL\020\003B\004\n\002_xB\004\n\002_yB\004\n\002_zB\016\n\014_data_sourc" - + - "e\"\227\001\n\nUserAction\022\014\n\004name\030\001 \001(\t\022C\n\020action" - + - "_arguments\030\002 \003(\0132).messages.UserAction.A" - + - "ctionArgumentsEntry\0326\n\024ActionArgumentsEn" - + - "try\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"|\n\014T" - + - "rackerAdded\022\022\n\ntracker_id\030\001 \001(\005\022\026\n\016track" - + - "er_serial\030\002 \001(\t\022\024\n\014tracker_name\030\003 \001(\t\022\024\n" - + - "\014tracker_role\030\004 \001(\005\022\024\n\014manufacturer\030\005 \001(" - + - "\t\"\374\002\n\rTrackerStatus\022\022\n\ntracker_id\030\001 \001(\005\022" - + - ".\n\006status\030\002 \001(\0162\036.messages.TrackerStatus" - + - ".Status\0221\n\005extra\030\003 \003(\0132\".messages.Tracke" - + - "rStatus.ExtraEntry\022;\n\nconfidence\030\004 \001(\0162\"" - + - ".messages.TrackerStatus.ConfidenceH\000\210\001\001\032" - + - ",\n\nExtraEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(" - + - "\t:\0028\001\"E\n\006Status\022\020\n\014DISCONNECTED\020\000\022\006\n\002OK\020" - + - "\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003\022\014\n\010OCCLUDED\020\004\"3\n\n" - + - "Confidence\022\006\n\002NO\020\000\022\007\n\003LOW\020\001\022\n\n\006MEDIUM\020\005\022" - + - "\010\n\004HIGH\020\nB\r\n\013_confidence\"I\n\007Battery\022\022\n\nt" - + - "racker_id\030\001 \001(\005\022\025\n\rbattery_level\030\002 \001(\002\022\023" - + - "\n\013is_charging\030\003 \001(\010\"\241\002\n\017ProtobufMessage\022" - + - "&\n\010position\030\001 \001(\0132\022.messages.PositionH\000\022" - + - "+\n\013user_action\030\002 \001(\0132\024.messages.UserActi" - + - "onH\000\022/\n\rtracker_added\030\003 \001(\0132\026.messages.T" - + - "rackerAddedH\000\0221\n\016tracker_status\030\004 \001(\0132\027." - + - "messages.TrackerStatusH\000\022$\n\007battery\030\005 \001(" - + - "\0132\021.messages.BatteryH\000\022$\n\007version\030\006 \001(\0132" - + - "\021.messages.VersionH\000B\t\n\007messageB2\n\034dev.s" - + - "limevr.desktop.platformB\020ProtobufMessage" - + - "sH\003b\006proto3" - }; - descriptor = com.google.protobuf.Descriptors.FileDescriptor - .internalBuildGeneratedFileFrom( - descriptorData, - new com.google.protobuf.Descriptors.FileDescriptor[] { - } - ); - internal_static_messages_PingPong_descriptor = getDescriptor().getMessageTypes().get(0); - internal_static_messages_PingPong_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_PingPong_descriptor, - new java.lang.String[] {} - ); - internal_static_messages_Version_descriptor = getDescriptor().getMessageTypes().get(1); - internal_static_messages_Version_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_Version_descriptor, - new java.lang.String[] { "ProtocolVersion", } - ); - internal_static_messages_Position_descriptor = getDescriptor().getMessageTypes().get(2); - internal_static_messages_Position_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_Position_descriptor, - new java.lang.String[] { "TrackerId", "X", "Y", "Z", "Qx", "Qy", "Qz", "Qw", - "DataSource", } - ); - internal_static_messages_UserAction_descriptor = getDescriptor().getMessageTypes().get(3); - internal_static_messages_UserAction_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_UserAction_descriptor, - new java.lang.String[] { "Name", "ActionArguments", } - ); - internal_static_messages_UserAction_ActionArgumentsEntry_descriptor = internal_static_messages_UserAction_descriptor - .getNestedTypes() - .get(0); - internal_static_messages_UserAction_ActionArgumentsEntry_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_UserAction_ActionArgumentsEntry_descriptor, - new java.lang.String[] { "Key", "Value", } - ); - internal_static_messages_TrackerAdded_descriptor = getDescriptor().getMessageTypes().get(4); - internal_static_messages_TrackerAdded_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_TrackerAdded_descriptor, - new java.lang.String[] { "TrackerId", "TrackerSerial", "TrackerName", "TrackerRole", - "Manufacturer", } - ); - internal_static_messages_TrackerStatus_descriptor = getDescriptor() - .getMessageTypes() - .get(5); - internal_static_messages_TrackerStatus_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_TrackerStatus_descriptor, - new java.lang.String[] { "TrackerId", "Status", "Extra", "Confidence", } - ); - internal_static_messages_TrackerStatus_ExtraEntry_descriptor = internal_static_messages_TrackerStatus_descriptor - .getNestedTypes() - .get(0); - internal_static_messages_TrackerStatus_ExtraEntry_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_TrackerStatus_ExtraEntry_descriptor, - new java.lang.String[] { "Key", "Value", } - ); - internal_static_messages_Battery_descriptor = getDescriptor().getMessageTypes().get(6); - internal_static_messages_Battery_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_Battery_descriptor, - new java.lang.String[] { "TrackerId", "BatteryLevel", "IsCharging", } - ); - internal_static_messages_ProtobufMessage_descriptor = getDescriptor() - .getMessageTypes() - .get(7); - internal_static_messages_ProtobufMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_messages_ProtobufMessage_descriptor, - new java.lang.String[] { "Position", "UserAction", "TrackerAdded", "TrackerStatus", - "Battery", "Version", "Message", } - ); - descriptor.resolveAllFeaturesImmutable(); - } - - // @@protoc_insertion_point(outer_class_scope) -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt deleted file mode 100644 index 581a873849..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt +++ /dev/null @@ -1,425 +0,0 @@ -package dev.slimevr.desktop.platform - -import dev.slimevr.VRServer -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.VRServer.Companion.instance -import dev.slimevr.bridge.BridgeThread -import dev.slimevr.config.BridgeConfig -import dev.slimevr.desktop.platform.ProtobufMessages.* -import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByTrackerRole -import dev.slimevr.tracking.trackers.TrackerRole -import dev.slimevr.tracking.trackers.TrackerRole.Companion.getById -import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton -import dev.slimevr.util.ann.VRServerThread -import io.eiren.util.collections.FastList - -abstract class SteamVRBridge( - protected val server: VRServer, - threadName: String, - bridgeName: String, - val bridgeSettingsKey: String, - protected val shareableTrackers: List, -) : ProtobufBridge(bridgeName), - Runnable { - protected val runnerThread: Thread = Thread(this, threadName) - protected val config: BridgeConfig = server.configManager.vrConfig.getBridge(bridgeSettingsKey) - var connected: Boolean = false - - @VRServerThread - override fun startBridge() { - for (tr in shareableTrackers) { - val role = tr.trackerPosition?.trackerRole - changeShareSettings( - role, - config.getBridgeTrackerRole(role, false), - ) - } - runnerThread.start() - } - - @VRServerThread - override fun getShareSetting(role: TrackerRole): Boolean { - for (tr in shareableTrackers) { - if (tr.trackerPosition?.trackerRole == role) { - return sharedTrackers.contains(tr) - } - } - return false - } - - override fun updateShareSettingsAutomatically(): Boolean { - // Return false if automatic trackers is disabled or if tracking is paused - if (!config.automaticSharedTrackersToggling || server.getPauseTracking()) return false - - val skeleton = instance.humanPoseManager.skeleton - val isWaistSteamVr = skeleton.hipTracker?.device?.isOpenVrDevice == true || - skeleton.waistTracker?.device?.isOpenVrDevice == true - // Enable waist if skeleton has an spine tracker - changeShareSettings(TrackerRole.WAIST, skeleton.hasSpineTracker && !isWaistSteamVr) - - // hasChest if waist and/or hip is on, and chest and/or upper chest is also on - val hasChest = skeleton.upperChestTracker != null || skeleton.chestTracker != null - val isChestSteamVr = skeleton.upperChestTracker?.device?.isOpenVrDevice == true || - skeleton.chestTracker?.device?.isOpenVrDevice == true - changeShareSettings( - TrackerRole.CHEST, - hasChest && !isChestSteamVr, - ) - - // hasFeet if lower and/or upper leg tracker is on - val hasLeftFoot = - (skeleton.leftUpperLegTracker != null || skeleton.leftLowerLegTracker != null) - val isLeftFootSteamVr = - skeleton.leftLowerLegTracker?.device?.isOpenVrDevice == true || - skeleton.leftFootTracker?.device?.isOpenVrDevice == true - - val hasRightFoot = - (skeleton.rightUpperLegTracker != null || skeleton.rightLowerLegTracker != null) - val isRightFootSteamVr = - skeleton.rightLowerLegTracker?.device?.isOpenVrDevice == true || - skeleton.rightFootTracker?.device?.isOpenVrDevice == true - changeShareSettings( - TrackerRole.LEFT_FOOT, - hasLeftFoot && !isLeftFootSteamVr, - ) - changeShareSettings( - TrackerRole.RIGHT_FOOT, - hasRightFoot && !isRightFootSteamVr, - ) - - // hasKnees is just hasFeet - val isLeftKneeSteamVr = skeleton.leftUpperLegTracker?.device?.isOpenVrDevice == true - - val isRightKneeSteamVr = skeleton.rightUpperLegTracker?.device?.isOpenVrDevice == true - changeShareSettings(TrackerRole.LEFT_KNEE, hasLeftFoot && !isLeftKneeSteamVr) - changeShareSettings(TrackerRole.RIGHT_KNEE, hasRightFoot && !isRightKneeSteamVr) - - // hasElbows if an upper arm or a lower arm tracker is on - val hasLeftElbow = skeleton.hasLeftArmTracker - val isLeftElbowSteamVr = skeleton.leftUpperArmTracker?.device?.isOpenVrDevice == true || - skeleton.leftLowerArmTracker?.device?.isOpenVrDevice == true - - val hasRightElbow = skeleton.hasRightArmTracker - val isRightElbowSteamVr = skeleton.rightUpperArmTracker?.device?.isOpenVrDevice == true || - skeleton.rightLowerArmTracker?.device?.isOpenVrDevice == true - changeShareSettings(TrackerRole.LEFT_ELBOW, hasLeftElbow && !isLeftElbowSteamVr) - changeShareSettings(TrackerRole.RIGHT_ELBOW, hasRightElbow && !isRightElbowSteamVr) - - // Hands aren't touched as they will override the controller's tracking - // Return true to say that trackers were successfully toggled automatically - return true - } - - override fun getAutomaticSharedTrackers(): Boolean = config.automaticSharedTrackersToggling - - override fun setAutomaticSharedTrackers(value: Boolean) { - if (value == config.automaticSharedTrackersToggling) return - - config.automaticSharedTrackersToggling = value - if (value) { - updateShareSettingsAutomatically() - RPCSettingsHandler.sendSteamVRUpdatedSettings(instance.protocolAPI, instance.protocolAPI.rpcHandler) - } - instance.configManager.saveConfig() - } - - @VRServerThread - override fun changeShareSettings(role: TrackerRole?, share: Boolean) { - if (role == null) return - for (tr in shareableTrackers) { - if (tr.trackerPosition?.trackerRole == role) { - if (share) { - addSharedTracker(tr) - } else { - removeSharedTracker(tr) - } - config.setBridgeTrackerRole(role, share) - instance.configManager.saveConfig() - } - } - } - - @VRServerThread - override fun createNewTracker(trackerAdded: TrackerAdded): Tracker { - val device = instance.deviceManager - .createDevice( - trackerAdded.trackerName, - null, - trackerAdded.manufacturer.ifEmpty { "OpenVR" }, - ) - - // Display name, needsReset and isHmd - val displayName: String = trackerAdded.trackerName - val isHmd = trackerAdded.trackerId == 0 - - // trackerPosition - val role = getById(trackerAdded.trackerRole) - val trackerPosition = if (role != null) { - getByTrackerRole(role) - } else { - null - } - - // Make the tracker - val tracker = Tracker( - device, - getNextLocalTrackerId(), - trackerAdded.trackerSerial, - displayName, - trackerPosition, - trackerAdded.trackerId, - hasPosition = true, - hasRotation = true, - userEditable = true, - isComputed = true, - allowReset = true, - isHmd = isHmd, - ) - - device.trackers[0] = tracker - instance.deviceManager.addDevice(device) - return tracker - } - - // Battery Status - @VRServerThread - override fun writeBatteryUpdate(localTracker: Tracker) { - var lowestLevel = 200f // Arbitrarily higher than expected battery - // percentage - var trackerLevel = 0f // Tracker battery percentage on a scale from 0 - // to 100. SteamVR expects a value from 0 to 1. - var trackerVoltage = 0f // Tracker voltage. This is used to determine - // if the tracker is being charged. owoTrack - // devices do not have a tracker voltage. - var isCharging = false - - val allTrackers = instance.allTrackers - val role = localTracker.trackerPosition?.trackerRole ?: return - - val batteryTrackers: MutableList = FastList() - - // batteryTrackers is filled with trackers that would give battery data - // for the SteamVR tracker according to its role. Warning: trackers - // inside batteryTrackers could be null, so there must be a null check - // when accessing its data. - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - getByTrackerRole(role) ?: return, - ), - ) - when (role) { - TrackerRole.WAIST -> { - // Add waist because the first tracker is hip - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.WAIST, - ), - ) - // When the chest SteamVR tracking point is disabled, aggregate - // its battery level alongside waist and hip. - if (!(config.getBridgeTrackerRole(TrackerRole.CHEST, true))) { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.CHEST, - ), - ) - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.UPPER_CHEST, - ), - ) - } - } - - TrackerRole.CHEST -> { - // Add chest because the first tracker is upperChest - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.CHEST, - ), - ) - // When the waist SteamVR tracking point is disabled, aggregate - // waist and hip battery level with the chest. - if (!(config.getBridgeTrackerRole(TrackerRole.WAIST, true))) { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.WAIST, - ), - ) - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.HIP, - ), - ) - } - } - - TrackerRole.LEFT_FOOT -> { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.LEFT_LOWER_LEG, - ), - ) - // When the left knee SteamVR tracking point is disabled, - // aggregate its battery level with left ankle and left foot. - if (!(config.getBridgeTrackerRole(TrackerRole.LEFT_KNEE, true))) { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.LEFT_UPPER_LEG, - ), - ) - } - } - - TrackerRole.RIGHT_FOOT -> { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.RIGHT_LOWER_LEG, - ), - ) - // When the right knee SteamVR tracking point is disabled, - // aggregate its battery level with right ankle and right foot. - if (!(config.getBridgeTrackerRole(TrackerRole.RIGHT_KNEE, true))) { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.RIGHT_UPPER_LEG, - ), - ) - } - } - - TrackerRole.LEFT_ELBOW -> { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.LEFT_LOWER_ARM, - ), - ) - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.LEFT_SHOULDER, - ), - ) - } - - TrackerRole.RIGHT_ELBOW -> { - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.RIGHT_LOWER_ARM, - ), - ) - batteryTrackers - .add( - getTrackerForSkeleton( - allTrackers, - TrackerPosition.RIGHT_SHOULDER, - ), - ) - } - - else -> {} - } - // If the battery level of the tracker is lower than lowestLevel, then - // the battery level of the tracker position becomes lowestLevel. - // Tracker voltage is set if the tracker position has a battery level - // lower than the lowest level and has a battery voltage (owoTrack - // devices do not). - for (batteryTracker in batteryTrackers) { - if (batteryTracker?.batteryLevel?.let { it < lowestLevel } == true) { - lowestLevel = batteryTracker.batteryLevel!! - - trackerVoltage = if (batteryTracker.batteryVoltage != null) { - batteryTracker.batteryVoltage!! - } else { - 0f - } - } - } - - // Internal battery reporting 5V max, and <= 3.2V when >=50mV - // lower than initial reading (e.g. 3.25V down from 3.3V), and ~0V when - // battery is fine. - // 3.2V is technically 0%, but the last 5% of battery level is ignored, - // which makes 3.36V 0% in practice. Refer to batterymonitor.cpp in - // SlimeVR-Tracker-ESP for exact details. - // External battery reporting anything > 0V. - // The following should catch internal battery reporting and erroneous - // readings. - if ((lowestLevel >= 200 || lowestLevel < 0) || - (trackerVoltage < 3.2 && lowestLevel <= 0) || - (trackerVoltage >= 5 && lowestLevel > 150) - ) { - return - } else { - trackerLevel = lowestLevel / 100 - if (trackerVoltage >= 4.3) { - // TO DO: Add sending whether the tracker is charging from the - // tracker itself rather than checking voltage. - isCharging = true - } - } - - val builder = Battery.newBuilder().setTrackerId(localTracker.trackerNum) - - builder.setBatteryLevel(trackerLevel) - builder.setIsCharging(isCharging) - - sendMessage(ProtobufMessage.newBuilder().setBattery(builder).build()) - } - - @VRServerThread - override fun batteryReceived(batteryMessage: Battery) { - val tracker = getInternalRemoteTrackerById(batteryMessage.trackerId) ?: return - - tracker.batteryLevel = batteryMessage.batteryLevel - - // Purely for cosmetic purposes, SteamVR does not report device - // voltage. - if (batteryMessage.isCharging) { - tracker.batteryVoltage = 4.3f - // TO DO: Add "tracker.setIsCharging()" - } else { - tracker.batteryVoltage = 3.7f - } - } - - @BridgeThread - protected fun reportDisconnected() { - connected = false - } - - @BridgeThread - protected fun reportConnected() { - connected = true - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java deleted file mode 100644 index f36af175cb..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.channels.SocketChannel; - - -public class SocketUtils { - - static boolean isSocketInUse(String socketPath) { - try (SocketChannel testChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) { - testChannel.connect(UnixDomainSocketAddress.of(socketPath)); - return true; - } catch (IOException e) { - return false; - } - } - -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java deleted file mode 100644 index 1c29cedd29..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java +++ /dev/null @@ -1,263 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import com.google.protobuf.InvalidProtocolBufferException; -import dev.slimevr.VRServer; -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.desktop.platform.ProtobufMessages; -import dev.slimevr.desktop.platform.SteamVRBridge; -import dev.slimevr.tracking.trackers.Tracker; -import io.eiren.util.ann.ThreadSafe; -import io.eiren.util.logging.LogManager; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.nio.channels.Selector; -import java.nio.channels.SelectionKey; -import java.util.List; - - -public class UnixSocketBridge extends SteamVRBridge implements AutoCloseable { - public final String socketPath; - public final UnixDomainSocketAddress socketAddress; - private final String bridgeSettingsKey; - private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - private final ByteBuffer src = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - - private ServerSocketChannel server; - private SocketChannel channel; - private Selector selector; - private boolean socketError = false; - - public UnixSocketBridge( - VRServer server, - String bridgeSettingsKey, - String bridgeName, - String socketPath, - List shareableTrackers - ) { - super(server, "Named socket thread", bridgeName, bridgeSettingsKey, shareableTrackers); - this.bridgeSettingsKey = bridgeSettingsKey; - this.socketPath = socketPath; - this.socketAddress = UnixDomainSocketAddress.of(socketPath); - - File socketFile = new File(socketPath); - if (socketFile.exists()) { - if (SocketUtils.isSocketInUse(socketPath)) { - throw new RuntimeException( - socketPath + " socket is already in use by another process." - ); - } else { - LogManager.warning("[" + bridgeName + "] Cleaning up stale socket: " + socketPath); - if (!socketFile.delete()) { - throw new RuntimeException("Failed to delete stale socket: " + socketPath); - } - } - } - socketFile.deleteOnExit(); - } - - @Override - @BridgeThread - public void run() { - try { - this.server = createSocket(); - while (true) { - if (this.channel == null) { - reportDisconnected(); - this.selector = Selector.open(); - this.channel = server.accept(); - this.channel.configureBlocking(false); - this.channel.register(this.selector, SelectionKey.OP_READ); - if (this.channel == null) - continue; - VRServer.Companion.getInstance().queueTask(this::reconnected); - LogManager - .info( - "[" - + bridgeName - + "]" - + " Connected to " - + this.channel.getRemoteAddress().toString() - ); - } else { - if (this.socketError || !this.channel.isConnected()) { - this.resetChannel(); - continue; - } - try { - boolean updated = this.updateSocket(); - updateMessageQueue(); - if (updated) { - reportConnected(); - } else { - this.waitForData(10); - } - } catch (IOException ioError) { - this.resetChannel(); - ioError.printStackTrace(); - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Override - @ThreadSafe - protected void signalSend() { - Selector selector = this.selector; - if (selector == null) { - return; - } - selector.wakeup(); - } - - @BridgeThread - private void waitForData(long timeoutMs) throws IOException { - this.selector.select(timeoutMs); - } - - @Override - @BridgeThread - protected boolean sendMessageReal(ProtobufMessages.ProtobufMessage message) { - if (this.channel != null) { - try { - int size = message.getSerializedSize() + 4; - this.src.putInt(size); - byte[] serialized = message.toByteArray(); - this.src.put(serialized); - this.src.flip(); - - while (this.src.hasRemaining()) { - channel.write(this.src); - } - - this.src.clear(); - return true; - } catch (IOException e) { - e.printStackTrace(); - } - } - return false; - } - - private boolean updateSocket() throws IOException { - int read = channel.read(dst); - if (read == -1) { - LogManager - .info( - "[" - + bridgeName - + "] Reached end-of-stream on connection of " - + this.channel.getRemoteAddress().toString() - ); - socketError = true; - return false; - } else if (read == 0) { - return false; - } - - boolean readAnything = false; - // if buffer has 4 bytes at least, we got the message size! - // processs all messages - while (dst.position() >= 4) { - int messageLength = dst.getInt(0); - if (messageLength > 1024) { // Overflow - LogManager - .severe( - "[" - + bridgeName - + "] Buffer overflow on socket. Message length: " - + messageLength - ); - socketError = true; - break; - } else if (dst.position() >= messageLength) { - // Parse the message (this reads the array directly from the - // dst, so we need to move position ourselves) - try { - var message = parseMessage(dst.array(), 4, messageLength - 4); - this.messageReceived(message); - } catch (InvalidProtocolBufferException e) { - LogManager.severe("Failed to read protocol message", e); - } - int originalpos = dst.position(); - dst.position(messageLength); - dst.compact(); - // move position after compacting - dst.position(originalpos - messageLength); - readAnything = true; - } else { - break; - } - } - return readAnything; - } - - private static ProtobufMessages.ProtobufMessage parseMessage( - byte[] data, - int offset, - int length - ) throws InvalidProtocolBufferException { - return ProtobufMessages.ProtobufMessage - .parser() - .parseFrom(data, offset, length); - } - - private void resetChannel() throws IOException { - LogManager - .info( - "[" - + bridgeName - + "] Disconnected from " - + this.channel.getRemoteAddress().toString() - ); - this.selector.close(); - this.selector = null; - this.channel.close(); - this.channel = null; - this.socketError = false; - this.dst.clear(); - VRServer.Companion.getInstance().queueTask(this::disconnected); - } - - private ServerSocketChannel createSocket() throws IOException { - ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX); - server.bind(this.socketAddress); - LogManager.info("[" + bridgeName + "] Socket " + this.socketPath + " created"); - return server; - } - - @Override - public void close() throws Exception { - if (this.server != null) { - this.server.close(); - } - } - - @Override - public boolean isConnected() { - return channel != null && channel.isConnected(); - } - - @NotNull - @Override - public String getBridgeConfigKey() { - return bridgeSettingsKey; - } -} - diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java deleted file mode 100644 index 45ccb81b0b..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import dev.slimevr.protocol.ConnectionContext; -import dev.slimevr.protocol.GenericConnection; -import io.eiren.util.logging.LogManager; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.SocketChannel; -import java.util.UUID; - - -public class UnixSocketConnection implements GenericConnection { - public final UUID id; - public final ConnectionContext context; - private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - private final SocketChannel channel; - private int remainingBytes; - - public UnixSocketConnection(SocketChannel channel) { - this.id = UUID.randomUUID(); - this.context = new ConnectionContext(); - this.channel = channel; - } - - @Override - public UUID getConnectionId() { - return id; - } - - @Override - public ConnectionContext getContext() { - return this.context; - } - - public boolean isConnected() { - return this.channel.isConnected(); - } - - private void resetChannel() { - try { - this.channel.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void send(ByteBuffer bytes) { - if (!this.channel.isConnected()) - return; - try { - ByteBuffer[] src = new ByteBuffer[] { - ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN), - bytes.slice(), - }; - src[0].putInt(src[1].remaining() + 4); - src[0].flip(); - synchronized (this) { - while (src[1].hasRemaining()) { - this.channel.write(src); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - public ByteBuffer read() { - if (dst.position() < 4 || dst.position() < dst.getInt(0)) { - if (!this.channel.isConnected()) - return null; - try { - int result = this.channel.read(dst); - if (result == -1) { - LogManager.info("[SolarXR Bridge] Reached end-of-stream on connection"); - this.resetChannel(); - return null; - } - if (dst.position() < 4) { - return null; - } - } catch (IOException e) { - e.printStackTrace(); - this.resetChannel(); - return null; - } - } - int messageLength = dst.getInt(0); - if (messageLength > 1024) { - LogManager - .severe( - "[SolarXR Bridge] Buffer overflow on socket. Message length: " + messageLength - ); - this.resetChannel(); - return null; - } - if (dst.position() < messageLength) { - return null; - } - remainingBytes = dst.position() - messageLength; - dst.position(4); - dst.limit(messageLength); - return dst; - } - - public void next() { - dst.position(dst.limit()); - dst.limit(dst.limit() + remainingBytes); - dst.compact(); - dst.limit(dst.capacity()); - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java deleted file mode 100644 index 42dab98fbb..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java +++ /dev/null @@ -1,151 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.protocol.GenericConnection; -import dev.slimevr.protocol.ProtocolAPI; -import dev.slimevr.tracking.trackers.Tracker; -import dev.slimevr.util.ann.VRServerThread; -import dev.slimevr.VRServer; -import io.eiren.util.logging.LogManager; - -import java.io.File; -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.List; - - -public class UnixSocketRpcBridge implements dev.slimevr.bridge.Bridge, - dev.slimevr.protocol.ProtocolAPIServer, Runnable, AutoCloseable { - private final Thread runnerThread = new Thread(this, "Named socket thread"); - private final String socketPath; - private final ProtocolAPI protocolAPI; - private final ServerSocketChannel socket; - private final Selector selector; - - public UnixSocketRpcBridge( - VRServer server, - String socketPath, - List shareableTrackers - ) { - this.socketPath = socketPath; - this.protocolAPI = server.protocolAPI; - - File socketFile = new File(socketPath); - if (socketFile.exists()) { - if (SocketUtils.isSocketInUse(socketPath)) { - throw new RuntimeException( - socketPath + " socket is already in use by another process." - ); - } else { - LogManager.warning("[SolarXR Bridge] Cleaning up stale socket: " + socketPath); - if (!socketFile.delete()) { - throw new RuntimeException("Failed to delete stale socket: " + socketPath); - } - } - } - socketFile.deleteOnExit(); - - try { - socket = ServerSocketChannel.open(StandardProtocolFamily.UNIX); - selector = Selector.open(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Socket open failed."); - } - - server.protocolAPI.registerAPIServer(this); - } - - @VRServerThread - private void disconnected() { - } - - @Override - @VRServerThread - public void dataRead() { - } - - @Override - @VRServerThread - public void dataWrite() { - } - - @Override - @VRServerThread - public void addSharedTracker(Tracker tracker) { - } - - @Override - @VRServerThread - public void removeSharedTracker(Tracker tracker) { - } - - @Override - @VRServerThread - public void startBridge() { - this.runnerThread.start(); - } - - @Override - @BridgeThread - public void run() { - try { - this.socket.bind(UnixDomainSocketAddress.of(this.socketPath)); - this.socket.configureBlocking(false); - this.socket.register(this.selector, SelectionKey.OP_ACCEPT); - LogManager.info("[SolarXR Bridge] Socket " + this.socketPath + " created"); - while (this.socket.isOpen()) { - this.selector.select(0); - for (SelectionKey key : this.selector.selectedKeys()) { - UnixSocketConnection conn = (UnixSocketConnection) key.attachment(); - if (conn != null) { - for (ByteBuffer message; (message = conn.read()) != null; conn.next()) - this.protocolAPI.onMessage(conn, message); - } else - for (SocketChannel channel; (channel = socket.accept()) != null;) { - channel.configureBlocking(false); - channel - .register( - this.selector, - SelectionKey.OP_READ, - new UnixSocketConnection(channel) - ); - LogManager - .info( - "[SolarXR Bridge] Connected to " - + channel.getRemoteAddress().toString() - ); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void close() throws Exception { - this.socket.close(); - this.selector.close(); - } - - @Override - public boolean isConnected() { - return this.selector.keys().stream().anyMatch(key -> key.attachment() != null); - } - - @Override - public java.util.stream.Stream getApiConnections() { - return this.selector - .keys() - .stream() - .map(key -> (GenericConnection) key.attachment()) - .filter(conn -> conn != null); - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java deleted file mode 100644 index 81173b42d7..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -public enum PipeState { - CREATED, - OPEN, - ERROR -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java deleted file mode 100644 index 5099fccd8d..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java +++ /dev/null @@ -1,333 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -import com.google.protobuf.CodedOutputStream; -import com.google.protobuf.InvalidProtocolBufferException; -import com.sun.jna.Native; -import com.sun.jna.platform.win32.*; -import com.sun.jna.platform.win32.Kernel32; -import com.sun.jna.ptr.IntByReference; -import com.sun.jna.win32.W32APIOptions; -import dev.slimevr.VRServer; -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage; -import dev.slimevr.desktop.platform.SteamVRBridge; -import dev.slimevr.tracking.trackers.Tracker; -import io.eiren.util.ann.ThreadSafe; -import io.eiren.util.logging.LogManager; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.util.List; - - -interface Kernel32IO extends Kernel32 { - Kernel32IO INSTANCE = Native.load("kernel32", Kernel32IO.class, W32APIOptions.DEFAULT_OPTIONS); - - boolean GetOverlappedResult( - /* [in] */ HANDLE hFile, - /* [in] */ WinBase.OVERLAPPED lpOverlapped, - /* [out] */ IntByReference lpNumberOfBytesTransferred, - /* [in] */ boolean bWait - ); -} - - -public class WindowsNamedPipeBridge extends SteamVRBridge { - private static final Kernel32 k32 = Kernel32.INSTANCE; - private static final Kernel32IO k32io = Kernel32IO.INSTANCE; - private static final Advapi32 adv32 = Advapi32.INSTANCE; - - protected final String pipeName; - protected final String bridgeSettingsKey; - private final byte[] buffArray = new byte[2048]; - protected WindowsPipe pipe; - protected WinNT.HANDLE openEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE readEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE writeEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE rxEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE txEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE[] events = new WinNT.HANDLE[] { rxEvent, txEvent }; - private final WinBase.OVERLAPPED overlappedOpen = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedWrite = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedRead = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedWait = new WinBase.OVERLAPPED(); - private final IntByReference bytesWritten = new IntByReference(0); - private final IntByReference bytesAvailable = new IntByReference(0); - private final IntByReference bytesRead = new IntByReference(0); - private boolean pendingWait = false; - - public WindowsNamedPipeBridge( - VRServer server, - String bridgeSettingsKey, - String bridgeName, - String pipeName, - List shareableTrackers - ) { - super(server, "Named pipe thread", bridgeName, bridgeSettingsKey, shareableTrackers); - this.pipeName = pipeName; - this.bridgeSettingsKey = bridgeSettingsKey; - overlappedWait.hEvent = rxEvent; - } - - @Override - @BridgeThread - public void run() { - try { - createPipe(); - while (true) { - boolean pipesUpdated = false; - if (pipe.state == PipeState.CREATED) { - // Report that our pipe is disconnected right now - reportDisconnected(); - tryOpeningPipe(pipe); - } - if (pipe.state == PipeState.OPEN) { - pipesUpdated = updatePipe(); - if (pipesUpdated) { - reportConnected(); - } - updateMessageQueue(); - } - if (pipe.state == PipeState.ERROR) { - resetPipe(); - } - if (!pipesUpdated) { - if (pipe.state == PipeState.OPEN) { - waitForData(10); - } else { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Override - @ThreadSafe - protected void signalSend() { - k32.SetEvent(txEvent); - } - - @BridgeThread - private void waitForData(int timeoutMs) { - if (pipe.state != PipeState.OPEN) - return; - if (!pendingWait) { - k32.ReadFile(pipe.pipeHandle, null, 0, null, overlappedWait); - pendingWait = true; - } - int evIdx = k32.WaitForMultipleObjects(events.length, events, false, timeoutMs); - if (evIdx == 0) { - // events[0] == overlappedWait.hEvent == rxEvent - pendingWait = false; - } - } - - @Override - @BridgeThread - protected boolean sendMessageReal(ProtobufMessage message) { - if (pipe.state != PipeState.OPEN) { - return false; - } - try { - int size = message.getSerializedSize(); - CodedOutputStream os = CodedOutputStream.newInstance(buffArray, 4, size); - message.writeTo(os); - size += 4; - buffArray[0] = (byte) (size & 0xFF); - buffArray[1] = (byte) ((size >> 8) & 0xFF); - buffArray[2] = (byte) ((size >> 16) & 0xFF); - buffArray[3] = (byte) ((size >> 24) & 0xFF); - - overlappedWrite.clear(); - overlappedWrite.hEvent = writeEvent; - boolean immediate = k32 - .WriteFile(pipe.pipeHandle, buffArray, size, null, overlappedWrite); - int err = k32.GetLastError(); - if (!immediate && err != WinError.ERROR_IO_PENDING) { - setPipeError("WriteFile failed: " + err); - return false; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedWrite, bytesWritten, true)) { - setPipeError( - "sendMessageReal/GetOverlappedResult failed: " + k32.GetLastError() - ); - return false; - } - - if (bytesWritten.getValue() != size) { - setPipeError("Bytes written " + bytesWritten.getValue() + ", expected " + size); - return false; - } - - return true; - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - private boolean updatePipe() throws IOException { - if (pipe.state != PipeState.OPEN) { - return false; - } - boolean readAnything = false; - while (k32.PeekNamedPipe(pipe.pipeHandle, buffArray, 4, null, bytesAvailable, null)) { - if (bytesAvailable.getValue() < 4) { - return readAnything; // Wait for more data - } - int messageLength = (Byte.toUnsignedInt(buffArray[3]) << 24) - | (Byte.toUnsignedInt(buffArray[2]) << 16) - | (Byte.toUnsignedInt(buffArray[1]) << 8) - | Byte.toUnsignedInt(buffArray[0]); - if (messageLength > 1024) { // Overflow - setPipeError("Pipe overflow. Message length: " + messageLength); - return readAnything; - } - if (bytesAvailable.getValue() < messageLength) { - return readAnything; // Wait for more data - } - - overlappedRead.clear(); - overlappedRead.hEvent = readEvent; - boolean immediate = k32 - .ReadFile(pipe.pipeHandle, buffArray, messageLength, null, overlappedRead); - int err = k32.GetLastError(); - if (!immediate && err != WinError.ERROR_IO_PENDING) { - setPipeError("ReadFile failed: " + err); - return readAnything; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedRead, bytesRead, true)) { - setPipeError( - "updatePipe/GetOverlappedResult failed: " + k32.GetLastError() - ); - return readAnything; - } - - if (bytesRead.getValue() != messageLength) { - setPipeError( - "Bytes read " + bytesRead.getValue() + ", expected " + messageLength - ); - return readAnything; - } - - try { - ProtobufMessage message = ProtobufMessage - .parser() - .parseFrom(buffArray, 4, messageLength - 4); - messageReceived(message); - readAnything = true; - } catch (InvalidProtocolBufferException parseEx) { - parseEx.printStackTrace(); - setPipeError("Failed to parse message: " + parseEx.getMessage()); - return readAnything; - } - } - - int err = k32.GetLastError(); - if (err == WinError.ERROR_BROKEN_PIPE) { - setPipeError("Pipe closed"); - } else { - setPipeError("Pipe error: " + err); - } - return readAnything; - } - - private void setPipeError(String message) { - pipe.state = PipeState.ERROR; - LogManager.severe("[" + bridgeName + "] " + message); - } - - private void resetPipe() { - WindowsPipe.safeDisconnect(pipe); - pipe.state = PipeState.CREATED; - VRServer.Companion.getInstance().queueTask(this::disconnected); - } - - private void createPipe() throws IOException { - try { - WinNT.SECURITY_DESCRIPTOR descriptor = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); - adv32.InitializeSecurityDescriptor(descriptor, WinNT.SECURITY_DESCRIPTOR_REVISION); - adv32.SetSecurityDescriptorDacl(descriptor, true, null, false); - adv32 - .SetSecurityDescriptorControl( - descriptor, - (short) WinNT.SE_DACL_PROTECTED, - (short) WinNT.SE_DACL_PROTECTED - ); - - WinBase.SECURITY_ATTRIBUTES attributes = new WinBase.SECURITY_ATTRIBUTES(); - attributes.lpSecurityDescriptor = descriptor.getPointer(); - attributes.bInheritHandle = false; - - pipe = new WindowsPipe( - k32 - .CreateNamedPipe( - pipeName, - WinBase.PIPE_ACCESS_DUPLEX | WinNT.FILE_FLAG_OVERLAPPED, // dwOpenMode - WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, // dwPipeMode - 1, // nMaxInstances, - 1024 * 16, // nOutBufferSize, - 1024 * 16, // nInBufferSize, - 0, // nDefaultTimeOut, - attributes // lpSecurityAttributes - ), - pipeName - ); - LogManager.info("[" + bridgeName + "] Pipe " + pipe.name + " created"); - if (WinBase.INVALID_HANDLE_VALUE.equals(pipe.pipeHandle)) { - throw new IOException("Can't open " + pipeName + " pipe: " + k32.GetLastError()); - } - LogManager.info("[" + bridgeName + "] Pipes are created"); - } catch (IOException e) { - WindowsPipe.safeDisconnect(pipe); - throw e; - } - } - - private boolean tryOpeningPipe(WindowsPipe pipe) { - overlappedOpen.clear(); - overlappedOpen.hEvent = openEvent; - - boolean ok = k32.ConnectNamedPipe(pipe.pipeHandle, overlappedOpen); - int err = k32.GetLastError(); - if (!ok && err != WinError.ERROR_PIPE_CONNECTED) { - if (err != WinError.ERROR_IO_PENDING) { - setPipeError("ConnectNamedPipe failed: " + err); - return false; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedOpen, bytesRead, true)) { - setPipeError( - "tryOpeningPipe/GetOverlappedResult failed: " + k32.GetLastError() - ); - return false; - } - } - - pipe.state = PipeState.OPEN; - LogManager.info("[" + bridgeName + "] Pipe " + pipe.name + " is open"); - VRServer.Companion.getInstance().queueTask(this::reconnected); - return true; - } - - @Override - public boolean isConnected() { - return pipe != null && pipe.state == PipeState.OPEN; - } - - @NotNull - @Override - public String getBridgeConfigKey() { - return this.bridgeSettingsKey; - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java deleted file mode 100644 index 6aabd03adc..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -import com.sun.jna.platform.win32.Kernel32; -import com.sun.jna.platform.win32.WinNT.HANDLE; - - -public class WindowsPipe { - - public final String name; - public final HANDLE pipeHandle; - public PipeState state = PipeState.CREATED; - - public WindowsPipe(HANDLE pipeHandle, String name) { - this.pipeHandle = pipeHandle; - this.name = name; - } - - public static void safeDisconnect(WindowsPipe pipe) { - try { - if (pipe != null && pipe.pipeHandle != null) - Kernel32.INSTANCE.DisconnectNamedPipe(pipe.pipeHandle); - } catch (Exception ignored) {} - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt b/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt deleted file mode 100644 index 1b911837be..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt +++ /dev/null @@ -1,268 +0,0 @@ -package dev.slimevr.desktop.serial - -import com.fazecast.jSerialComm.SerialPort -import com.fazecast.jSerialComm.SerialPortEvent -import com.fazecast.jSerialComm.SerialPortMessageListener -import dev.slimevr.serial.SerialHandler -import dev.slimevr.serial.SerialListener -import io.eiren.util.logging.LogManager -import java.io.IOException -import java.io.OutputStreamWriter -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets -import java.util.* -import java.util.concurrent.CopyOnWriteArrayList -import java.util.stream.Stream -import kotlin.concurrent.timerTask -import kotlin.streams.asSequence -import kotlin.streams.asStream -import dev.slimevr.serial.SerialPort as SlimeSerialPort - -class SerialPortWrapper(val port: SerialPort) : SlimeSerialPort() { - override val portLocation: String - get() = port.portLocation - override val descriptivePortName: String - get() = port.descriptivePortName - - override val vendorId: Int - get() = port.vendorID - - override val productId: Int - get() = port.productID -} - -class DesktopSerialHandler : - SerialHandler(), - SerialPortMessageListener { - private val listeners: MutableList = CopyOnWriteArrayList() - private val getDevicesTimer = Timer("GetDevicesTimer") - private var currentPort: SerialPort? = null - private var watchingNewDevices = false - private var lastKnownPorts = setOf() - - init { - startWatchingNewDevices() - } - - fun startWatchingNewDevices() { - if (watchingNewDevices) return - watchingNewDevices = true - getDevicesTimer.scheduleAtFixedRate( - timerTask { - try { - detectNewPorts() - } catch (t: Throwable) { - LogManager.severe( - "[SerialHandler] Error while watching for new devices, cancelling the \"getDevicesTimer\".", - t, - ) - getDevicesTimer.cancel() - } - }, - 0, - 3000, - ) - } - - fun stopWatchingNewDevices() { - if (!watchingNewDevices) return - watchingNewDevices = false - getDevicesTimer.cancel() - getDevicesTimer.purge() - } - - private fun onNewDevice(port: SerialPort) { - listeners.forEach { it.onNewSerialDevice(SerialPortWrapper(port)) } - } - - private fun onDeviceDel(port: SerialPort) { - listeners.forEach { it.onSerialDeviceDeleted(SerialPortWrapper(port)) } - } - - override fun addListener(channel: SerialListener) { - listeners.add(channel) - } - - override fun removeListener(channel: SerialListener) { - listeners.removeIf { channel === it } - } - - @Synchronized - override fun openSerial(portLocation: String?, auto: Boolean): Boolean { - LogManager.info("[SerialHandler] Trying to open: $portLocation, auto: $auto") - val ports = SerialPort.getCommPorts() - lastKnownPorts = ports.map { SerialPortWrapper(it) }.toSet() - val newPort: SerialPort? = ports.find { - (!auto && it.portLocation == portLocation) || - (auto && isKnownBoard(SerialPortWrapper(it))) - } - if (newPort == null) { - LogManager.info( - "[SerialHandler] No serial ports found to connect to (${ports.size}) total ports", - ) - return false - } - if (isConnected) { - if (SerialPortWrapper(newPort) != currentPort?.let { SerialPortWrapper(it) }) { - LogManager.info( - "[SerialHandler] Closing current serial port " + - currentPort!!.descriptivePortName, - ) - currentPort!!.removeDataListener() - currentPort!!.closePort() - } else { - LogManager.info("[SerialHandler] Reusing already open port") - listeners.forEach { it.onSerialConnected(SerialPortWrapper(currentPort!!)) } - return true - } - } - currentPort = newPort - LogManager.info( - "[SerialHandler] Trying to connect to new serial port " + - currentPort!!.descriptivePortName, - ) - currentPort?.setBaudRate(115200) - currentPort?.clearRTS() - currentPort?.clearDTR() - if (currentPort?.openPort(1000) == false) { - LogManager.warning( - "[SerialHandler] Can't open serial port ${currentPort?.descriptivePortName}, last error: ${currentPort?.lastErrorCode}", - - ) - currentPort = null - return false - } - currentPort?.addDataListener(this) - listeners.forEach { it.onSerialConnected(SerialPortWrapper(currentPort!!)) } - LogManager.info("[SerialHandler] Serial port ${newPort.descriptivePortName} is open") - return true - } - - override fun rebootRequest() { - writeSerial("REBOOT") - } - - override fun factoryResetRequest() { - writeSerial("FRST") - } - - override fun infoRequest() { - writeSerial("GET INFO") - } - - override fun wifiScanRequest() { - writeSerial("GET WIFISCAN") - } - - override fun customCommandRequest(command: String) { - writeSerial(command) - } - - @Synchronized - override fun closeSerial() { - try { - currentPort?.closePort() - listeners.forEach { it.onSerialDisconnected() } - LogManager.info( - "[SerialHandler] Port ${currentPort?.descriptivePortName} closed okay", - ) - currentPort = null - } catch (e: Exception) { - LogManager.warning( - "[SerialHandler] Error closing port ${currentPort?.descriptivePortName}", - e, - ) - } - } - - @Synchronized - private fun writeSerial(serialText: String) { - val os = currentPort?.outputStream ?: return - val writer = OutputStreamWriter(os) - try { - writer.append(serialText).append("\n") - writer.flush() - addLog("-> $serialText\n") - } catch (e: IOException) { - addLog("[!] Serial error: ${e.message}\n") - LogManager.warning("[SerialHandler] Serial port write error", e) - } - } - - override fun write(buff: ByteArray) { - LogManager.info("[SerialHandler] WRITING $buff") - currentPort?.outputStream?.write(buff) - } - - @Synchronized - override fun setWifi(ssid: String, passwd: String) { - val os = currentPort?.outputStream ?: return - val writer = OutputStreamWriter(os) - try { - writer.append("SET WIFI \"").append(ssid).append("\" \"").append(passwd).append("\"\n") - writer.flush() - addLog("-> SET WIFI \"$ssid\" \"${passwd.replace(".".toRegex(), "*")}\"\n") - } catch (e: IOException) { - addLog("$e\n") - LogManager.warning("[SerialHandler] Serial port write error", e) - } - } - - fun addLog(str: String, server: Boolean = true) { - LogManager.info("[Serial] $str") - listeners.forEach { it.onSerialLog(str, server) } - } - - override fun getListeningEvents(): Int = ( - SerialPort.LISTENING_EVENT_PORT_DISCONNECTED - or SerialPort.LISTENING_EVENT_DATA_RECEIVED - ) - - override fun serialEvent(event: SerialPortEvent) { - when (event.eventType) { - SerialPort.LISTENING_EVENT_DATA_RECEIVED -> { - val newData = event.receivedData - val s = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(newData)).toString() - addLog(s, false) - } - - SerialPort.LISTENING_EVENT_PORT_DISCONNECTED -> { - closeSerial() - } - } - } - - @get:Synchronized - override val isConnected: Boolean - get() = currentPort?.isOpen ?: false - - override fun getMessageDelimiter(): ByteArray = byteArrayOf(0x0A.toByte()) - - override fun delimiterIndicatesEndOfMessage(): Boolean = true - - override val knownPorts: Stream - get() = SerialPort.getCommPorts() - .asSequence() - .map { SerialPortWrapper(it) } - .filter { isKnownBoard(it) } - .asStream() - - private fun detectNewPorts() { - try { - val addDifferences = knownPorts.asSequence() - lastKnownPorts - val delDifferences = lastKnownPorts - knownPorts.asSequence().toSet() - lastKnownPorts = SerialPort.getCommPorts().map { SerialPortWrapper(it) }.toSet() - addDifferences.forEach { onNewDevice(it.port) } - delDifferences.forEach { onDeviceDel(it.port) } - } catch (e: Throwable) { - LogManager - .severe("[SerialHandler] Using serial ports is not supported on this platform", e) - throw RuntimeException("Serial unsupported") - } - } - - override fun getCurrentPort(): dev.slimevr.serial.SerialPort? { - val port = this.currentPort ?: return null - return SerialPortWrapper(port) - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/serial/flash.kt b/server/desktop/src/main/java/dev/slimevr/desktop/serial/flash.kt new file mode 100644 index 0000000000..5fd74dd2ab --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/serial/flash.kt @@ -0,0 +1,68 @@ +package dev.slimevr.desktop.serial + +import dev.slimevr.serial.FlashingHandler +import com.fazecast.jSerialComm.SerialPort as JSerialPort + +class DesktopFlashingHandler : FlashingHandler { + private var port: JSerialPort? = null + + override fun openSerial(portObj: Any) { + val portLocation = portObj as? String ?: error("expected port location string") + val comPort = JSerialPort.getCommPorts().find { it.portLocation == portLocation } + ?: error("Unable to find port $portLocation") + if (comPort.isOpen) { + comPort.closePort() + } + if (!comPort.openPort(1000)) { + error("Unable to open port $portLocation") + } + this.port = comPort + } + + override fun closeSerial() { + port?.closePort() + port = null + } + + override fun setDTR(value: Boolean) { + val p = port ?: error("no port to set DTR") + if (value) p.setDTR() else p.clearDTR() + } + + override fun setRTS(value: Boolean) { + val p = port ?: error("no port to set RTS") + if (value) p.setRTS() else p.clearRTS() + } + + override fun write(data: ByteArray) { + val p = port ?: error("no port to write") + p.writeBytes(data, data.size) + } + + override fun read(length: Int): ByteArray { + val p = port ?: error("no port to read") + val data = ByteArray(length) + p.readBytes(data, length) + return data + } + + override fun changeBaud(baud: Int) { + val p = port ?: error("no port to set baud") + if (!p.setBaudRate(baud)) error("Unable to change baudrate to $baud") + } + + override fun setReadTimeout(timeout: Long) { + val p = port ?: error("no port to set timeout") + p.setComPortTimeouts(JSerialPort.TIMEOUT_READ_BLOCKING, timeout.toInt(), 0) + } + + override fun availableBytes(): Int { + val p = port ?: error("no port to check available bytes") + return p.bytesAvailable() + } + + override fun flushIOBuffers() { + val p = port ?: error("no port to flush") + p.flushIOBuffers() + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/serial/serial.kt b/server/desktop/src/main/java/dev/slimevr/desktop/serial/serial.kt new file mode 100644 index 0000000000..2af92a6d86 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/serial/serial.kt @@ -0,0 +1,114 @@ +package dev.slimevr.desktop.serial + +import com.fazecast.jSerialComm.SerialPortEvent +import com.fazecast.jSerialComm.SerialPortMessageListener +import dev.slimevr.AppLogger +import dev.slimevr.serial.FlashingHandler +import dev.slimevr.serial.SerialPortHandle +import dev.slimevr.serial.SerialPortInfo +import dev.slimevr.serial.SerialServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.OutputStreamWriter +import com.fazecast.jSerialComm.SerialPort as JSerialPort + +private val SUPPORTED_BOARDS: Set> = setOf( + Pair(0x1A86, 0x7522), // CH340 + Pair(0x1A86, 0x7523), // CH340 + Pair(0x1A86, 0x5523), // CH341 + Pair(0x1A86, 0x55D3), // CH343 + Pair(0x1A86, 0x55D4), // CH9102x + Pair(0x10C4, 0xEA60), // CP210x + Pair(0x303A, 0x1001), // ESP32-S3 + Pair(0x303A, 0x0002), // ESP32 + Pair(0x0403, 0x6001), // FTDI FT232 +) + +private fun isKnownBoard(vid: Int, pid: Int) = SUPPORTED_BOARDS.contains(vid to pid) + +private fun openPort( + portLocation: String, + scope: CoroutineScope, + onDataReceived: suspend (String, String) -> Unit, + onPortDisconnected: suspend (String) -> Unit, +): SerialPortHandle? { + val port = JSerialPort.getCommPorts().find { it.portLocation == portLocation } ?: return null + port.baudRate = 115200 + port.clearRTS() + port.clearDTR() + if (!port.openPort(1000)) return null + + // Anonymous object is required by the jSerialComm API + port.addDataListener(object : SerialPortMessageListener { + override fun getListeningEvents() = JSerialPort.LISTENING_EVENT_DATA_RECEIVED or JSerialPort.LISTENING_EVENT_PORT_DISCONNECTED + + override fun getMessageDelimiter() = byteArrayOf(0x0A) + override fun delimiterIndicatesEndOfMessage() = true + + override fun serialEvent(event: SerialPortEvent) { + when (event.eventType) { + JSerialPort.LISTENING_EVENT_DATA_RECEIVED -> { + val line = event.receivedData.toString(Charsets.UTF_8).trimEnd() + scope.launch { onDataReceived(portLocation, line) } + } + + JSerialPort.LISTENING_EVENT_PORT_DISCONNECTED -> + scope.launch { onPortDisconnected(portLocation) } + } + } + }) + + return SerialPortHandle( + portLocation = portLocation, + descriptivePortName = port.descriptivePortName, + writeCommand = { text -> + OutputStreamWriter(port.outputStream).append(text).append("\n").flush() + }, + close = { + port.removeDataListener() + port.closePort() + }, + ) +} + +private suspend fun runSerialPoller(server: SerialServer) { + var lastKnown: Set = emptySet() + + while (true) { + val current = withContext(Dispatchers.IO) { + JSerialPort.getCommPorts() + .filter { isKnownBoard(it.vendorID, it.productID) } + .associate { port -> + port.portLocation to SerialPortInfo( + portLocation = port.portLocation, + descriptivePortName = port.descriptivePortName, + vendorId = port.vendorID, + productId = port.productID, + ) + } + } + + val added = current.keys - lastKnown + val removed = lastKnown - current.keys + + added.forEach { loc -> server.onPortDetected(current.getValue(loc)) } + removed.forEach { loc -> server.onPortLost(loc) } + + lastKnown = current.keys + delay(3000) + } +} + +fun createDesktopSerialServer(scope: CoroutineScope): SerialServer { + val server = SerialServer.create( + openPort = { portLocation, onDataReceived, onPortDisconnected -> openPort(portLocation, scope, onDataReceived, onPortDisconnected) }, + openFlashingPort = { DesktopFlashingHandler() }, + scope = scope, + ) + scope.launch { runSerialPoller(server) } + return server +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/DesktopHIDManager.kt b/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/DesktopHIDManager.kt deleted file mode 100644 index 3f89940a10..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/tracking/trackers/hid/DesktopHIDManager.kt +++ /dev/null @@ -1,289 +0,0 @@ -package dev.slimevr.desktop.tracking.trackers.hid - -import dev.slimevr.VRServer -import dev.slimevr.config.config -import dev.slimevr.tracking.trackers.Device -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerStatus -import dev.slimevr.tracking.trackers.hid.HIDCommon -import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.HID_TRACKER_PID -import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.HID_TRACKER_RECEIVER_PID -import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.HID_TRACKER_RECEIVER_VID -import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.PACKET_SIZE -import dev.slimevr.tracking.trackers.hid.HIDDevice -import io.eiren.util.logging.LogManager -import org.hid4java.HidDevice -import org.hid4java.HidException -import org.hid4java.HidManager -import org.hid4java.HidServices -import org.hid4java.HidServicesListener -import org.hid4java.HidServicesSpecification -import org.hid4java.event.HidServicesEvent -import org.hid4java.jna.HidApi -import org.hid4java.jna.HidDeviceInfoStructure -import java.nio.ByteBuffer -import java.util.function.Consumer - -/** - * Handles desktop USB HID dongles and receives tracker data from them. - */ -class DesktopHIDManager(name: String, private val trackersConsumer: Consumer) : - Thread(name), - HidServicesListener { - private val devices: MutableList = mutableListOf() - private val devicesBySerial: MutableMap> = HashMap() - private val devicesByHID: MutableMap> = HashMap() - private val lastDataByHID: MutableMap = HashMap() - private val hidServicesSpecification = HidServicesSpecification() - private var hidServices: HidServices? = null - - init { - hidServicesSpecification.setAutoStart(false) - try { - hidServices = HidManager.getHidServices(hidServicesSpecification) - hidServices?.addHidServicesListener(this) - val dataReadThread = Thread(dataReadRunnable) - dataReadThread.isDaemon = true - dataReadThread.name = "hid4java data reader" - dataReadThread.start() - // We use hid4java but actually do not start the service ever, because it will just enumerate everything and cause problems - // Do enumeration ourself - val deviceEnumerateThread = Thread(deviceEnumerateRunnable) - deviceEnumerateThread.isDaemon = true - deviceEnumerateThread.name = "hid4java device enumerator" - deviceEnumerateThread.start() - } catch (e: HidException) { - LogManager.severe("Error initializing HID services: ${e.message}", e) - } - } - - private fun checkConfigureDevice(hidDevice: HidDevice) { - if (hidDevice.vendorId == HID_TRACKER_RECEIVER_VID && (hidDevice.productId == HID_TRACKER_RECEIVER_PID || hidDevice.productId == HID_TRACKER_PID)) { // TODO: Use list of valid ids - val serial = hidDevice.serialNumber ?: "Unknown HID Device" - if (hidDevice.isClosed) { - if (!hidDevice.open()) { - LogManager.warning("[TrackerServer] Unable to open device: $serial") - return - } - } - // TODO: Configure the device here - // val product = hidDevice.product - // val manufacturer = hidDevice.manufacturer - this.devicesBySerial[serial]?.let { - this.devicesByHID[hidDevice] = it - synchronized(this.devices) { - for (id in it) { - val device = this.devices[id] - for (value in device.trackers.values) { - if (value.status == TrackerStatus.DISCONNECTED) value.status = TrackerStatus.OK - } - } - } - LogManager.info("[TrackerServer] Linked HID device reattached: $serial") - return - } - - // TODO: Need to check that the hidDevice is the same or different? - // TODO: Get firmware and manufacturer from the device? - val list: MutableList = mutableListOf() - this.devicesBySerial[serial] = list - this.devicesByHID[hidDevice] = list - this.lastDataByHID[hidDevice] = 0 // initialize last data received - LogManager.info("[TrackerServer] (Probably) Compatible HID device detected: $serial") - } - } - - private fun removeDevice(hidDevice: HidDevice) { - this.devicesByHID[hidDevice]?.let { - synchronized(this.devices) { - for (id in it) { - val device = this.devices[id] - for (value in device.trackers.values) { - if (value.status == TrackerStatus.OK) { - value.status = - TrackerStatus.DISCONNECTED - } - } - } - } - this.devicesByHID.remove(hidDevice) - LogManager.info("[TrackerServer] Linked HID device removed: ${hidDevice.serialNumber}") - } - } - - @get:Synchronized - private val dataReadRunnable: Runnable - get() = Runnable { - while (true) { - try { - sleep(0) // Possible performance impact - } catch (e: InterruptedException) { - currentThread().interrupt() - break - } - dataRead() // not in try catch? - } - } - - @get:Synchronized - private val deviceEnumerateRunnable: Runnable - get() = Runnable { - try { - sleep(100) // Delayed start - } catch (e: InterruptedException) { - currentThread().interrupt() - return@Runnable - } - while (true) { - try { - sleep(1000) - } catch (e: InterruptedException) { - currentThread().interrupt() - break - } - deviceEnumerate() // not in try catch? - } - } - - private fun dataRead() { - synchronized(devicesByHID) { - var devicesPresent = false - var devicesDataReceived = false - val q = intArrayOf(0, 0, 0, 0) - val a = intArrayOf(0, 0, 0) - val m = intArrayOf(0, 0, 0) - for ((hidDevice, deviceList) in devicesByHID) { - val dataReceived: ByteArray = try { - hidDevice.readAll(0) // multiples 64 bytes - } catch (e: NegativeArraySizeException) { - continue // Skip devices with read error (Maybe disconnected) - } - devicesPresent = true // Even if the device has no data - if (dataReceived.isNotEmpty()) { - // Process data - // The data is always received as 64 bytes, this check no longer works - if (dataReceived.size % PACKET_SIZE != 0) { - LogManager.info("[TrackerServer] Malformed HID packet, ignoring") - continue // Don't continue with this data - } - devicesDataReceived = true // Data is received and is valid (not malformed) - lastDataByHID[hidDevice] = 0 // reset last data received - val packetCount = dataReceived.size / PACKET_SIZE - var i = 0 - while (i < packetCount * PACKET_SIZE) { - // Common packet data - val packetType = dataReceived[i].toUByte().toInt() - val id = dataReceived[i + 1].toUByte().toInt() - val deviceId = id - - // Register device - if (packetType == 255) { // device register packet from receiver - val buffer = ByteBuffer.wrap(dataReceived, i + 2, 8) - buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN) - val addr = buffer.getLong() and 0xFFFFFFFFFFFF - val deviceName = String.format("%012X", addr) - HIDCommon.deviceIdLookup(devices, hidDevice.serialNumber, deviceId, deviceName, deviceList) // register device - // server wants tracker to be unique, so use combination of hid serial and full id - i += PACKET_SIZE - continue - } - - val device: HIDDevice? = HIDCommon.deviceIdLookup(devices, hidDevice.serialNumber, deviceId, null, deviceList) - if (device == null) { // not registered yet - i += PACKET_SIZE - continue - } - - HIDCommon.processPacket(dataReceived, i, packetType, device, q, a, m, trackersConsumer) - i += PACKET_SIZE - } - // LogManager.info("[TrackerServer] HID received $packetCount tracker packets") - } else { - lastDataByHID[hidDevice] = lastDataByHID[hidDevice]!! + 1 // increment last data received - } - } - if (!devicesPresent) { - sleep(10) // No hid device, "empty loop" so sleep to save the poor cpu - } else if (!devicesDataReceived) { - sleep(1) // read has no timeout, no data also causes an "empty loop" - } - } - } - - private fun deviceEnumerate() { - var rootReceivers: HidDeviceInfoStructure? = null - var rootTrackers: HidDeviceInfoStructure? = null - val trackersOverHID: Boolean = VRServer.instance.configManager.vrConfig.hidConfig.trackersOverHID - try { - rootReceivers = HidApi.enumerateDevices(HID_TRACKER_RECEIVER_VID, HID_TRACKER_RECEIVER_PID) // TODO: Use list of ids - rootTrackers = if (trackersOverHID) { - HidApi.enumerateDevices(HID_TRACKER_RECEIVER_VID, HID_TRACKER_PID) - } else { - null - } // TODO: Use list of ids - } catch (e: Throwable) { - LogManager.severe("[TrackerServer] Couldn't enumerate HID devices", e) - } - var root: HidDeviceInfoStructure? = rootReceivers - if (root == null) { - root = rootTrackers - } else { - var last: HidDeviceInfoStructure = root - while (last.hasNext()) { - last = last.next() - } - last.next = rootTrackers - } - val hidDeviceList: MutableList = mutableListOf() - if (root != null) { - var hidDeviceInfoStructure: HidDeviceInfoStructure? = root - do { - hidDeviceList.add(HidDevice(hidDeviceInfoStructure, null, hidServicesSpecification)) - hidDeviceInfoStructure = hidDeviceInfoStructure?.next() - } while (hidDeviceInfoStructure != null) - HidApi.freeEnumeration(root) - } - synchronized(devicesByHID) { - // Work on devicesByHid and add/remove as necessary - val removeList: MutableList = devicesByHID.keys.toMutableList() - removeList.removeAll(hidDeviceList) - for (device in removeList) { - removeDevice(device) - } - // Quickly reattaching a device may not be detected, so always try to open existing devices - for (device in devicesByHID.keys) { - // a receiver sends keep-alive data at 10 packets/s - if (lastDataByHID[device]!! > 100) { // try to reopen device if no data was received recently (about >100ms) - LogManager.info("[TrackerServer] Reopening device ${device.serialNumber} after no data received") - device.open() - } - } - hidDeviceList.removeAll(devicesByHID.keys) // addList - for (device in hidDeviceList) { - checkConfigureDevice(device) - } - } - } - - override fun run() { // Doesn't seem to run - } - - fun getDevices(): List = devices - - // We don't use these - override fun hidDeviceAttached(event: HidServicesEvent) { - } - - override fun hidDeviceDetached(event: HidServicesEvent) { - } - - override fun hidFailure(event: HidServicesEvent) { - } - - override fun hidDataReceived(p0: HidServicesEvent?) { - } - - companion object { - private const val resetSourceName = "TrackerServer" - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/linux.kt b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/linux.kt new file mode 100644 index 0000000000..828f6333f7 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/linux.kt @@ -0,0 +1,113 @@ +package dev.slimevr.desktop.vrchat + +import dev.slimevr.AppLogger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.FileReader +import java.io.InvalidObjectException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.io.path.Path +import kotlin.io.path.exists + +private const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg" +private val KEY_VALUE_PATTERN = Regex(""""(.+)"=(.+)""") +private val HEX_FORMAT = HexFormat { + upperCase = false + bytes.byteSeparator = "," +} + +internal val linuxUserRegPath = System.getenv("HOME")?.let { home -> + listOf( + Path(home, ".steam", "root", USER_REG_SUBPATH), + Path(home, ".steam", "debian-installation", USER_REG_SUBPATH), + Path(home, ".var", "app", "com.valvesoftware.Steam", "data", "Steam", USER_REG_SUBPATH), + ).firstOrNull { it.exists() } +} + +internal suspend fun linuxGetVRChatKeys(path: String, registry: MutableMap): Map { + val keysMap = mutableMapOf() + registry.clear() + try { + withContext(Dispatchers.IO) { + BufferedReader(FileReader(linuxUserRegPath?.toFile() ?: return@withContext)).use { reader -> + val actualPath = "[${path.replace("\\", """\\""")}]" + while (reader.ready()) { + val line = reader.readLine() + if (!line.startsWith(actualPath)) continue + reader.readLine() // skip `#time` line + while (reader.ready()) { + val keyValue = reader.readLine() + if (keyValue == "") break + KEY_VALUE_PATTERN.matchEntire(keyValue)?.let { + registry[it.groupValues[1]] = it.groupValues[2] + keysMap[it.groupValues[1].replace("""_h\d+$""".toRegex(), "")] = it.groupValues[1] + } + } + break + } + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + AppLogger.vrc.error("[VRChatRegEdit] Error reading VRC registry values: ${e.message}") + } + return keysMap +} + +internal suspend fun linuxGetQwordValue(registry: Map, key: String): Double? { + val value = registry[key] ?: return null + if (!value.startsWith("hex(4):")) { + AppLogger.vrc.error("[VRChatRegEdit] Unexpected registry value type for key $key") + return null + } + return ByteBuffer.wrap(value.substring(7).hexToByteArray(HEX_FORMAT)).order(ByteOrder.LITTLE_ENDIAN).double +} + +internal suspend fun linuxGetDwordValue(registry: Map, key: String): Int? = try { + val value = registry[key] ?: return null + if (value.startsWith("dword:")) { + value.substring(6).toInt(16) + } else { + throw InvalidObjectException("Expected DWORD but got: $value") + } +} catch (e: CancellationException) { + throw e +} catch (e: Exception) { + AppLogger.vrc.error("[VRChatRegEdit] Error reading DWORD: ${e.message}") + null +} + +internal fun linuxVRCConfigFlow(): Flow = flow { + val regPath = linuxUserRegPath ?: run { + AppLogger.vrc.info("[VRChatRegEdit] Couldn't find any VRChat registry file") + return@flow + } + AppLogger.vrc.info("[VRChatRegEdit] Using VRChat registry file: $regPath") + + val registry = mutableMapOf() + while (true) { + val keys = linuxGetVRChatKeys(VRC_REG_PATH, registry) + if (keys.isEmpty()) { + emit(null) + } else { + emit( + buildVRCConfigValues( + intValue = { key -> keys[key]?.let { linuxGetDwordValue(registry, it) } }, + doubleValue = { key -> keys[key]?.let { linuxGetQwordValue(registry, it) } }, + ), + ) + } + delay(3000) + // it seems that on linux, steam writes to the reg file is unpredictable. + // I tried multiple things to just watch for file change instead of polling + // without success. Polling was the simplest and most reliable + } +}.flowOn(Dispatchers.IO) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/vrc-config.kt b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/vrc-config.kt new file mode 100644 index 0000000000..fad93dc5ed --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/vrc-config.kt @@ -0,0 +1,67 @@ +package dev.slimevr.desktop.vrchat + +import dev.slimevr.CURRENT_PLATFORM +import dev.slimevr.Phase1ContextProvider +import dev.slimevr.Platform +import dev.slimevr.vrchat.VRCConfigManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow +import solarxr_protocol.rpc.VRCAvatarMeasurementType +import solarxr_protocol.rpc.VRCConfigValues +import solarxr_protocol.rpc.VRCSpineMode +import solarxr_protocol.rpc.VRCTrackerModel + +internal const val VRC_REG_PATH = "Software\\VRChat\\VRChat" + +fun createDesktopVRCConfigManager(ctx: Phase1ContextProvider, scope: CoroutineScope): VRCConfigManager = when (CURRENT_PLATFORM) { + Platform.WINDOWS -> VRCConfigManager.create( + config = ctx.config, + scope = scope, + isSupported = true, + values = windowsVRCConfigFlow(), + ) + + Platform.LINUX -> VRCConfigManager.create( + config = ctx.config, + scope = scope, + isSupported = true, + values = linuxVRCConfigFlow(), + ) + + else -> VRCConfigManager.create( + config = ctx.config, + scope = scope, + isSupported = false, + values = emptyFlow(), + ) +} + +internal suspend fun buildVRCConfigValues( + intValue: suspend (String) -> Int?, + doubleValue: suspend (String) -> Double?, +): VRCConfigValues = VRCConfigValues( + legacyMode = intValue("VRC_IK_LEGACY") == 1, + shoulderTrackingDisabled = intValue("VRC_IK_DISABLE_SHOULDER_TRACKING") == 1, + shoulderWidthCompensation = intValue("VRC_IK_SHOULDER_WIDTH_COMPENSATION") == 1, + userHeight = doubleValue("PlayerHeight")?.toFloat() ?: -1.0f, + calibrationRange = doubleValue("VRC_IK_CALIBRATION_RANGE")?.toFloat() ?: -1.0f, + trackerModel = when (intValue("VRC_IK_TRACKER_MODEL")) { + 0 -> VRCTrackerModel.SPHERE + 1 -> VRCTrackerModel.SYSTEM + 2 -> VRCTrackerModel.BOX + 3 -> VRCTrackerModel.AXIS + else -> VRCTrackerModel.UNKNOWN + }, + spineMode = when (intValue("VRC_IK_FBT_SPINE_MODE")) { + 0 -> VRCSpineMode.LOCK_HIP + 1 -> VRCSpineMode.LOCK_HEAD + 2 -> VRCSpineMode.LOCK_BOTH + else -> VRCSpineMode.UNKNOWN + }, + calibrationVisuals = intValue("VRC_IK_CALIBRATION_VIS") == 1, + avatarMeasurementType = when (intValue("VRC_IK_AVATAR_MEASUREMENT_TYPE")) { + 0 -> VRCAvatarMeasurementType.ARM_SPAN + 1 -> VRCAvatarMeasurementType.HEIGHT + else -> VRCAvatarMeasurementType.UNKNOWN + }, +) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/windows.kt b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/windows.kt new file mode 100644 index 0000000000..dd17b0869d --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/vrchat/windows.kt @@ -0,0 +1,118 @@ +package dev.slimevr.desktop.vrchat + +import com.sun.jna.Library +import com.sun.jna.Memory +import com.sun.jna.Native +import com.sun.jna.platform.win32.Advapi32 +import com.sun.jna.platform.win32.Advapi32Util +import com.sun.jna.platform.win32.Kernel32 +import com.sun.jna.platform.win32.WinBase +import com.sun.jna.platform.win32.WinNT +import com.sun.jna.platform.win32.WinReg +import com.sun.jna.ptr.IntByReference +import dev.slimevr.AppLogger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import solarxr_protocol.rpc.VRCConfigValues + +// RegNotifyChangeKeyValue is not in JNA's standard Advapi32 +private interface RegistryNotify : Library { + companion object { + val INSTANCE: RegistryNotify = Native.load("Advapi32", RegistryNotify::class.java) + const val REG_NOTIFY_CHANGE_LAST_SET = 0x00000004 + } + + @Suppress("FunctionName") + fun RegNotifyChangeKeyValue( + hKey: WinReg.HKEY, + bWatchSubtree: Boolean, + dwNotifyFilter: Int, + hEvent: WinNT.HANDLE, + fAsynchronous: Boolean, + ): Int +} + +// VRChat writes 64-bit doubles as DWORD instead of QWORD, so we read raw bytes. +internal suspend fun windowsGetQwordValue(path: String, key: String): Double? { + val phkResult = WinReg.HKEYByReference() + if (Advapi32.INSTANCE.RegOpenKeyEx(WinReg.HKEY_CURRENT_USER, path, 0, WinNT.KEY_READ, phkResult) != 0) { + AppLogger.vrc.error("[VRChatRegEdit] Cannot open registry key") + return null + } + val lpData = Memory(8) + val lpcbData = IntByReference(8) + val result = Advapi32.INSTANCE.RegQueryValueEx(phkResult.value, key, 0, null, lpData, lpcbData) + Advapi32.INSTANCE.RegCloseKey(phkResult.value) + if (result != 0) { + AppLogger.vrc.error("[VRChatRegEdit] Cannot read registry key") + return null + } + return lpData.getDouble(0) +} + +internal suspend fun windowsGetDwordValue(path: String, key: String): Int? = try { + Advapi32Util.registryGetIntValue(WinReg.HKEY_CURRENT_USER, path, key) +} catch (e: Exception) { + AppLogger.vrc.error("[VRChatRegEdit] Error reading DWORD: ${e.message}") + null +} + +internal suspend fun windowsGetVRChatKeys(path: String): Map { + val keysMap = mutableMapOf() + try { + Advapi32Util.registryGetValues(WinReg.HKEY_CURRENT_USER, path).forEach { + keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.key + } + } catch (e: Exception) { + AppLogger.vrc.error("[VRChatRegEdit] Error reading VRC registry values: ${e.message}") + } + return keysMap +} + +internal fun windowsVRCConfigFlow(): Flow = flow { + while (true) { + // Open key and register notification BEFORE reading to avoid race conditions: + // any change that happens between registration and the read will trigger a re-read on next iteration + val phkResult = WinReg.HKEYByReference() + if (Advapi32.INSTANCE.RegOpenKeyEx(WinReg.HKEY_CURRENT_USER, VRC_REG_PATH, 0, WinNT.KEY_NOTIFY, phkResult) != 0) { + // VRChat not installed + emit(null) + return@flow + } + + val hEvent = Kernel32.INSTANCE.CreateEvent(null, true, false, null) + try { + if (hEvent != null) { + RegistryNotify.INSTANCE.RegNotifyChangeKeyValue( + phkResult.value, + false, + RegistryNotify.REG_NOTIFY_CHANGE_LAST_SET, + hEvent, + true, + ) + } + + val keys = windowsGetVRChatKeys(VRC_REG_PATH) + emit( + if (keys.isEmpty()) { + null + } else { + buildVRCConfigValues( + intValue = { key -> keys[key]?.let { windowsGetDwordValue(VRC_REG_PATH, it) } }, + doubleValue = { key -> keys[key]?.let { windowsGetQwordValue(VRC_REG_PATH, it) } }, + ) + }, + ) + + if (hEvent != null) { + withContext(Dispatchers.IO) { Kernel32.INSTANCE.WaitForSingleObject(hEvent, WinBase.INFINITE) } + } + } finally { + hEvent?.let { Kernel32.INSTANCE.CloseHandle(it) } + Advapi32.INSTANCE.RegCloseKey(phkResult.value) + } + } +}.flowOn(Dispatchers.IO) diff --git a/settings.gradle.kts b/settings.gradle.kts index e2691d0b82..7c7f9abf25 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,7 @@ pluginManagement { val shadowJarVersion: String by settings val buildconfigVersion: String by settings val grgitVersion: String by settings + val wireVersion: String by settings plugins { kotlin("plugin.serialization") version kotlinVersion kotlin("jvm") version kotlinVersion @@ -29,11 +30,12 @@ pluginManagement { id("com.gradleup.shadow") version shadowJarVersion id("com.github.gmazzo.buildconfig") version buildconfigVersion id("org.ajoberstar.grgit") version grgitVersion + id("com.squareup.wire") version wireVersion } } include(":solarxr-protocol") -project(":solarxr-protocol").projectDir = File("solarxr-protocol/protocol/java") +project(":solarxr-protocol").projectDir = File("solarxr-protocol/protocol/kotlin") include(":server") project(":server").projectDir = File("server") diff --git a/solarxr-protocol b/solarxr-protocol index fa2895b19a..6ef8a144ff 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit fa2895b19a53d9b1686de8c2a6efe2b3e9ca4fc6 +Subproject commit 6ef8a144ffc57bcadf5168740aa0b49c50faa823