Compare commits

...

62 Commits

Author SHA1 Message Date
Butterscotch!
340cff8be5 Minimize ProGuard rules & enable optimization 2026-03-06 01:43:02 -05:00
Sapphire
0236a05f26 Allow showing 'Up to date' version for third-party trackers (#1756) 2026-02-25 23:08:31 +01:00
Butterscotch!
28deb357da Revert TrackerResetsHandler & add comments (#1748) 2026-02-20 11:26:23 +01:00
jabberrock
4d93f87a01 Clarify AI policy on contributions (#1752) 2026-02-17 03:59:11 +03:00
Sapphire
88adfce242 Hide tracker firmware version when board type is unknown (#1721) 2026-02-09 21:58:36 +01:00
Sapphire
3d02795dbc Don't trigger timeout when Wi-Fi provisioning is done (#1725)
Co-authored-by: lucas lelievre <loucass003@gmail.com>
2026-02-09 21:57:56 +01:00
Sapphire
7ff50f78eb Don't ask for full reset on timeout with manual or saved mounting (#1727) 2026-02-02 18:06:38 +01:00
Sapphire
0e3aaf105c Display more accurate info for OpenVR devices (#1731) 2026-02-02 18:06:17 +01:00
sctanf
f638540886 TrackerBattery disable tooltip while charging (#1733) 2026-02-02 18:05:50 +01:00
Sapphire
343d69d690 Support feet mounting reset over Protobuf bridge (#1737) 2026-02-02 18:05:31 +01:00
H3
e2d7d354c6 Add ESP32-S2 to USB-Serial Accept-List (#1730)
Co-authored-by: unlogisch04 <98281608+unlogisch04@users.noreply.github.com>
2026-01-29 22:54:37 +03:00
sctanf
cc6f297b92 Re-add missing battery runtime estimate to DataFeedBuilder (#1732) 2026-01-29 14:00:18 +04:00
H114514191981
2add43e71a add CH343 support (#1568)
Co-authored-by: unlogisch04 <98281608+unlogisch04@users.noreply.github.com>
2026-01-28 10:39:25 +02:00
Sapphire
0a493ac345 Fix scheduled resets triggering immediately when delay is unspecified (#1724) 2026-01-25 07:08:25 +02:00
Sapphire
17bb2703d1 Fix unassigned HMD flightlist step never showing (#1723) 2026-01-24 07:34:37 +02:00
Sapphire
f0981bf709 ProtocolAPI: Initialise RPCHandler after other members (#1722) 2026-01-24 01:17:25 +03:00
peelz
99de554c18 Add delay param to ResetRequest (#1712) 2026-01-23 03:38:08 +04:00
Sapphire
f95a4d56d7 Add feet mounting reset keybind (#1717) 2026-01-23 03:14:27 +04:00
Sapphire
1df3c9d322 Hide devices with unknown board type in DIY firmware tool (#1718) 2026-01-23 03:14:12 +04:00
sctanf
e0838cce6c Battery indicators (#1714) 2026-01-23 03:13:36 +04:00
Sapphire
e25d3201c2 Fix translation key for Autobone processing text (#1719) 2026-01-23 03:12:18 +04:00
Sapphire
5d14f14139 Fix alignment of neck and head dots in tracker assignment page (#1720) 2026-01-23 03:11:37 +04:00
Eiren Rain
09e81f5ace Fix trackers table having an extra header 2026-01-19 20:29:23 +01:00
Maya
8f57ef2de4 Migrate core/dev.slimevr.protocol to Kotlin (#1688)
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2026-01-14 15:50:04 +01:00
loucass003
ea242960b3 Lint 2026-01-12 21:34:19 +01:00
Eiren Rain
35ac14a7de Packet loss (#1687)
Co-authored-by: loucass003 <loucass003@gmail.com>
2026-01-12 21:22:53 +01:00
Aed
690a8b5c6e add manual mounting to stay alligned setup (#1692) 2026-01-08 02:16:08 +01:00
sctanf
255b8b2865 Detect and use tracker PID during HID enumeration (#1556) 2026-01-07 10:13:33 +01:00
Eiren Rain
e27ec63985 Accept fixed tracker accel (#1706) 2026-01-07 08:57:24 +03:00
Butterscotch!
9f8be6551c Accept fixed tracker accel 2026-01-06 22:51:17 -05:00
lucas lelievre
f09cd687c7 Make ignore buttons be session only in checklist (#1675) 2026-01-05 16:21:36 +01:00
lucas lelievre
686499f8dd fix fw tool source (#1690) 2026-01-05 16:20:45 +01:00
Butterscotch!
a3bcc61892 Fix race conditions from Play Store (#1696) 2025-12-26 10:28:39 +01:00
sctanf
faf70c9a39 Add nRF consts (#1685) 2025-12-26 09:26:32 +01:00
sctanf
2aa8d3a056 hid add "unknown" runtime state (#1693) 2025-12-25 16:41:46 +01:00
Meia
23df46ca33 Amplify UI sounds by 8dB (#1694) 2025-12-25 16:41:08 +01:00
lucas lelievre
8407f52777 Better columns width for table view (#1691) 2025-12-19 00:24:28 +01:00
lucas lelievre
b44dcaa9c2 Runtime (#1678) 2025-12-19 00:11:10 +01:00
sctanf
ff0d823aff Merge remote-tracking branch 'upstream/main' into runtime 2025-12-18 10:46:44 -06:00
sctanf
2e8bfa5373 Update solarxr-protocol 2025-12-18 02:35:33 -06:00
sctanf
87940ddd03 TrackerBattery tooltip remove classes 2025-12-18 00:04:07 -06:00
sctanf
6208979ce9 TrackerBattery battery level tooltip if not shown 2025-12-17 23:48:00 -06:00
lucas lelievre
9a27fb1320 separate firmware date from version (#1650) 2025-12-18 06:46:10 +01:00
sctanf
53129328d0 Update solarxr-protocol 2025-12-17 22:59:24 -06:00
lucas lelievre
2d79c5a0e9 Add @ImSapphire as code owner for i18n directory 2025-12-18 05:03:19 +01:00
sctanf
74f5a92ce1 fw date default 2025-12-15 09:16:47 -06:00
sctanf
146930279c add firmware date to new field 2025-12-15 09:16:47 -06:00
sctanf
0c33579858 HIDCommon don't show firmware date in version 2025-12-15 09:16:46 -06:00
sctanf
c9783d097b show voltage in less situations 2025-12-15 09:16:30 -06:00
sctanf
d3eafb8d06 only show runtime if nonzero 2025-12-15 09:16:30 -06:00
sctanf
09d44b51d6 linter borked it 2025-12-15 09:16:30 -06:00
sctanf
cf357e71f5 lint 2025-12-15 09:16:29 -06:00
sctanf
122efacc52 show runtime in gui 2025-12-15 09:16:29 -06:00
sctanf
7f536528d0 Parse runtime from HID tracker 2025-12-15 09:16:28 -06:00
Eiren Rain
3982249ebf Bump actions/upload-artifact from 5 to 6 (#1683) 2025-12-15 17:53:59 +03:00
Eiren Rain
388bea2e72 Bump actions/download-artifact from 6 to 7 (#1684) 2025-12-15 17:53:41 +03:00
dependabot[bot]
921a760817 Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 14:12:16 +00:00
dependabot[bot]
55bcec4dda Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 14:12:07 +00:00
Eiren Rain
bb08e8dc6a DesktopHID only warn if device cannot open (#1682) 2025-12-15 11:08:44 +03:00
sctanf
a82f950eb6 DesktopHID only warn if device cannot open 2025-12-14 20:52:14 -06:00
Eiren Rain
e2dbaab8ba hid packet 6 and packet 7 (data2) (#1679) 2025-12-14 10:25:49 +03:00
sctanf
3611bb5cc7 hid packet 6 and packet 7 (data2)
this is the worst meme ever
2025-12-13 23:59:36 -06:00
100 changed files with 2842 additions and 2314 deletions

2
.github/CODEOWNERS vendored
View File

@@ -10,7 +10,7 @@
/pnpm-workspace.yaml @loucass003
# loucass003 and Erimel responsible for i18n
/gui/public/i18n/ @loucass003 @Erimelowo
/gui/public/i18n/ @loucass003 @Erimelowo @ImSapphire
/gui/src/i18n/ @loucass003 @Erimelowo
/l10n.toml @loucass003 @Erimelowo

View File

@@ -103,7 +103,7 @@ jobs:
- if: startsWith(matrix.os, 'windows')
name: Upload a Build Artifact (Windows)
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: ${{ format('SlimeVR-GUI-Windows-{0}', env.BUILD_ARCH) }}
@@ -112,7 +112,7 @@ jobs:
- if: startsWith(matrix.os, 'ubuntu')
name: Upload a Build Artifact (Linux)
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: ${{ format('SlimeVR-GUI-Linux-{0}', env.BUILD_ARCH) }}
@@ -121,7 +121,7 @@ jobs:
- if: matrix.os == 'macos-latest'
name: Upload a Build Artifact (macOS)
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: SlimeVR-GUI-macOS

View File

@@ -20,7 +20,7 @@ jobs:
run: |
npx @slimevr/update-manifest-generator@latest
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: "update-manifest.json"
path: ./update-manifest.json

View File

@@ -68,7 +68,7 @@ jobs:
run: ./gradlew :server:desktop:shadowJar
- name: Upload the Server JAR as a Build Artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: 'SlimeVR-Server' # optional, default is artifact
@@ -127,7 +127,7 @@ jobs:
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_KEY_PASSWD }}
- name: Upload the Android build artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: 'SlimeVR-Android' # optional, default is artifact
@@ -158,7 +158,7 @@ jobs:
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }}
- name: Upload the Google Play artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
if: startsWith(github.ref, 'refs/tags/')
with:
# Artifact name
@@ -181,7 +181,7 @@ jobs:
with:
submodules: recursive
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
@@ -230,23 +230,23 @@ jobs:
run: |
tar czf slimevr-gui-dist.tar.gz -C gui/dist/ .
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
if: matrix.os == 'ubuntu-latest'
with:
name: SlimeVR-GUI-Dist
path: ./slimevr-gui-dist.tar.gz
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: ${{ format('SlimeVR-GUI-Deb-{0}', env.BUILD_ARCH) }}
path: target/release/bundle/deb/slimevr*.deb
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: ${{ format('SlimeVR-GUI-AppImage-{0}', env.BUILD_ARCH) }}
path: target/release/bundle/appimage/slimevr*.AppImage
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: ${{ format('SlimeVR-GUI-RPM-{0}', env.BUILD_ARCH) }}
path: target/release/bundle/rpm/slimevr*.rpm
@@ -279,7 +279,7 @@ jobs:
with:
submodules: recursive
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
@@ -320,12 +320,12 @@ jobs:
--volicon ../macos/SlimeVR.app/Contents/Resources/icon.icns --skip-jenkins \
--eula ../../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: SlimeVR-GUI-MacApp
path: target/universal-apple-darwin/release/bundle/macos/SlimeVR*.app
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: SlimeVR-GUI-MacDmg
path: target/universal-apple-darwin/release/bundle/dmg/slimevr.dmg
@@ -359,7 +359,7 @@ jobs:
with:
submodules: recursive
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
@@ -399,7 +399,7 @@ jobs:
cp target/release/slimevr.exe ./SlimeVR/
7z a -tzip "SlimeVR-$BUILD_ARCH.zip" ./SlimeVR/
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: ${{ format('SlimeVR-GUI-Windows-{0}', env.BUILD_ARCH) }}
path: ./SlimeVR*.zip

View File

@@ -116,3 +116,9 @@ licensed under `GPL-v3`.
## Discord
We use discord *a lot* to coordinate and discuss development. Come join us at
https://discord.gg/SlimeVR!
## Use of AI
We DO NOT accept contributions that are generated with AI (for example, "vibe-coding").
If you do use AI, and you believe your usage of AI is reasonable, you must clearly disclose
how you used AI in your submission.

View File

@@ -109,6 +109,11 @@ board_type-XIAO_ESP32C3 = Seeed Studio XIAO ESP32C3
board_type-HARITORA = Haritora
board_type-ESP32C6DEVKITC1 = Espressif ESP32-C6 DevKitC-1
board_type-GLOVE_IMU_SLIMEVR_DEV = SlimeVR Dev IMU Glove
board_type-GESTURES = Gestures
board_type-ESP32S3_SUPERMINI = ESP32-S3 Supermini
board_type-GENERIC_NRF = Generic nRF
board_type-SLIMEVR_BUTTERFLY_DEV = SlimeVR Dev Butterfly
board_type-SLIMEVR_BUTTERFLY = SlimeVR Butterfly
## Proportions
skeleton_bone-NONE = None
@@ -331,6 +336,7 @@ tracker-table-column-name = Name
tracker-table-column-type = Type
tracker-table-column-battery = Battery
tracker-table-column-ping = Ping
tracker-table-column-packet_loss = Packet Loss
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
@@ -370,6 +376,10 @@ tracker-infos-magnetometer-status-v1 = { $status ->
[ENABLED] Enabled
}
tracker-infos-packet_loss = Packet Loss
tracker-infos-packets_lost = Packets Lost
tracker-infos-packets_received = Packets Received
## Tracker settings
tracker-settings-back = Go back to trackers list
tracker-settings-title = Tracker settings
@@ -407,6 +417,7 @@ tracker-settings-update = Update now
tracker-settings-update-title = Firmware version
tracker-settings-current-version = Current
tracker-settings-latest-version = Latest
tracker-settings-build-date = Build Date
## Tracker part card info
@@ -567,6 +578,10 @@ settings-general-tracker_mechanics-use_mag_on_all_trackers-description =
Uses magnetometer on all trackers that have a compatible firmware for it, reducing drift in stable magnetic environments.
Can be disabled per tracker in the tracker's settings. <b>Please don't shutdown any of the trackers while toggling this!</b>
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = Use magnetometer on trackers
settings-general-tracker_mechanics-trackers_over_usb = Trackers over USB
settings-general-tracker_mechanics-trackers_over_usb-description =
Enables receiving HID tracker data over USB. Make sure connected trackers have <b>connection over HID</b> enabled!
settings-general-tracker_mechanics-trackers_over_usb-enabled-label = Allow HID trackers to connect directly over USB
settings-stay_aligned = Stay Aligned
settings-stay_aligned-description = Stay Aligned reduces drift by gradually adjusting your trackers to match your relaxed poses.
@@ -1342,6 +1357,7 @@ onboarding-stay_aligned-previous_step = Previous
onboarding-stay_aligned-next_step = Next
onboarding-stay_aligned-restart = Restart
onboarding-stay_aligned-done = Done
onboarding-stay_aligned-manual_mounting-done = Done
## Home
home-no_trackers = No trackers detected or assigned
@@ -1393,6 +1409,9 @@ firmware_tool-select_source-firmware = Firmware Source
firmware_tool-select_source-version = Firmware Version
firmware_tool-select_source-official = Official
firmware_tool-select_source-dev = Dev
firmware_tool-select_source-not_selected = No source selected
firmware_tool-select_source-no_boards = No available boards for this source
firmware_tool-select_source-no_versions = No available versions for this source
firmware_tool-board_defaults = Configure your board
firmware_tool-board_defaults-description = Set the pins or settings relative to your hardware

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -18,6 +18,8 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="7522", MODE="0660
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="7523", MODE="0660", TAG+="uaccess"
# CH341
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="5523", MODE="0660", TAG+="uaccess"
# CH343
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="55D3", MODE="0660", TAG+="uaccess"
# CH9102x
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="55D4", MODE="0660", TAG+="uaccess"
@@ -26,8 +28,10 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="55D4", MODE="0660
SUBSYSTEMS=="usb", ATTRS{idVendor}=="10C4", ATTRS{idProduct}=="EA60", MODE="0660", TAG+="uaccess"
## Espressif
# ESP32-C3
# ESP32-S3 / ESP32-C3 / ESP32-C5 / ESP32-C6 / ESP32-C61 / ESP32-H2 / ESP32-P4
SUBSYSTEMS=="usb", ATTRS{idVendor}=="303A", ATTRS{idProduct}=="1001", MODE="0660", TAG+="uaccess"
# ESP32-S2
SUBSYSTEMS=="usb", ATTRS{idVendor}=="303A", ATTRS{idProduct}=="0002", MODE="0660", TAG+="uaccess"
## FTDI
# FT232BM/L/Q, FT245BM/L/Q

View File

@@ -156,9 +156,9 @@ export function TopBar({
<>
<div className="flex gap-0 flex-col">
<div className="h-[3px]" />
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-49">
<div
className="flex px-2 py-2 justify-around z-50"
className="flex px-2 py-2 justify-around z-49"
data-tauri-drag-region
>
<div className="flex gap-2" data-tauri-drag-region>

View File

@@ -59,14 +59,14 @@ export function PersonFrontIcon({ mirror = true }: { mirror?: boolean }) {
/>
<circle
className="body-part-circle"
cx="81.5"
cx="82"
cy="80"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.NECK]}
/>
<circle
className="body-part-circle"
cx="81.5"
cx="82"
cy="35"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.HEAD]}

View File

@@ -10,7 +10,7 @@ export function TipBox({
whitespace = false,
className,
}: {
children: ReactNode;
children?: ReactNode;
hideIcon?: boolean;
whitespace?: boolean;
className?: string;

View File

@@ -11,6 +11,9 @@ export function BatteryIcon({
charging: boolean;
}) {
const col = useMemo(() => {
if (disabled) return 'fill-background-40';
else if (charging) return 'fill-status-success';
const colorsMap: { [key: number]: string } = {
0.4: 'fill-status-success',
0.2: 'fill-status-warning',
@@ -20,10 +23,8 @@ export function BatteryIcon({
const val = Object.keys(colorsMap)
.filter((key) => +key < value)
.sort((a, b) => +b - +a)[0];
return disabled
? 'fill-background-40'
: colorsMap[+val] || 'fill-background-10';
}, [value, disabled]);
return colorsMap[+val] || 'fill-background-10';
}, [value, disabled, charging]);
return (
<svg
@@ -59,13 +60,21 @@ export function BatteryIcon({
/>
</mask>
<g mask="url(#mask0_4_39)" className={classNames(col, 'opacity-100')}>
<rect width={value * 18} height="9" />
<rect width={charging ? 18 : value * 18} height="9" />
</g>
{charging && (
{charging && value <= 1 && (
<path
d="M 0.93561138,11.744353 2.4349252,6.1488377 H 0.0312815 L 3.5761014,0.00903018 2.2061799,5.1216451 h 2.4534885 z"
d="M 7.7638355,8.4189633 8.0112251,4.9834646 5.7712838,4.9834645 8.5644084,0.07977871 8.3170195,3.5152773 H 10.55696 Z"
fill="#081e30"
transform="translate(5,-1)"
/>
)}
{charging && value > 1 && (
<path
d="M 5.5342464,4.6225095 C 6.1777799,5.0106205 6.6131537,5.2516456 7.5253371,6.545223 8.4340868,4.4016445 8.7809738,3.661475 10.605195,1.5520288"
fill="none"
stroke="#081e30"
strokeWidth={1.5}
strokeLinecap="square"
/>
)}
</svg>

View File

@@ -231,7 +231,10 @@ function OTADevicesList({
const allDevices = useAtomValue(devicesAtom);
const devices =
allDevices.filter(({ trackers }) => {
allDevices.filter(({ hardwareInfo, trackers }) => {
// filter out devices we can't update
if (!hardwareInfo?.officialBoardType) return false;
// if the device has no trackers it is prob misconfigured so we skip for safety
if (trackers.length <= 0) return false;

View File

@@ -101,13 +101,10 @@ export function SelectSourceSetep({
curr.push({
name: source.source,
official: source.official,
disabled:
!partialBoard?.board ||
!source.availableBoards.includes(partialBoard.board),
});
return curr;
},
[] as { name: string; official: boolean; disabled: boolean }[]
[] as { name: string; official: boolean }[]
)
.sort((a, b) => {
if (a.official !== b.official) return a.official ? -1 : 1;
@@ -115,6 +112,7 @@ export function SelectSourceSetep({
}),
possibleBoards: sources
?.reduce((curr, source) => {
if (source.source !== partialBoard?.source) return curr;
const unknownBoards = source.availableBoards.filter(
(b) => !curr.includes(b)
);
@@ -136,6 +134,7 @@ export function SelectSourceSetep({
possibleVersions: sources
?.reduce(
(curr, source) => {
if (source.source !== partialBoard?.source) return curr;
if (!curr.find(({ name }) => source.version === name))
curr.push({
disabled:
@@ -193,6 +192,25 @@ export function SelectSourceSetep({
{!isFetching && !isError && (
<div className="flex flex-col gap-2">
<div className="grid md:grid-cols-3 gap-4">
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-firmware">
<Typography variant="section-title" />
</Localized>
<div className="flex flex-col gap-4 md:max-h-[305px] overflow-y-auto bg-background-80 rounded-lg p-4">
{sourcesGroupped?.map(({ name, official }) => (
<Selector
active={partialBoard?.source === name}
key={`${name}`}
tag={official ? 'official' : undefined}
onClick={() => {
if (partialBoard?.source !== name)
setPartialBoard({ source: name });
}}
text={formatSource(name, official)}
/>
))}
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-board_type">
<Typography variant="section-title" />
@@ -203,7 +221,7 @@ export function SelectSourceSetep({
active={partialBoard?.board === board}
key={`${board}`}
onClick={() => {
setPartialBoard({ board });
setPartialBoard((curr) => ({ ...curr, board }));
}}
tag={
board.startsWith('BOARD_SLIMEVR')
@@ -219,30 +237,15 @@ export function SelectSourceSetep({
}
/>
))}
{partialBoard?.source && possibleBoards?.length === 0 && (
<Typography id="firmware_tool-select_source-no_boards" />
)}
{!partialBoard?.source && (
<Typography id="firmware_tool-select_source-not_selected" />
)}
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-firmware">
<Typography variant="section-title" />
</Localized>
<div className="flex flex-col gap-4 md:max-h-[305px] overflow-y-auto bg-background-80 rounded-lg p-4">
{sourcesGroupped?.map(({ name, official, disabled }) => (
<Selector
active={partialBoard?.source === name}
disabled={disabled}
key={`${name}`}
tag={official ? 'official' : undefined}
onClick={() => {
setPartialBoard((curr) => ({
...curr,
source: name,
}));
}}
text={formatSource(name, official)}
/>
))}
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-version">
<Typography variant="section-title" />
@@ -268,6 +271,12 @@ export function SelectSourceSetep({
text={name}
/>
))}
{partialBoard?.source && possibleVersions?.length === 0 && (
<Typography id="firmware_tool-select_source-no_versions" />
)}
{!partialBoard?.source && (
<Typography id="firmware_tool-select_source-not_selected" />
)}
</div>
</div>
</div>

View File

@@ -28,18 +28,23 @@ export function ResetButtonIcon(options: UseResetOptions) {
}
export function ResetButton({
onClick,
className,
onReseted,
children,
onFailed,
...options
}: {
onClick?: () => void;
className?: string;
children?: ReactNode;
onReseted?: () => void;
onFailed?: () => void;
} & UseResetOptions) {
const { triggerReset, status, timer, disabled, name, error } = useReset(
options,
onReseted
onReseted,
onFailed
);
return (
@@ -60,7 +65,10 @@ export function ResetButton({
>
<Button
icon={<ResetButtonIcon {...options} />}
onClick={triggerReset}
onClick={() => {
if (onClick) onClick();
triggerReset();
}}
className={classNames(
'border-2 py-[5px]',
status === 'finished'

View File

@@ -75,7 +75,7 @@ export function VerifyResultsStep({
hasRecording === ProcessStatus.FULFILLED && (
<Typography>
{l10n.getString(
'onboarding-automatic-proportions-verify-results-processing'
'onboarding-automatic_proportions-verify_results-processing'
)}
</Typography>
)}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
import { useOnboarding } from '@/hooks/onboarding';
import { useWebsocketAPI } from '@/hooks/websocket-api';
@@ -12,7 +12,7 @@ import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { BodyAssignment } from '@/components/onboarding/BodyAssignment';
import { MountingSelectionMenu } from './MountingSelectionMenu';
import { useLocalization } from '@fluent/react';
import { Localized } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { Quaternion } from 'three';
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
@@ -22,7 +22,6 @@ import * as Sentry from '@sentry/react';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
const { config } = useConfig();
@@ -103,28 +102,26 @@ export function ManualMountingPage() {
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
<div className="flex flex-col w-full xs:max-w-sm gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-manual_mounting')}
</Typography>
<Typography>
{l10n.getString('onboarding-manual_mounting-description')}
</Typography>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<Typography variant="main-title" id="onboarding-manual_mounting" />
<Typography id="onboarding-manual_mounting-description" />
<Typography id="tips-find_tracker" />
<Localized id="tips-find_tracker">
<TipBox />
</Localized>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="secondary"
to="/onboarding/mounting/choose"
state={state}
>
{l10n.getString('onboarding-previous_step')}
</Button>
id="onboarding-previous_step"
/>
{!state.alonePage && (
<Button
variant="primary"
to="/onboarding/body-proportions/scaled"
>
{l10n.getString('onboarding-manual_mounting-next')}
</Button>
id="onboarding-manual_mounting-next"
/>
)}
</div>
</div>
@@ -142,3 +139,109 @@ export function ManualMountingPage() {
</>
);
}
export function ManualMountingPageStayAligned({
children,
}: {
children: ReactNode;
}) {
const { isMobile } = useBreakpoint('mobile');
const { sendRPCPacket } = useWebsocketAPI();
const { config } = useConfig();
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackerPartGrouped = useMemo(
() =>
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
(curr, td) => {
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
return {
...curr,
[key]: [...(curr[key] || []), td],
};
},
{}
),
[assignedTrackers]
);
const onDirectionSelected = (mountingOrientationDegrees: Quaternion) => {
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
const assignreq = new AssignTrackerRequestT();
assignreq.bodyPosition = td.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.mountingOrientation = MountingOrientationDegreesToQuatT(
mountingOrientationDegrees
);
assignreq.trackerId = td.tracker.trackerId;
assignreq.allowDriftCompensation = false;
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
Sentry.metrics.count('manual_mounting_set', 1, {
attributes: {
part: BodyPart[assignreq.bodyPosition],
direction: assignreq.mountingOrientation,
},
});
});
setSelectRole(BodyPart.NONE);
};
const getCurrRotation = useCallback(
(role: BodyPart) => {
if (role === BodyPart.NONE) return undefined;
const trackers = trackerPartGrouped[role] || [];
const [mountingOrientation, ...orientation] = trackers
.map((td) => td.tracker.info?.mountingOrientation)
.filter((orientation) => !!orientation)
.map((orientation) => QuaternionFromQuatT(orientation));
const identicalOrientations =
mountingOrientation !== undefined &&
orientation.every((quat) =>
similarQuaternions(quat, mountingOrientation)
);
return identicalOrientations ? mountingOrientation : undefined;
},
[trackerPartGrouped]
);
return (
<>
<MountingSelectionMenu
bodyPart={selectedRole}
currRotation={getCurrRotation(selectedRole)}
isOpen={selectedRole !== BodyPart.NONE}
onClose={() => setSelectRole(BodyPart.NONE)}
onDirectionSelected={onDirectionSelected}
/>
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
<div className="flex flex-col w-full xs:max-w-sm gap-3">
<Typography variant="main-title" id="onboarding-manual_mounting" />
<Typography id="onboarding-manual_mounting-description" />
<Typography id="tips-find_tracker" />
<Localized id="tips-find_tracker">
<TipBox />
</Localized>
{children}
</div>
<div className="flex flex-row justify-center">
<BodyAssignment
width={isMobile ? 160 : undefined}
mirror={config?.mirrorView ?? defaultConfig.mirrorView}
onlyAssigned={true}
assignMode={AssignMode.All}
onRoleSelected={setSelectRole}
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -238,7 +238,7 @@ export function MountingSelectionMenu({
shouldCloseOnEsc
onRequestClose={onClose}
overlayClassName={classNames(
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-background-90 bg-opacity-90 z-20'
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-background-90 bg-opacity-90 z-50'
)}
className={classNames(
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'

View File

@@ -1,32 +1,33 @@
import { useState } from 'react';
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { ResetType } from 'solarxr-protocol';
import { ResetButton } from '@/components/home/ResetButton';
import { useLocalization } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper';
import { BaseModal } from '@/components/commons/BaseModal';
import { ManualMountingPageStayAligned } from '@/components/onboarding/pages/mounting/ManualMounting';
export function VerifyMountingStep({
nextStep,
prevStep,
}: VerticalStepComponentProps) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const [isOpen, setOpen] = useState(false);
const [disableMounting, setDisableMounting] = useState(false);
const goNextStep = () => {
setDisableMounting(false);
setOpen(false);
nextStep();
};
return (
<div className="flex flex-col flex-grow justify-between py-2 gap-2">
<div className="flex flex-col flex-grow">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div className="flex flex-col gap-2">
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-0'
)}
</Typography>
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-1'
)}
</Typography>
<Typography id="onboarding-automatic_mounting-mounting_reset-step-0" />
<Typography id="onboarding-automatic_mounting-mounting_reset-step-1" />
</div>
</div>
@@ -50,13 +51,36 @@ export function VerifyMountingStep({
</div>
)}
<div className="flex gap-3 justify-between">
<Button variant={'secondary'} onClick={prevStep}>
{l10n.getString('onboarding-automatic_mounting-prev_step')}
</Button>
<Button
variant={'secondary'}
onClick={prevStep}
id="onboarding-automatic_mounting-prev_step"
/>
<Button
disabled={disableMounting}
variant={'secondary'}
className="self-start mt-auto"
onClick={() => setOpen(true)}
id="onboarding-automatic_mounting-manual_mounting"
/>
<BaseModal isOpen={isOpen} onRequestClose={() => setOpen(false)}>
<ManualMountingPageStayAligned>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="primary"
onClick={goNextStep}
id="onboarding-stay_aligned-manual_mounting-done"
/>
</div>
</ManualMountingPageStayAligned>
</BaseModal>
<ResetButton
onClick={() => setDisableMounting(true)}
type={ResetType.Mounting}
group="default"
onReseted={nextStep}
onReseted={goNextStep}
onFailed={() => setDisableMounting(false)}
/>
</div>
</div>

View File

@@ -14,6 +14,7 @@ import {
SettingsResponseT,
SteamVRTrackersSettingT,
TapDetectionSettingsT,
HIDSettingsT,
} from 'solarxr-protocol';
import { useConfig } from '@/hooks/config';
import { useWebsocketAPI } from '@/hooks/websocket-api';
@@ -101,6 +102,9 @@ export type SettingsForm = {
};
resetsSettings: ResetSettingsForm;
stayAligned: StayAlignedSettingsForm;
hidSettings: {
trackersOverHID: boolean;
};
};
const defaultValues: SettingsForm = {
@@ -156,6 +160,7 @@ const defaultValues: SettingsForm = {
legTweaks: { correctionStrength: 0.3 },
resetsSettings: defaultResetSettings,
stayAligned: defaultStayAlignedSettings,
hidSettings: { trackersOverHID: false },
};
export function GeneralSettings() {
@@ -277,6 +282,10 @@ export function GeneralSettings() {
settings.stayAligned = serializeStayAlignedSettings(values.stayAligned);
const hidSettings = new HIDSettingsT();
hidSettings.trackersOverHid = values.hidSettings.trackersOverHID;
settings.hidSettings = hidSettings;
if (values.resetsSettings) {
settings.resetsSettings = loadResetSettings(values.resetsSettings);
}
@@ -392,6 +401,12 @@ export function GeneralSettings() {
);
}
if (settings.hidSettings) {
formData.hidSettings = {
trackersOverHID: settings.hidSettings.trackersOverHid,
};
}
reset({ ...getValues(), ...formData });
});
@@ -689,6 +704,28 @@ export function GeneralSettings() {
settingType="general"
id="mechanics-magnetometer"
/>
<div className="flex flex-col pt-5 pb-3">
<Typography variant="section-title">
{l10n.getString(
'settings-general-tracker_mechanics-trackers_over_usb'
)}
</Typography>
<Localized
id="settings-general-tracker_mechanics-trackers_over_usb-description"
elems={{ b: <b /> }}
>
<Typography />
</Localized>
</div>
<CheckBox
variant="toggle"
outlined
control={control}
name="hidSettings.trackersOverHID"
label={l10n.getString(
'settings-general-tracker_mechanics-trackers_over_usb-enabled-label'
)}
/>
</>
</SettingsPagePaneLayout>
<SettingsPagePaneLayout icon={<WrenchIcon />} id="fksettings">

View File

@@ -54,11 +54,11 @@ export function TrackingChecklistSettings({
// doing it this way prevents calling ignore step for every step.
// that prevent sending a packet for steps that didnt change
if (!value && !ignoredSteps.includes(stepId)) {
ignoreStep(stepId, true);
ignoreStep(stepId, true, false);
}
if (value && ignoredSteps.includes(stepId)) {
ignoreStep(stepId, false);
ignoreStep(stepId, false, false);
}
}
};

View File

@@ -2,11 +2,14 @@ import { useConfig } from '@/hooks/config';
import { useLocaleConfig } from '@/i18n/config';
import { BatteryIcon } from '@/components/commons/icon/BatteryIcon';
import { Typography } from '@/components/commons/Typography';
import { Tooltip } from '@/components/commons/Tooltip';
export function TrackerBattery({
value,
voltage,
runtime,
disabled,
moreInfo = false,
textColor = 'primary',
}: {
/**
@@ -14,7 +17,9 @@ export function TrackerBattery({
*/
value: number;
voltage?: number | null;
runtime?: bigint | null;
disabled?: boolean;
moreInfo?: boolean;
textColor?: string;
}) {
const { currentLocales } = useLocaleConfig();
@@ -28,31 +33,49 @@ export function TrackerBattery({
});
const charging = (voltage || 0) > 4.3;
const showVoltage = voltage && config?.debug;
const debug = config?.debug || config?.devSettings.moreInfo;
const showVoltage = moreInfo && voltage && debug;
return (
<div className="flex gap-2">
<div className="flex flex-col justify-around">
<BatteryIcon value={value} disabled={disabled} charging={charging} />
<Tooltip
disabled={charging || !runtime || debug}
preferedDirection="left"
content=<Typography>{percentFormatter.format(value)}</Typography>
>
<div className="flex gap-2">
<div className="flex flex-col justify-around">
<BatteryIcon value={value} disabled={disabled} charging={charging} />
</div>
{((!charging || showVoltage) && (
<div className="w-15">
{!charging && runtime != null && runtime > 0 && (
<Typography color={textColor}>
{(runtime / BigInt(3600000000)).toString() +
'h ' +
(
(runtime % BigInt(3600000000)) /
BigInt(60000000)
).toString() +
'min'}
</Typography>
)}
{!charging && (!runtime || debug) && (
<Typography color={textColor}>
{percentFormatter.format(value)}
</Typography>
)}
{showVoltage && (
<Typography color={textColor}>
{voltageFormatter.format(voltage)}V
</Typography>
)}
</div>
)) || (
<div className="flex flex-col justify-center w-15">
<div className="w-5 h-1 bg-background-30 rounded-full" />
</div>
)}
</div>
{((!charging || showVoltage) && (
<div className="w-10">
{!charging && (
<Typography color={textColor}>
{percentFormatter.format(value)}
</Typography>
)}
{showVoltage && (
<Typography color={textColor}>
{voltageFormatter.format(voltage)}V
</Typography>
)}
</div>
)) || (
<div className="flex flex-col justify-center w-10">
<div className="w-5 h-1 bg-background-30 rounded-full" />
</div>
)}
</div>
</Tooltip>
);
}

View File

@@ -51,7 +51,9 @@ function TrackerBig({
<TrackerBattery
voltage={device.hardwareStatus.batteryVoltage}
value={device.hardwareStatus.batteryPctEstimate / 100}
runtime={device.hardwareStatus.batteryRuntimeEstimate}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
moreInfo={true}
/>
)}
<div className="flex gap-2">
@@ -119,6 +121,7 @@ function TrackerSmol({
<TrackerBattery
voltage={device.hardwareStatus.batteryVoltage}
value={device.hardwareStatus.batteryPctEstimate / 100}
runtime={device.hardwareStatus.batteryRuntimeEstimate}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
/>
)}

View File

@@ -199,7 +199,7 @@ export function TrackerSettingsPage() {
shakeHighlight={false}
/>
)}
{
{tracker?.device?.hardwareInfo?.hardwareIdentifier != 'Unknown' && (
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
<Typography
variant="section-title"
@@ -208,40 +208,53 @@ export function TrackerSettingsPage() {
Firmware version
</Typography>
<div className="flex gap-2 flex-col">
<div className="flex justify-between gap-2">
<Typography id="tracker-settings-build-date" />
<Typography
whitespace="whitespace-pre-wrap"
textAlign="text-end"
>
{tracker?.device?.hardwareInfo?.firmwareDate || '--'}
</Typography>
</div>
<div className="flex justify-between gap-2">
<Typography id="tracker-settings-current-version" />
<Typography
whitespace="whitespace-pre-wrap"
textAlign="text-end"
>
v{tracker?.device?.hardwareInfo?.firmwareVersion}
{tracker?.device?.hardwareInfo?.firmwareVersion
? `v${tracker?.device?.hardwareInfo?.firmwareVersion}`
: '--'}
</Typography>
</div>
<div className="flex justify-between gap-2">
<Typography id="tracker-settings-latest-version" />
{!updateUnavailable && (
<>
{currentFirmwareRelease && (
<Typography
color={
needUpdate === 'updated'
? undefined
: 'text-accent-background-10'
}
textAlign="text-end"
whitespace="whitespace-pre-wrap"
>
{currentFirmwareRelease.name}
</Typography>
)}
</>
)}
{updateUnavailable && (
<Typography id="tracker-settings-update-unavailable-v2">
No releases found
</Typography>
)}
</div>
{!!tracker?.device?.hardwareInfo?.officialBoardType && (
<div className="flex justify-between gap-2">
<Typography id="tracker-settings-latest-version" />
{!updateUnavailable && (
<>
{currentFirmwareRelease && (
<Typography
color={
needUpdate === 'updated'
? undefined
: 'text-accent-background-10'
}
textAlign="text-end"
whitespace="whitespace-pre-wrap"
>
{currentFirmwareRelease.name}
</Typography>
)}
</>
)}
{updateUnavailable && (
<Typography id="tracker-settings-update-unavailable-v2">
No releases found
</Typography>
)}
</div>
)}
</div>
{!updateUnavailable && (
<Tooltip
@@ -280,7 +293,7 @@ export function TrackerSettingsPage() {
</Tooltip>
)}
</div>
}
)}
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2 overflow-x-auto">
<div className="flex justify-between">
@@ -308,10 +321,11 @@ export function TrackerSettingsPage() {
<div className="flex justify-between">
<Typography>{l10n.getString('tracker-infos-url')}</Typography>
<Typography>
udp://
{IPv4.fromNumber(
tracker?.device?.hardwareInfo?.ipAddress?.addr || 0
).toString()}
{tracker?.device?.hardwareInfo?.ipAddress?.addr
? `udp://${IPv4.fromNumber(
tracker?.device?.hardwareInfo?.ipAddress?.addr || 0
).toString()}`
: '--'}
</Typography>
</div>
<div className="flex justify-between">
@@ -367,6 +381,37 @@ export function TrackerSettingsPage() {
{tracker?.device?.hardwareInfo?.networkProtocolVersion || '--'}
</Typography>
</div>
{tracker?.device?.hardwareStatus?.packetsReceived !== null && (
<>
<div className="flex justify-between">
<Typography>
{l10n.getString('tracker-infos-packet_loss')}
</Typography>
<Typography>
{(
(tracker?.device?.hardwareStatus?.packetLoss ?? 0) * 100
).toFixed(0)}
%
</Typography>
</div>
<div className="flex justify-between">
<Typography>
{l10n.getString('tracker-infos-packets_lost')}
</Typography>
<Typography>
{tracker?.device?.hardwareStatus?.packetsLost ?? '0'}
</Typography>
</div>
<div className="flex justify-between">
<Typography>
{l10n.getString('tracker-infos-packets_received')}
</Typography>
<Typography>
{tracker?.device?.hardwareStatus?.packetsReceived ?? '0'}
</Typography>
</div>
</>
)}
</div>
{tracker?.tracker && (
<IMUVisualizerWidget tracker={tracker?.tracker} />

View File

@@ -1,15 +1,24 @@
import { WifiIcon } from '@/components/commons/icon/WifiIcon';
import { Typography } from '@/components/commons/Typography';
import { Tooltip } from '@/components/commons/Tooltip';
export function TrackerWifi({
rssi,
ping,
rssiShowNumeric,
disabled,
packetLoss,
packetsLost,
packetsReceived,
showPacketLoss = false,
textColor = 'primary',
}: {
rssi: number | null;
ping: number | null;
packetLoss?: number | null;
packetsLost?: number | null;
packetsReceived?: number | null;
showPacketLoss?: boolean;
rssiShowNumeric?: boolean;
disabled?: boolean;
textColor?: string;
@@ -31,6 +40,17 @@ export function TrackerWifi({
{rssi} dBm
</Typography>
)}
{showPacketLoss && packetsReceived != null && (
<Tooltip
preferedDirection="top"
content={<Typography id="tracker-infos-packet_loss" />}
>
<Typography
color={textColor}
whitespace="whitespace-nowrap"
>{`${((packetLoss ?? 0) * 100).toFixed(0)}% (${packetsLost ?? 0} / ${packetsReceived})`}</Typography>
</Tooltip>
)}
</div>
)) || (
<div className="flex flex-col justify-center w-12">

View File

@@ -229,7 +229,9 @@ function Row({
<TrackerBattery
value={device.hardwareStatus.batteryPctEstimate / 100}
voltage={device.hardwareStatus.batteryVoltage}
runtime={device.hardwareStatus.batteryRuntimeEstimate}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
moreInfo={config?.devSettings.moreInfo}
textColor={fontColor}
/>
)}
@@ -243,6 +245,10 @@ function Row({
ping={device?.hardwareStatus?.ping}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
textColor={fontColor}
showPacketLoss
packetLoss={device.hardwareStatus.packetLoss}
packetsLost={device.hardwareStatus.packetsLost}
packetsReceived={device.hardwareStatus.packetsReceived}
/>
)}
</Cell>
@@ -327,20 +333,20 @@ export function TrackersTable({
const gridTemplateColumns = useMemo(() => {
const cols = [
'minmax(150px, 1.5fr)', // Name
'minmax(100px, 1fr)', // Type
'minmax(60px, 1fr)', // Battery
'6rem', // Ping (w-24)
'minmax(60px, 1fr)', // TPS
config?.devSettings?.preciseRotation ? '11rem' : '8rem', // Rotation
'minmax(60px, 1fr)', // Temp
'minmax(15rem, 1.5fr)', // Name
'9rem', // Type
'9rem', // Battery
'9rem', // Ping (w-24)
'5rem', // TPS
config?.devSettings?.preciseRotation ? '11rem' : '9rem', // Rotation
'9rem', // Temp
];
if (moreInfo) {
cols.push('9rem'); // Linear Acc
cols.push('9rem'); // Position
cols.push('9rem'); // Stay Aligned
cols.push('minmax(150px, 1fr)'); // URL
cols.push('11rem'); // URL
}
return cols.join(' ');

View File

@@ -109,7 +109,10 @@ const stepContentLookup: Record<
context: TrackingChecklistContext
) => JSX.Element
> = {
[TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]: (step, { toggle }) => {
[TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]: (
step,
{ toggleSession }
) => {
return (
<div className="space-y-2.5">
<Typography id="tracking_checklist-TRACKERS_REST_CALIBRATION-desc" />
@@ -118,7 +121,7 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
@@ -166,7 +169,7 @@ const stepContentLookup: Record<
</div>
);
},
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => {
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggleSession }) => {
return (
<>
<div className="space-y-2.5">
@@ -181,7 +184,7 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
@@ -195,7 +198,10 @@ const stepContentLookup: Record<
[TrackingChecklistStepId.UNASSIGNED_HMD]: () => {
return <Typography id="tracking_checklist-UNASSIGNED_HMD-desc" />;
},
[TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => {
[TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: (
step,
{ toggleSession }
) => {
const data = step.extraData as TrackingChecklistPublicNetworksT | null;
return (
<>
@@ -226,7 +232,7 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
@@ -234,7 +240,7 @@ const stepContentLookup: Record<
</>
);
},
[TrackingChecklistStepId.VRCHAT_SETTINGS]: (step, { toggle }) => {
[TrackingChecklistStepId.VRCHAT_SETTINGS]: (step, { toggleSession }) => {
return (
<>
<div className="space-y-2.5">
@@ -249,7 +255,7 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
@@ -257,7 +263,7 @@ const stepContentLookup: Record<
</>
);
},
[TrackingChecklistStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => {
[TrackingChecklistStepId.MOUNTING_CALIBRATION]: (step, { toggleSession }) => {
return (
<div className="space-y-2.5">
<Typography id="onboarding-automatic_mounting-mounting_reset-step-0" />
@@ -275,14 +281,17 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
</div>
);
},
[TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: (step, { toggle }) => {
[TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: (
step,
{ toggleSession }
) => {
return (
<div className="space-y-2.5">
<Typography id="onboarding-automatic_mounting-mounting_reset-feet-step-0" />
@@ -309,14 +318,17 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>
</div>
);
},
[TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => {
[TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (
step,
{ toggleSession }
) => {
return (
<>
<div className="space-y-2.5">
@@ -332,7 +344,7 @@ const stepContentLookup: Record<
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggle(step.id)}
onClick={() => toggleSession(step.id)}
/>
)}
</div>

View File

@@ -129,9 +129,6 @@ export function checkForUpdate(
if (
!device.hardwareInfo?.officialBoardType ||
![BoardType.SLIMEVR, BoardType.SLIMEVR_V1_2].includes(
device.hardwareInfo.officialBoardType
) ||
!semver.valid(currentFirmwareRelease.version) ||
!semver.valid(device.hardwareInfo.firmwareVersion?.toString() ?? 'none')
) {
@@ -143,6 +140,14 @@ export function checkForUpdate(
currentFirmwareRelease.version
);
if (
![BoardType.SLIMEVR, BoardType.SLIMEVR_V1_2].includes(
device.hardwareInfo.officialBoardType
)
) {
return canUpdate ? 'unavailable' : 'updated';
}
if (
canUpdate &&
device.hardwareStatus?.batteryPctEstimate != null &&

View File

@@ -27,13 +27,16 @@ export const BODY_PARTS_GROUPS: Record<MountingResetGroup, BodyPart[]> = {
fingers: FINGER_BODY_PARTS,
};
export function useReset(options: UseResetOptions, onReseted?: () => void) {
export function useReset(
options: UseResetOptions,
onReseted?: () => void,
onFailed?: () => void
) {
if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
const serverGuards = useAtomValue(serverGuardsAtom);
const { currentLocales } = useLocaleConfig();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const finishedTimeoutRef = useRef<NodeJS.Timeout>();
const [status, setStatus] = useState<ResetBtnStatus>('idle');
const [progress, setProgress] = useState(0);
@@ -62,6 +65,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
const onResetCanceled = () => {
if (status !== 'finished') setStatus('idle');
if (onFailed) onFailed();
};
useEffect(() => {

View File

@@ -92,8 +92,16 @@ export type Steps = {
visibleSteps: TrackingChecklistStep[];
ignoredSteps: TrackingChecklistStepId[];
};
const filterActive =
(ignoredSteps: TrackingChecklistStepId[]) => (step: TrackingChecklistStepT) =>
!ignoredSteps.includes(step.id) && step.enabled;
export function provideTrackingChecklist() {
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [sessionIgnoredSteps, setSessionIgnoredSteps] = useState<
TrackingChecklistStepId[]
>([]);
const [steps, setSteps] = useState<Steps>({
steps: [],
visibleSteps: [],
@@ -104,7 +112,7 @@ export function provideTrackingChecklist() {
RpcMessage.TrackingChecklistResponse,
(data: TrackingChecklistResponseT) => {
const activeSteps = data.steps.filter(
(step) => !data.ignoredSteps.includes(step.id) && step.enabled
filterActive([...data.ignoredSteps, ...sessionIgnoredSteps])
);
setSteps({
steps: data.steps,
@@ -161,26 +169,48 @@ export function provideTrackingChecklist() {
[steps]
);
const ignoreStep = (step: TrackingChecklistStepId, ignore: boolean) => {
const res = new IgnoreTrackingChecklistStepRequestT();
res.stepId = step;
res.ignore = ignore;
sendRPCPacket(RpcMessage.IgnoreTrackingChecklistStepRequest, res);
Sentry.metrics.count(ignore ? 'mute_checklist_step' : 'unmute_checklist_step', 1, {
attributes: { step: TrackingChecklistStepId[step] },
const ignoreStep = (
step: TrackingChecklistStepId,
ignore: boolean,
session = true
) => {
setSessionIgnoredSteps((curr) => {
if (ignore && !curr.includes(step)) return [...curr, step];
if (!ignore && curr.includes(step)) {
curr.splice(curr.indexOf(step), 1);
return curr;
}
return curr;
});
Sentry.metrics.count(ignore ? 'mute_checklist_step' : 'unmute_checklist_step', 1, {
attributes: { step: TrackingChecklistStepId[step], session },
});
if (session) {
// Force refresh of the flightlist when ignoring a step as the filtering
// is done only in one place to simplify the data flow
sendRPCPacket(
RpcMessage.TrackingChecklistRequest,
new TrackingChecklistRequestT()
);
} else {
const res = new IgnoreTrackingChecklistStepRequestT();
res.stepId = step;
res.ignore = ignore;
sendRPCPacket(RpcMessage.IgnoreTrackingChecklistStepRequest, res);
}
};
return {
...steps,
sessionIgnoredSteps,
firstRequired,
highlightedTrackers,
progress,
completion,
warnings,
ignoreStep,
toggle: (step: TrackingChecklistStepId) =>
ignoreStep(step, !steps.ignoredSteps.includes(step)),
toggleSession: (step: TrackingChecklistStepId) =>
ignoreStep(step, !sessionIgnoredSteps.includes(step)),
};
}

View File

@@ -1,62 +1,29 @@
-dontwarn java.awt.Color
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor
-dontwarn java.lang.management.ManagementFactory
-dontwarn java.lang.management.RuntimeMXBean
-dontwarn org.apache.logging.log4j.Level
-dontwarn org.apache.logging.log4j.LogManager
-dontwarn org.apache.logging.log4j.Logger
-dontwarn org.apache.logging.log4j.message.MessageFactory
-dontwarn org.apache.logging.log4j.spi.ExtendedLogger
-dontwarn org.apache.logging.log4j.spi.ExtendedLoggerWrapper
-dontwarn org.apache.log4j.Level
-dontwarn org.apache.log4j.Logger
-dontwarn org.apache.log4j.Priority
-dontwarn org.conscrypt.BufferAllocator
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.HandshakeListener
-dontwarn org.eclipse.jetty.npn.NextProtoNego$ClientProvider
-dontwarn org.eclipse.jetty.npn.NextProtoNego$Provider
-dontwarn org.eclipse.jetty.npn.NextProtoNego$ServerProvider
-dontwarn org.eclipse.jetty.npn.NextProtoNego
-dontwarn org.jetbrains.annotations.Async$Execute
-dontwarn org.jetbrains.annotations.Async$Schedule
-dontwarn reactor.blockhound.integration.BlockHoundIntegration
-keep class io.ktor.** { *; }
-keep class io.netty.** { *; }
-keep class kotlin.reflect.jvm.internal.** { *; }
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.atomicfu.**
-dontwarn java.beans.**
-dontwarn io.netty.**
-dontwarn com.typesafe.**
-dontwarn org.slf4j.**
-keep class io.netty.** { *; }
# Proguard configuration for Jackson 2.x
# Jackson 2.x
# https://github.com/FasterXML/jackson-docs/wiki/JacksonOnAndroid
#-keep class java.beans.** { *; }
#-dontwarn java.beans.**
#
#-keep class com.fasterxml.jackson.** { *; }
#-dontwarn com.fasterxml.jackson.databind.**
#
#-keep class com.github.jonpeterson.jackson.** { *; }
#
#-keepclassmembers class * {
# @com.fasterxml.jackson.annotation.* *;
#}
-keepattributes *Annotation*,Signature,EnclosingMethod
# Proguard configuration for SnakeYAML 2.X
#-keep class org.yaml.snakeyaml.** { *; }
#-dontwarn org.yaml.snakeyaml.**
-keep class com.fasterxml.jackson.**
-keepclassmembers class * {
@com.fasterxml.jackson.annotation.* *;
}
# Jackson 2.x Model Versioning Module
-keep class com.github.jonpeterson.jackson.**
# SnakeYAML 2.X
-keep class org.yaml.snakeyaml.**
# Don't mess with SlimeVR config, the class structure is essential for serialization
-keep class dev.slimevr.config.** { *; }
# Obfuscation is fine but it makes crash logs unreadable, we don't really need it for our app
-dontobfuscate
# Temporary measure to keep config functional, beware Jackson issues if removing!!
-dontoptimize

View File

@@ -8,10 +8,13 @@ import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.core.content.ContextCompat
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
@@ -91,7 +94,7 @@ class AndroidHIDManager(
}
fun checkConfigureDevice(usbDevice: UsbDevice, requestPermission: Boolean = false) {
if (usbDevice.vendorId == HID_TRACKER_RECEIVER_VID && usbDevice.productId == HID_TRACKER_RECEIVER_PID) {
if (usbDevice.vendorId == HID_TRACKER_RECEIVER_VID && (usbDevice.productId == HID_TRACKER_RECEIVER_PID || usbDevice.productId == HID_TRACKER_PID)) {
if (usbManager.hasPermission(usbDevice)) {
LogManager.info("[TrackerServer] Already have permission for ${usbDevice.deviceName}")
proceedWithDeviceConfiguration(usbDevice)
@@ -199,8 +202,9 @@ class AndroidHIDManager(
}
private fun deviceEnumerate(requestPermission: Boolean = false) {
val trackersOverHID: Boolean = VRServer.instance.configManager.vrConfig.hidConfig.trackersOverHID
val hidDeviceList: MutableList<UsbDevice> = usbManager.deviceList.values.filter {
it.vendorId == HID_TRACKER_RECEIVER_VID && it.productId == HID_TRACKER_RECEIVER_PID
it.vendorId == HID_TRACKER_RECEIVER_VID && (it.productId == HID_TRACKER_RECEIVER_PID || (trackersOverHID && it.productId == HID_TRACKER_PID))
}.toMutableList()
synchronized(devicesByHID) {
// Work on devicesByHid and add/remove as necessary

View File

@@ -3,6 +3,7 @@ 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
@@ -37,6 +38,11 @@ class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
.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)
@@ -63,6 +69,12 @@ class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
config.mountingResetDelay,
)
FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting(
RESET_SOURCE_NAME,
config.feetMountingResetDelay,
TrackerUtils.feetsBodyParts,
)
PAUSE_TRACKING ->
server
.scheduleTogglePauseTracking(
@@ -78,6 +90,7 @@ class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
private const val FULL_RESET = 1
private const val YAW_RESET = 2
private const val MOUNTING_RESET = 3
private const val PAUSE_TRACKING = 4
private const val FEET_MOUNTING_RESET = 4
private const val PAUSE_TRACKING = 5
}
}

View File

@@ -339,6 +339,15 @@ public class CurrentVRConfigConverter implements VersionedModelConverter {
}
}
}
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);
}

View File

@@ -0,0 +1,7 @@
package dev.slimevr.config
import com.fasterxml.jackson.annotation.JsonIgnore
class HIDConfig {
var trackersOverHID = false
}

View File

@@ -8,6 +8,8 @@ public class KeybindingsConfig {
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;
@@ -16,6 +18,8 @@ public class KeybindingsConfig {
private long mountingResetDelay = 0L;
private long feetMountingResetDelay = 0L;
private long pauseTrackingDelay = 0L;
@@ -34,6 +38,10 @@ public class KeybindingsConfig {
return mountingResetBinding;
}
public String getFeetMountingResetBinding() {
return feetMountingResetBinding;
}
public String getPauseTrackingBinding() {
return pauseTrackingBinding;
}
@@ -62,6 +70,14 @@ public class KeybindingsConfig {
mountingResetDelay = delay;
}
public long getFeetMountingResetDelay() {
return feetMountingResetDelay;
}
public void setFeetMountingResetDelay(long delay) {
feetMountingResetDelay = delay;
}
public long getPauseTrackingDelay() {
return pauseTrackingDelay;
}

View File

@@ -10,8 +10,8 @@ import dev.slimevr.tracking.trackers.Tracker
import dev.slimevr.tracking.trackers.TrackerRole
@JsonVersionedModel(
currentVersion = "14",
defaultDeserializeToVersion = "14",
currentVersion = "15",
defaultDeserializeToVersion = "15",
toCurrentConverterClass = CurrentVRConfigConverter::class,
)
class VRConfig {
@@ -42,6 +42,8 @@ class VRConfig {
val stayAlignedConfig = StayAlignedConfig()
val hidConfig = HIDConfig()
@JsonDeserialize(using = TrackerConfigMapDeserializer::class)
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class)
private val trackers: MutableMap<String, TrackerConfig> = HashMap()

View File

@@ -1,49 +0,0 @@
package dev.slimevr.protocol;
import java.util.ArrayList;
import java.util.List;
public class ConnectionContext {
private final List<DataFeed> dataFeedList = new ArrayList<>();
private final List<Integer> subscribedTopics = new ArrayList<>();
private boolean useSerial = false;
private boolean useProvisioning = false;
private boolean useAutoBone = false;
public List<DataFeed> getDataFeedList() {
return dataFeedList;
}
public List<Integer> getSubscribedTopics() {
return subscribedTopics;
}
public boolean useSerial() {
return useSerial;
}
public void setUseSerial(boolean useSerial) {
this.useSerial = useSerial;
}
public boolean useAutoBone() {
return useAutoBone;
}
public void setUseAutoBone(boolean useAutoBone) {
this.useAutoBone = useAutoBone;
}
public boolean useProvisioning() {
return useProvisioning;
}
public void setUseProvisioning(boolean useProvisioning) {
this.useProvisioning = useProvisioning;
}
}

View File

@@ -0,0 +1,10 @@
package dev.slimevr.protocol
class ConnectionContext {
val dataFeedList: MutableList<DataFeed> = mutableListOf()
val subscribedTopics: MutableList<Int> = mutableListOf()
var useSerial: Boolean = false
var useProvisioning: Boolean = false
var useAutoBone: Boolean = false
}

View File

@@ -1,26 +0,0 @@
package dev.slimevr.protocol;
import solarxr_protocol.data_feed.DataFeedConfigT;
public class DataFeed {
private DataFeedConfigT config;
private Long timeLastSent;
public DataFeed(DataFeedConfigT config) {
this.config = config;
this.timeLastSent = System.currentTimeMillis();
}
public DataFeedConfigT getConfig() {
return config;
}
public Long getTimeLastSent() {
return timeLastSent;
}
public void setTimeLastSent(Long timeLastSent) {
this.timeLastSent = timeLastSent;
}
}

View File

@@ -0,0 +1,7 @@
package dev.slimevr.protocol
import solarxr_protocol.data_feed.DataFeedConfigT
class DataFeed(val config: DataFeedConfigT) {
var timeLastSent: Long = System.currentTimeMillis()
}

View File

@@ -1,14 +0,0 @@
package dev.slimevr.protocol;
import java.nio.ByteBuffer;
import java.util.UUID;
public interface GenericConnection {
UUID getConnectionId();
ConnectionContext getContext();
void send(ByteBuffer bytes);
}

View File

@@ -0,0 +1,12 @@
package dev.slimevr.protocol
import java.nio.ByteBuffer
import java.util.UUID
interface GenericConnection {
val connectionId: UUID
val context: ConnectionContext
fun send(bytes: ByteBuffer)
}

View File

@@ -1,69 +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 solarxr_protocol.data_feed.DataFeedMessageHeader;
import solarxr_protocol.pub_sub.PubSubHeader;
import solarxr_protocol.rpc.RpcMessageHeader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ProtocolAPI {
public final VRServer server;
public final RPCHandler rpcHandler;
public final DataFeedHandler dataFeedHandler;
public final PubSubHandler pubSubHandler;
private final List<ProtocolAPIServer> servers = new ArrayList<>();
public ProtocolAPI(VRServer server) {
this.server = server;
this.rpcHandler = new RPCHandler(this);
this.dataFeedHandler = new DataFeedHandler(this);
this.pubSubHandler = new PubSubHandler(this);
}
public void onMessage(GenericConnection conn, ByteBuffer message) {
MessageBundle messageBundle = MessageBundle.getRootAsMessageBundle(message);
try {
for (int index = 0; index < messageBundle.dataFeedMsgsLength(); index++) {
DataFeedMessageHeader header = messageBundle.dataFeedMsgsVector().get(index);
this.dataFeedHandler.onMessage(conn, header);
}
for (int index = 0; index < messageBundle.rpcMsgsLength(); index++) {
RpcMessageHeader header = messageBundle.rpcMsgsVector().get(index);
this.rpcHandler.onMessage(conn, header);
}
for (int index = 0; index < messageBundle.pubSubMsgsLength(); index++) {
PubSubHeader header = messageBundle.pubSubMsgsVector().get(index);
this.pubSubHandler.onMessage(conn, header);
}
} catch (AssertionError e) {
// Catch flatbuffer errors
e.printStackTrace();
}
}
public List<ProtocolAPIServer> getAPIServers() {
return servers;
}
public void registerAPIServer(ProtocolAPIServer server) {
this.servers.add(server);
}
public void removeAPIServer(ProtocolAPIServer server) {
this.servers.remove(server);
}
}

View File

@@ -0,0 +1,47 @@
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<ProtocolAPIServer> = 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..<messageBundle.dataFeedMsgsLength()) {
val header = messageBundle.dataFeedMsgsVector().get(index)
this.dataFeedHandler.onMessage(conn, header)
}
for (index in 0..<messageBundle.rpcMsgsLength()) {
val header = messageBundle.rpcMsgsVector().get(index)
this.rpcHandler.onMessage(conn, header)
}
for (index in 0..<messageBundle.pubSubMsgsLength()) {
val header = messageBundle.pubSubMsgsVector().get(index)
this.pubSubHandler.onMessage(conn, header)
}
} catch (e: AssertionError) {
// Catch flatbuffer errors
e.printStackTrace()
}
}
fun registerAPIServer(server: ProtocolAPIServer) {
this.apiServers.add(server)
}
fun removeAPIServer(server: ProtocolAPIServer) {
this.apiServers.remove(server)
}
}

View File

@@ -1,9 +0,0 @@
package dev.slimevr.protocol;
import java.util.stream.Stream;
public interface ProtocolAPIServer {
Stream<GenericConnection> getAPIConnections();
}

View File

@@ -0,0 +1,7 @@
package dev.slimevr.protocol
import java.util.stream.Stream
interface ProtocolAPIServer {
val apiConnections: Stream<GenericConnection>
}

View File

@@ -1,403 +0,0 @@
package dev.slimevr.protocol.datafeed;
import com.google.flatbuffers.FlatBufferBuilder;
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.Bone;
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.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.ArrayList;
import java.util.List;
public class DataFeedBuilder {
public static int createHardwareInfo(FlatBufferBuilder fbb, Device device) {
int nameOffset = device.getFirmwareVersion() != null
? fbb.createString(device.getFirmwareVersion())
: 0;
int manufacturerOffset = device.getManufacturer() != null
? fbb.createString(device.getManufacturer())
: 0;
int hardwareIdentifierOffset = fbb.createString(device.getHardwareIdentifier());
HardwareInfo.startHardwareInfo(fbb);
HardwareInfo.addFirmwareVersion(fbb, nameOffset);
HardwareInfo.addManufacturer(fbb, manufacturerOffset);
HardwareInfo.addHardwareIdentifier(fbb, hardwareIdentifierOffset);
if (device instanceof UDPDevice udpDevice) {
var address = udpDevice.getIpAddress().getAddress();
HardwareInfo
.addIpAddress(
fbb,
Ipv4Address
.createIpv4Address(
fbb,
ByteBuffer.wrap(address).getInt()
)
);
HardwareInfo.addNetworkProtocolVersion(fbb, udpDevice.protocolVersion);
}
// BRUH MOMENT
// TODO need support: HardwareInfo.addHardwareRevision(fbb,
// hardwareRevisionOffset);
// TODO need support: HardwareInfo.addDisplayName(fbb, de);
HardwareInfo.addMcuId(fbb, device.getMcuType().getSolarType());
HardwareInfo.addOfficialBoardType(fbb, device.getBoardType().getSolarType());
return HardwareInfo.endHardwareInfo(fbb);
}
public static int createTrackerId(FlatBufferBuilder fbb, Tracker tracker) {
TrackerId.startTrackerId(fbb);
TrackerId.addTrackerNum(fbb, tracker.getTrackerNum());
if (tracker.getDevice() != null)
TrackerId.addDeviceId(fbb, DeviceId.createDeviceId(fbb, tracker.getDevice().getId()));
return TrackerId.endTrackerId(fbb);
}
public static int createVec3(FlatBufferBuilder fbb, Vector3 vec) {
return Vec3f
.createVec3f(
fbb,
vec.getX(),
vec.getY(),
vec.getZ()
);
}
public static int createQuat(FlatBufferBuilder fbb, Quaternion quaternion) {
return Quat
.createQuat(
fbb,
quaternion.getX(),
quaternion.getY(),
quaternion.getZ(),
quaternion.getW()
);
}
public static int createTrackerInfos(
FlatBufferBuilder fbb,
boolean infoMask,
Tracker tracker
) {
if (!infoMask)
return 0;
int displayNameOffset = fbb.createString(tracker.getDisplayName());
int customNameOffset = tracker.getCustomName() != null
? fbb.createString(tracker.getCustomName())
: 0;
TrackerInfo.startTrackerInfo(fbb);
if (tracker.getTrackerPosition() != null)
TrackerInfo.addBodyPart(fbb, tracker.getTrackerPosition().getBodyPart());
TrackerInfo.addEditable(fbb, tracker.getUserEditable());
TrackerInfo.addIsComputed(fbb, tracker.isComputed());
TrackerInfo.addDisplayName(fbb, displayNameOffset);
TrackerInfo.addCustomName(fbb, customNameOffset);
if (tracker.getImuType() != null) {
TrackerInfo.addImuType(fbb, tracker.getImuType().getSolarType());
}
// TODO need support: TrackerInfo.addPollRate(fbb, tracker.);
if (tracker.isImu()) {
TrackerInfo.addIsImu(fbb, true);
TrackerInfo
.addAllowDriftCompensation(
fbb,
tracker.getResetsHandler().getAllowDriftCompensation()
);
} else {
TrackerInfo.addIsImu(fbb, false);
TrackerInfo.addAllowDriftCompensation(fbb, false);
}
if (tracker.getAllowMounting()) {
Quaternion quaternion = tracker.getResetsHandler().getMountingOrientation();
Quaternion mountResetFix = tracker.getResetsHandler().getMountRotFix();
TrackerInfo.addMountingOrientation(fbb, createQuat(fbb, quaternion));
TrackerInfo.addMountingResetOrientation(fbb, createQuat(fbb, mountResetFix));
}
TrackerInfo.addMagnetometer(fbb, tracker.getMagStatus().getSolarType());
TrackerInfo.addIsHmd(fbb, tracker.isHmd());
TrackerInfo.addDataSupport(fbb, tracker.getTrackerDataType().getSolarType());
return TrackerInfo.endTrackerInfo(fbb);
}
public static int createTrackerPosition(FlatBufferBuilder fbb, Tracker tracker) {
return createVec3(fbb, tracker.getPosition());
}
public static int createTrackerRotation(FlatBufferBuilder fbb, Tracker tracker) {
return createQuat(fbb, tracker.getRawRotation());
}
public static int createTrackerAcceleration(FlatBufferBuilder fbb, Tracker tracker) {
return createVec3(fbb, tracker.getAcceleration());
}
public static int createTrackerMagneticVector(FlatBufferBuilder fbb, Tracker tracker) {
return createVec3(fbb, tracker.getMagVector());
}
public static int createTrackerTemperature(FlatBufferBuilder fbb, Tracker tracker) {
if (tracker.getTemperature() == null)
return 0;
return Temperature.createTemperature(fbb, tracker.getTemperature());
}
public static int createTrackerData(
FlatBufferBuilder fbb,
TrackerDataMaskT mask,
Tracker tracker
) {
int trackerInfosOffset = DataFeedBuilder.createTrackerInfos(fbb, mask.getInfo(), tracker);
int trackerIdOffset = DataFeedBuilder.createTrackerId(fbb, tracker);
int stayAlignedOffset = 0;
if (mask.getStayAligned()) {
stayAlignedOffset = DataFeedBuilderKotlin.INSTANCE
.createTrackerStayAlignedTracker(fbb, tracker.getStayAligned());
}
TrackerData.startTrackerData(fbb);
TrackerData.addTrackerId(fbb, trackerIdOffset);
if (trackerInfosOffset != 0)
TrackerData.addInfo(fbb, trackerInfosOffset);
if (mask.getStatus())
TrackerData.addStatus(fbb, tracker.getStatus().getId() + 1);
if (mask.getPosition() && tracker.getHasPosition())
TrackerData.addPosition(fbb, DataFeedBuilder.createTrackerPosition(fbb, tracker));
if (mask.getRotation() && tracker.getHasRotation())
TrackerData.addRotation(fbb, DataFeedBuilder.createTrackerRotation(fbb, tracker));
if (mask.getLinearAcceleration() && tracker.getHasAcceleration())
TrackerData
.addLinearAcceleration(
fbb,
DataFeedBuilder.createTrackerAcceleration(fbb, tracker)
);
if (mask.getTemp()) {
int trackerTemperatureOffset = DataFeedBuilder.createTrackerTemperature(fbb, tracker);
if (trackerTemperatureOffset != 0)
TrackerData.addTemp(fbb, trackerTemperatureOffset);
}
if (tracker.getAllowMounting() && tracker.getHasRotation()) {
if (mask.getRotationReferenceAdjusted()) {
TrackerData
.addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation()));
}
if (mask.getRotationIdentityAdjusted()) {
TrackerData
.addRotationIdentityAdjusted(
fbb,
createQuat(fbb, tracker.getIdentityAdjustedRotation())
);
}
} else if (tracker.getAllowReset() && tracker.getHasRotation()) {
if (mask.getRotationReferenceAdjusted()) {
TrackerData
.addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation()));
}
if (mask.getRotationIdentityAdjusted()) {
TrackerData
.addRotationIdentityAdjusted(fbb, createQuat(fbb, tracker.getRawRotation()));
}
}
if (mask.getTps()) {
TrackerData.addTps(fbb, (int) tracker.getTps());
}
if (mask.getRawMagneticVector() && tracker.getMagStatus() == MagnetometerStatus.ENABLED) {
TrackerData.addRawMagneticVector(fbb, createTrackerMagneticVector(fbb, tracker));
}
if (mask.getStayAligned()) {
TrackerData.addStayAligned(fbb, stayAlignedOffset);
}
return TrackerData.endTrackerData(fbb);
}
public static int createTrackersData(
FlatBufferBuilder fbb,
DeviceDataMaskT mask,
Device device
) {
if (mask.getTrackerData() == null)
return 0;
List<Integer> trackersOffsets = new ArrayList<>();
device
.getTrackers()
.forEach(
(key, value) -> trackersOffsets
.add(DataFeedBuilder.createTrackerData(fbb, mask.getTrackerData(), value))
);
DeviceData.startTrackersVector(fbb, trackersOffsets.size());
trackersOffsets.forEach(offset -> DeviceData.addTrackers(fbb, offset));
return fbb.endVector();
}
public static int createDeviceData(
FlatBufferBuilder fbb,
int id,
DeviceDataMaskT mask,
Device device
) {
if (!mask.getDeviceData())
return 0;
if (device.getTrackers().size() <= 0)
return 0;
Tracker firstTracker = device.getTrackers().get(0);
if (firstTracker == null) {
// Not actually the "first" tracker, but do we care?
firstTracker = device.getTrackers().entrySet().iterator().next().getValue();
}
Tracker tracker = firstTracker;
if (tracker == null)
return 0;
HardwareStatus.startHardwareStatus(fbb);
HardwareStatus.addErrorStatus(fbb, tracker.getStatus().getId());
if (tracker.getBatteryVoltage() != null) {
HardwareStatus.addBatteryVoltage(fbb, tracker.getBatteryVoltage());
}
if (tracker.getBatteryLevel() != null) {
HardwareStatus.addBatteryPctEstimate(fbb, (int) tracker.getBatteryLevel().floatValue());
}
if (tracker.getPing() != null) {
HardwareStatus.addPing(fbb, tracker.getPing());
}
if (tracker.getSignalStrength() != null) {
HardwareStatus.addRssi(fbb, (short) tracker.getSignalStrength().floatValue());
}
int hardwareDataOffset = HardwareStatus.endHardwareStatus(fbb);
int hardwareInfoOffset = DataFeedBuilder.createHardwareInfo(fbb, device);
int trackersOffset = DataFeedBuilder.createTrackersData(fbb, mask, device);
int nameOffset = device.getName() != null
? fbb.createString(device.getName())
: 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);
}
public static int createSyntheticTrackersData(
FlatBufferBuilder fbb,
TrackerDataMaskT trackerDataMaskT,
List<Tracker> trackers
) {
if (trackerDataMaskT == null)
return 0;
List<Integer> trackerOffsets = new ArrayList<>();
trackers
.forEach(
(tracker) -> trackerOffsets
.add(DataFeedBuilder.createTrackerData(fbb, trackerDataMaskT, tracker))
);
DataFeedUpdate.startSyntheticTrackersVector(fbb, trackerOffsets.size());
trackerOffsets.forEach((tracker -> DataFeedUpdate.addSyntheticTrackers(fbb, tracker)));
return fbb.endVector();
}
public static int createDevicesData(
FlatBufferBuilder fbb,
DeviceDataMaskT deviceDataMaskT,
List<Device> devices
) {
if (deviceDataMaskT == null)
return 0;
int[] devicesDataOffsets = new int[devices.size()];
for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i);
devicesDataOffsets[i] = DataFeedBuilder
.createDeviceData(fbb, device.getId(), deviceDataMaskT, device);
}
return DataFeedUpdate.createDevicesVector(fbb, devicesDataOffsets);
}
public static int createBonesData(
FlatBufferBuilder fbb,
boolean shouldSend,
List<dev.slimevr.tracking.processor.Bone> bones
) {
if (!shouldSend) {
return 0;
}
var boneOffsets = new int[bones.size()];
for (var i = 0; i < bones.size(); ++i) {
var bi = bones.get(i);
var headPosG = bi.getPosition();
var rotG = bi.getGlobalRotation();
var length = bi.getLength();
Bone.startBone(fbb);
var rotGOffset = createQuat(fbb, rotG);
Bone.addRotationG(fbb, rotGOffset);
var headPosGOffset = Vec3f
.createVec3f(fbb, headPosG.getX(), headPosG.getY(), headPosG.getZ());
Bone.addHeadPositionG(fbb, headPosGOffset);
Bone.addBodyPart(fbb, bi.getBoneType().bodyPart);
Bone.addBoneLength(fbb, length);
boneOffsets[i] = Bone.endBone(fbb);
}
return DataFeedUpdate.createBonesVector(fbb, boneOffsets);
}
}

View File

@@ -0,0 +1,506 @@
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<Int> = 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<Tracker>,
): Int {
if (trackerDataMaskT == null) return 0
val trackerOffsets: MutableList<Int> = 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<Device>,
): 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<Bone>,
): 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,
)

View File

@@ -1,50 +0,0 @@
package dev.slimevr.protocol.datafeed
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.guards.ServerGuards
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 solarxr_protocol.data_feed.stay_aligned.StayAlignedPose
import solarxr_protocol.data_feed.stay_aligned.StayAlignedTracker
object DataFeedBuilderKotlin {
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,
)
}

View File

@@ -1,192 +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.ProtocolHandler;
import dev.slimevr.tracking.trackers.Tracker;
import io.eiren.util.logging.LogManager;
import solarxr_protocol.MessageBundle;
import solarxr_protocol.data_feed.*;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
private final ProtocolAPI api;
public DataFeedHandler(ProtocolAPI api) {
this.api = api;
registerPacketListener(DataFeedMessage.StartDataFeed, this::onStartDataFeed);
registerPacketListener(DataFeedMessage.PollDataFeed, this::onPollDataFeedRequest);
this.api.server.addOnTick(this::sendDataFeedUpdate);
}
private void onStartDataFeed(GenericConnection conn, DataFeedMessageHeader header) {
StartDataFeed req = (StartDataFeed) header.message(new StartDataFeed());
if (req == null)
return;
int dataFeeds = req.dataFeedsLength();
List<DataFeed> feedList = conn.getContext().getDataFeedList();
synchronized (feedList) {
feedList.clear();
for (int i = 0; i < dataFeeds; i++) {
// Using the object api here because we
// need to copy from the buffer, anyway let's
// do it from here and send the reference to an arraylist
DataFeedConfigT config = req.dataFeeds(i).unpack();
feedList.add(new DataFeed(config));
}
}
}
private void onPollDataFeedRequest(
GenericConnection conn,
DataFeedMessageHeader messageHeader
) {
PollDataFeed req = (PollDataFeed) messageHeader.message(new PollDataFeed());
if (req == null)
return;
FlatBufferBuilder fbb = new FlatBufferBuilder(300);
int messageOffset = this.buildDatafeed(fbb, req.config().unpack(), 0);
DataFeedMessageHeader.startDataFeedMessageHeader(fbb);
DataFeedMessageHeader.addMessage(fbb, messageOffset);
DataFeedMessageHeader.addMessageType(fbb, DataFeedMessage.DataFeedUpdate);
int headerOffset = DataFeedMessageHeader.endDataFeedMessageHeader(fbb);
MessageBundle.startDataFeedMsgsVector(fbb, 1);
MessageBundle.addDataFeedMsgs(fbb, headerOffset);
int datafeedMessagesOffset = fbb.endVector();
int packet = createMessage(fbb, datafeedMessagesOffset);
fbb.finish(packet);
conn.send(fbb.dataBuffer());
}
public int buildDatafeed(FlatBufferBuilder fbb, DataFeedConfigT config, int index) {
int devicesOffset = DataFeedBuilder
.createDevicesData(
fbb,
config.getDataMask(),
this.api.server.deviceManager
.getDevices()
);
// Synthetic tracker is computed tracker apparently
int trackersOffset = DataFeedBuilder
.createSyntheticTrackersData(
fbb,
config.getSyntheticTrackersMask(),
this.api.server
.getAllTrackers()
.stream()
.filter(Tracker::isComputed)
.collect(Collectors.toList())
);
var h = this.api.server.humanPoseManager;
int bonesOffset = DataFeedBuilder
.createBonesData(
fbb,
config.getBoneMask(),
h.getAllBones()
);
int stayAlignedPoseOffset = 0;
if (config.getStayAlignedPoseMask()) {
stayAlignedPoseOffset = DataFeedBuilderKotlin.INSTANCE
.createStayAlignedPose(fbb, this.api.server.humanPoseManager.skeleton);
}
int serverGuardsOffset = 0;
if (config.getServerGuardsMask()) {
serverGuardsOffset = DataFeedBuilderKotlin.INSTANCE
.createServerGuard(fbb, this.api.server.getServerGuards());
}
return DataFeedUpdate
.createDataFeedUpdate(
fbb,
devicesOffset,
trackersOffset,
bonesOffset,
stayAlignedPoseOffset,
index,
serverGuardsOffset
);
}
public void sendDataFeedUpdate() {
long currTime = System.currentTimeMillis();
this.api.getAPIServers().forEach((server) -> server.getAPIConnections().forEach((conn) -> {
FlatBufferBuilder fbb = null;
List<DataFeed> feedList = conn.getContext().getDataFeedList();
synchronized (feedList) {
int configsCount = feedList.size();
int[] data = new int[configsCount];
for (int index = 0; index < configsCount; index++) {
DataFeed feed = feedList.get(index);
Long lastTimeSent = feed.getTimeLastSent();
DataFeedConfigT configT = feed.getConfig();
if (currTime - lastTimeSent > configT.getMinimumTimeSinceLast()) {
if (fbb == null) {
// That way we create a buffer only when needed
fbb = new FlatBufferBuilder(300);
}
int messageOffset = this.buildDatafeed(fbb, configT, index);
DataFeedMessageHeader.startDataFeedMessageHeader(fbb);
DataFeedMessageHeader.addMessage(fbb, messageOffset);
DataFeedMessageHeader.addMessageType(fbb, DataFeedMessage.DataFeedUpdate);
data[index] = DataFeedMessageHeader.endDataFeedMessageHeader(fbb);
feed.setTimeLastSent(currTime);
int messages = MessageBundle.createDataFeedMsgsVector(fbb, data);
int packet = createMessage(fbb, messages);
fbb.finish(packet);
conn.send(fbb.dataBuffer());
}
}
}
}));
}
@Override
public void onMessage(GenericConnection conn, DataFeedMessageHeader message) {
BiConsumer<GenericConnection, DataFeedMessageHeader> consumer = this.handlers[message
.messageType()];
if (consumer != null)
consumer.accept(conn, message);
else
LogManager
.info(
"[ProtocolAPI] Unhandled Datafeed packet received id: " + message.messageType()
);
}
@Override
public int messagesCount() {
return DataFeedMessage.names.length;
}
public int createMessage(FlatBufferBuilder fbb, int datafeedMessagesOffset) {
MessageBundle.startMessageBundle(fbb);
if (datafeedMessagesOffset > -1)
MessageBundle.addDataFeedMsgs(fbb, datafeedMessagesOffset);
return MessageBundle.endMessageBundle(fbb);
}
}

View File

@@ -0,0 +1,177 @@
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<DataFeedMessageHeader>() {
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..<dataFeeds) {
// Using the object api here because we
// need to copy from the buffer, anyway let's
// do it from here and send the reference to an arraylist
val config = req.dataFeeds(i).unpack()
feedList.add(DataFeed(config))
}
}
}
private fun onPollDataFeedRequest(
conn: GenericConnection,
messageHeader: DataFeedMessageHeader,
) {
val req = messageHeader.message(PollDataFeed()) as PollDataFeed? ?: return
val fbb = FlatBufferBuilder(300)
val messageOffset = this.buildDatafeed(fbb, req.config().unpack(), 0)
DataFeedMessageHeader.startDataFeedMessageHeader(fbb)
DataFeedMessageHeader.addMessage(fbb, messageOffset)
DataFeedMessageHeader.addMessageType(fbb, DataFeedMessage.DataFeedUpdate)
val headerOffset = DataFeedMessageHeader.endDataFeedMessageHeader(fbb)
MessageBundle.startDataFeedMsgsVector(fbb, 1)
MessageBundle.addDataFeedMsgs(fbb, headerOffset)
val datafeedMessagesOffset = fbb.endVector()
val packet = createMessage(fbb, datafeedMessagesOffset)
fbb.finish(packet)
conn.send(fbb.dataBuffer())
}
fun buildDatafeed(fbb: FlatBufferBuilder, config: DataFeedConfigT, index: Int): Int {
val devicesOffset = createDevicesData(
fbb,
config.dataMask,
this.api.server.deviceManager
.devices,
)
// Synthetic tracker is computed tracker apparently
val trackersOffset = createSyntheticTrackersData(
fbb,
config.syntheticTrackersMask,
this.api.server
.allTrackers
.stream()
.filter(Tracker::isComputed)
.collect(Collectors.toList()),
)
val h = this.api.server.humanPoseManager
val bonesOffset =
createBonesData(
fbb,
config.boneMask,
h.allBones.toMutableList(),
)
var stayAlignedPoseOffset = 0
if (config.stayAlignedPoseMask) {
stayAlignedPoseOffset = createStayAlignedPose(fbb, this.api.server.humanPoseManager.skeleton)
}
var serverGuardsOffset = 0
if (config.serverGuardsMask) {
serverGuardsOffset = createServerGuard(fbb, this.api.server.serverGuards)
}
return DataFeedUpdate
.createDataFeedUpdate(
fbb,
devicesOffset,
trackersOffset,
bonesOffset,
stayAlignedPoseOffset,
index,
serverGuardsOffset,
)
}
fun sendDataFeedUpdate() {
val currTime = System.currentTimeMillis()
this.api.apiServers.forEach(
Consumer { server: ProtocolAPIServer ->
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..<configsCount) {
val feed = feedList[index]
val lastTimeSent = feed.timeLastSent
val configT = feed.config
if (currTime - lastTimeSent > 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)
}
}

View File

@@ -1,45 +0,0 @@
package dev.slimevr.protocol.pubsub;
import solarxr_protocol.pub_sub.TopicIdT;
import java.util.Objects;
// 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
public class HashedTopicId {
private final TopicIdT inner;
private final int hashcode;
public HashedTopicId(TopicIdT topicIdT) {
this.inner = topicIdT;
this.hashcode = (inner.getAppName()
+ "."
+ inner.getOrganization()
+ "."
+ inner.getTopic()).hashCode();
}
public TopicIdT getInner() {
return inner;
}
@Override
public int hashCode() {
return hashcode;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
HashedTopicId that = (HashedTopicId) o;
return Objects.equals(inner.getOrganization(), that.getInner().getOrganization())
&& Objects.equals(inner.getAppName(), that.getInner().getAppName())
&& Objects.equals(inner.getTopic(), that.getInner().getTopic());
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -1,185 +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.ProtocolHandler;
import io.eiren.util.logging.LogManager;
import solarxr_protocol.MessageBundle;
import solarxr_protocol.pub_sub.*;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
public class PubSubHandler extends ProtocolHandler<PubSubHeader> {
private final ProtocolAPI api;
// Two ways maps for faster reading when handling lots of packets
public HashMap<HashedTopicId, Integer> topicsHandle = new HashMap<>();
public HashMap<Integer, HashedTopicId> handleTopics = new HashMap<>();
public AtomicInteger nextLocalHandle = new AtomicInteger();
public PubSubHandler(ProtocolAPI api) {
super();
this.api = api;
registerPacketListener(PubSubUnion.SubscriptionRequest, this::onSubscriptionRequest);
registerPacketListener(PubSubUnion.TopicHandleRequest, this::onTopicHandleRequest);
registerPacketListener(PubSubUnion.Message, this::onTopicMessage);
}
private int getTopicHandle(TopicIdT topicIdT) {
HashedTopicId hashedTopicId = new HashedTopicId(topicIdT);
Integer 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.put(hashedTopicId, handleT);
handleTopics.put(handleT, hashedTopicId);
}
return handleT;
}
public void onSubscriptionRequest(GenericConnection conn, PubSubHeader messageHeader) {
SubscriptionRequest req = (SubscriptionRequest) messageHeader.u(new SubscriptionRequest());
if (req == null)
return;
int subHandle = -1;
if (req.topicType() == Topic.TopicHandle) {
TopicHandle handle = (TopicHandle) req.topic(new TopicHandle());
if (handle != null && handleTopics.containsKey(handle.id()))
subHandle = handle.id();
} else if (req.topicType() == Topic.TopicId) {
TopicId topicId = (TopicId) req.topic(new TopicId());
if (topicId != null)
subHandle = getTopicHandle(topicId.unpack());
}
assert subHandle != -1;
final int finalSubHandle = subHandle;
Optional<Integer> first = conn
.getContext()
.getSubscribedTopics()
.stream()
.filter((handle) -> handle == finalSubHandle)
.findFirst();
if (!first.isPresent()) {
conn.getContext().getSubscribedTopics().add(finalSubHandle);
}
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int topicIdOffset = TopicId.pack(fbb, handleTopics.get(finalSubHandle).getInner());
int topicHandleOffset = TopicHandle.createTopicHandle(fbb, finalSubHandle);
int outbound = createMessage(
fbb,
PubSubUnion.TopicMapping,
TopicMapping.createTopicMapping(fbb, topicIdOffset, topicHandleOffset)
);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
}
public void onTopicHandleRequest(GenericConnection conn, PubSubHeader messageHeader) {
TopicHandleRequest req = (TopicHandleRequest) messageHeader.u(new TopicHandleRequest());
if (req == null)
return;
TopicHandleRequestT topicRequest = req.unpack();
int handle = getTopicHandle(topicRequest.getId());
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int topicIdOffset = TopicId.pack(fbb, topicRequest.getId());
int topicHandleOffset = TopicHandle.createTopicHandle(fbb, handle);
int outbound = createMessage(
fbb,
PubSubUnion.TopicMapping,
TopicMapping.createTopicMapping(fbb, topicIdOffset, topicHandleOffset)
);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
}
public void onTopicMessage(GenericConnection c, PubSubHeader messageHeader) {
Message req = (Message) messageHeader.u(new Message());
if (req == null)
return;
MessageT messageT = req.unpack();
int subHandle = 1;
if (messageT.getTopic().getType() == Topic.TopicHandle) {
subHandle = messageT.getTopic().asTopicHandle().getId();
} else if (messageT.getTopic().getType() == Topic.TopicId) {
subHandle = getTopicHandle(messageT.getTopic().asTopicId());
}
assert subHandle != -1;
int finalSubHandle = subHandle;
this.api.getAPIServers().forEach((server) -> {
server.getAPIConnections().forEach((conn) -> {
// Make sure that we are not sending a message to ourselves
// And check that the receiver has subscribed to the topic
if (
!conn.getConnectionId().equals(c.getConnectionId())
&& conn.getContext().getSubscribedTopics().contains(finalSubHandle)
) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int outbound = createMessage(
fbb,
PubSubUnion.Message,
Message.pack(fbb, messageT)
);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
}
});
});
}
@Override
public void onMessage(GenericConnection conn, PubSubHeader message) {
BiConsumer<GenericConnection, PubSubHeader> consumer = this.handlers[message.uType()];
if (consumer != null)
consumer.accept(conn, message);
else
LogManager
.info("[ProtocolAPI] Unhandled PubSub packet received id: " + message.uType());
}
@Override
public int messagesCount() {
return PubSubUnion.names.length;
}
public int createMessage(FlatBufferBuilder fbb, byte messageType, int messageOffset) {
int[] data = new int[1];
data[0] = PubSubHeader.createPubSubHeader(fbb, messageType, messageOffset);
int messages = MessageBundle.createPubSubMsgsVector(fbb, data);
MessageBundle.startMessageBundle(fbb);
MessageBundle.addPubSubMsgs(fbb, messages);
return MessageBundle.endMessageBundle(fbb);
}
}

View File

@@ -0,0 +1,170 @@
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<PubSubHeader>() {
// Two ways maps for faster reading when handling lots of packets
var topicsHandle: HashMap<HashedTopicId, Int> = HashMap()
var handleTopics: HashMap<Int, HashedTopicId> = 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)
}
}

View File

@@ -1,28 +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;
public class RPCBuilder {
public static int createSkeletonConfig(
FlatBufferBuilder fbb,
HumanPoseManager humanPoseManager
) {
int[] partsOffsets = new int[SkeletonConfigOffsets.values().length];
for (int index = 0; index < SkeletonConfigOffsets.values().length; index++) {
SkeletonConfigOffsets val = SkeletonConfigOffsets.values[index];
int part = SkeletonPart
.createSkeletonPart(fbb, val.id, humanPoseManager.getOffset(val));
partsOffsets[index] = part;
}
int parts = SkeletonConfigResponse.createSkeletonPartsVector(fbb, partsOffsets);
return SkeletonConfigResponse.createSkeletonConfigResponse(fbb, parts, 0);
}
}

View File

@@ -6,15 +6,15 @@ 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.DataFeedBuilder
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.RPCSettingsBuilder
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
@@ -460,7 +460,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
}
val tracker = api.server.getTrackerById(req.trackerId().unpack()) ?: return
val trackerId = DataFeedBuilder.createTrackerId(fbb, tracker)
val trackerId = createTrackerId(fbb, tracker)
val response = MagToggleResponse.createMagToggleResponse(
fbb,
trackerId,
@@ -499,7 +499,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
// 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 = DataFeedBuilder.createTrackerId(fbb, tracker)
val trackerId = createTrackerId(fbb, tracker)
val response = MagToggleResponse.createMagToggleResponse(
fbb,
trackerId,
@@ -516,7 +516,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
}
val fbb = FlatBufferBuilder(32)
val trackerId = DataFeedBuilder.createTrackerId(fbb, tracker)
val trackerId = createTrackerId(fbb, tracker)
val response = MagToggleResponse.createMagToggleResponse(
fbb,
trackerId,
@@ -607,7 +607,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
fun sendSettingsChangedResponse(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
val fbb = FlatBufferBuilder(32)
val settings = RPCSettingsBuilder.createSettingsResponse(fbb, api.server)
val settings = createSettingsResponse(fbb, api.server)
val outbound = createRPCMessage(fbb, RpcMessage.SettingsResponse, settings, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())

View File

@@ -50,8 +50,8 @@ class RPCAutoBoneHandler(
) {
val req = messageHeader
.message(AutoBoneProcessRequest()) as AutoBoneProcessRequest
if (conn.context.useAutoBone()) return
conn.context.setUseAutoBone(true)
if (conn.context.useAutoBone) return
conn.context.useAutoBone = true
api.server
.autoBoneHandler
.startProcessByType(getById(req.processType()))
@@ -67,7 +67,7 @@ class RPCAutoBoneHandler(
success: Boolean,
) {
forAllListeners { conn ->
if (!conn.context.useAutoBone()) {
if (!conn.context.useAutoBone) {
return@forAllListeners
}
@@ -95,7 +95,7 @@ class RPCAutoBoneHandler(
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
if (completed) {
conn.context.setUseAutoBone(false)
conn.context.useAutoBone = false
}
}
}
@@ -106,7 +106,7 @@ class RPCAutoBoneHandler(
override fun onAutoBoneEpoch(epoch: Epoch) {
forAllListeners { conn ->
if (!conn.context.useAutoBone()) {
if (!conn.context.useAutoBone) {
return@forAllListeners
}

View File

@@ -39,24 +39,39 @@ class RPCResetHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : ResetL
}
if (req.resetType() == ResetType.Yaw) {
if (bodyParts.isEmpty()) {
api.server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (resetsConfig.yawResetDelay * 1000).toLong())
val delay = if (req.hasDelay()) {
req.delay()
} else {
api.server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, (resetsConfig.yawResetDelay * 1000).toLong(), bodyParts.toList())
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) {
if (bodyParts.isEmpty()) {
api.server.scheduleResetTrackersFull(RESET_SOURCE_NAME, (resetsConfig.fullResetDelay * 1000).toLong())
val delay = if (req.hasDelay()) {
req.delay()
} else {
api.server.scheduleResetTrackersFull(RESET_SOURCE_NAME, (resetsConfig.fullResetDelay * 1000).toLong(), bodyParts.toList())
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) {
if (bodyParts.isEmpty()) {
api.server.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (resetsConfig.mountingResetDelay * 1000).toLong())
val delay = if (req.hasDelay()) {
req.delay()
} else {
api.server.scheduleResetTrackersMounting(RESET_SOURCE_NAME, (resetsConfig.mountingResetDelay * 1000).toLong(), bodyParts.toList())
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())
}
}
}
@@ -108,11 +123,11 @@ class RPCResetHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : ResetL
fun forAllListeners(action: Consumer<in GenericConnection?>?) {
this.api
.getAPIServers()
.apiServers
.forEach(
Consumer { server: ProtocolAPIServer? ->
server!!
.getAPIConnections()
Consumer { server: ProtocolAPIServer ->
server
.apiConnections
.forEach(action)
},
)

View File

@@ -1,87 +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.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;
public class RPCProvisioningHandler implements ProvisioningListener {
public RPCHandler rpcHandler;
public ProtocolAPI api;
public RPCProvisioningHandler(RPCHandler rpcHandler, ProtocolAPI api) {
this.rpcHandler = rpcHandler;
this.api = api;
rpcHandler
.registerPacketListener(
RpcMessage.StartWifiProvisioningRequest,
this::onStartWifiProvisioningRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.StopWifiProvisioningRequest,
this::onStopWifiProvisioningRequest
);
this.api.server.provisioningHandler.addListener(this);
}
public void onStartWifiProvisioningRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
StartWifiProvisioningRequest req = (StartWifiProvisioningRequest) messageHeader
.message(new StartWifiProvisioningRequest());
if (req == null)
return;
this.api.server.provisioningHandler.start(req.ssid(), req.password(), req.port());
conn.getContext().setUseProvisioning(true);
}
public void onStopWifiProvisioningRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
StopWifiProvisioningRequest req = (StopWifiProvisioningRequest) messageHeader
.message(new StopWifiProvisioningRequest());
if (req == null)
return;
conn.getContext().setUseProvisioning(false);
this.api.server.provisioningHandler.stop();
}
@Override
public void onProvisioningStatusChange(ProvisioningStatus status, SerialPort port) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
WifiProvisioningStatusResponse.startWifiProvisioningStatusResponse(fbb);
WifiProvisioningStatusResponse.addStatus(fbb, status.id);
int update = WifiProvisioningStatusResponse.endWifiProvisioningStatusResponse(fbb);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.WifiProvisioningStatusResponse, update);
fbb.finish(outbound);
this.forAllListeners((conn) -> conn.send(fbb.dataBuffer()));
}
private void forAllListeners(Consumer<? super GenericConnection> action) {
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.filter(conn -> conn.getContext().useProvisioning())
.forEach(action)
);
}
}

View File

@@ -0,0 +1,68 @@
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<in GenericConnection?>?) {
this.api
.apiServers
.forEach(
Consumer { server: ProtocolAPIServer ->
server
.apiConnections
.filter { conn: GenericConnection -> conn.context.useProvisioning }
.forEach(action)
},
)
}
}

View File

@@ -1,303 +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.rpc.RPCHandler;
import dev.slimevr.serial.SerialListener;
import dev.slimevr.serial.SerialPort;
import io.eiren.util.logging.LogManager;
import org.jetbrains.annotations.NotNull;
import solarxr_protocol.rpc.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
public class RPCSerialHandler implements SerialListener {
public RPCHandler rpcHandler;
public ProtocolAPI api;
public RPCSerialHandler(RPCHandler rpcHandler, ProtocolAPI api) {
this.rpcHandler = rpcHandler;
this.api = api;
rpcHandler
.registerPacketListener(
RpcMessage.SerialTrackerRebootRequest,
this::onSerialTrackerRebootRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.SerialTrackerGetInfoRequest,
this::onSerialTrackerGetInfoRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.SerialTrackerFactoryResetRequest,
this::onSerialTrackerFactoryResetRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.SerialTrackerGetWifiScanRequest,
this::onSerialTrackerGetWifiScanRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.SerialTrackerCustomCommandRequest,
this::onSerialTrackerCustomCommandRequest
);
rpcHandler.registerPacketListener(RpcMessage.SetWifiRequest, this::onSetWifiRequest);
rpcHandler.registerPacketListener(RpcMessage.OpenSerialRequest, this::onOpenSerialRequest);
rpcHandler
.registerPacketListener(RpcMessage.CloseSerialRequest, this::onCloseSerialRequest);
rpcHandler
.registerPacketListener(RpcMessage.SerialDevicesRequest, this::onRequestSerialDevices);
this.api.server.serialHandler.addListener(this);
}
@Override
public void onSerialDisconnected() {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
SerialUpdateResponse.startSerialUpdateResponse(fbb);
SerialUpdateResponse.addClosed(fbb, true);
int update = SerialUpdateResponse.endSerialUpdateResponse(fbb);
int outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update);
fbb.finish(outbound);
this.forAllListeners((conn) -> {
conn.send(fbb.dataBuffer());
conn.getContext().setUseSerial(false);
});
}
@Override
public void onSerialLog(String str, boolean server) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int logOffset = fbb.createString(str);
SerialUpdateResponse.startSerialUpdateResponse(fbb);
SerialUpdateResponse.addLog(fbb, logOffset);
int update = SerialUpdateResponse.endSerialUpdateResponse(fbb);
int outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update);
fbb.finish(outbound);
this.forAllListeners((conn) -> {
conn.send(fbb.dataBuffer());
});
}
@Override
public void onNewSerialDevice(SerialPort port) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int portOffset = fbb.createString(port.getPortLocation());
int nameOffset = fbb.createString(port.getDescriptivePortName());
int deviceOffset = SerialDevice.createSerialDevice(fbb, portOffset, nameOffset);
int newSerialOffset = NewSerialDeviceResponse
.createNewSerialDeviceResponse(fbb, deviceOffset);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.NewSerialDeviceResponse, newSerialOffset);
fbb.finish(outbound);
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.forEach((conn) -> {
conn.send(fbb.dataBuffer());
})
);
}
@Override
public void onSerialConnected(SerialPort port) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
SerialUpdateResponse.startSerialUpdateResponse(fbb);
SerialUpdateResponse.addClosed(fbb, false);
int update = SerialUpdateResponse.endSerialUpdateResponse(fbb);
int outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update);
fbb.finish(outbound);
this.forAllListeners((conn) -> {
conn.send(fbb.dataBuffer());
});
}
public void onSerialTrackerRebootRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
SerialTrackerRebootRequest req = (SerialTrackerRebootRequest) messageHeader
.message(new SerialTrackerRebootRequest());
if (req == null)
return;
this.api.server.serialHandler.rebootRequest();
}
public void onSerialTrackerGetInfoRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
SerialTrackerGetInfoRequest req = (SerialTrackerGetInfoRequest) messageHeader
.message(new SerialTrackerGetInfoRequest());
if (req == null)
return;
this.api.server.serialHandler.infoRequest();
}
public void onSerialTrackerFactoryResetRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
SerialTrackerFactoryResetRequest req = (SerialTrackerFactoryResetRequest) messageHeader
.message(new SerialTrackerFactoryResetRequest());
if (req == null)
return;
this.api.server.serialHandler.factoryResetRequest();
}
public void onSerialTrackerGetWifiScanRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
SerialTrackerGetWifiScanRequest req = (SerialTrackerGetWifiScanRequest) messageHeader
.message(new SerialTrackerGetWifiScanRequest());
if (req == null)
return;
this.api.server.serialHandler.wifiScanRequest();
}
public void onSerialTrackerCustomCommandRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
SerialTrackerCustomCommandRequest req = (SerialTrackerCustomCommandRequest) messageHeader
.message(new SerialTrackerCustomCommandRequest());
if (req == null || req.command() == null)
return;
this.api.server.serialHandler.customCommandRequest(Objects.requireNonNull(req.command()));
}
private void onRequestSerialDevices(GenericConnection conn, RpcMessageHeader messageHeader) {
SerialDevicesRequest req = (SerialDevicesRequest) messageHeader
.message(new SerialDevicesRequest());
if (req == null)
return;
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
List<Integer> devicesOffsets = new ArrayList<>();
try {
this.api.server.serialHandler.getKnownPorts().forEach((port) -> {
int portOffset = fbb.createString(port.getPortLocation());
int nameOffset = fbb.createString(port.getDescriptivePortName());
devicesOffsets.add(SerialDevice.createSerialDevice(fbb, portOffset, nameOffset));
});
} catch (Throwable e) {
LogManager.severe("Using serial ports is not supported on this platform", e);
}
SerialDevicesResponse.startDevicesVector(fbb, devicesOffsets.size());
devicesOffsets.forEach(offset -> SerialDevicesResponse.addDevices(fbb, offset));
int devices = fbb.endVector();
int serialDeviceOffsets = SerialDevicesResponse.createSerialDevicesResponse(fbb, devices);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.SerialDevicesResponse, serialDeviceOffsets);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
}
public void onSetWifiRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
SetWifiRequest req = (SetWifiRequest) messageHeader.message(new SetWifiRequest());
if (req == null)
return;
if (
req.password() == null
|| req.ssid() == null
|| !this.api.server.serialHandler.isConnected()
)
return;
this.api.server.serialHandler.setWifi(req.ssid(), req.password());
}
public void onOpenSerialRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
OpenSerialRequest req = (OpenSerialRequest) messageHeader.message(new OpenSerialRequest());
if (req == null)
return;
conn.getContext().setUseSerial(true);
this.api.server.queueTask(() -> {
try {
this.api.server.serialHandler.openSerial(req.port(), req.auto());
} catch (Exception e) {
LogManager.severe("Unable to open serial port", e);
} catch (Throwable e) {
LogManager.severe("Using serial ports is not supported on this platform", e);
}
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
SerialUpdateResponse.startSerialUpdateResponse(fbb);
SerialUpdateResponse.addClosed(fbb, !this.api.server.serialHandler.isConnected());
int update = SerialUpdateResponse.endSerialUpdateResponse(fbb);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
});
}
public void onCloseSerialRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
CloseSerialRequest req = (CloseSerialRequest) messageHeader
.message(new CloseSerialRequest());
if (req == null)
return;
conn.getContext().setUseSerial(false);
this.api.server.serialHandler.closeSerial();
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
SerialUpdateResponse.startSerialUpdateResponse(fbb);
SerialUpdateResponse.addClosed(fbb, !this.api.server.serialHandler.isConnected());
int update = SerialUpdateResponse.endSerialUpdateResponse(fbb);
int outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
}
public void forAllListeners(Consumer<? super GenericConnection> action) {
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.filter(conn -> conn.getContext().useSerial())
.forEach(action)
);
}
@Override
public void onSerialDeviceDeleted(@NotNull SerialPort port) {
}
}

View File

@@ -0,0 +1,263 @@
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<Int> = 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<in GenericConnection?>?) {
this.api
.apiServers
.forEach(
Consumer { server: ProtocolAPIServer ->
server
.apiConnections
.filter { conn: GenericConnection -> conn.context.useSerial }
.forEach(action)
},
)
}
override fun onSerialDeviceDeleted(port: SerialPort) {
}
}

View File

@@ -1,418 +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.*;
import dev.slimevr.filtering.TrackerFilters;
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.*;
import solarxr_protocol.rpc.settings.*;
public class RPCSettingsBuilder {
public static int createOSCRouterSettings(
FlatBufferBuilder fbb,
OSCConfig config
) {
int addressStringOffset = fbb.createString(config.getAddress());
int oscSettingOffset = OSCSettings
.createOSCSettings(
fbb,
config.getEnabled(),
config.getPortIn(),
config.getPortOut(),
addressStringOffset
);
OSCRouterSettings.startOSCRouterSettings(fbb);
OSCRouterSettings.addOscSettings(fbb, oscSettingOffset);
return OSCRouterSettings.endOSCRouterSettings(fbb);
}
public static int createVRCOSCSettings(
FlatBufferBuilder fbb,
VRCOSCConfig config
) {
int addressStringOffset = fbb.createString(config.getAddress());
int generalSettingOffset = OSCSettings
.createOSCSettings(
fbb,
config.getEnabled(),
config.getPortIn(),
config.getPortOut(),
addressStringOffset
);
int 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.getOscqueryEnabled());
return VRCOSCSettings.endVRCOSCSettings(fbb);
}
public static int createVMCOSCSettings(
FlatBufferBuilder fbb,
VMCConfig config
) {
int addressStringOffset = fbb.createString(config.getAddress());
int generalSettingOffset = OSCSettings
.createOSCSettings(
fbb,
config.getEnabled(),
config.getPortIn(),
config.getPortOut(),
addressStringOffset
);
String vrmJson = config.getVrmJson();
int 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.getAnchorHip());
VMCOSCSettings.addMirrorTracking(fbb, config.getMirrorTracking());
return VMCOSCSettings.endVMCOSCSettings(fbb);
}
public static int createFilterSettings(
FlatBufferBuilder fbb,
FiltersConfig filtersConfig
) {
return FilteringSettings
.createFilteringSettings(
fbb,
TrackerFilters.getByConfigkey(filtersConfig.getType()).getId(),
filtersConfig.getAmount()
);
}
public static int createDriftCompensationSettings(
FlatBufferBuilder fbb,
DriftCompensationConfig driftCompensationConfig
) {
return DriftCompensationSettings
.createDriftCompensationSettings(
fbb,
driftCompensationConfig.getEnabled(),
driftCompensationConfig.getPrediction(),
driftCompensationConfig.getAmount(),
driftCompensationConfig.getMaxResets()
);
}
public static int createTapDetectionSettings(
FlatBufferBuilder fbb,
TapDetectionConfig tapDetectionConfig
) {
return TapDetectionSettings
.createTapDetectionSettings(
fbb,
tapDetectionConfig.getFullResetDelay(),
tapDetectionConfig.getFullResetEnabled(),
tapDetectionConfig.getFullResetTaps(),
tapDetectionConfig.getYawResetDelay(),
tapDetectionConfig.getYawResetEnabled(),
tapDetectionConfig.getYawResetTaps(),
tapDetectionConfig.getMountingResetDelay(),
tapDetectionConfig.getMountingResetEnabled(),
tapDetectionConfig.getMountingResetTaps(),
tapDetectionConfig.getSetupMode(),
tapDetectionConfig.getNumberTrackersOverThreshold()
);
}
public static int createSteamVRSettings(FlatBufferBuilder fbb, ISteamVRBridge bridge) {
int 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;
}
public static int createModelSettings(
FlatBufferBuilder fbb,
HumanPoseManager humanPoseManager,
LegTweaksConfig legTweaksConfig,
SkeletonConfig skeletonConfig
) {
int 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)
);
int 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)
);
int legTweaksOffset = LegTweaksSettings
.createLegTweaksSettings(
fbb,
legTweaksConfig.getCorrectionStrength()
);
int skeletonConfigOffset = SkeletonHeight
.createSkeletonHeight(
fbb,
skeletonConfig.getHmdHeight(),
skeletonConfig.getFloorHeight()
);
return ModelSettings
.createModelSettings(
fbb,
togglesOffset,
ratiosOffset,
legTweaksOffset,
skeletonConfigOffset
);
}
public static int createAutoBoneSettings(FlatBufferBuilder fbb, AutoBoneConfig autoBoneConfig) {
return AutoBoneSettings
.createAutoBoneSettings(
fbb,
autoBoneConfig.getCursorIncrement(),
autoBoneConfig.getMinDataDistance(),
autoBoneConfig.getMaxDataDistance(),
autoBoneConfig.getNumEpochs(),
autoBoneConfig.getPrintEveryNumEpochs(),
autoBoneConfig.getInitialAdjustRate(),
autoBoneConfig.getAdjustRateDecay(),
autoBoneConfig.getSlideErrorFactor(),
autoBoneConfig.getOffsetSlideErrorFactor(),
autoBoneConfig.getFootHeightOffsetErrorFactor(),
autoBoneConfig.getBodyProportionErrorFactor(),
autoBoneConfig.getHeightErrorFactor(),
autoBoneConfig.getPositionErrorFactor(),
autoBoneConfig.getPositionOffsetErrorFactor(),
autoBoneConfig.getCalcInitError(),
autoBoneConfig.getRandomizeFrameOrder(),
autoBoneConfig.getScaleEachStep(),
autoBoneConfig.getSampleCount(),
autoBoneConfig.getSampleRateMs(),
autoBoneConfig.getSaveRecordings(),
autoBoneConfig.getUseSkeletonHeight(),
autoBoneConfig.getRandSeed()
);
}
/**
* 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.
*/
public static AutoBoneConfig readAutoBoneSettings(
AutoBoneSettings autoBoneSettings,
AutoBoneConfig autoBoneConfig
) {
if (autoBoneSettings.hasCursorIncrement()) {
autoBoneConfig.setCursorIncrement(autoBoneSettings.cursorIncrement());
}
if (autoBoneSettings.hasMinDataDistance()) {
autoBoneConfig.setMinDataDistance(autoBoneSettings.minDataDistance());
}
if (autoBoneSettings.hasMaxDataDistance()) {
autoBoneConfig.setMaxDataDistance(autoBoneSettings.maxDataDistance());
}
if (autoBoneSettings.hasNumEpochs()) {
autoBoneConfig.setNumEpochs(autoBoneSettings.numEpochs());
}
if (autoBoneSettings.hasPrintEveryNumEpochs()) {
autoBoneConfig.setPrintEveryNumEpochs(autoBoneSettings.printEveryNumEpochs());
}
if (autoBoneSettings.hasInitialAdjustRate()) {
autoBoneConfig.setInitialAdjustRate(autoBoneSettings.initialAdjustRate());
}
if (autoBoneSettings.hasAdjustRateDecay()) {
autoBoneConfig.setAdjustRateDecay(autoBoneSettings.adjustRateDecay());
}
if (autoBoneSettings.hasSlideErrorFactor()) {
autoBoneConfig.setSlideErrorFactor(autoBoneSettings.slideErrorFactor());
}
if (autoBoneSettings.hasOffsetSlideErrorFactor()) {
autoBoneConfig.setOffsetSlideErrorFactor(autoBoneSettings.offsetSlideErrorFactor());
}
if (autoBoneSettings.hasFootHeightOffsetErrorFactor()) {
autoBoneConfig
.setFootHeightOffsetErrorFactor(autoBoneSettings.footHeightOffsetErrorFactor());
}
if (autoBoneSettings.hasBodyProportionErrorFactor()) {
autoBoneConfig
.setBodyProportionErrorFactor(autoBoneSettings.bodyProportionErrorFactor());
}
if (autoBoneSettings.hasHeightErrorFactor()) {
autoBoneConfig.setHeightErrorFactor(autoBoneSettings.heightErrorFactor());
}
if (autoBoneSettings.hasPositionErrorFactor()) {
autoBoneConfig.setPositionErrorFactor(autoBoneSettings.positionErrorFactor());
}
if (autoBoneSettings.hasPositionOffsetErrorFactor()) {
autoBoneConfig
.setPositionOffsetErrorFactor(autoBoneSettings.positionOffsetErrorFactor());
}
if (autoBoneSettings.hasCalcInitError()) {
autoBoneConfig.setCalcInitError(autoBoneSettings.calcInitError());
}
if (autoBoneSettings.hasRandomizeFrameOrder()) {
autoBoneConfig.setRandomizeFrameOrder(autoBoneSettings.randomizeFrameOrder());
}
if (autoBoneSettings.hasScaleEachStep()) {
autoBoneConfig.setScaleEachStep(autoBoneSettings.scaleEachStep());
}
if (autoBoneSettings.hasSampleCount()) {
autoBoneConfig.setSampleCount(autoBoneSettings.sampleCount());
}
if (autoBoneSettings.hasSampleRateMs()) {
autoBoneConfig.setSampleRateMs(autoBoneSettings.sampleRateMs());
}
if (autoBoneSettings.hasSaveRecordings()) {
autoBoneConfig.setSaveRecordings(autoBoneSettings.saveRecordings());
}
if (autoBoneSettings.hasUseSkeletonHeight()) {
autoBoneConfig.setUseSkeletonHeight(autoBoneSettings.useSkeletonHeight());
}
if (autoBoneSettings.hasRandSeed()) {
autoBoneConfig.setRandSeed(autoBoneSettings.randSeed());
}
return autoBoneConfig;
}
public static int createArmsResetModeSettings(
FlatBufferBuilder fbb,
ResetsConfig resetsConfig
) {
return ResetsSettings
.createResetsSettings(
fbb,
resetsConfig.getResetMountingFeet(),
resetsConfig.getMode().getId(),
resetsConfig.getYawResetSmoothTime(),
resetsConfig.getSaveMountingReset(),
resetsConfig.getResetHmdPitch()
);
}
public static int createSettingsResponse(FlatBufferBuilder fbb, VRServer server) {
ISteamVRBridge bridge = server.getVRBridge(ISteamVRBridge.class);
return SettingsResponse
.createSettingsResponse(
fbb,
RPCSettingsBuilder.createSteamVRSettings(fbb, bridge),
RPCSettingsBuilder
.createFilterSettings(
fbb,
server.configManager.getVrConfig().getFilters()
),
RPCSettingsBuilder
.createDriftCompensationSettings(
fbb,
server.configManager.getVrConfig().getDriftCompensation()
),
RPCSettingsBuilder
.createOSCRouterSettings(
fbb,
server.configManager.getVrConfig().getOscRouter()
),
RPCSettingsBuilder
.createVRCOSCSettings(
fbb,
server.configManager.getVrConfig().getVrcOSC()
),
RPCSettingsBuilder
.createVMCOSCSettings(
fbb,
server.configManager.getVrConfig().getVMC()
),
RPCSettingsBuilder
.createModelSettings(
fbb,
server.humanPoseManager,
server.configManager.getVrConfig().getLegTweaks(),
server.configManager.getVrConfig().getSkeleton()
),
RPCSettingsBuilder
.createTapDetectionSettings(
fbb,
server.configManager.getVrConfig().getTapDetection()
),
RPCSettingsBuilder
.createAutoBoneSettings(
fbb,
server.configManager.getVrConfig().getAutoBone()
),
RPCSettingsBuilder
.createArmsResetModeSettings(
fbb,
server.configManager.getVrConfig().getResetsConfig()
),
RPCSettingsBuilderKotlin.INSTANCE
.createStayAlignedSettings(
fbb,
server.configManager.getVrConfig().getStayAlignedConfig()
)
);
}
}

View File

@@ -0,0 +1,458 @@
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,
)

View File

@@ -1,32 +0,0 @@
package dev.slimevr.protocol.rpc.settings
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.config.StayAlignedConfig
import solarxr_protocol.rpc.StayAlignedSettings
object RPCSettingsBuilderKotlin {
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,
)
}

View File

@@ -18,24 +18,9 @@ import kotlin.math.*
class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
init {
rpcHandler.registerPacketListener(RpcMessage.SettingsRequest) { conn: GenericConnection, messageHeader: RpcMessageHeader? ->
this.onSettingsRequest(
conn,
messageHeader,
)
}
rpcHandler
.registerPacketListener(
RpcMessage.ChangeSettingsRequest,
) { conn: GenericConnection?, messageHeader: RpcMessageHeader ->
this.onChangeSettingsRequest(
conn,
messageHeader,
)
}
rpcHandler.registerPacketListener(RpcMessage.SettingsResetRequest) { conn: GenericConnection, messageHeader: RpcMessageHeader? ->
this.onSettingsResetRequest(conn, messageHeader)
}
rpcHandler.registerPacketListener(RpcMessage.SettingsRequest, ::onSettingsRequest)
rpcHandler.registerPacketListener(RpcMessage.ChangeSettingsRequest, ::onChangeSettingsRequest)
rpcHandler.registerPacketListener(RpcMessage.SettingsResetRequest, ::onSettingsResetRequest)
}
fun onSettingsRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
@@ -331,7 +316,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
.vrConfig
.autoBone
RPCSettingsBuilder.readAutoBoneSettings(autoBoneSettings, autoBoneConfig)
readAutoBoneSettings(autoBoneSettings, autoBoneConfig)
}
if (req.resetsSettings() != null) {
@@ -369,6 +354,12 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
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()
}
@@ -385,7 +376,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
val settings = SettingsResponse
.createSettingsResponse(
fbb,
RPCSettingsBuilder.createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
)
val outbound =
rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings)

View File

@@ -3,7 +3,7 @@ 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.DataFeedBuilder
import dev.slimevr.protocol.datafeed.createTrackerId
import dev.slimevr.protocol.rpc.RPCHandler
import dev.slimevr.setup.TapSetupListener
import dev.slimevr.tracking.trackers.Tracker
@@ -20,7 +20,7 @@ class RPCTapSetupHandler(
override fun onStarted(tracker: Tracker) {
val fbb = FlatBufferBuilder(32)
val idOffset = DataFeedBuilder.createTrackerId(fbb, tracker)
val idOffset = createTrackerId(fbb, tracker)
val update = TapDetectionSetupNotification.createTapDetectionSetupNotification(fbb, idOffset)
val outbound =
rpcHandler.createRPCMessage(fbb, RpcMessage.TapDetectionSetupNotification, update)

View File

@@ -36,7 +36,7 @@ public class ProvisioningHandler implements SerialListener {
this.provisioningTickTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (!isRunning)
if (!isRunning || provisioningStatus == ProvisioningStatus.DONE)
return;
provisioningTick();
}

View File

@@ -30,14 +30,18 @@ abstract class SerialHandler {
Pair(0x1A86, 0x7523),
// CH341
Pair(0x1A86, 0x5523),
// CH343
Pair(0x1A86, 0x55D3),
// CH9102x
Pair(0x1A86, 0x55D4),
// / Silabs
// CP210x
Pair(0x10C4, 0xEA60),
// / Espressif
// ESP32-C3
// 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

View File

@@ -13,6 +13,7 @@ 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<Int, Tracker> = ConcurrentHashMap()

View File

@@ -112,9 +112,14 @@ class Tracker @JvmOverloads constructor(
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

View File

@@ -24,8 +24,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
Math.PI.toFloat(),
0f,
).toQuaternion()
private val QuarterPitch = Quaternion.rotationAroundXAxis(FastMath.HALF_PI)
private var driftAmount = 0f
private var averagedDriftQuat = Quaternion.IDENTITY
private var rotationSinceReset = Quaternion.IDENTITY
@@ -54,6 +52,16 @@ class TrackerResetsHandler(val tracker: Tracker) {
// 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
@@ -180,9 +188,12 @@ class TrackerResetsHandler(val tracker: Tracker) {
/**
* Get the reference adjusted accel.
*/
// All IMU axis corrections are inverse to undo `adjustToReference` after local yaw offsets are added
// Order is VERY important here! Please be extremely careful! >~>
fun getReferenceAdjustedAccel(rawRot: Quaternion, accel: Vector3): Vector3 = (adjustToReference(rawRot) * (attachmentFix * mountingOrientation * mountRotFix * tposeDownFix).inv()).sandwich(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
@@ -191,17 +202,22 @@ class TrackerResetsHandler(val tracker: Tracker) {
*/
private fun adjustToReference(rotation: Quaternion): Quaternion {
var rot = rotation
// Correct for global pitch/roll offset
rot *= attachmentFix
// Correct for global yaw offset without affecting local yaw so we can change this
// later without invalidating local yaw offset corrections
// Align heading axis with bone space
if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) {
rot = mountingOrientation.inv() * rot * mountingOrientation
rot *= mountingOrientation
}
rot = mountRotFix.inv() * rot * mountRotFix
// T-pose global correction
// 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
// Align local yaw with reference
// More heading correction
rot = yawFix * rot
rot = constraintFix * rot
return rot
@@ -211,6 +227,8 @@ class TrackerResetsHandler(val tracker: Tracker) {
* 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
@@ -265,23 +283,15 @@ class TrackerResetsHandler(val tracker: Tracker) {
lastResetQuaternion = oldRot
// Adjust raw rotation to mountingOrientation
val rotation = tracker.getRawRotation()
val mountingAdjustedRotation = tracker.getRawRotation() * mountingOrientation
// Gyrofix
val gyroFix = if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
if (tracker.isComputed) {
fixGyroscope(rotation)
if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
gyroFix = if (tracker.isComputed) {
fixGyroscope(tracker.getRawRotation())
} else {
if (tracker.trackerPosition.isFoot()) {
// Feet are rotated by 90 deg pitch, this means we're relying on IMU rotation
// to be set correctly here.
fixGyroscope(rotation * tposeDownFix * QuarterPitch)
} else {
fixGyroscope(rotation * tposeDownFix)
}
fixGyroscope(mountingAdjustedRotation * tposeDownFix)
}
} else {
Quaternion.IDENTITY
}
// Mounting for computed trackers
@@ -296,7 +306,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
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(rotation).inv() * rotation
val rotBuf = getYawQuaternion(tracker.getRawRotation()).inv() * tracker.getRawRotation()
// Isolate pitch
Quaternion(rotBuf.w, -rotBuf.x, 0f, 0f).unit()
} else {
@@ -304,7 +314,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
Quaternion.IDENTITY
}
} else {
(gyroFix * rotation).inv()
fixAttachment(mountingAdjustedRotation)
}
// Rotate attachmentFix by 180 degrees as a workaround for t-pose (down)
@@ -316,7 +326,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
// Don't adjust yaw if head and computed
if (tracker.trackerPosition != TrackerPosition.HEAD || !tracker.isComputed) {
yawFix = gyroFix * reference.project(Vector3.POS_Y).unit()
yawFix = fixYaw(mountingAdjustedRotation, reference)
tracker.yawResetSmoothing.reset()
}
@@ -360,7 +370,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
lastResetQuaternion = oldRot
val yawFixOld = yawFix
yawFix = fixYaw(tracker.getRawRotation(), reference)
yawFix = fixYaw(tracker.getRawRotation() * mountingOrientation, reference)
tracker.yawResetSmoothing.reset()
makeIdentityAdjustmentQuatsYaw()
@@ -399,9 +409,9 @@ class TrackerResetsHandler(val tracker: Tracker) {
constraintFix = Quaternion.IDENTITY
// Get the current calibrated rotation
var rotBuf = adjustToDrift(tracker.getRawRotation())
var rotBuf = adjustToDrift(tracker.getRawRotation() * mountingOrientation)
rotBuf = gyroFix * rotBuf
rotBuf *= attachmentFix
rotBuf = mountingOrientation.inv() * rotBuf * mountingOrientation
rotBuf = yawFix * rotBuf
// Adjust buffer to reference
@@ -457,22 +467,14 @@ class TrackerResetsHandler(val tracker: Tracker) {
mountRotFix = Quaternion.IDENTITY
}
// EulerOrder.YXZ is actually better for gyroscope fix, as it can get yaw at any roll.
// Consequentially, instead of the roll being limited, the pitch is limited to
// 90 degrees from the yaw plane. This means trackers may be mounted upside down
// or with incorrectly configured IMU rotation, but we will need to compensate for
// the pitch.
private fun fixGyroscope(sensorRotation: Quaternion): Quaternion = getYawQuaternion(sensorRotation, EulerOrder.YXZ).inv()
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 = sensorRotation * attachmentFix
// We need to fix the global yaw offset for the euler yaw calculation
if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) {
rot = mountingOrientation.inv() * rot * mountingOrientation
}
rot = mountRotFix.inv() * rot * mountRotFix
// TODO: Get diff from ref to rot, use euler angle (YZX) yaw as output.
// This prevents pitch and roll from affecting the alignment.
var rot = gyroFix * sensorRotation
rot *= attachmentFix
rot = mountRotFix.inv() * (rot * mountRotFix)
rot = getYawQuaternion(rot)
return rot.inv() * reference.project(Vector3.POS_Y).unit()
}
@@ -483,7 +485,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
// 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, order: EulerOrder = EulerOrder.YZX): Quaternion = EulerAngles(order, 0f, rot.toEulerAngles(order).y, 0f).toQuaternion().twinNearest(rot)
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()

View File

@@ -27,6 +27,7 @@ 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
@@ -138,11 +139,13 @@ class HIDCommon {
}
// 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
@@ -152,6 +155,10 @@ class HIDCommon {
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) {
@@ -206,6 +213,10 @@ class HIDCommon {
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()
}
@@ -220,11 +231,42 @@ class HIDCommon {
}
}
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()
}
@@ -248,12 +290,23 @@ class HIDCommon {
device.mcuType = mcuType!!
}
}
if (fw_date != null && fw_major != null && fw_minor != null && fw_patch != null) {
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
val firmwareDate = String.format("%04d-%02d-%02d", firmwareYear, firmwareMonth, firmwareDay)
device.firmwareVersion = "$fw_major.$fw_minor.$fw_patch (Build $firmwareDate)"
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)
@@ -264,6 +317,11 @@ class HIDCommon {
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) {
@@ -274,7 +332,7 @@ class HIDCommon {
rot = AXES_OFFSET.times(scaleRot).times(rot) // no division
tracker.setRotation(rot)
}
if (packetType == 2) {
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()
@@ -293,7 +351,7 @@ class HIDCommon {
rot = AXES_OFFSET.times(rot) // no division
tracker.setRotation(rot)
}
if (packetType == 1 || packetType == 2) {
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
@@ -317,7 +375,7 @@ class HIDCommon {
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) {
if (packetType == 1 || packetType == 2 || packetType == 4 || packetType == 7) {
tracker.dataTick() // only data tick if there is rotation data
}
}

View File

@@ -58,6 +58,9 @@ enum class BoardType(val id: UInt) {
GESTURES(21u),
SLIMEVR_V1_2(22u),
ESP32S3_SUPERMINI(23u),
GENERIC_NRF(24u),
SLIMEVR_BUTTERFLY_DEV(25u),
SLIMEVR_BUTTERFLY(26u),
DEV_RESERVED(250u),
;
@@ -88,6 +91,9 @@ enum class BoardType(val id: UInt) {
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"
}
@@ -109,6 +115,8 @@ enum class MCUType(val id: UInt) {
ESP32_C3(6u),
MOCOPI(7u),
HARITORA(8u),
NRF52(9u),
NRF54L(10u),
DEV_RESERVED(250u),
;

View File

@@ -397,7 +397,9 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
} catch (e: Exception) {
e.printStackTrace()
} finally {
Util.close(socket)
if (::socket.isInitialized) {
Util.close(socket)
}
}
}
@@ -415,9 +417,13 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
if (tracker == null) return
tracker.setRotation(rot)
if (packet is UDPPacket23RotationAndAcceleration) {
// If sensorOffset was applied to accel correctly, the axes will already
// be correct for SlimeVR
tracker.setAcceleration(SENSOR_OFFSET_CORRECTION.sandwich(packet.acceleration))
// 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()
}
@@ -449,9 +455,13 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
is UDPPacket4Acceleration -> {
tracker = connection?.getTracker(packet.sensorId)
if (tracker == null) return
// If sensorOffset was applied to accel correctly, the axes will already
// be correct for SlimeVR
tracker.setAcceleration(SENSOR_OFFSET_CORRECTION.sandwich(packet.acceleration))
// 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 -> {

View File

@@ -11,6 +11,7 @@ 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.*
@@ -199,7 +200,8 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen
}
// 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 needFullReset = (!resetMountingCompleted && !vrServer.serverGuards.canDoMounting) || trackerRequireReset.isNotEmpty()
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()) {
@@ -216,7 +218,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen
}
}
val hmd =
assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData }
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) {

View File

@@ -9,6 +9,7 @@ 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;
@@ -101,7 +102,7 @@ public class WebsocketAPI extends WebSocketServer implements ProtocolAPIServer {
}
@Override
public Stream<GenericConnection> getAPIConnections() {
public @NotNull Stream<GenericConnection> getApiConnections() {
return this.getConnections().stream().map(conn -> {
var c = conn.<WebsocketConnection>getAttachment();
return (GenericConnection) c;

View File

@@ -3,6 +3,7 @@ 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;
@@ -27,8 +28,13 @@ public class WebsocketConnection implements GenericConnection {
@Override
public void send(ByteBuffer bytes) {
if (this.conn.isOpen())
this.conn.send(bytes.slice());
if (this.conn.isOpen()) {
try {
this.conn.send(bytes.slice());
} catch (WebsocketNotConnectedException ignored) {
// Race condition if it closes between our check and sending
}
}
}
@Override

View File

@@ -93,13 +93,13 @@ class SkeletonResetTests {
TrackerPosition.HIP,
TrackerPosition.LEFT_LOWER_LEG,
TrackerPosition.RIGHT_LOWER_LEG,
-> mountRot
-> mountRot * Quaternion.SLIMEVR.FRONT
TrackerPosition.LEFT_UPPER_LEG,
TrackerPosition.RIGHT_UPPER_LEG,
-> mountRot * Quaternion.SLIMEVR.FRONT
-> mountRot
else -> mountRot * Quaternion.SLIMEVR.FRONT
else -> mountRot
}
val actualMounting = tracker.resetsHandler.mountRotFix

View File

@@ -7,6 +7,7 @@ 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
@@ -218,6 +219,11 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte
"mounting_reset" -> instance.resetTrackersMounting(resetSourceName)
"feet_mounting_reset" -> instance.resetTrackersMounting(
resetSourceName,
TrackerUtils.feetsBodyParts,
)
"pause_tracking" ->
instance
.togglePauseTracking(resetSourceName)

View File

@@ -3558,6 +3558,20 @@ public final class ProtobufMessages {
* @return The trackerRole.
*/
int getTrackerRole();
/**
* <code>string manufacturer = 5;</code>
*
* @return The manufacturer.
*/
java.lang.String getManufacturer();
/**
* <code>string manufacturer = 5;</code>
*
* @return The bytes for manufacturer.
*/
com.google.protobuf.ByteString getManufacturerBytes();
}
/**
@@ -3588,6 +3602,7 @@ public final class ProtobufMessages {
private TrackerAdded() {
trackerSerial_ = "";
trackerName_ = "";
manufacturer_ = "";
}
public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
@@ -3713,6 +3728,48 @@ public final class ProtobufMessages {
return trackerRole_;
}
public static final int MANUFACTURER_FIELD_NUMBER = 5;
@SuppressWarnings("serial")
private volatile java.lang.Object manufacturer_ = "";
/**
* <code>string manufacturer = 5;</code>
*
* @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;
}
}
/**
* <code>string manufacturer = 5;</code>
*
* @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
@@ -3742,6 +3799,9 @@ public final class ProtobufMessages {
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);
}
@@ -3766,6 +3826,9 @@ public final class ProtobufMessages {
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;
@@ -3801,6 +3864,11 @@ public final class ProtobufMessages {
!= other.getTrackerRole()
)
return false;
if (
!getManufacturer()
.equals(other.getManufacturer())
)
return false;
if (!getUnknownFields().equals(other.getUnknownFields()))
return false;
return true;
@@ -3821,6 +3889,8 @@ public final class ProtobufMessages {
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;
@@ -3993,6 +4063,7 @@ public final class ProtobufMessages {
trackerSerial_ = "";
trackerName_ = "";
trackerRole_ = 0;
manufacturer_ = "";
return this;
}
@@ -4044,6 +4115,9 @@ public final class ProtobufMessages {
if (((from_bitField0_ & 0x00000008) != 0)) {
result.trackerRole_ = trackerRole_;
}
if (((from_bitField0_ & 0x00000010) != 0)) {
result.manufacturer_ = manufacturer_;
}
}
@java.lang.Override
@@ -4083,6 +4157,11 @@ public final class ProtobufMessages {
if (other.getTrackerRole() != 0) {
setTrackerRole(other.getTrackerRole());
}
if (!other.getManufacturer().isEmpty()) {
manufacturer_ = other.manufacturer_;
bitField0_ |= 0x00000010;
onChanged();
}
this.mergeUnknownFields(other.getUnknownFields());
onChanged();
return this;
@@ -4130,6 +4209,11 @@ public final class ProtobufMessages {
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
@@ -4398,6 +4482,93 @@ public final class ProtobufMessages {
return this;
}
private java.lang.Object manufacturer_ = "";
/**
* <code>string manufacturer = 5;</code>
*
* @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;
}
}
/**
* <code>string manufacturer = 5;</code>
*
* @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;
}
}
/**
* <code>string manufacturer = 5;</code>
*
* @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;
}
/**
* <code>string manufacturer = 5;</code>
*
* @return This builder for chaining.
*/
public Builder clearManufacturer() {
manufacturer_ = getDefaultInstance().getManufacturer();
bitField0_ = (bitField0_ & ~0x00000010);
onChanged();
return this;
}
/**
* <code>string manufacturer = 5;</code>
*
* @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)
}
@@ -8872,53 +9043,55 @@ public final class ProtobufMessages {
+
"ctionArgumentsEntry\0326\n\024ActionArgumentsEn"
+
"try\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"f\n\014T"
"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\"\374\002\n\rTrackerStatus\022\022\n"
"\014tracker_role\030\004 \001(\005\022\024\n\014manufacturer\030\005 \001("
+
"\ntracker_id\030\001 \001(\005\022.\n\006status\030\002 \001(\0162\036.mess"
"\t\"\374\002\n\rTrackerStatus\022\022\n\ntracker_id\030\001 \001(\005\022"
+
"ages.TrackerStatus.Status\0221\n\005extra\030\003 \003(\013"
".\n\006status\030\002 \001(\0162\036.messages.TrackerStatus"
+
"2\".messages.TrackerStatus.ExtraEntry\022;\n\n"
".Status\0221\n\005extra\030\003 \003(\0132\".messages.Tracke"
+
"confidence\030\004 \001(\0162\".messages.TrackerStatu"
"rStatus.ExtraEntry\022;\n\nconfidence\030\004 \001(\0162\""
+
"s.ConfidenceH\000\210\001\001\032,\n\nExtraEntry\022\013\n\003key\030\001"
".messages.TrackerStatus.ConfidenceH\000\210\001\001\032"
+
" \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"E\n\006Status\022\020\n\014DIS"
",\n\nExtraEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001("
+
"CONNECTED\020\000\022\006\n\002OK\020\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003"
"\t:\0028\001\"E\n\006Status\022\020\n\014DISCONNECTED\020\000\022\006\n\002OK\020"
+
"\022\014\n\010OCCLUDED\020\004\"3\n\nConfidence\022\006\n\002NO\020\000\022\007\n\003"
"\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003\022\014\n\010OCCLUDED\020\004\"3\n\n"
+
"LOW\020\001\022\n\n\006MEDIUM\020\005\022\010\n\004HIGH\020\nB\r\n\013_confiden"
"Confidence\022\006\n\002NO\020\000\022\007\n\003LOW\020\001\022\n\n\006MEDIUM\020\005\022"
+
"ce\"I\n\007Battery\022\022\n\ntracker_id\030\001 \001(\005\022\025\n\rbat"
"\010\n\004HIGH\020\nB\r\n\013_confidence\"I\n\007Battery\022\022\n\nt"
+
"tery_level\030\002 \001(\002\022\023\n\013is_charging\030\003 \001(\010\"\241\002"
"racker_id\030\001 \001(\005\022\025\n\rbattery_level\030\002 \001(\002\022\023"
+
"\n\017ProtobufMessage\022&\n\010position\030\001 \001(\0132\022.me"
"\n\013is_charging\030\003 \001(\010\"\241\002\n\017ProtobufMessage\022"
+
"ssages.PositionH\000\022+\n\013user_action\030\002 \001(\0132\024"
"&\n\010position\030\001 \001(\0132\022.messages.PositionH\000\022"
+
".messages.UserActionH\000\022/\n\rtracker_added\030"
"+\n\013user_action\030\002 \001(\0132\024.messages.UserActi"
+
"\003 \001(\0132\026.messages.TrackerAddedH\000\0221\n\016track"
"onH\000\022/\n\rtracker_added\030\003 \001(\0132\026.messages.T"
+
"er_status\030\004 \001(\0132\027.messages.TrackerStatus"
"rackerAddedH\000\0221\n\016tracker_status\030\004 \001(\0132\027."
+
"H\000\022$\n\007battery\030\005 \001(\0132\021.messages.BatteryH\000"
"messages.TrackerStatusH\000\022$\n\007battery\030\005 \001("
+
"\022$\n\007version\030\006 \001(\0132\021.messages.VersionH\000B\t"
"\0132\021.messages.BatteryH\000\022$\n\007version\030\006 \001(\0132"
+
"\n\007messageB2\n\034dev.slimevr.desktop.platfor"
"\021.messages.VersionH\000B\t\n\007messageB2\n\034dev.s"
+
"mB\020ProtobufMessagesH\003b\006proto3"
"limevr.desktop.platformB\020ProtobufMessage"
+
"sH\003b\006proto3"
};
descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(
@@ -8957,7 +9130,8 @@ public final class ProtobufMessages {
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", }
new java.lang.String[] { "TrackerId", "TrackerSerial", "TrackerName", "TrackerRole",
"Manufacturer", }
);
internal_static_messages_TrackerStatus_descriptor = getDescriptor()
.getMessageTypes()

View File

@@ -147,23 +147,13 @@ abstract class SteamVRBridge(
val device = instance.deviceManager
.createDevice(
trackerAdded.trackerName,
trackerAdded.trackerSerial,
"OpenVR", // TODO : We need the manufacturer
null,
trackerAdded.manufacturer.ifEmpty { "OpenVR" },
)
// Display name, needsReset and isHmd
val displayName: String
val isHmd = if (trackerAdded.trackerId == 0) {
displayName = if (trackerAdded.trackerName == "HMD") {
"SteamVR Driver HMD"
} else {
"Feeder App HMD"
}
true
} else {
displayName = trackerAdded.trackerName
false
}
val displayName: String = trackerAdded.trackerName
val isHmd = trackerAdded.trackerId == 0
// trackerPosition
val role = getById(trackerAdded.trackerRole)

View File

@@ -141,7 +141,7 @@ public class UnixSocketRpcBridge implements dev.slimevr.bridge.Bridge,
}
@Override
public java.util.stream.Stream<GenericConnection> getAPIConnections() {
public java.util.stream.Stream<GenericConnection> getApiConnections() {
return this.selector
.keys()
.stream()

View File

@@ -1,9 +1,12 @@
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
@@ -55,12 +58,15 @@ class DesktopHIDManager(name: String, private val trackersConsumer: Consumer<Tra
}
private fun checkConfigureDevice(hidDevice: HidDevice) {
if (hidDevice.vendorId == HID_TRACKER_RECEIVER_VID && hidDevice.productId == HID_TRACKER_RECEIVER_PID) { // TODO: Use correct ids
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) {
check(hidDevice.open()) { "Unable to open device" }
if (!hidDevice.open()) {
LogManager.warning("[TrackerServer] Unable to open device: $serial")
return
}
}
// TODO: Configure the device here
val serial = hidDevice.serialNumber ?: "Unknown HID Device"
// val product = hidDevice.product
// val manufacturer = hidDevice.manufacturer
this.devicesBySerial[serial]?.let {
@@ -205,12 +211,29 @@ class DesktopHIDManager(name: String, private val trackersConsumer: Consumer<Tra
}
private fun deviceEnumerate() {
var root: HidDeviceInfoStructure? = null
var rootReceivers: HidDeviceInfoStructure? = null
var rootTrackers: HidDeviceInfoStructure? = null
val trackersOverHID: Boolean = VRServer.instance.configManager.vrConfig.hidConfig.trackersOverHID
try {
root = HidApi.enumerateDevices(HID_TRACKER_RECEIVER_VID, HID_TRACKER_RECEIVER_PID) // TODO: change to proper vendorId and productId, need to enum all appropriate productId
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<HidDevice> = mutableListOf()
if (root != null) {
var hidDeviceInfoStructure: HidDeviceInfoStructure? = root