Compare commits

...

110 Commits

Author SHA1 Message Date
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
lucas lelievre
f01f599526 New Pontoon translations (#1672) 2025-12-11 20:39:52 +01:00
SlimeVR-bot
6847526ce8 Pontoon/GUI: Update Simplified Chinese (zh-Hans)
Co-authored-by: pwnint <ahndkp@gmail.com> (zh-Hans)
Co-authored-by: 寂听 <jiting@jtcat.com> (zh-Hans)
Co-authored-by: nekomona <nekomona@163.com> (zh-Hans)
2025-12-11 19:03:14 +00:00
SlimeVR-bot
c5f28a6a01 Pontoon/GUI: Update Simplified Chinese (zh-Hans)
Co-authored-by: 寂听 <jiting@jtcat.com> (zh-Hans)
Co-authored-by: Joshh <josh229411@icloud.com> (zh-Hans)
Co-authored-by: pwnint <ahndkp@gmail.com> (zh-Hans)
Co-authored-by: nekomona <nekomona@163.com> (zh-Hans)
2025-12-11 19:03:14 +00:00
lucas lelievre
86d7d5fdc6 Fix tracker set selection (#1668) 2025-12-11 20:02:52 +01:00
lucas lelievre
781f4d489a Better Android signing & Google Play bundle building (#1670) 2025-12-11 19:56:07 +01:00
jabberrock
5a42426048 [Stay Aligned] Fix broken tracker rotations (#1669) 2025-12-11 07:26:12 -05:00
Butterscotch!
44643f2cc6 Rename Google Play artifact 2025-12-11 07:06:02 -05:00
Butterscotch!
d902515f4f Change message 2025-12-11 06:30:40 -05:00
Butterscotch!
f9df08aefd Include all Android tasks for signing & Play CI 2025-12-11 06:27:20 -05:00
Butterscotch!
28b18e0d42 Handle KeyStore secret directly in Gradle 2025-12-11 06:27:20 -05:00
loucass003
247c063791 Lint 2025-12-11 10:11:16 +01:00
lucas lelievre
ab248287cc Update gui/src/components/onboarding/pages/trackers-assign/TrackerAssignOptions.tsx
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-11 09:44:33 +01:00
lucas lelievre
9a26fc98b8 Update gui/src/components/onboarding/pages/trackers-assign/TrackerAssignOptions.tsx
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-11 09:44:28 +01:00
loucass003
16a2ac8474 Lint 2025-12-11 09:36:02 +01:00
loucass003
c4acf4cc41 Better selection + only set the prefered once for a new user 2025-12-11 09:30:21 +01:00
loucass003
4b0a2d27d0 Fix tracker set selection 2025-12-11 09:19:25 +01:00
lucas lelievre
2c6708bfe7 Don't show fw update icon when update is blocked (#1667) 2025-12-11 08:35:46 +01:00
Sapphire
2880623cce Fix fw update icon low battery warning 2025-12-11 00:30:54 -06:00
Sapphire
17400ca337 Don't show fw update icon when update is blocked 2025-12-11 00:24:06 -06:00
lucas lelievre
3276f6db7a Fix tooltips on table and reset buttons (#1666) 2025-12-11 06:21:56 +01:00
loucass003
db59537adc Fix tooltips on table and reset buttons 2025-12-11 06:17:05 +01:00
lucas lelievre
4f1fd82923 Tooltip: Better handling of disabled buttons (#1665) 2025-12-11 04:02:12 +01:00
loucass003
f6ccb5970f Lint 2025-12-11 03:57:06 +01:00
loucass003
c937b91267 Better handling of disabled buttons 2025-12-11 03:54:23 +01:00
lucas lelievre
2d1f32b950 Disable R8 optimization for Android (#1664) 2025-12-11 03:28:49 +01:00
Butterscotch!
8acba98bcc Temporarily disable Proguard optimization 2025-12-10 21:23:38 -05:00
lucas lelievre
d7ba1b8335 Fix mobile tooltip (#1662) 2025-12-11 03:13:13 +01:00
lucas lelievre
d20e9bfd94 Fix tiertiary dropdown text color (#1663) 2025-12-11 02:56:37 +01:00
loucass003
3d54a86bd8 Fix tiertiary dropdown text color 2025-12-11 02:54:22 +01:00
loucass003
c9883f5eb4 Fix mobile tooltip 2025-12-11 02:28:09 +01:00
lucas lelievre
8bd36fac25 Increase accent text contrast for ace theme (#1656) 2025-12-11 02:26:43 +01:00
lucas lelievre
ab4d507d9f Snep theme (#1659) 2025-12-11 02:26:26 +01:00
lucas lelievre
9efb985260 Sentry fixes (#1658) 2025-12-11 02:26:10 +01:00
loucass003
2c2c227187 Fix scaled proportions event not triggering 2025-12-11 02:24:00 +01:00
sctanf
63cca6756e lint 2025-12-10 19:07:28 -06:00
lucas lelievre
b0d7fefa5e Only skip server start when CLI arg is passed (#1660) 2025-12-11 02:02:57 +01:00
Butterscotch!
35a5cb47d9 Disable Android obfuscation 2025-12-10 19:07:54 -05:00
lucas lelievre
dfc4383271 Update gui/src/hooks/app.ts
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-10 21:58:47 +01:00
lucas lelievre
185431a733 Update gui/src/hooks/tracking-checklist.ts
Co-authored-by: Sapphire <imsapphire0@gmail.com>
2025-12-10 21:58:35 +01:00
Sapphire
5b68a01186 Only skip server start when CLI arg is passed 2025-12-10 14:36:56 -06:00
lucas lelievre
2c4dd4085f Remove Ktor dependency (#1655) 2025-12-10 21:34:35 +01:00
sctanf
4d3ff0e9c9 snep 2025-12-10 11:42:16 -06:00
loucass003
ee6182bb23 lint 2025-12-10 17:15:07 +01:00
loucass003
9576d6e034 Sentry fixes 2025-12-10 17:12:25 +01:00
lucas lelievre
227ddc87d2 Increment Android version code and log config (#1657) 2025-12-10 13:51:46 +01:00
Butterscotch!
b3b7730b2c Increment Android version code and log config 2025-12-10 07:14:54 -05:00
Butterscotch!
075a155f13 Increase accent text contrast for ace theme 2025-12-10 05:31:43 -05:00
Butterscotch!
79a3b66e43 Fix formatting 2025-12-10 05:08:29 -05:00
Butterscotch!
276e73e724 Remove Ktor dependency 2025-12-10 00:39:27 -05:00
134 changed files with 3393 additions and 2489 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
@@ -118,22 +118,16 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: cd gui && pnpm run build
- name: Decode keystore secret to file
env:
ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }}
run: |
mkdir -p server/android/secrets/
echo $ANDROID_STORE_FILE | base64 --decode > server/android/secrets/keystore.jks
- name: Build with Gradle
run: ./gradlew :server:android:build
env:
ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }}
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_STORE_PASSWD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_KEY_PASSWD }}
- name: Upload the Android Build Artifact
uses: actions/upload-artifact@v5
- name: Upload the Android build artifact
uses: actions/upload-artifact@v6
with:
# Artifact name
name: 'SlimeVR-Android' # optional, default is artifact
@@ -154,6 +148,24 @@ jobs:
files: |
./SlimeVR-android.apk
- name: Build Google Play release bundle
if: startsWith(github.ref, 'refs/tags/')
run: ./gradlew :server:android:bundleRelease
env:
ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }}
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }}
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }}
- name: Upload the Google Play artifact
uses: actions/upload-artifact@v6
if: startsWith(github.ref, 'refs/tags/')
with:
# Artifact name
name: 'SlimeVR-Android-GPDev' # optional, default is artifact
# A file, directory or wildcard pattern that describes what to upload
path: server/android/build/outputs/bundle/release/*
bundle-linux:
strategy:
matrix:
@@ -169,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/
@@ -218,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
@@ -267,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/
@@ -308,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
@@ -347,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/
@@ -387,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

@@ -20,8 +20,8 @@
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "2.4.1",
"@tauri-apps/plugin-http": "^2.5.0",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-log": "~2",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.4.1",
@@ -52,6 +52,7 @@
"ts-pattern": "^5.4.0",
"typescript": "^5.6.3",
"use-double-tap": "^1.3.6",
"uuid": "^13.0.0",
"yup": "^1.4.0"
},
"scripts": {

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

View File

@@ -24,7 +24,7 @@ version_update-close = 关闭
## Tips
tips-find_tracker = 分不清哪个追踪器是哪个了?摇一摇它,对应的那个将高亮显示。
tips-find_tracker = 不确定哪个追踪器是哪个?在现实中摇动一个追踪器,对应的那个将在屏幕上高亮显示。
tips-do_not_move_heels = 确保你的脚跟在录制的时候不会发生移动!
tips-file_select = 拖放文档或 <u>浏览文档</u> 以使用
tips-tap_setup = 你可以缓慢地敲击2次追踪器来选中它而不是从菜单中选取。
@@ -33,6 +33,10 @@ tips-failed_webgl = WebGL初始化失败
## Units
unit-meter = 米
unit-foot = 英尺
unit-inch = 英寸
unit-cm = 厘米
## Body parts
@@ -241,6 +245,10 @@ reset-mounting = 重置佩戴
reset-mounting-feet = 重置脚部佩戴
reset-mounting-fingers = 重置手指佩戴
reset-yaw = 重置航向轴
reset-error-no_feet_tracker = 未分配脚部追踪器
reset-error-no_fingers_tracker = 未分配手指追踪器
reset-error-mounting-need_full_reset = 佩戴校准前需要先执行完整重置
reset-error-yaw-need_full_reset = 航向轴重置前需要先执行完整重置
## Serial detection stuff
@@ -260,10 +268,12 @@ navbar-trackers_assign = 追踪器分配
navbar-mounting = 佩戴校准
navbar-onboarding = 向导
navbar-settings = 设置
navbar-connect_trackers = 连接追踪器
## Biovision hierarchy recording
bvh-start_recording = 录制 BVH 文件
bvh-stop_recording = 保存 BVH 记录
bvh-recording = 录制中...
bvh-save_title = 保存BVH记录
@@ -407,6 +417,8 @@ tracker-settings-update-up_to_date = 已是最新
tracker-settings-update-blocked = 更新不可用。没有其他可用版本
tracker-settings-update = 立即更新
tracker-settings-update-title = 固件版本
tracker-settings-current-version = 当前版本
tracker-settings-latest-version = 最新版本
## Tracker part card info
@@ -472,6 +484,7 @@ mounting_selection_menu-close = 关闭
settings-sidebar-title = 设置
settings-sidebar-general = 通用设置
settings-sidebar-steamvr = SteamVR
settings-sidebar-tracker_mechanics = 追踪器设置
settings-sidebar-stay_aligned = 持续校准
settings-sidebar-fk_settings = FK 设置
@@ -479,9 +492,12 @@ settings-sidebar-gesture_control = 手势控制
settings-sidebar-interface = 交互界面
settings-sidebar-osc_router = OSC 路由
settings-sidebar-osc_trackers = VRChat OSC 追踪器
settings-sidebar-osc_vmc = VMC
settings-sidebar-utils = 工具
settings-sidebar-serial = 串口控制台
settings-sidebar-appearance = 外观
settings-sidebar-home = 主界面
settings-sidebar-checklist = 追踪检查清单
settings-sidebar-notifications = 通知
settings-sidebar-behavior = 行为
settings-sidebar-firmware-tool = DIY固件工具
@@ -904,9 +920,15 @@ settings-utils-advanced-open_logs-label = 打开文件夹
## Home Screen
settings-home-list-layout = 追踪器列表布局
settings-home-list-layout-desc = 选择主界面的显示布局
settings-home-list-layout-grid = 网格
settings-home-list-layout-table = 列表
## Tracking Checlist
settings-tracking_checklist-active_steps = 启用的检查项
settings-tracking_checklist-active_steps-desc = 追踪检查清单中所有项目的列表。您可以禁用不需要的步骤。
## Setup/onboarding menu
@@ -923,6 +945,13 @@ onboarding-setup_warning-cancel = 继续设置
## Wi-Fi setup
onboarding-wifi_creds-back = 返回简介
onboarding-wifi_creds-v2 = 通过 Wi-Fi 连接
# This cares about multilines
onboarding-wifi_creds-description-v2 =
大多数追踪器(例如官方的 SlimeVR 追踪器)都通过 Wi-Fi 连接服务器。
请输入当前设备连接的网络的 Wi-Fi 凭证。
请确保输入的是 2.4GHz 频段的 Wi-Fi 凭证!
onboarding-wifi_creds-skip = 跳过 Wi-Fi 设置
onboarding-wifi_creds-submit = 提交!
onboarding-wifi_creds-ssid =
@@ -932,6 +961,10 @@ onboarding-wifi_creds-ssid-required = Wi-Fi 名称为必填项
onboarding-wifi_creds-password =
.label = 密码
.placeholder = 输入密码
onboarding-wifi_creds-dongle-title = 通过接收器连接
onboarding-wifi_creds-dongle-description = 如果你的追踪器附带接收器,将其插入电脑即可直接开始使用!
onboarding-wifi_creds-dongle-wip = 此部分仍在开发中。将来会推出用于管理接收器连接追踪器的专属页面。
onboarding-wifi_creds-dongle-continue = 继续,使用接收器
## Mounting setup
@@ -1035,7 +1068,8 @@ onboarding-assignment_tutorial-done = 我把贴纸和绑带都弄好了!
onboarding-assign_trackers-back = 返回 Wi-Fi 凭据设置
onboarding-assign_trackers-title = 分配追踪器
onboarding-assign_trackers-description = 让我们选择哪个追踪器在哪里。单击要放置追踪器的部位
onboarding-assign_trackers-description = 让我们选择追踪器的佩戴位置。点击对应部位即可分配。
onboarding-assign_trackers-unassign_all = 取消分配所有追踪器
# Look at translation of onboarding-connect_tracker-connected_trackers on how to use plurals
# $assigned (Number) - Trackers that have been assigned a body part
# $trackers (Number) - Trackers connected to the server
@@ -1170,6 +1204,8 @@ onboarding-automatic_mounting-done-restart = 再试一次
onboarding-automatic_mounting-mounting_reset-title = 佩戴重置
onboarding-automatic_mounting-mounting_reset-step-0 = 1. 双腿弯曲以滑雪的姿势蹲下,上身向前倾斜,手臂弯曲。
onboarding-automatic_mounting-mounting_reset-step-1 = 按下佩戴重置按钮并等待 3 秒钟,然后追踪器的佩戴方向将被重置。
onboarding-automatic_mounting-mounting_reset-feet-step-0 = 1. 双脚朝前,踮起脚尖站立。或者,您也可以坐在椅子上完成这个动作。
onboarding-automatic_mounting-mounting_reset-feet-step-1 = 2. 点击“脚部校准”按钮并等待 3 秒,追踪器的佩戴方向将会重置。
onboarding-automatic_mounting-preparation-title = 准备
onboarding-automatic_mounting-preparation-v2-step-0 = 1. 按下“完全重置”按钮。
onboarding-automatic_mounting-preparation-v2-step-1 = 2. 站直并向前看,双臂放在身体两侧。
@@ -1181,6 +1217,7 @@ onboarding-automatic_mounting-return-home = 完成
## Tracker manual proportions setupa
onboarding-manual_proportions-back-scaled = 返回使用缩放比例
onboarding-manual_proportions-title = 手动调整身体比例
onboarding-manual_proportions-fine_tuning_button = 自动微调身体比例
onboarding-manual_proportions-fine_tuning_button-disabled-tooltip = 请连接 VR头戴显示器 以使用自动微调
@@ -1276,6 +1313,30 @@ onboarding-automatic_proportions-smol_warning-cancel = 返回
## User height calibration
onboarding-user_height-title = 你的身高是多少?
onboarding-user_height-description = 我们需要你的身高来计算躯干比例,以准确呈现你的动作。你可以让 SlimeVR 自动计算身高,也可以手动输入。
onboarding-user_height-need_head_tracker = 进行校准需要具备定位功能的头戴显示器与控制器。
onboarding-user_height-calculate = 自动计算我的身高
onboarding-user_height-next_step = 保存并继续
onboarding-user_height-manual-proportions = 手动调整躯干比例
onboarding-user_height-calibration-title = 校准进度
onboarding-user_height-calibration-RECORDING_FLOOR = 用控制器的前端触碰地面
onboarding-user_height-calibration-WAITING_FOR_RISE = 回到站姿
onboarding-user_height-calibration-WAITING_FOR_FW_LOOK = 回到站姿并向前看
onboarding-user_height-calibration-WAITING_FOR_FW_LOOK-ok = 确保你的头部水平
onboarding-user_height-calibration-WAITING_FOR_FW_LOOK-low = 不要往地面看
onboarding-user_height-calibration-WAITING_FOR_FW_LOOK-high = 不要往高处看
onboarding-user_height-calibration-WAITING_FOR_CONTROLLER_PITCH = 确保控制器方向朝下
onboarding-user_height-calibration-RECORDING_HEIGHT = 重新站直并保持姿势不动!
onboarding-user_height-calibration-DONE = 完成!
onboarding-user_height-calibration-ERROR_TIMEOUT = 校准超时,请重试。
onboarding-user_height-calibration-ERROR_TOO_HIGH = 检测到的用户身高数值过大,请重试。
onboarding-user_height-calibration-ERROR_TOO_SMALL = 检测到的用户身高数值过小。请确保在校准结束时身体站直并平视前方。
onboarding-user_height-calibration-error = 校准失败
onboarding-user_height-manual-tip = 在调整身高时,尝试不同姿势,看看骨架是否与你的身体动作匹配。
onboarding-user_height-reset-warning =
<b>警告:</b> 这会将您的身体比例重置为仅基于身高的默认比例。
您确定要执行此操作吗?
## Stay Aligned setup
@@ -1314,6 +1375,8 @@ onboarding-stay_aligned-done = 完成
## Home
home-no_trackers = 未检测到或未分配追踪器
home-settings = 主界面设置
home-settings-close = 关闭
## Trackers Still On notification
@@ -1352,7 +1415,7 @@ firmware_tool-not_available = 哦不,固件工具目前不可用。稍后再
firmware_tool-not_compatible = 固件工具与此版本的服务端不兼容。请更新您的服务端!
firmware_tool-select_source = 选择要刷写的固件
firmware_tool-select_source-description = 选择要在电路板上刷写的固件
firmware_tool-select_source-error = 无法加载固件
firmware_tool-select_source-error = 无法加载固件源代码
firmware_tool-select_source-board_type = 电路板类型
firmware_tool-select_source-firmware = 固件来源
firmware_tool-select_source-version = 固件版本
@@ -1377,6 +1440,9 @@ firmware_tool-flash_method_step-serial-v2 =
firmware_tool-flashbtn_step = 按下启动/Boot按钮
firmware_tool-flashbtn_step-description = 在进入下一步之前,您需要做几件事情。
firmware_tool-flashbtn_step-board_SLIMEVR = 关闭追踪器,拆下外壳(如果有的话),使用 USB 数据线连接到计算机,然后根据您的 SlimeVR 电路板版本执行以下步骤之一:
firmware_tool-flashbtn_step-board_SLIMEVR-r11-v2 = 保持短接电路板正面边缘第二个矩形 FLASH 焊盘和单片机模块的金属屏蔽罩,同时打开追踪器电源。追踪器的指示灯将会短暂闪烁。
firmware_tool-flashbtn_step-board_SLIMEVR-r12-v2 = 保持短接电路板正面圆形 FLASH 焊盘和单片机模块的金属屏蔽罩,同时打开追踪器电源。追踪器的指示灯将会短暂闪烁。
firmware_tool-flashbtn_step-board_SLIMEVR-r14-v2 = 按住电路板正面的 FLASH 按钮的同时打开追踪器电源。追踪器的指示灯将会短暂闪烁。
firmware_tool-flashbtn_step-board_OTHER =
在烧录固件之前您可能需要将追踪器置于bootloader模式。
通常这意味着在开始固件烧录过程之前,按下板上的引导/boot按钮。
@@ -1519,3 +1585,46 @@ error_collection_modal-cancel = 还是算了
## Tracking checklist section
tracking_checklist = 追踪检查清单
tracking_checklist-settings = 追踪检查清单设置
tracking_checklist-settings-close = 关闭
tracking_checklist-status-incomplete = 使用 SlimeVR 前的准备工作尚未完成!
tracking_checklist-status-partial = 你有 { $count } 个警告!
tracking_checklist-status-complete = 已经准备好使用 SlimeVR
tracking_checklist-MOUNTING_CALIBRATION = 进行佩戴校准
tracking_checklist-FEET_MOUNTING_CALIBRATION = 进行脚部佩戴校准
tracking_checklist-FULL_RESET = 进行完整重置
tracking_checklist-FULL_RESET-desc = 有些追踪器需要进行重置
tracking_checklist-STEAMVR_DISCONNECTED = SteamVR 未在运行
tracking_checklist-STEAMVR_DISCONNECTED-desc = SteamVR 未在运行。你要将追踪器用于 VR 吗?
tracking_checklist-STEAMVR_DISCONNECTED-open = 启动 SteamVR
tracking_checklist-TRACKERS_REST_CALIBRATION = 校准追踪器
tracking_checklist-TRACKERS_REST_CALIBRATION-desc = 您尚未执行追踪器校准。请将(黄色高亮显示的)追踪器放置在平稳表面上,并静置数秒。
tracking_checklist-TRACKER_ERROR = 追踪器出现错误
tracking_checklist-TRACKER_ERROR-desc = 有追踪器发生错误,请重启黄色高亮标记的追踪器。
tracking_checklist-VRCHAT_SETTINGS = 调整 VRChat 设置
tracking_checklist-VRCHAT_SETTINGS-desc = VRChat 的设置有问题!这会影响到在 VRChat 中使用 SlimeVR 的体验。
tracking_checklist-VRCHAT_SETTINGS-open = 前往 VRChat 警告页面
tracking_checklist-UNASSIGNED_HMD = VR 头戴显示器未分配给头部
tracking_checklist-UNASSIGNED_HMD-desc = VR 头戴显示器应该被分配为头部追踪器。
tracking_checklist-NETWORK_PROFILE_PUBLIC = 更改网络配置文件类型
tracking_checklist-NETWORK_PROFILE_PUBLIC-desc =
检测到您的部分网卡被设为“公用网络”:
{ $adapters }
这可能会影响 SlimeVR 的正常运行。
<PublicFixLink>点击此处查看如何更改设置。</PublicFixLink>
tracking_checklist-NETWORK_PROFILE_PUBLIC-open = 打开控制面板
tracking_checklist-STAY_ALIGNED_CONFIGURED = 调整持续校准设置
tracking_checklist-STAY_ALIGNED_CONFIGURED-desc = 记录持续校准所使用的姿势以减缓漂移现象
tracking_checklist-STAY_ALIGNED_CONFIGURED-open = 打开持续校准设置
tracking_checklist-ignore = 忽略
preview-mocap_mode_soon = 动作捕捉模式(即将推出™)
preview-disable_render = 禁用预览
preview-disabled_render = 预览已禁用
toolbar-mounting_calibration = 佩戴校准
toolbar-mounting_calibration-default = 身体
toolbar-mounting_calibration-feet = 脚部
toolbar-mounting_calibration-fingers = 手指
toolbar-drift_reset = 漂移重置
toolbar-assigned_trackers = { $count } 个已分配的追踪器
toolbar-unassigned_trackers = { $count } 个未分配的追踪器

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

@@ -274,12 +274,7 @@ fn setup_tauri(
app.manage(Mutex::new(window_state));
if cli.no_start_server {
log::info!("Skipping server start.");
return Ok(());
}
if server_running() {
if cli.skip_server_start_if_running && server_running() {
log::info!("Skipping server start: server is already running.");
return Ok(());
}

View File

@@ -48,7 +48,7 @@ pub struct Cli {
#[clap(long)]
pub launch_from_path: Option<PathBuf>,
#[clap(long)]
pub no_start_server: bool,
pub skip_server_start_if_running: bool,
#[clap(flatten)]
verbose: clap_verbosity_flag::Verbosity,
}

View File

@@ -85,7 +85,9 @@ function BasicResetButton(options: UseResetOptions & { customName?: string }) {
spacing={5}
preferedDirection={error ? 'bottom' : 'top'}
>
<div
<button
type="button"
disabled={disabled}
className={classNames(
MAINBUTTON_CLASSES({ disabled }),
'rounded-lg',
@@ -135,7 +137,7 @@ function BasicResetButton(options: UseResetOptions & { customName?: string }) {
{timer}
</Typography>
</div>
</div>
</button>
</Tooltip>
);
}

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,7 +59,7 @@ function DropdownItem({
secondary:
'text-background-20 checked-hover:text-background-10 checked-hover:bg-background-60 focus:text-background-10 focus:bg-background-60',
tertiary:
'bg-accent-background-30 checked-hover:bg-accent-background-20 focus:bg-accent-background-20',
'bg-accent-background-30 checked-hover:bg-accent-background-20 focus:bg-accent-background-20 text-background-10',
};
const ref = useRef<HTMLDivElement>(null);

View File

@@ -80,7 +80,8 @@ export function FirmwareIcon({
<div>
{showUpdate &&
showUpdate !== 'unavailable' &&
showUpdate !== 'updated' && <UpdateIcon showUpdate={'can-update'} />}
showUpdate !== 'updated' &&
showUpdate !== 'blocked' && <UpdateIcon showUpdate={showUpdate} />}
</div>
);
}

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

@@ -24,6 +24,7 @@ interface TooltipProps {
disabled?: boolean;
tag?: string;
spacing?: number;
bindTo?: string;
}
interface TooltipPos {
@@ -344,9 +345,13 @@ export function DrawerTooltip({
elem.classList.add(classNames('animate-pulse'));
elem.classList.add(classNames('scale-[110%]'));
elem.classList.add(classNames('duration-500'));
touchTimeout.current = setTimeout(() => {
if (elem.hasAttribute('disabled')) {
open();
}, TOOLTIP_DELAY) as unknown as number;
} else {
touchTimeout.current = setTimeout(() => {
open();
}, TOOLTIP_DELAY) as unknown as number;
}
}
};
@@ -360,12 +365,16 @@ export function DrawerTooltip({
};
const touchEnd = (e: MouseEvent | TouchEvent) => {
if (Date.now() - touchTimestamp.current > TOOLTIP_DELAY) {
// open drawer
e.preventDefault(); // cancel the click event
if (
e.currentTarget instanceof HTMLButtonElement &&
e.currentTarget.hasAttribute('disabled')
) {
e.preventDefault();
return;
}
if (Date.now() - touchTimestamp.current < TOOLTIP_DELAY) {
clearTimeout(touchTimeout.current);
open();
close();
}
};
@@ -394,12 +403,14 @@ export function DrawerTooltip({
elem.addEventListener('touchstart', touchStart);
elem.addEventListener('touchend', touchEnd);
elem.addEventListener('touchcancel', touchEnd);
return () => {
elem.removeEventListener('scroll', scroll);
elem.removeEventListener('touchstart', touchStart);
elem.removeEventListener('touchend', touchEnd);
elem.removeEventListener('touchcancel', touchEnd);
clearTimeout(touchTimeout.current);
};
}
@@ -458,11 +469,16 @@ export function Tooltip({
variant = 'auto',
disabled = false,
tag = 'div',
bindTo,
spacing = 10,
}: TooltipProps) {
const childRef = useRef<HTMLElement | null>(null);
const isAndroid = window.__ANDROID__?.isThere();
if (bindTo) {
childRef.current = document.querySelector(bindTo);
}
let portal = null;
if (variant === 'auto') {
portal = isAndroid ? (
@@ -498,7 +514,13 @@ export function Tooltip({
return (
<>
{createElement(tag, { className: 'contents', ref: childRef }, children)}
{bindTo
? children
: createElement(
tag,
{ className: 'contents', ref: childRef },
children
)}
{!disabled && createPortal(portal, document.body)}
</>
);

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

@@ -1,4 +1,51 @@
import { useConfig } from '@/hooks/config';
export function SlimeVRIcon({ drag }: { drag?: boolean }) {
const { config } = useConfig();
if (config?.theme == 'snep') {
return (
<svg
width="49"
height="29"
viewBox="-4 -2 49 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-tauri-drag-region={drag}
>
<path
d="m 1.6647024,15.257308 4.84329,-5.8061114 5.1394996,4.7526114"
stroke="#FFCCE5"
strokeWidth="3"
strokeLinecap="round"
/>
<path
d="m 22.099692,14.390108 5.7806,-4.8728814 4.2323,5.5751814"
stroke="#FFCCE5"
strokeWidth="3"
strokeLinecap="round"
/>
<path
d="m 9.7241618,27.517333 c 2.9071362,-0.836166 5.2501762,-1.583484 7.0857782,-3.854543 1.787374,2.222439 3.963276,3.063619 7.087706,3.839132"
stroke="#FFCCE5"
strokeWidth="3"
strokeLinecap="round"
/>
<path
d="m 19.337465,19.962745 c -1.861871,0.437141 -3.433485,0.530797 -5.209565,0.06165 1.286223,0.173275 2.222982,0.778091 2.686704,1.605299 0.327959,-0.839305 1.382466,-1.47415 2.522861,-1.666949 z"
stroke="#FFCCE5"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="m 35.942918,2.6356084 c 6.330566,-1.5164535 11.583704,-1.69795947 15.609729,0.9503118 2.180495,1.4343036 1.678869,4.6673575 0.754839,5.9005803 -2.596688,3.4655715 -9.485458,7.3237605 -5.116612,11.0623905 -4.998324,0.352073 -3.13787,5.686673 1.260384,6.928864"
stroke="#FFCCE5"
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
);
}
return (
<svg
width="49"

View File

@@ -1,4 +1,39 @@
import { useConfig } from '@/hooks/config';
export function SlimeVRIcon({ width = 28 }: { width?: number }) {
const { config } = useConfig();
if (config?.theme == 'snep') {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fillRule="evenodd"
strokeMiterlimit="10"
clipRule="evenodd"
width={width}
viewBox="0 0 380 380"
>
<g fill="none" stroke="#fff">
<path strokeWidth="13.62" d="m 58.065408,191.74 37,-39 39,36" />
<path strokeWidth="13.62" d="m 194.06861,187.74 38,-35 36,38" />
<path
strokeLinecap="square"
strokeWidth="17"
d="m 264.21323,100.54097 c 36.55564,-13.927358 80.48248,-20.252638 96.44182,-0.16058 15.95933,20.09207 -5.55378,62.57663 -18.85775,71.31398 -13.30397,8.73734 -24.9251,23.65102 11.38415,55.001 -41.88653,1.00415 -20.70613,38.05812 4.23915,51.07844"
/>
<path
strokeLinecap="round"
strokeWidth="17"
d="m 178.71549,220.85825 c -11.18717,2.62658 -20.63024,3.18933 -31.30189,0.37013 7.7283,1.04116 13.35686,4.67519 16.14313,9.6455 1.97058,-5.04296 8.30663,-8.85748 15.15876,-10.01593 z"
/>
<path
strokeLinecap="square"
strokeWidth="17"
d="m 114.0349,266.90992 c 14.41809,-4.43279 38.26495,-10.17404 49.29422,-23.81979 10.73948,13.35362 31.14902,18.81171 48.74742,23.621"
/>
</g>
</svg>
);
}
return (
<svg
xmlns="http://www.w3.org/2000/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

@@ -105,7 +105,7 @@ export function BodyAssignment({
onlyAssigned = false,
dotSize,
}: {
assignMode: AssignMode;
assignMode: AssignMode | null;
mirror: boolean;
onlyAssigned?: boolean;
rolesWithErrors?: Partial<Record<BodyPart, BodyPartError>>;
@@ -148,8 +148,7 @@ export function BodyAssignment({
const hasBodyPart = useCallback(
(part: BodyPart) =>
COMMONS.includes(part) ||
assignMode === AssignMode.All ||
ASSIGNMENT_MODES[assignMode].includes(part),
(assignMode && ASSIGNMENT_MODES[assignMode].includes(part)),
[assignMode]
);

View File

@@ -326,6 +326,14 @@ export function ScaledProportionsPage() {
}
);
useEffect(() => {
if (lastUsed !== null) {
Sentry.metrics.count('scaled_proportions', 1, {
attributes: { calibration: lastUsed },
});
}
}, [lastUsed]);
useEffect(() => {
sendRPCPacket(
RpcMessage.SkeletonConfigRequest,
@@ -334,11 +342,6 @@ export function ScaledProportionsPage() {
return () => {
cancel();
if (lastUsed !== null) {
Sentry.metrics.count('scaled_proportions', 1, {
attributes: { calibration: lastUsed },
});
}
};
}, []);

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

@@ -1,6 +1,6 @@
import { Radio } from '@/components/commons/Radio';
import { Typography } from '@/components/commons/Typography';
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
import { AssignMode, useConfig } from '@/hooks/config';
import { ASSIGNMENT_MODES } from '@/components/onboarding/BodyAssignment';
import { useLocalization } from '@fluent/react';
import { useForm } from 'react-hook-form';
@@ -61,11 +61,20 @@ export function TrackerAssignOptions({
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const { config, setConfig } = useConfig();
const { control, watch, setValue } = useForm<{
const getPreferredSet = () => {
return (
(Object.entries(ASSIGN_MODE_OPTIONS).find(
([_, count]) => count >= connectedIMUTrackers.length
)?.[0] as AssignMode) ?? AssignMode.All
);
};
const { control, watch } = useForm<{
assignMode: AssignMode;
}>({
defaultValues: {
assignMode: config?.assignMode ?? defaultConfig.assignMode,
assignMode: config?.assignMode || getPreferredSet(),
},
});
const { assignMode } = watch();
@@ -74,19 +83,6 @@ export function TrackerAssignOptions({
setConfig({ assignMode });
}, [assignMode]);
useEffect(() => {
if (connectedIMUTrackers.length <= ASSIGN_MODE_OPTIONS[assignMode]) return;
const selectedAssignMode =
(Object.entries(ASSIGN_MODE_OPTIONS).find(
([_, count]) => count >= connectedIMUTrackers.length
)?.[0] as AssignMode) ?? AssignMode.All;
if (assignMode !== selectedAssignMode) {
setValue('assignMode', selectedAssignMode);
}
}, [connectedIMUTrackers, assignMode]);
if (variant == 'dropdown')
return (
<Dropdown

View File

@@ -354,7 +354,7 @@ export function TrackersAssignPage() {
onlyAssigned={false}
highlightedRoles={firstError?.affectedRoles || []}
rolesWithErrors={rolesWithErrors}
assignMode={config?.assignMode ?? defaultConfig.assignMode}
assignMode={config?.assignMode ?? null}
mirror={mirrorView}
onRoleSelected={tryOpenChokerWarning}
/>

View File

@@ -1,30 +1,16 @@
import { ReactNode, useContext, useLayoutEffect } from 'react';
import { ReactNode } from 'react';
import { ConfigContextC, loadConfig, useConfigProvider } from '@/hooks/config';
import { DEFAULT_LOCALE, LangContext } from '@/i18n/config';
import { getSentryOrCompute } from '@/utils/sentry';
const config = await loadConfig();
if (config?.errorTracking !== undefined) {
// load sentry ASAP to catch early errors
getSentryOrCompute(config.errorTracking ?? false);
getSentryOrCompute(config.errorTracking ?? false, config.uuid);
}
export function ConfigContextProvider({ children }: { children: ReactNode }) {
const context = useConfigProvider(config);
const { changeLocales } = useContext(LangContext);
useLayoutEffect(() => {
changeLocales([config?.lang || DEFAULT_LOCALE]);
}, []);
useLayoutEffect(() => {
if (config?.errorTracking !== undefined) {
// Alows for sentry to refresh if user change the setting once the gui
// is initialized
getSentryOrCompute(config.errorTracking ?? false);
}
}, [config?.errorTracking]);
return (
<ConfigContextC.Provider value={context}>

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,17 +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);
Sentry.metrics.count('mute_checklist_step', 1, {
attributes: { step: TrackingChecklistStepId[stepId] },
});
ignoreStep(stepId, true, false);
}
if (value && ignoredSteps.includes(stepId)) {
ignoreStep(stepId, false);
Sentry.metrics.count('unmute_checklist_step', 1, {
attributes: { step: TrackingChecklistStepId[stepId] },
});
ignoreStep(stepId, false, false);
}
}
};

View File

@@ -464,6 +464,12 @@ export function InterfaceSettings() {
value={'asexual'}
colors="!bg-asexual-flag"
/>
<ThemeSelector
control={control}
name="appearance.theme"
value={'snep'}
colors="!bg-snep"
/>
</div>
</div>

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

@@ -104,7 +104,6 @@ export function TrackerRotCell({
function Header({
name,
className,
first = false,
last = false,
show = true,
@@ -116,17 +115,15 @@ function Header({
show?: boolean;
}) {
return (
<th
className={classNames('text-start px-2', {
<div
className={classNames('text-start px-2 flex items-center', {
hidden: !show,
'pl-4': first,
'pr-4': last,
})}
>
<div className={className}>
<Typography id={name} whitespace="whitespace-nowrap" />
</div>
</th>
<Typography id={name} whitespace="whitespace-nowrap" />
</div>
);
}
@@ -147,7 +144,9 @@ function Cell({
const velocity = useVelocity();
return (
<td className={classNames('py-2 group overflow-hidden', { hidden: !show })}>
<div
className={classNames('py-2 group overflow-hidden', { hidden: !show })}
>
<div
style={{
boxShadow: `0px 0px ${Math.floor(velocity * 8)}px ${Math.floor(
@@ -161,7 +160,7 @@ function Cell({
>
{children}
</div>
</td>
</div>
);
}
@@ -171,10 +170,12 @@ function Row({
data,
highlightedTrackers,
clickedTracker,
gridTemplateColumns,
}: {
data: FlatDeviceTracker;
highlightedTrackers: highlightedTrackers | undefined;
clickedTracker: (tracker: TrackerDataT) => void;
gridTemplateColumns: string;
}) {
const { config } = useConfig();
const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary';
@@ -191,6 +192,11 @@ function Row({
return (
<TrackerRowProvider.Provider value={data}>
<div className="relative z-10">
<div className="absolute top-2 left-5">
<FirmwareIcon tracker={tracker} device={device} />
</div>
</div>
<Tooltip
disabled={!warning}
preferedDirection="top"
@@ -202,16 +208,14 @@ function Row({
</div>
)
}
tag="tr"
spacing={-5}
>
<>
<div className="relative z-10">
<div className="absolute top-2 left-5">
<FirmwareIcon tracker={tracker} device={device} />
</div>
</div>
<tr className="group" onClick={() => clickedTracker(tracker)}>
<div
className="group grid items-center"
style={{ gridTemplateColumns }}
onClick={() => clickedTracker(tracker)}
>
<Cell first>
<TrackerNameCell tracker={tracker} warning={warning} />
</Cell>
@@ -225,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}
/>
)}
@@ -239,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>
@@ -287,7 +297,7 @@ function Row({
).toString()}
</Typography>
</Cell>
</tr>
</div>
</>
</Tooltip>
</TrackerRowProvider.Provider>
@@ -307,7 +317,7 @@ export function TrackersTable({
const filteringEnabled =
config?.debug && config?.devSettings?.filterSlimesAndHMD;
const sortingEnabled = config?.debug && config?.devSettings?.sortByName;
// TODO: fix memo
const filteredSortedTrackers = useMemo(() => {
const list = filteringEnabled
? flatTrackers.filter((t) => isHMD(t) || isSlime(t))
@@ -321,49 +331,62 @@ export function TrackersTable({
const moreInfo = config?.devSettings?.moreInfo;
const gridTemplateColumns = useMemo(() => {
const cols = [
'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('11rem'); // URL
}
return cols.join(' ');
}, [config?.devSettings?.preciseRotation, moreInfo]);
return (
<div className="w-full overflow-x-auto py-2 px-2">
<table className="w-full" cellPadding={0} cellSpacing={0}>
<tr>
<div className="min-w-fit">
<div className="grid items-center mb-1" style={{ gridTemplateColumns }}>
<Header name={'tracker-table-column-name'} first />
<Header name={'tracker-table-column-type'} />
<Header name={'tracker-table-column-battery'} />
<Header name={'tracker-table-column-ping'} className="w-24" />
<Header name={'tracker-table-column-ping'} />
<Header name={'tracker-table-column-tps'} />
<Header
name={'tracker-table-column-rotation'}
className={classNames({
'w-44': config?.devSettings?.preciseRotation,
'w-32': !config?.devSettings?.preciseRotation,
})}
/>
<Header name={'tracker-table-column-rotation'} />
<Header name={'tracker-table-column-temperature'} last={!moreInfo} />
<Header
name={'tracker-table-column-linear-acceleration'}
className="w-36"
show={moreInfo}
/>
<Header name={'tracker-table-column-position'} show={moreInfo} />
<Header name={'tracker-table-column-stay_aligned'} show={moreInfo} />
<Header
name={'tracker-table-column-position'}
className="w-36"
show={moreInfo}
/>
<Header
name={'tracker-table-column-stay_aligned'}
className="w-36"
name={'tracker-table-column-url'}
show={moreInfo}
last={moreInfo}
/>
<Header name={'tracker-table-column-url'} show={moreInfo} />
</tr>
{filteredSortedTrackers.map((data) => (
<Row
clickedTracker={clickedTracker}
data={data}
highlightedTrackers={highlightedTrackers}
/>
))}
</table>
</div>
<div className="flex flex-col gap-y-0">
{filteredSortedTrackers.map((data) => (
<Row
key={data.tracker.trackerId?.trackerNum}
clickedTracker={clickedTracker}
data={data}
highlightedTrackers={highlightedTrackers}
gridTemplateColumns={gridTemplateColumns}
/>
))}
</div>
</div>
</div>
);
}

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

@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { createContext, useContext, useEffect, useLayoutEffect, useState } from 'react';
import {
DataFeedMessage,
DataFeedUpdateT,
@@ -12,8 +12,9 @@ import { useBonesDataFeedConfig, useDataFeedConfig } from './datafeed-config';
import { useWebsocketAPI } from './websocket-api';
import { useAtomValue, useSetAtom } from 'jotai';
import { bonesAtom, datafeedAtom, devicesAtom } from '@/store/app-store';
import { updateSentryContext } from '@/utils/sentry';
import { getSentryOrCompute, updateSentryContext } from '@/utils/sentry';
import { fetchCurrentFirmwareRelease, FirmwareRelease } from './firmware-update';
import { DEFAULT_LOCALE, LangContext } from '@/i18n/config';
export interface AppContext {
currentFirmwareRelease: FirmwareRelease | null;
@@ -22,6 +23,7 @@ export interface AppContext {
export function useProvideAppContext(): AppContext {
const { useRPCPacket, sendDataFeedPacket, useDataFeedPacket, isConnected } =
useWebsocketAPI();
const { changeLocales } = useContext(LangContext);
const { config } = useConfig();
const { dataFeedConfig } = useDataFeedConfig();
const bonesDataFeedConfig = useBonesDataFeedConfig();
@@ -58,14 +60,30 @@ export function useProvideAppContext(): AppContext {
});
useEffect(() => {
if (!config) return;
const interval = setInterval(() => {
fetchCurrentFirmwareRelease().then((res) => setCurrentFirmwareRelease(res));
fetchCurrentFirmwareRelease(config.uuid).then(setCurrentFirmwareRelease);
}, 1000);
return () => {
clearInterval(interval);
};
}, [config?.uuid]);
useLayoutEffect(() => {
changeLocales([config?.lang || DEFAULT_LOCALE]);
}, []);
useLayoutEffect(() => {
if (!config) return;
if (config.errorTracking !== undefined) {
console.log('change');
// Alows for sentry to refresh if user change the setting once the gui
// is initialized
getSentryOrCompute(config.errorTracking ?? false, config.uuid);
}
}, [config]);
return {
currentFirmwareRelease,
};

View File

@@ -9,6 +9,7 @@ import { load, Store } from '@tauri-apps/plugin-store';
import { useIsTauri } from './breakpoint';
import { waitUntil } from '@/utils/a11y';
import { isTauri } from '@tauri-apps/api/core';
import { v4 as uuidv4 } from 'uuid';
export interface WindowConfig {
width: number;
@@ -26,6 +27,7 @@ export enum AssignMode {
}
export interface Config {
uuid: string;
debug: boolean;
lang: string;
doneOnboarding: boolean;
@@ -39,7 +41,7 @@ export interface Config {
fonts: string[];
useTray: boolean | null;
mirrorView: boolean;
assignMode: AssignMode;
assignMode: AssignMode | null;
discordPresence: boolean;
errorTracking: boolean | null;
decorations: boolean;
@@ -57,6 +59,7 @@ export interface ConfigContext {
}
export const defaultConfig: Config = {
uuid: uuidv4(),
lang: 'en',
debug: false,
doneOnboarding: false,
@@ -69,7 +72,7 @@ export const defaultConfig: Config = {
fonts: ['poppins'],
useTray: null,
mirrorView: true,
assignMode: AssignMode.Core,
assignMode: null,
discordPresence: false,
errorTracking: null,
decorations: false,
@@ -117,13 +120,16 @@ export const loadConfig = async () => {
if (!json) throw new Error('Config has ceased existing for some reason');
const loadedConfig = fallbackToDefaults(JSON.parse(json));
// set(loadedConfig);
// setLoading(false);
if (!loadedConfig.uuid) {
// Make sure the config always has a uuid
loadedConfig.uuid = uuidv4();
await store.set('config.json', JSON.stringify(loadedConfig));
}
return loadedConfig;
} catch (e) {
error(e);
// setConfig(defaultConfig);
// setLoading(false);
return null;
}
};

View File

@@ -1,5 +1,5 @@
// implemetation of https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
export function hash(str: string) {
export function normalizedHash(str: string) {
let hash = 2166136261;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);

View File

@@ -2,8 +2,7 @@ import { BoardType, DeviceDataT } from 'solarxr-protocol';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { cacheWrap } from './cache';
import semver from 'semver';
import { hash } from './crypto';
import { getUserID } from './user';
import { normalizedHash } from './crypto';
export interface FirmwareRelease {
name: string;
@@ -24,7 +23,7 @@ const todaysRange = (deployData: [number, Date][]): number => {
return maxRange;
};
const checkUserCanUpdate = async (url: string, fwVersion: string) => {
const checkUserCanUpdate = async (uuid: string, url: string, fwVersion: string) => {
const deployDataJson = JSON.parse(
(await cacheWrap(
`firmware-${fwVersion}-deploy`,
@@ -56,12 +55,13 @@ const checkUserCanUpdate = async (url: string, fwVersion: string) => {
const todayUpdateRange = todaysRange(deployData);
if (!todayUpdateRange) return false;
const uniqueUserKey = await getUserID();
// Make it so the hash change every version. Prevent the same user from getting the same delay
return hash(`${uniqueUserKey}-${fwVersion}`) <= todayUpdateRange;
return normalizedHash(`${uuid}-${fwVersion}`) <= todayUpdateRange;
};
export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | null> {
export async function fetchCurrentFirmwareRelease(
uuid: string
): Promise<FirmwareRelease | null> {
const releases: any[] | null = JSON.parse(
(await cacheWrap(
'firmware-releases',
@@ -93,6 +93,7 @@ export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | n
}
const userCanUpdate = await checkUserCanUpdate(
uuid,
deployAsset.browser_download_url,
version
);
@@ -128,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')
) {
@@ -142,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);
@@ -47,7 +50,12 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
req.bodyParts = parts;
sendRPCPacket(RpcMessage.ResetRequest, req);
Sentry.metrics.count('reset_click', 1, { attributes: options });
Sentry.metrics.count('reset_click', 1, {
attributes: {
resetType: ResetType[options.type],
group: options.type === ResetType.Mounting ? options.group : undefined,
},
});
};
const onResetFinished = () => {
@@ -57,6 +65,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
const onResetCanceled = () => {
if (status !== 'finished') setStatus('idle');
if (onFailed) onFailed();
};
useEffect(() => {

View File

@@ -10,6 +10,7 @@ import {
} from 'solarxr-protocol';
import { useWebsocketAPI } from './websocket-api';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import * as Sentry from '@sentry/react';
export const trackingchecklistIdtoLabel: Record<TrackingChecklistStepId, string> = {
[TrackingChecklistStepId.UNKNOWN]: '',
@@ -91,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: [],
@@ -103,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,
@@ -160,23 +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);
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,8 +0,0 @@
import { locale, hostname, platform, version } from '@tauri-apps/plugin-os';
import { hash } from './crypto';
export async function getUserID() {
// FIXME: This does not support android. It currently return the same id for all android users
return hash(`${await hostname()}-${await locale()}-${platform()}-${version()}`);
}

View File

@@ -302,11 +302,11 @@ body {
--background-80: 15, 15, 15;
--background-90: 0, 0, 0;
--accent-background-10: 230, 0, 230;
--accent-background-20: 210, 0, 210;
--accent-background-30: 150, 0, 150;
--accent-background-40: 130, 0, 130;
--accent-background-50: 70, 0, 70;
--accent-background-10: 230, 115, 230;
--accent-background-20: 210, 70, 210;
--accent-background-30: 150, 20, 150;
--accent-background-40: 115, 5, 115;
--accent-background-50: 75, 0, 75;
--success: 100, 230, 10;
--warning: 220, 200, 50;
@@ -317,6 +317,32 @@ body {
--default-color: 255, 255, 255;
}
:root[data-theme='snep'] {
--background-10: 255, 255, 255;
--background-20: 240, 222, 236;
--background-30: 210, 198, 203;
--background-40: 155, 140, 147;
--background-50: 97, 77, 86;
--background-60: 72, 54, 62;
--background-70: 55, 40, 47;
--background-80: 38, 27, 32;
--background-90: 0, 0, 0;
--accent-background-10: 255, 204, 229;
--accent-background-20: 234, 115, 176;
--accent-background-30: 184, 70, 127;
--accent-background-40: 143, 54, 98;
--accent-background-50: 91, 27, 58;
--success: 139, 223, 35;
--warning: 255, 187, 62;
--critical: 223, 54, 84;
--special: 230, 0, 230;
--window-icon-stroke: 234, 134, 185;
--default-color: 255, 255, 255;
}
#root {
height: 100%;
}

View File

@@ -8,9 +8,9 @@ import {
useNavigationType,
} from 'react-router-dom';
import { DeviceDataT } from 'solarxr-protocol';
import { getUserID } from '@/hooks/user';
export function getSentryOrCompute(enabled = false) {
export function getSentryOrCompute(enabled = false, uuid: string) {
Sentry.setUser({ id: uuid });
// if sentry is already initialized - SKIP
if (enabled && Sentry.isInitialized()) return;
@@ -63,10 +63,6 @@ export function getSentryOrCompute(enabled = false) {
log('Initialized the Sentry client');
}
getUserID().then((id) => {
Sentry.setUser({ id });
});
return newClient;
}

View File

@@ -158,6 +158,11 @@ const colors = {
300: '#FFFFFF',
400: '#800080',
},
snep: {
100: '#261B20',
200: '#5B1B3A',
300: '#FFCCE5',
},
};
const config = {
@@ -289,6 +294,7 @@ const config = {
light: `linear-gradient(135deg, ${colors['light-accent'][100]} 50%, ${colors['light-background'][700]} 50% 100%)`,
'trans-flag': `linear-gradient(135deg, ${colors['trans-blue'][800]} 40%, ${colors['trans-blue'][700]} 40% 70%, ${colors['trans-blue'][600]} 70% 100%)`,
'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`,
'snep': `linear-gradient(135deg, ${colors['snep'][100]} 40%, ${colors['snep'][200]} 40% 70%, ${colors['snep'][300]} 70% 100%)`,
},
animation: {
'spin-ccw': 'spin-ccw 1s linear infinite',

9
pnpm-lock.yaml generated
View File

@@ -161,6 +161,9 @@ importers:
use-double-tap:
specifier: ^1.3.6
version: 1.3.6(react@18.3.1)
uuid:
specifier: ^13.0.0
version: 13.0.0
yup:
specifier: ^1.4.0
version: 1.4.0
@@ -4534,6 +4537,10 @@ packages:
resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
engines: {node: '>= 4'}
uuid@13.0.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -9720,6 +9727,8 @@ snapshots:
utility-types@3.11.0: {}
uuid@13.0.0: {}
uuid@9.0.1: {}
vfile-message@4.0.2:

View File

@@ -1,3 +1,3 @@
/build
/src/main/resources/web-gui
/src/main/assets/web-gui
/secrets

View File

@@ -5,8 +5,11 @@
* For more details take a look at the Java Libraries chapter in the Gradle
* User Manual available at https://docs.gradle.org/6.3/userguide/java_library_plugin.html
*/
import com.android.build.gradle.internal.tasks.BaseTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Base64
plugins {
kotlin("android")
@@ -28,8 +31,8 @@ java {
}
}
tasks.register<Copy>("copyGuiAssets") {
val target = layout.projectDirectory.dir("src/main/resources/web-gui")
val copyGuiAssets = tasks.register<Copy>("copyGuiAssets") {
val target = layout.projectDirectory.dir("src/main/assets/web-gui")
delete(target)
from(rootProject.layout.projectDirectory.dir("gui/dist"))
into(target)
@@ -37,17 +40,45 @@ tasks.register<Copy>("copyGuiAssets") {
throw GradleException("You need to run \"pnpm run build\" on the gui folder first!")
}
}
tasks.register("validateKeyStore") {
val storeFile = android.buildTypes.getByName("release").signingConfig?.storeFile
// Only warn for now since this is run even when irrelevant
if (storeFile?.isFile != true) {
logger.error("Android KeyStore file does not exist or is not a file: ${storeFile?.path}")
} else if (storeFile.length() <= 0) {
logger.error("Android KeyStore file is empty: ${storeFile.path}")
tasks.preBuild {
dependsOn(copyGuiAssets)
}
// Set up signing pre/post tasks
val preSign = tasks.register("preSign") {
dependsOn(writeTempKeyStore)
}
val postSign = tasks.register("postSign") {
finalizedBy(deleteTempKeyStore)
}
tasks.withType<BaseTask> {
dependsOn(preSign)
finalizedBy(postSign)
}
// Handle GitHub secret Android KeyStore files
val envKeyStore: String? = System.getenv("ANDROID_STORE_FILE")?.takeIf { it.isNotBlank() }
val tempKeyStore = project.layout.buildDirectory.file("tmp/keystore.tmp.jks").get().asFile
val writeTempKeyStore = tasks.register("writeTempKeyStore") {
if (envKeyStore != null) {
doLast {
tempKeyStore.apply {
ensureParentDirsCreated()
tempKeyStore.writeBytes(Base64.getDecoder().decode(envKeyStore))
tempKeyStore.deleteOnExit()
}
}
finalizedBy(deleteTempKeyStore)
} else {
enabled = false
}
}
tasks.preBuild {
dependsOn(":server:android:copyGuiAssets", ":server:android:validateKeyStore")
val deleteTempKeyStore = tasks.register<Delete>("deleteTempKeyStore") {
if (envKeyStore != null) {
delete(tempKeyStore)
} else {
enabled = false
}
}
tasks.withType<KotlinCompile> {
@@ -86,10 +117,6 @@ dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
// For hosting web GUI
implementation("io.ktor:ktor-server-core:2.3.13")
implementation("io.ktor:ktor-server-netty:2.3.13")
implementation("io.ktor:ktor-server-caching-headers:2.3.13")
// Serial
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
@@ -133,22 +160,37 @@ android {
// adds an offset of the version code as we might do apk releases in the middle of actual
// releases if we failed on bundling or stuff
val versionCodeOffset = 2
val versionCodeOffset = 4
// Defines the version number of your app.
versionCode = (extra["gitVersionCode"] as? Int)?.plus(versionCodeOffset) ?: 0
// Defines a user-friendly version name for your app.
versionName = extra["gitVersionName"] as? String ?: "v0.0.0"
logger.lifecycle("i: Configured for SlimeVR Android version \"$versionName\" ($versionCode).")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
create("release") {
storeFile = file("./secrets/keystore.jks")
storePassword = System.getenv("ANDROID_STORE_PASSWD")
keyAlias = System.getenv("ANDROID_KEY_ALIAS")
keyPassword = System.getenv("ANDROID_KEY_PASSWD")
val inputKeyStore: File? = if (envKeyStore != null) {
logger.lifecycle("i: \"ANDROID_STORE_FILE\" environment variable found, using for signing config.")
tempKeyStore
} else {
file("secrets/keystore.jks").takeIf { it.canRead() && it.length() > 0 }
}
if (inputKeyStore != null) {
logger.info("i: Configuring signing for Android KeyStore file: \"${inputKeyStore.path}\".")
create("release") {
storeFile = inputKeyStore
storePassword = System.getenv("ANDROID_STORE_PASSWD")
keyAlias = System.getenv("ANDROID_KEY_ALIAS") ?: "key0"
keyPassword = System.getenv("ANDROID_KEY_PASSWD")
}
} else {
logger.warn("w: Android KeyStore file is not valid or not found, skipping signing.")
}
}
@@ -170,7 +212,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName("release")
signingConfig = signingConfigs.findByName("release")
}
}

View File

@@ -26,10 +26,37 @@
-dontwarn reactor.blockhound.integration.BlockHoundIntegration
-keep class io.ktor.** { *; }
-keep class io.netty.** {*; }
-keep class io.netty.** { *; }
-keep class kotlin.reflect.jvm.internal.** { *; }
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.atomicfu.**
-dontwarn io.netty.**
-dontwarn com.typesafe.**
-dontwarn org.slf4j.**
# Proguard configuration for 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.* *;
#}
# Proguard configuration for SnakeYAML 2.X
#-keep class org.yaml.snakeyaml.** { *; }
#-dontwarn 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

@@ -12,50 +12,15 @@ import dev.slimevr.android.tracking.trackers.hid.AndroidHIDManager
import dev.slimevr.config.ConfigManager
import dev.slimevr.tracking.trackers.Tracker
import io.eiren.util.logging.LogManager
import io.ktor.http.CacheControl
import io.ktor.http.CacheControl.Visibility
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.CachingOptions
import io.ktor.server.http.content.staticResources
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.plugins.cachingheaders.CachingHeaders
import io.ktor.server.routing.routing
import kotlinx.coroutines.runBlocking
import java.io.File
import java.time.ZonedDateTime
import kotlin.concurrent.thread
import kotlin.system.exitProcess
lateinit var webServer: NettyApplicationEngine
private set
val webServerInitialized: Boolean
get() = ::webServer.isInitialized
var webServerPort = 0
lateinit var vrServer: VRServer
private set
val vrServerInitialized: Boolean
get() = ::vrServer.isInitialized
fun startWebServer() {
// Host the web GUI server
webServer = embeddedServer(Netty, port = 0) {
routing {
install(CachingHeaders) {
options { _, _ ->
CachingOptions(CacheControl.NoStore(Visibility.Public), ZonedDateTime.now())
}
}
staticResources("/", "web-gui", "index.html")
}
}.start(wait = false)
webServerPort = runBlocking { webServer.resolvedConnectors().first().port }
}
fun startVRServer(activity: AppCompatActivity) {
thread(start = true, name = "Main VRServer Thread") {
try {

View File

@@ -3,10 +3,14 @@ package dev.slimevr.android
import android.content.Intent
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import io.eiren.util.logging.LogManager
import java.io.IOException
import java.net.URLConnection
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -28,14 +32,6 @@ class MainActivity : AppCompatActivity() {
}
initLock.withLock {
// Start the GUI if it isn't already running
if (!webServerInitialized) {
LogManager.info("[MainActivity] WebServer isn't running yet, starting it...")
startWebServer()
} else {
LogManager.info("[MainActivity] WebServer is already running, skipping initialization.")
}
// Start the server if it isn't already running
if (!vrServerInitialized) {
LogManager.info("[MainActivity] VRServer isn't running yet, starting it...")
@@ -53,6 +49,35 @@ class MainActivity : AppCompatActivity() {
// Enable debug mode
WebView.setWebContentsDebuggingEnabled(true)
// Handle path resolution
guiWebView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if ((request.url.scheme != "http" && request.url.scheme != "https") ||
request.url.host != "slimevr.gui"
) {
return null
}
val path = when (request.url.path) {
null, "", "/" -> "/index.html"
else -> request.url.path
}
return try {
WebResourceResponse(
URLConnection.guessContentTypeFromName(path) ?: "text/plain",
null,
assets.open("web-gui$path"),
)
} catch (_: IOException) {
WebResourceResponse(null, null, null)
}
}
}
// Set required features
guiWebView.settings.javaScriptEnabled = true
guiWebView.settings.domStorageEnabled = true
@@ -66,12 +91,8 @@ class MainActivity : AppCompatActivity() {
guiWebView.settings.loadWithOverviewMode = true
guiWebView.invokeZoomPicker()
// Disable cache! This is all local anyway
guiWebView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
guiWebView.clearCache(true)
// Load GUI page
guiWebView.loadUrl("http://127.0.0.1:$webServerPort/")
guiWebView.loadUrl("https://slimevr.gui/")
LogManager.info("[MainActivity] GUI WebView has been initialized and loaded.")
// Start a foreground service to notify the user the SlimeVR Server is running

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

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());
}
}

Some files were not shown because too many files have changed in this diff Show More