mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0236a05f26 | ||
|
|
28deb357da | ||
|
|
4d93f87a01 | ||
|
|
88adfce242 | ||
|
|
3d02795dbc | ||
|
|
7ff50f78eb | ||
|
|
0e3aaf105c | ||
|
|
f638540886 | ||
|
|
343d69d690 | ||
|
|
e2d7d354c6 | ||
|
|
cc6f297b92 | ||
|
|
2add43e71a | ||
|
|
0a493ac345 | ||
|
|
17bb2703d1 | ||
|
|
f0981bf709 | ||
|
|
99de554c18 | ||
|
|
f95a4d56d7 | ||
|
|
1df3c9d322 | ||
|
|
e0838cce6c | ||
|
|
e25d3201c2 | ||
|
|
5d14f14139 | ||
|
|
09e81f5ace | ||
|
|
8f57ef2de4 | ||
|
|
ea242960b3 | ||
|
|
35ac14a7de | ||
|
|
690a8b5c6e | ||
|
|
255b8b2865 | ||
|
|
e27ec63985 | ||
|
|
9f8be6551c | ||
|
|
f09cd687c7 | ||
|
|
686499f8dd | ||
|
|
a3bcc61892 | ||
|
|
faf70c9a39 | ||
|
|
2aa8d3a056 | ||
|
|
23df46ca33 | ||
|
|
8407f52777 | ||
|
|
b44dcaa9c2 | ||
|
|
ff0d823aff | ||
|
|
2e8bfa5373 | ||
|
|
87940ddd03 | ||
|
|
6208979ce9 | ||
|
|
9a27fb1320 | ||
|
|
53129328d0 | ||
|
|
2d79c5a0e9 | ||
|
|
74f5a92ce1 | ||
|
|
146930279c | ||
|
|
0c33579858 | ||
|
|
c9783d097b | ||
|
|
d3eafb8d06 | ||
|
|
09d44b51d6 | ||
|
|
cf357e71f5 | ||
|
|
122efacc52 | ||
|
|
7f536528d0 | ||
|
|
3982249ebf | ||
|
|
388bea2e72 | ||
|
|
921a760817 | ||
|
|
55bcec4dda | ||
|
|
bb08e8dc6a | ||
|
|
a82f950eb6 | ||
|
|
e2dbaab8ba | ||
|
|
3611bb5cc7 | ||
|
|
f01f599526 | ||
|
|
6847526ce8 | ||
|
|
c5f28a6a01 | ||
|
|
86d7d5fdc6 | ||
|
|
781f4d489a | ||
|
|
5a42426048 | ||
|
|
44643f2cc6 | ||
|
|
d902515f4f | ||
|
|
f9df08aefd | ||
|
|
28b18e0d42 | ||
|
|
247c063791 | ||
|
|
ab248287cc | ||
|
|
9a26fc98b8 | ||
|
|
16a2ac8474 | ||
|
|
c4acf4cc41 | ||
|
|
4b0a2d27d0 | ||
|
|
2c6708bfe7 | ||
|
|
2880623cce | ||
|
|
17400ca337 | ||
|
|
3276f6db7a | ||
|
|
db59537adc | ||
|
|
4f1fd82923 | ||
|
|
f6ccb5970f | ||
|
|
c937b91267 | ||
|
|
2d1f32b950 | ||
|
|
8acba98bcc | ||
|
|
d7ba1b8335 | ||
|
|
d20e9bfd94 | ||
|
|
3d54a86bd8 | ||
|
|
c9883f5eb4 | ||
|
|
8bd36fac25 | ||
|
|
ab4d507d9f | ||
|
|
9efb985260 | ||
|
|
2c2c227187 | ||
|
|
63cca6756e | ||
|
|
b0d7fefa5e | ||
|
|
35a5cb47d9 | ||
|
|
dfc4383271 | ||
|
|
185431a733 | ||
|
|
5b68a01186 | ||
|
|
2c4dd4085f | ||
|
|
4d3ff0e9c9 | ||
|
|
ee6182bb23 | ||
|
|
9576d6e034 | ||
|
|
227ddc87d2 | ||
|
|
b3b7730b2c | ||
|
|
075a155f13 | ||
|
|
79a3b66e43 | ||
|
|
276e73e724 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
|
||||
6
.github/workflows/build-gui.yml
vendored
6
.github/workflows/build-gui.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
52
.github/workflows/gradle.yaml
vendored
52
.github/workflows/gradle.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function TipBox({
|
||||
whitespace = false,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
hideIcon?: boolean;
|
||||
whitespace?: boolean;
|
||||
className?: string;
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}`);
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
9
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
2
server/android/.gitignore
vendored
2
server/android/.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
/build
|
||||
/src/main/resources/web-gui
|
||||
/src/main/assets/web-gui
|
||||
/secrets
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
server/android/proguard-rules.pro
vendored
29
server/android/proguard-rules.pro
vendored
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
|
||||
class HIDConfig {
|
||||
var trackersOverHID = false
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.slimevr.protocol
|
||||
|
||||
import solarxr_protocol.data_feed.DataFeedConfigT
|
||||
|
||||
class DataFeed(val config: DataFeedConfigT) {
|
||||
var timeLastSent: Long = System.currentTimeMillis()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dev.slimevr.protocol;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
public interface ProtocolAPIServer {
|
||||
|
||||
Stream<GenericConnection> getAPIConnections();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.slimevr.protocol
|
||||
|
||||
import java.util.stream.Stream
|
||||
|
||||
interface ProtocolAPIServer {
|
||||
val apiConnections: Stream<GenericConnection>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user