Merge branch 'main' into fingertracking

This commit is contained in:
Erimel
2025-09-26 18:45:45 -04:00
102 changed files with 1668 additions and 1181 deletions

32
.github/CODEOWNERS vendored
View File

@@ -2,23 +2,23 @@
* @Eirenliel
# Make everyone be able to approve SolarXR submodule changes
/solarxr-protocol @ButterscotchV @Erimelowo @ImUrX @loucass003
/solarxr-protocol @ButterscotchV @Erimelowo @loucass003
# Make Loucas and Uriel the owners of all GUI stuff
/gui/ @ImUrX @loucass003
/pnpm-lock.yaml @ImUrX @loucass003
/pnpm-workspace.yaml @ImUrX @loucass003
# Make Loucass the owner of all GUI stuff
/gui/ @loucass003
/pnpm-lock.yaml @loucass003
/pnpm-workspace.yaml @loucass003
# Uriel and Erimel responsible for i18n
/gui/public/i18n/ @ImUrX @Erimelowo
/gui/src/i18n/ @ImUrX @Erimelowo
/l10n.toml @ImUrX @Erimelowo
# loucass003 and Erimel responsible for i18n
/gui/public/i18n/ @loucass003 @Erimelowo
/gui/src/i18n/ @loucass003 @Erimelowo
/l10n.toml @loucass003 @Erimelowo
/gui/src/components/settings/ @Erimelowo @ImUrX @loucass003
/gui/src/components/settings/ @Erimelowo @loucass003
# Rust part of the GUI
/gui/src-tauri/ @ImUrX
/Cargo.lock @ImUrX
/gui/src-tauri/ @loucass003
/Cargo.lock @loucass003
# Some server code~
/server/ @ButterscotchV @Eirenliel @Erimelowo
@@ -32,7 +32,7 @@
/server/src/main/java/dev/slimevr/filtering/ @Erimelowo
# Linux files
*.nix @ImUrX
/flake.lock @ImUrX
/dev.slimevr.SlimeVR.metainfo.xml @ImUrX
/.envrc @ImUrX
*.nix @loucass003
/flake.lock @loucass003
/dev.slimevr.SlimeVR.metainfo.xml @loucass003
/.envrc @loucass003

View File

@@ -21,13 +21,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -59,13 +59,13 @@ jobs:
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'aarch64' || 'amd64' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- if: startsWith(matrix.os, 'ubuntu')
name: Set up Linux dependencies
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: libgtk-3-dev webkit2gtk-4.1 libappindicator3-dev librsvg2-dev patchelf
# Increment to invalidate the cache
@@ -85,7 +85,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: '22.x'

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
@@ -27,7 +27,7 @@ jobs:
run: git fetch --tags origin --recurse-submodules=no --force
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'adopt'
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
@@ -56,7 +56,7 @@ jobs:
run: git fetch --tags origin --recurse-submodules=no --force
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'adopt'
@@ -87,7 +87,7 @@ jobs:
bundle-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
@@ -95,7 +95,7 @@ jobs:
run: git fetch --tags origin --recurse-submodules=no --force
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'adopt'
@@ -105,7 +105,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -154,17 +154,17 @@ jobs:
env:
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'aarch64' || 'amd64' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
- name: Set up Linux dependencies
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: |
build-essential curl wget file libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev
@@ -190,7 +190,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -252,11 +252,11 @@ jobs:
needs: [build, test]
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
@@ -266,7 +266,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -332,11 +332,11 @@ jobs:
env:
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'win-aarch64' || 'win64' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: 'SlimeVR-Server'
path: server/desktop/build/libs/
@@ -351,7 +351,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -17,6 +17,6 @@ jobs:
pull-requests: write
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -12,7 +12,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: repo-sync/pull-request@v2

View File

@@ -15,7 +15,7 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: pontoon
submodules: recursive

View File

@@ -1125,6 +1125,7 @@ onboarding-automatic_mounting-preparation-v2-step-2 = 3. Hold the position until
onboarding-automatic_mounting-put_trackers_on-title = Put on your trackers
onboarding-automatic_mounting-put_trackers_on-description = To calibrate mounting orientations, we're gonna use the trackers you just assigned. Put on all your trackers, you can see which are which in the figure to the right.
onboarding-automatic_mounting-put_trackers_on-next = I have all my trackers on
onboarding-automatic_mounting-return-home = Done
## Tracker manual proportions setupa
onboarding-manual_proportions-back = Go Back to Reset tutorial

View File

@@ -65,6 +65,10 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
</provides>
<releases>
<release version="0.16.2" date="2025-08-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.2</url></release>
<release version="0.16.1" date="2025-07-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1</url></release>
<release version="0.16.1~rc.2" type="development" date="2025-07-17"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.2</url></release>
<release version="0.16.1~rc.1" type="development" date="2025-07-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.1</url></release>
<release version="0.16.0" date="2025-07-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0</url></release>
<release version="0.16.0~rc.2" type="development" date="2025-06-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.2</url></release>
<release version="0.16.0~rc.1" type="development" date="2025-05-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.1</url></release>

View File

@@ -206,6 +206,11 @@ export default function App() {
}, []);
useEffect(() => {
// don't show update stuff when on android
if (window.__ANDROID__?.isThere()) {
setUpdateFound('');
return;
}
async function fetchReleases() {
const releases = await fetch(
`https://api.github.com/repos/${GH_REPO}/releases`

View File

@@ -66,9 +66,7 @@ export function WidgetsComponent() {
bodyPartsToReset="fingers"
></ResetButton>
<ClearMountingButton></ClearMountingButton>
{(typeof __ANDROID__ === 'undefined' || !__ANDROID__?.isThere()) && (
<BVHButton></BVHButton>
)}
{!window.__ANDROID__?.isThere() && <BVHButton></BVHButton>}
<TrackingPauseButton></TrackingPauseButton>
</div>
<div className="w-full">

View File

@@ -28,7 +28,7 @@ export function ArrowLink({
};
return classNames(
variantsMap[variant],
'flex gap-2 hover:fill-background-10 hover:text-background-10 fill-background-30 text-background-30'
'flex gap-2 hover:fill-background-10 fill-background-30 text-background-10'
);
}, [variant]);

View File

@@ -53,7 +53,7 @@ export function CheckBox({
className={classNames(
{
'rounded-lg': outlined,
'text-background-30': !outlined || disabled,
'text-background-10': !outlined || disabled,
'bg-background-60': outlined && color === 'primary',
'bg-background-70': outlined && color === 'secondary',
'bg-background-50': outlined && color === 'tertiary',

View File

@@ -12,6 +12,8 @@ import { FileIcon } from './icon/FileIcon';
import { UploadFileIcon } from './icon/UploadFileIcon';
import { Typography } from './Typography';
import { CloseIcon } from './icon/CloseIcon';
import { FolderIcon } from './icon/FolderIcon';
import { UploadFolderIcon } from './icon/UploadFolderIcon';
interface InputProps {
variant?: 'primary' | 'secondary';
@@ -22,9 +24,11 @@ interface InputProps {
export const FileInputContentBlank = ({
isDragging,
label,
directory = false,
}: {
isDragging: boolean;
label: string;
directory?: boolean;
}) => {
return (
<div
@@ -36,7 +40,11 @@ export const FileInputContentBlank = ({
)}
>
<span className="flex items-center space-x-2 pointer-events-none">
<UploadFileIcon isDragging={isDragging} />
{directory ? (
<UploadFolderIcon isDragging={isDragging} />
) : (
<UploadFileIcon isDragging={isDragging} />
)}
<div>
<Localized
id={label}
@@ -57,10 +65,12 @@ export const FileInputContentBlank = ({
export const FileInputContentFile = ({
importedFileName,
directory = false,
onClearPicker,
}: {
importedFileName: string;
onClearPicker: () => any;
directory?: boolean;
}) => {
return (
<div
@@ -72,7 +82,7 @@ export const FileInputContentFile = ({
>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 px-4">
<FileIcon />
{directory ? <FolderIcon /> : <FileIcon />}
<span>{importedFileName}</span>
</div>
<span className="flex-grow"></span>

View File

@@ -47,19 +47,19 @@ export const InputInside = forwardRef<
const classes = useMemo(() => {
const variantsMap = {
primary: classNames({
'placeholder:text-background-30 bg-background-60 border-background-60':
'placeholder:text-background-10 placeholder:italic bg-background-60 border-background-60':
!disabled,
'text-background-30 placeholder:text-background-30 border-background-70 bg-background-70':
disabled,
}),
secondary: classNames({
'placeholder:text-background-30 bg-background-50 border-background-50':
'placeholder:text-background-10 placeholder:italic bg-background-50 border-background-50':
!disabled,
'text-background-40 placeholder:text-background-40 border-background-70 bg-background-70':
disabled,
}),
tertiary: classNames({
'placeholder:text-background-30 bg-background-40 border-background-40':
'placeholder:text-background-10 placeholder:italic bg-background-40 border-background-40':
!disabled,
'text-background-30 placeholder:text-background-30 border-background-70 bg-background-70':
disabled,

View File

@@ -51,9 +51,7 @@ export function Radio({
<div className="flex flex-col gap-2 pointer-events-none">
{children ? children : <Typography bold>{label}</Typography>}
{description && (
<Typography variant="standard" color="secondary">
{description}
</Typography>
<Typography variant="standard">{description}</Typography>
)}
</div>
</label>

View File

@@ -24,10 +24,11 @@ export function InnerTauriFileInput({
<div ref={ref} onClick={async () => onChange(await open({ directory }))}>
{value !== null
? FileInputContentFile({
directory,
importedFileName: value,
onClearPicker: () => onChange(null),
})
: FileInputContentBlank({ isDragging: false, label })}
: FileInputContentBlank({ isDragging: false, label, directory })}
</div>
);
}

View File

@@ -0,0 +1,12 @@
export function FolderIcon({ width = 24 }: { width?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
width={width}
>
<path d="M19.5 21a3 3 0 0 0 3-3v-4.5a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3V18a3 3 0 0 0 3 3h15ZM1.5 10.146V6a3 3 0 0 1 3-3h5.379a2.25 2.25 0 0 1 1.59.659l2.122 2.121c.14.141.331.22.53.22H19.5a3 3 0 0 1 3 3v1.146A4.483 4.483 0 0 0 19.5 9h-15a4.483 4.483 0 0 0-3 1.146Z" />
</svg>
);
}

View File

@@ -0,0 +1,25 @@
import classNames from 'classnames';
export function UploadFolderIcon({
width = 24,
isDragging = false,
}: {
width?: number;
isDragging?: boolean;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 5.5499997 4.7600007"
width={width}
fill="currentColor"
className={classNames('transition-transform', isDragging && 'scale-150')}
>
<path
fill-rule="evenodd"
d="m 4.76,4.76 c 0.44,0 0.79,-0.36 0.79,-0.79 v -2.39 c 0,-0.44 -0.36,-0.79 -0.79,-0.79 H 3.34 c -0.05,0 -0.1,-0.02 -0.14,-0.06 L 2.64,0.17 C 2.53,0.06 2.38,0 2.22,0 H 0.79 C 0.35,0 0,0.36 0,0.79 V 3.97 C 0,4.41 0.36,4.76 0.79,4.76 Z M 2.58,3.71 c 0,0.26 0.4,0.26 0.4,0 V 2.6 L 3.44,3.06 C 3.63,3.23 3.89,2.97 3.72,2.78 L 2.93,1.99 c -0.08,-0.08 -0.2,-0.08 -0.28,0 L 1.86,2.78 C 1.65,2.97 1.95,3.26 2.14,3.06 L 2.6,2.6 Z"
clip-rule="evenodd"
/>
</svg>
);
}

View File

@@ -240,7 +240,7 @@ export function AddImusStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-add_imus_step-description')}
</Typography>
</div>
@@ -297,7 +297,7 @@ export function AddImusStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -53,7 +53,7 @@ export function BoardPinsStep({
<>
<div className="flex flex-col w-full justify-between text-background-10">
<div className="flex flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-board_pins_step-description')}
</Typography>
</div>
@@ -172,7 +172,7 @@ export function BoardPinsStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -68,7 +68,7 @@ export function BuildStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-build_step-description')}
</Typography>
</div>
@@ -82,7 +82,7 @@ export function BuildStep({
: SlimeState.SAD
}
></LoaderIcon>
<Typography variant="section-title" color="secondary">
<Typography variant="section-title">
{l10n.getString('firmware_tool-build-' + buildStatus.status)}
</Typography>
</div>
@@ -91,7 +91,7 @@ export function BuildStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -85,9 +85,7 @@ function FirmwareToolContent() {
.getString('firmware_tool-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>

View File

@@ -22,7 +22,7 @@ export function FlashBtnStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-flashbtn_step-description')}
</Typography>
{defaultConfig?.boardConfig.type ===

View File

@@ -160,7 +160,7 @@ function SerialDevicesList({
</Localized>
{Object.keys(devices).length === 0 ? (
<Localized id="firmware_tool-flash_method_serial-no_devices">
<Typography variant="standard" color="secondary"></Typography>
<Typography variant="standard"></Typography>
</Localized>
) : (
<Dropdown
@@ -274,7 +274,7 @@ function OTADevicesList({
</Localized>
{devices.length === 0 && (
<Localized id="firmware_tool-flash_method_ota-no_devices">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
)}
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
@@ -347,7 +347,7 @@ export function FlashingMethodStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-flash_method_step-description')}
</Typography>
</div>
@@ -416,7 +416,7 @@ export function FlashingMethodStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -164,7 +164,7 @@ export function FlashingStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-flashing_step-description')}
</Typography>
</div>

View File

@@ -27,7 +27,7 @@ export function SelectBoardStep({
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-board_step-description')}
</Typography>
</div>
@@ -91,7 +91,7 @@ export function SelectBoardStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -40,7 +40,7 @@ export function SelectFirmwareStep({
<>
<div className="flex flex-col w-full">
<div className="flex justify-between items-center mobile:flex-col gap-4">
<Typography color="secondary">
<Typography>
{l10n.getString('firmware_tool-select_firmware_step-description')}
</Typography>
<div>
@@ -109,7 +109,7 @@ export function SelectFirmwareStep({
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
)}

View File

@@ -320,7 +320,7 @@ export function FirmwareUpdate() {
<Typography variant="section-title"></Typography>
</Localized>
<Localized id="firmware_update-devices-description">
<Typography variant="standard" color="secondary"></Typography>
<Typography variant="standard"></Typography>
</Localized>
<div className="flex flex-col gap-4 overflow-y-auto xs:max-h-[530px]">
{devices.length === 0 &&

View File

@@ -28,8 +28,8 @@ export function SkipSetupButton({
<button
type="button"
className={classNames(
'text-background-40 hover:text-background-30',
'stroke-background-40 hover:stroke-background-30',
'text-background-10 hover:text-background-20',
'stroke-background-10 hover:stroke-background-20',
'absolute xs:-top-10 xs:right-4 mobile:top-0 mobile:right-0'
)}
onClick={onClick}

View File

@@ -132,9 +132,7 @@ export function CalibrationTutorialPage() {
id="onboarding-calibration_tutorial-description-v1"
elems={{ b: <b></b> }}
>
<Typography color="secondary">
Description on calibration of IMU
</Typography>
<Typography>Description on calibration of IMU</Typography>
</Localized>
<div>
<div className="xs:hidden flex flex-row justify-center">

View File

@@ -221,10 +221,10 @@ export function ConnectTrackersPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-connect_tracker-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-connect_tracker-description-p0-v1')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-connect_tracker-description-p1-v1')}
</Typography>
<div className="flex flex-col gap-2 py-5">
@@ -281,7 +281,7 @@ export function ConnectTrackersPage() {
{l10n.getString('onboarding-connect_tracker-usb')}
</Typography>
<div className="flex fill-background-10 gap-1">
<Typography color="secondary">
<Typography>
{l10n.getString(statusLabelMap[provisioningStatus])}
</Typography>
</div>
@@ -319,7 +319,7 @@ export function ConnectTrackersPage() {
</div>
</div>
<div style={{ gridArea: 't' }} className="flex items-center px-5">
<Typography color="secondary" bold>
<Typography bold>
{l10n.getString('onboarding-connect_tracker-connected_trackers', {
amount: connectedIMUTrackers.length,
})}

View File

@@ -20,7 +20,7 @@ export function DonePage() {
{l10n.getString('onboarding-done-title')}
</Typography>
<div className="flex flex-col items-center">
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-done-description')}
</Typography>
</div>

View File

@@ -22,7 +22,7 @@ export function EnterVRPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-enter_vr-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-enter_vr-description')}
</Typography>
</div>

View File

@@ -125,7 +125,7 @@ export function ResetTutorialPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-reset_tutorial')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-reset_tutorial-explanation')}
</Typography>
<div className="flex">
@@ -165,7 +165,7 @@ export function ResetTutorialPage() {
curIndex >= order.length && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
<Typography whitespace="whitespace-pre-line">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}
@@ -184,7 +184,7 @@ export function ResetTutorialPage() {
curIndex >= order.length && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
<Typography whitespace="whitespace-pre-line">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}

View File

@@ -35,9 +35,7 @@ export function WifiCredsPage() {
.getString('onboarding-wifi_creds-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
{!state.alonePage && (

View File

@@ -32,7 +32,7 @@ export function AutomaticProportionsPage() {
{l10n.getString('onboarding-automatic_proportions-title')}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-description'
)}

View File

@@ -52,7 +52,7 @@ export function ScaledProportionsPage() {
{l10n.getString('onboarding-scaled_proportions-title')}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-scaled_proportions-description')}
</Typography>
</div>

View File

@@ -72,7 +72,7 @@ export function CheckFloorHeightStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-check_floor_height-description'
)}
@@ -81,7 +81,7 @@ export function CheckFloorHeightStep({
id="onboarding-automatic_proportions-check_floor_height-calculation_warning-v2"
elems={{ u: <span className="underline"></span> }}
>
<Typography color="secondary" bold>
<Typography bold>
Press the button to get your height!
</Typography>
</Localized>

View File

@@ -60,7 +60,7 @@ export function CheckHeightStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-check_height-description-v2'
)}
@@ -69,7 +69,7 @@ export function CheckHeightStep({
id="onboarding-automatic_proportions-check_height-calculation_warning-v3"
elems={{ u: <span className="underline"></span> }}
>
<Typography color="secondary" bold>
<Typography bold>
Press the button to get your height!
</Typography>
</Localized>

View File

@@ -12,7 +12,7 @@ export function DoneStep({ variant }: { variant: 'onboarding' | 'alone' }) {
<Typography variant="section-title">
{l10n.getString('onboarding-automatic_proportions-done-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-automatic_proportions-done-description')}
</Typography>
</div>

View File

@@ -26,13 +26,13 @@ export function PreparationStep({
</Typography>
<div>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
</div>

View File

@@ -28,7 +28,7 @@ export function PutTrackersOnStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-put_trackers_on-description'
)}

View File

@@ -97,7 +97,7 @@ export function Recording({
'onboarding-automatic_proportions-recording-description-p0'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-recording-description-p1'
)}
@@ -110,7 +110,7 @@ export function Recording({
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
<Typography>{line}</Typography>
</li>
))}
</>
@@ -137,7 +137,7 @@ export function Recording({
)
.otherwise(() => undefined)}
></ProgressBar>
<Typography color="secondary">
<Typography>
{match([hasCalibration, hasRecording])
.returnType<ReactNode>()
.with([ProcessStatus.PENDING, ProcessStatus.FULFILLED], () =>

View File

@@ -31,7 +31,7 @@ export function RequirementsStep({
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
<Typography>{line}</Typography>
</li>
))}
</>

View File

@@ -63,7 +63,7 @@ export function StartRecording({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-start_recording-description'
)}
@@ -76,7 +76,7 @@ export function StartRecording({
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
<Typography>{line}</Typography>
</li>
))}
</>

View File

@@ -42,7 +42,7 @@ export function VerifyResultsStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_proportions-verify_results-description'
)}
@@ -65,7 +65,7 @@ export function VerifyResultsStep({
>
{bodyParts?.map(({ bone, label, value }) => (
<div className="flex justify-between" key={bone}>
<Typography color="secondary">{label}</Typography>
<Typography>{label}</Typography>
<Typography bold sentryMask>
{(value * 100).toFixed(2)} CM
</Typography>

View File

@@ -12,7 +12,7 @@ export function DoneStep({ variant }: { variant: 'onboarding' | 'alone' }) {
<Typography variant="section-title">
{l10n.getString('onboarding-scaled_proportions-done-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-scaled_proportions-done-description')}
</Typography>
</div>

View File

@@ -90,7 +90,7 @@ export function ManualHeightStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-scaled_proportions-manual_height-description-v2'
)}

View File

@@ -26,7 +26,7 @@ export function ResetProportionsStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-scaled_proportions-reset_proportion-description'
)}

View File

@@ -33,7 +33,7 @@ export function AutomaticMountingPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-automatic_mounting-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-automatic_mounting-description')}
</Typography>
</div>

View File

@@ -99,7 +99,7 @@ export function ManualMountingPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-manual_mounting')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-manual_mounting-description')}
</Typography>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>

View File

@@ -17,16 +17,12 @@ export function MountingChoose() {
return (
<>
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
<div className="flex flex-col gap-4 justify-center">
<div className="flex flex-col gap-8 justify-center">
<div className="xs:w-10/12 xs:max-w-[666px]">
<Typography variant="main-title">
{l10n.getString('onboarding-choose_mounting')}
</Typography>
<Typography
variant="standard"
color="secondary"
whitespace="whitespace-pre-line"
>
<Typography variant="standard" whitespace="whitespace-pre-line">
{l10n.getString('onboarding-choose_mounting-description')}
</Typography>
</div>
@@ -37,12 +33,19 @@ export function MountingChoose() {
>
<div
className={classNames(
'rounded-lg p-4 flex',
'rounded-lg p-4 flex relative',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="bg-accent-background-30 absolute -left-4 -top-5 p-1.5 rounded-lg">
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-label-v2'
)}
</Typography>
</div>
<div className="flex flex-col gap-4 ">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<Typography variant="main-title" bold>
@@ -50,14 +53,9 @@ export function MountingChoose() {
'onboarding-choose_mounting-auto_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-label-v2'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-description'
)}
@@ -98,14 +96,9 @@ export function MountingChoose() {
'onboarding-choose_mounting-manual_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-label-v2'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-description'
)}

View File

@@ -20,7 +20,7 @@ export function DoneStep({
<Typography variant="section-title">
{l10n.getString('onboarding-automatic_mounting-done-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-automatic_mounting-done-description')}
</Typography>
</div>
@@ -38,6 +38,11 @@ export function DoneStep({
{l10n.getString('onboarding-automatic_mounting-next')}
</Button>
)}
{variant === 'alone' && (
<Button className="flex gap-3" variant="primary" to="/">
{l10n.getString('onboarding-automatic_mounting-return-home')}
</Button>
)}
</div>
<SkeletonVisualizerWidget />

View File

@@ -27,12 +27,12 @@ export function MountingResetStep({
)}
</Typography>
<div className="flex flex-col gap-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-0'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-1'
)}

View File

@@ -26,13 +26,13 @@ export function PreparationStep({
</Typography>
<div>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
</div>

View File

@@ -28,7 +28,7 @@ export function PutTrackersOnStep({
)}
</Typography>
<div>
<Typography color="secondary">
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-put_trackers_on-description'
)}

View File

@@ -136,7 +136,7 @@ export function StayAlignedSetup() {
<Typography variant="main-title">
{l10n.getString('onboarding-stay_aligned-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-stay_aligned-description')}
</Typography>
</div>

View File

@@ -11,7 +11,7 @@ export function DoneStep({ goTo }: VerticalStepComponentProps) {
<Typography variant="main-title"></Typography>
</Localized>
<Localized id="onboarding-stay_aligned-done-description-2">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
<div className="flex gap-3 justify-between">

View File

@@ -16,13 +16,13 @@ export function PreparationStep({
<div className="flex flex-col flex-grow justify-between py-2 gap-2">
<div className="flex flex-col gap-1">
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
<Localized id="onboarding-stay_aligned-preparation-tip">

View File

@@ -19,7 +19,7 @@ export function PutTrackersOnStep({ nextStep }: VerticalStepComponentProps) {
<div className="flex flex-grow flex-col gap-4">
<div>
<Localized id="onboarding-stay_aligned-put_trackers_on-description">
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
<div className="flex">

View File

@@ -30,9 +30,7 @@ function PosePage({
<div className="flex flex-col py-2">
<div className="flex flex-col gap-2">
{descriptionKeys.map((descriptionKey) => (
<Typography color="secondary">
{l10n.getString(descriptionKey)}
</Typography>
<Typography>{l10n.getString(descriptionKey)}</Typography>
))}
</div>
<div className="flex pt-1 items-center fill-background-50 justify-center px-12">

View File

@@ -11,16 +11,16 @@ export function VerifyMountingStep({
<div className="flex flex-grow flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Localized id="onboarding-stay_aligned-verify_mounting-step-0">
<Typography color="secondary" />
<Typography />
</Localized>
<Localized id="onboarding-stay_aligned-verify_mounting-step-1">
<Typography color="secondary" />
<Typography />
</Localized>
<Localized id="onboarding-stay_aligned-verify_mounting-step-2">
<Typography color="secondary" />
<Typography />
</Localized>
<Localized id="onboarding-stay_aligned-verify_mounting-step-3">
<Typography color="secondary" />
<Typography />
</Localized>
</div>
<div className="flex gap-3 justify-between">

View File

@@ -43,7 +43,7 @@ const ItemContent = ({
mode,
})}
</Typography>
<Typography variant="standard" color="secondary">
<Typography variant="standard">
{l10n.getString('onboarding-assign_trackers-option-description', {
mode,
})}

View File

@@ -284,11 +284,11 @@ export function TrackersAssignPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-assign_trackers-title')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-assign_trackers-description')}
</Typography>
<div className="flex gap-1">
<Typography color="secondary">
<Typography>
{l10n.getString('onboarding-assign_trackers-assigned', {
assigned: assignedTrackers.length,
trackers: trackers.length,

View File

@@ -63,11 +63,11 @@ export function AdvancedSettings() {
<div className="grid gap-4">
<div className="sm:grid sm:grid-cols-[1.75fr,_1fr] items-center">
<div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-utils-advanced-reset-gui')}
</Typography>
<div className="flex flex-col">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-utils-advanced-reset-gui-description'
)}
@@ -95,11 +95,11 @@ export function AdvancedSettings() {
<div className="sm:grid sm:grid-cols-[1.75fr,_1fr] items-center">
<div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-utils-advanced-reset-server')}
</Typography>
<div className="flex flex-col">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-utils-advanced-reset-server-description'
)}
@@ -132,11 +132,11 @@ export function AdvancedSettings() {
<div className="sm:grid sm:grid-cols-[1.75fr,_1fr] items-center">
<div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-utils-advanced-reset-all')}
</Typography>
<div className="flex flex-col">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-utils-advanced-reset-all-description'
)}
@@ -168,11 +168,11 @@ export function AdvancedSettings() {
<div className="sm:grid sm:grid-cols-[1.75fr,_1fr] items-center">
<div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-utils-advanced-open_data-v1')}
</Typography>
<div className="flex flex-col">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-utils-advanced-open_data-description-v1'
)}
@@ -188,11 +188,11 @@ export function AdvancedSettings() {
<div className="sm:grid sm:grid-cols-[1.75fr,_1fr] items-center">
<div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-utils-advanced-open_logs')}
</Typography>
<div className="flex flex-col">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-utils-advanced-open_logs-description'
)}

View File

@@ -459,7 +459,7 @@ export function GeneralSettings() {
<Typography variant="main-title">
{l10n.getString('settings-general-steamvr')}
</Typography>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-steamvr-subtitle')}
</Typography>
<div className="flex flex-col py-2">
@@ -467,13 +467,11 @@ export function GeneralSettings() {
.getString('settings-general-steamvr-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</div>
<div className="flex flex-col pt-4"></div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-steamvr-trackers-tracker_toggling'
)}
@@ -485,9 +483,7 @@ export function GeneralSettings() {
)
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</div>
<CheckBox
@@ -608,7 +604,7 @@ export function GeneralSettings() {
<Typography variant="main-title">
{l10n.getString('settings-general-tracker_mechanics')}
</Typography>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-tracker_mechanics-filtering')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
@@ -618,9 +614,7 @@ export function GeneralSettings() {
)
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</div>
<Typography>
@@ -690,7 +684,7 @@ export function GeneralSettings() {
/>
</div>
<div className="flex flex-col pt-5 pb-3">
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-tracker_mechanics-save_mounting_reset'
)}
@@ -699,7 +693,7 @@ export function GeneralSettings() {
id="settings-general-tracker_mechanics-save_mounting_reset-description"
elems={{ b: <b></b> }}
>
<Typography color="secondary"></Typography>
<Typography></Typography>
</Localized>
</div>
<CheckBox
@@ -725,19 +719,19 @@ export function GeneralSettings() {
<Typography variant="main-title">
{l10n.getString('settings-general-fk_settings')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<Typography bold>
<div className="flex flex-col pt-2 pb-4 gap-2">
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-leg_tweak-skating_correction'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-leg_tweak-skating_correction-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap-3 pb-4">
<div className="grid sm:grid-cols-1 gap-2 pb-4">
<CheckBox
variant="toggle"
outlined
@@ -761,18 +755,18 @@ export function GeneralSettings() {
</div>
<div className="flex flex-col pt-2 pb-2">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-fk_settings-leg_fk')}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap-3 pb-3">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-leg_tweak-floor_clip-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap-3 pb-3">
<div className="grid sm:grid-cols-1 gap-2 pb-3">
<CheckBox
variant="toggle"
outlined
@@ -784,7 +778,7 @@ export function GeneralSettings() {
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-leg_tweak-foot_plant-description'
)}
@@ -802,7 +796,7 @@ export function GeneralSettings() {
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-leg_tweak-toe_snap-description'
)}
@@ -821,10 +815,10 @@ export function GeneralSettings() {
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-fk_settings-arm_fk')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-arm_fk-description'
)}
@@ -843,110 +837,114 @@ export function GeneralSettings() {
</div>
<div className="flex flex-col pt-2">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-fk_settings-reset_settings')}
</Typography>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-reset_settings-reset_hmd_pitch-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap-3 pb-3">
<CheckBox
variant="toggle"
outlined
control={control}
name="resetsSettings.resetHmdPitch"
label={l10n.getString(
'settings-general-fk_settings-reset_settings-reset_hmd_pitch'
)}
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-leg_fk-reset_mounting_feet-description-v1'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap-3 pb-3">
<CheckBox
variant="toggle"
outlined
control={control}
name="resetsSettings.resetMountingFeet"
label={l10n.getString(
'settings-general-fk_settings-leg_fk-reset_mounting_feet-v1'
)}
/>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-2">
<Typography>
{l10n.getString(
'settings-general-fk_settings-reset_settings-reset_hmd_pitch-description'
)}
</Typography>
<CheckBox
variant="toggle"
outlined
control={control}
name="resetsSettings.resetHmdPitch"
label={l10n.getString(
'settings-general-fk_settings-reset_settings-reset_hmd_pitch'
)}
/>
</div>
<div className="flex flex-col gap-2 justify-end">
<Typography>
{l10n.getString(
'settings-general-fk_settings-leg_fk-reset_mounting_feet-description-v1'
)}
</Typography>
<CheckBox
variant="toggle"
outlined
control={control}
name="resetsSettings.resetMountingFeet"
label={l10n.getString(
'settings-general-fk_settings-leg_fk-reset_mounting_feet-v1'
)}
/>
</div>
</div>
</div>
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-arm_fk-reset_mode-description'
)}
</Typography>
<div className="grid md:grid-cols-2 flex-col gap-3 pt-2 pb-3">
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-back'
<div>
<Typography>
{l10n.getString(
'settings-general-fk_settings-arm_fk-reset_mode-description'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-back-description'
)}
value={'0'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-forward'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-forward-description'
)}
value={'1'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_up'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_up-description'
)}
value={'2'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_down'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_down-description'
)}
value={'3'}
></Radio>
</Typography>
<div className="grid md:grid-cols-2 flex-col gap-3 pt-2 pb-3">
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-back'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-back-description'
)}
value={'0'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-forward'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-forward-description'
)}
value={'1'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_up'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_up-description'
)}
value={'2'}
></Radio>
<Radio
control={control}
name="resetsSettings.armsMountingResetMode"
label={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_down'
)}
description={l10n.getString(
'settings-general-fk_settings-arm_fk-tpose_down-description'
)}
value={'3'}
></Radio>
</div>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
<div className="flex flex-col pt-2 pb-1">
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-enforce_joint_constraints'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description'
)}
</Typography>
<div className="pt-2">
<Typography>
{l10n.getString(
'settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description'
)}
</Typography>
</div>
</div>
<div className="grid sm:grid-cols-1 pb-3">
<CheckBox
@@ -963,12 +961,12 @@ export function GeneralSettings() {
{config?.debug && (
<>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-toggles'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-description'
)}
@@ -1003,14 +1001,14 @@ export function GeneralSettings() {
)}
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
<div className="flex flex-col">
<div className="flex flex-col pt-2 pb-3 gap-2">
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-ratios'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-ratios-description'
)}
@@ -1112,12 +1110,12 @@ export function GeneralSettings() {
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-fk_settings-self_localization-title'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-fk_settings-self_localization-description'
)}
@@ -1147,11 +1145,12 @@ export function GeneralSettings() {
<Typography variant="main-title">
{l10n.getString('settings-general-gesture_control')}
</Typography>
<Typography bold>
{l10n.getString('settings-general-gesture_control-subtitle')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<Typography color="secondary">
<div className="flex flex-col pt-2 pb-4 gap-2">
<Typography variant="section-title">
{l10n.getString('settings-general-gesture_control-subtitle')}
</Typography>
<Typography>
{l10n.getString('settings-general-gesture_control-description')}
</Typography>
</div>
@@ -1267,12 +1266,12 @@ export function GeneralSettings() {
/>
</div>
<div className="grid sm:grid-cols-1 gap-2 pt-2">
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-gesture_control-numberTrackersOverThreshold'
)}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-gesture_control-numberTrackersOverThreshold-description'
)}

View File

@@ -130,11 +130,14 @@ export function InterfaceSettings() {
{l10n.getString('settings-interface-notifications')}
</Typography>
<Typography bold>
{l10n.getString('settings-general-interface-serial_detection')}
</Typography>
<div className="pt-2">
<Typography variant="section-title">
{l10n.getString('settings-general-interface-serial_detection')}
</Typography>
</div>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-interface-serial_detection-description'
)}
@@ -152,11 +155,11 @@ export function InterfaceSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-interface-feedback_sound')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-interface-feedback_sound-description'
)}
@@ -187,13 +190,13 @@ export function InterfaceSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-interface-connected_trackers_warning'
)}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-interface-connected_trackers_warning-description'
)}
@@ -221,130 +224,134 @@ export function InterfaceSettings() {
<Typography variant="main-title">
{l10n.getString('settings-interface-behavior')}
</Typography>
<div className="pt-2">
{isTrayAvailable && (
<>
<Typography variant="section-title">
{l10n.getString('settings-general-interface-use_tray')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography>
{l10n.getString(
'settings-general-interface-use_tray-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.useTray"
label={l10n.getString(
'settings-general-interface-use_tray-label'
)}
/>
</div>
</>
)}
{isTrayAvailable && (
<>
<Typography bold>
{l10n.getString('settings-general-interface-use_tray')}
<Typography variant="section-title">
{l10n.getString('settings-general-interface-discord_presence')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography>
{l10n.getString(
'settings-general-interface-discord_presence-description'
)}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.discordPresence"
label={l10n.getString(
'settings-general-interface-discord_presence-label'
)}
/>
</div>
<Typography variant="section-title">
{l10n.getString('settings-general-interface-dev_mode')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography>
{l10n.getString(
'settings-general-interface-dev_mode-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.devmode"
label={l10n.getString(
'settings-general-interface-dev_mode-label'
)}
/>
</div>
<Typography variant="section-title">
{l10n.getString('settings-interface-behavior-error_tracking')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Localized
id={
'settings-interface-behavior-error_tracking-description_v2'
}
elems={{
b: <b></b>,
}}
>
<Typography whitespace="whitespace-pre-line"></Typography>
</Localized>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.errorTracking"
label={l10n.getString(
'settings-interface-behavior-error_tracking-label'
)}
/>
</div>
{isTauri() && (
<>
<Typography variant="section-title">
{l10n.getString(
'settings-general-interface-use_tray-description'
'settings-interface-behavior-bvh_directory'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.useTray"
label={l10n.getString(
'settings-general-interface-use_tray-label'
)}
/>
</div>
</>
)}
<Typography bold>
{l10n.getString('settings-general-interface-discord_presence')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-discord_presence-description'
)}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Localized
id={
'settings-interface-behavior-bvh_directory-description'
}
>
<Typography></Typography>
</Localized>
</div>
<div className="grid gap-3 pb-5">
<TauriFileInput
name="behavior.bvhDirectory"
rules={{
required: false,
}}
control={control}
label="settings-interface-behavior-bvh_directory-label"
directory
/>
</div>
</>
)}
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.discordPresence"
label={l10n.getString(
'settings-general-interface-discord_presence-label'
)}
/>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-dev_mode')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-dev_mode-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.devmode"
label={l10n.getString(
'settings-general-interface-dev_mode-label'
)}
/>
</div>
<Typography bold>
{l10n.getString('settings-interface-behavior-error_tracking')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Localized
id={'settings-interface-behavior-error_tracking-description_v2'}
elems={{
b: <b></b>,
}}
>
<Typography
color="secondary"
whitespace="whitespace-pre-line"
></Typography>
</Localized>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="behavior.errorTracking"
label={l10n.getString(
'settings-interface-behavior-error_tracking-label'
)}
/>
</div>
{isTauri() && (
<>
<Typography bold>
{l10n.getString('settings-interface-behavior-bvh_directory')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Localized
id={'settings-interface-behavior-bvh_directory-description'}
>
<Typography color="secondary"></Typography>
</Localized>
</div>
<div className="grid gap-3 pb-5">
<TauriFileInput
name="behavior.bvhDirectory"
rules={{
required: false,
}}
control={control}
label="settings-interface-behavior-bvh_directory-label"
directory
/>
</div>
</>
)}
</>
</SettingsPagePaneLayout>
@@ -356,11 +363,13 @@ export function InterfaceSettings() {
<Typography variant="main-title">
{l10n.getString('settings-interface-appearance')}
</Typography>
<Typography bold>
{l10n.getString('settings-interface-appearance-decorations')}
</Typography>
<div className="pt-2">
<Typography variant="section-title">
{l10n.getString('settings-interface-appearance-decorations')}
</Typography>
</div>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-interface-appearance-decorations-description'
)}
@@ -379,7 +388,7 @@ export function InterfaceSettings() {
</div>
<div className="pb-4">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-interface-theme')}
</Typography>
<div className="flex flex-wrap gap-3 pt-2">
@@ -440,13 +449,13 @@ export function InterfaceSettings() {
</div>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-interface-show-navbar-onboarding'
)}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-general-interface-show-navbar-onboarding-description'
)}
@@ -464,11 +473,11 @@ export function InterfaceSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-interface-appearance-font')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-interface-appearance-font-description'
)}
@@ -513,11 +522,11 @@ export function InterfaceSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-interface-appearance-font_size')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-interface-appearance-font_size-description'
)}
@@ -541,11 +550,11 @@ export function InterfaceSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-general-interface-lang')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-general-interface-lang-description')}
</Typography>
</div>

View File

@@ -95,7 +95,7 @@ export function MagnetometerToggleSetting({
return settingType === 'general' ? (
<>
<div className="flex flex-col pt-5 pb-3" id={id}>
<Typography bold>
<Typography variant="section-title">
{l10n.getString(
'settings-general-tracker_mechanics-use_mag_on_all_trackers'
)}
@@ -104,10 +104,7 @@ export function MagnetometerToggleSetting({
id="settings-general-tracker_mechanics-use_mag_on_all_trackers-description"
elems={{ b: <b></b> }}
>
<Typography
color="secondary"
whitespace="whitespace-pre-line"
></Typography>
<Typography whitespace="whitespace-pre-line"></Typography>
</Localized>
</div>
<CheckBox
@@ -139,10 +136,7 @@ export function MagnetometerToggleSetting({
),
}}
>
<Typography
color="secondary"
whitespace="whitespace-pre-line"
></Typography>
<Typography whitespace="whitespace-pre-line"></Typography>
</Localized>
<div className="flex">
<CheckBox

View File

@@ -110,17 +110,15 @@ export function OSCRouterSettings() {
.getString('settings-osc-router-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-router-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-router-enable-description')}
</Typography>
</div>
@@ -133,7 +131,7 @@ export function OSCRouterSettings() {
label={l10n.getString('settings-osc-router-enable-label')}
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-router-network')}
</Typography>
<div className="flex flex-col pb-2">
@@ -142,9 +140,7 @@ export function OSCRouterSettings() {
.getString('settings-osc-router-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>
@@ -176,11 +172,11 @@ export function OSCRouterSettings() {
></Input>
</Localized>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-router-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-osc-router-network-address-description'
)}

View File

@@ -240,9 +240,7 @@ export function Serial() {
.getString('settings-serial-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>

View File

@@ -137,17 +137,15 @@ export function VMCSettings() {
.getString('settings-osc-vmc-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vmc-enable-description')}
</Typography>
</div>
@@ -160,7 +158,7 @@ export function VMCSettings() {
label={l10n.getString('settings-osc-vmc-enable-label')}
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-network')}
</Typography>
<div className="flex flex-col pb-2">
@@ -169,9 +167,7 @@ export function VMCSettings() {
.getString('settings-osc-vmc-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>
@@ -203,11 +199,11 @@ export function VMCSettings() {
></Input>
</Localized>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vmc-network-address-description')}
</Typography>
</div>
@@ -227,11 +223,11 @@ export function VMCSettings() {
label=""
></Input>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-vrm')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vmc-vrm-description')}
</Typography>
</div>
@@ -254,11 +250,11 @@ export function VMCSettings() {
></FileInput>
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-anchor_hip')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vmc-anchor_hip-description')}
</Typography>
</div>
@@ -271,11 +267,11 @@ export function VMCSettings() {
label={l10n.getString('settings-osc-vmc-anchor_hip-label')}
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vmc-mirror_tracking')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vmc-mirror_tracking-description')}
</Typography>
</div>

View File

@@ -140,17 +140,15 @@ export function VRCOSCSettings() {
.getString('settings-osc-vrchat-description-v1')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vrchat-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vrchat-enable-description')}
</Typography>
</div>
@@ -164,18 +162,16 @@ export function VRCOSCSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vrchat-oscqueryEnabled')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n
.getString('settings-osc-vrchat-oscqueryEnabled-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
<Typography key={i}>{line}</Typography>
))}
</Typography>
</div>
@@ -191,11 +187,11 @@ export function VRCOSCSettings() {
/>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vrchat-network')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-osc-vrchat-network-description-v1')}
</Typography>
</div>
@@ -227,11 +223,11 @@ export function VRCOSCSettings() {
></Input>
</Localized>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vrchat-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-osc-vrchat-network-address-description-v1'
)}
@@ -253,11 +249,11 @@ export function VRCOSCSettings() {
label=""
></Input>
</div>
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-osc-vrchat-network-trackers')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-osc-vrchat-network-trackers-description'
)}

View File

@@ -40,9 +40,7 @@ function StaAlignedPoseModal({
</div>
<div className="flex flex-col gap-1">
{descriptionKeys.map((descriptionKey) => (
<Typography color="secondary">
{l10n.getString(descriptionKey)}
</Typography>
<Typography>{l10n.getString(descriptionKey)}</Typography>
))}
</div>
<div className="flex pt-1 items-center fill-background-50 justify-center px-12">

View File

@@ -182,10 +182,10 @@ export function StayAlignedSettings({
{l10n.getString('settings-stay_aligned')}
</Typography>
<div className="mt-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-stay_aligned-description')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('settings-stay_aligned-setup-description')}
</Typography>
<div className="flex mt-2">
@@ -199,7 +199,7 @@ export function StayAlignedSettings({
</div>
</div>
<div className="mt-6">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-stay_aligned-general-label')}
</Typography>
<div className="grid sm:grid-cols-2 gap-3 mt-2">
@@ -224,11 +224,11 @@ export function StayAlignedSettings({
</div>
</div>
<div className="mt-6">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-stay_aligned-relaxed_poses-label')}
</Typography>
<div className="mt-2">
<Typography color="secondary">
<Typography>
{l10n.getString(
'settings-stay_aligned-relaxed_poses-description'
)}
@@ -305,11 +305,11 @@ export function StayAlignedSettings({
</div>
</div>
<div className="mt-6">
<Typography bold>
<Typography variant="section-title">
{l10n.getString('settings-stay_aligned-debug-label')}
</Typography>
<div className="mt-2">
<Typography color="secondary">
<Typography>
{l10n.getString('settings-stay_aligned-debug-description')}
</Typography>
</div>

View File

@@ -49,7 +49,7 @@ export function SingleTrackerBodyAssignmentMenu({
<Typography variant="mobile-title" bold>
{l10n.getString('body_assignment_menu')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('body_assignment_menu-description')}
</Typography>
<div className="flex">

View File

@@ -7,7 +7,7 @@ export function TrackerBattery({
value,
voltage,
disabled,
textColor = 'secondary',
textColor = 'primary',
}: {
/**
* a [0, 1] value range is expected

View File

@@ -99,7 +99,7 @@ export function TrackerPartCard({
<WarningIcon></WarningIcon>
</div>
)}
<Typography color="secondary">
<Typography variant="section-title">
{l10n.getString('body_part-' + BodyPart[role])}
</Typography>
{td?.map(({ tracker }, index) => (
@@ -110,7 +110,7 @@ export function TrackerPartCard({
/>
))}
{!td && (
<Typography>
<Typography color="text-background-30">
{l10n.getString('tracker-part_card-unassigned')}
</Typography>
)}

View File

@@ -211,10 +211,10 @@ export function TrackerSettingsPage() {
</Typography>
</Localized>
<div className="flex gap-2">
<Typography color="secondary">
<Typography>
v{tracker?.device?.hardwareInfo?.firmwareVersion}
</Typography>
<Typography color="secondary">-</Typography>
<Typography>-</Typography>
{updateUnavailable && (
<Localized id="tracker-settings-update-unavailable">
<Typography>Cannot be updated (DIY)</Typography>
@@ -272,7 +272,7 @@ export function TrackerSettingsPage() {
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2 overflow-x-auto">
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-manufacturer')}
</Typography>
<Typography>
@@ -280,13 +280,13 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-display_name')}
</Typography>
<Typography>{tracker?.tracker.info?.displayName}</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-custom_name')}
</Typography>
<Typography sentry-mask>
@@ -294,9 +294,7 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
{l10n.getString('tracker-infos-url')}
</Typography>
<Typography>{l10n.getString('tracker-infos-url')}</Typography>
<Typography>
udp://
{IPv4.fromNumber(
@@ -305,7 +303,7 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-hardware_identifier')}
</Typography>
<Typography>
@@ -313,7 +311,7 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-data_support')}
</Typography>
<Typography>
@@ -323,9 +321,7 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
{l10n.getString('tracker-infos-imu')}
</Typography>
<Typography>{l10n.getString('tracker-infos-imu')}</Typography>
<Typography>
{tracker?.tracker.info?.imuType
? ImuType[tracker?.tracker.info?.imuType]
@@ -333,13 +329,13 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-board_type')}
</Typography>
<Typography>{boardType}</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-magnetometer')}
</Typography>
<Typography>
@@ -352,7 +348,7 @@ export function TrackerSettingsPage() {
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-network_version')}
</Typography>
<Typography>
@@ -377,7 +373,7 @@ export function TrackerSettingsPage() {
<Typography variant="section-title">
{l10n.getString('tracker-settings-assignment_section')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'tracker-settings-assignment_section-description'
)}
@@ -419,7 +415,7 @@ export function TrackerSettingsPage() {
<Typography variant="section-title">
{l10n.getString('tracker-settings-mounting_section')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString(
'tracker-settings-mounting_section-description'
)}
@@ -468,7 +464,7 @@ export function TrackerSettingsPage() {
<Typography variant="section-title">
{l10n.getString('tracker-settings-name_section')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-settings-name_section-description')}
</Typography>
<Input
@@ -488,7 +484,7 @@ export function TrackerSettingsPage() {
<Typography variant="section-title">
{l10n.getString('tracker-settings-forget')}
</Typography>
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-settings-forget-description')}
</Typography>
<Button

View File

@@ -35,7 +35,7 @@ export function TrackerStatus({ status }: { status: number }) {
<div className="flex flex-col justify-center">
<div className={classNames('w-2 h-2 rounded-full', statusClass)}></div>
</div>
<Typography color="secondary" whitespace="whitespace-nowrap">
<Typography whitespace="whitespace-nowrap">
{l10n.getString(statusLabel)}
</Typography>
</div>

View File

@@ -130,7 +130,7 @@ export function VRCWarningsPage() {
<Typography variant="main-title" />
</Localized>
<Localized id={'vrc_config-page-desc'}>
<Typography variant="standard" color="secondary" />
<Typography variant="standard" />
</Localized>
</div>
<div className="w-full mt-4 gap-2 flex flex-col">
@@ -142,7 +142,7 @@ export function VRCWarningsPage() {
<Typography variant="section-title" />
</Localized>
<Localized id="vrc_config-page-big_menu-desc">
<Typography color="secondary" />
<Typography />
</Localized>
<Table>
<SettingRow
@@ -248,7 +248,7 @@ export function VRCWarningsPage() {
<Typography variant="section-title" />
</Localized>
<Localized id="vrc_config-page-wrist_menu-desc">
<Typography color="secondary" />
<Typography />
</Localized>
<Table>
<SettingRow
@@ -304,7 +304,7 @@ export function VRCWarningsPage() {
a: <A href="https://docs.slimevr.dev/tools/vrchat-config.html"></A>,
}}
>
<Typography color="secondary" />
<Typography />
</Localized>
</div>
</div>

View File

@@ -75,9 +75,7 @@ export function DeveloperModeWidget() {
return (
<form className="bg-background-60 flex flex-col w-full rounded-md px-2">
<div className="mt-2 px-1">
<Typography color="secondary">
{l10n.getString('widget-developer_mode')}
</Typography>
<Typography>{l10n.getString('widget-developer_mode')}</Typography>
</div>
{Object.entries(toggles).map(makeToggle)}
</form>

View File

@@ -128,7 +128,7 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
{tracker.position && (
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('widget-imu_visualizer-position')}
</Typography>
<Typography>{formatVector3(tracker.position, 2)}</Typography>
@@ -136,14 +136,14 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
)}
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('widget-imu_visualizer-rotation_raw')}
</Typography>
<Typography>{formatVector3(rotationRaw, 2)}</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('widget-imu_visualizer-rotation_preview')}
</Typography>
<Typography>{formatVector3(rotationIdent, 2)}</Typography>
@@ -151,7 +151,7 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
{tracker.linearAcceleration && (
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('widget-imu_visualizer-acceleration')}
</Typography>
<Typography>
@@ -162,7 +162,7 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
{tracker.rawMagneticVector && (
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('tracker-infos-magnetometer')}
</Typography>
<Typography>{formatVector3(tracker.rawMagneticVector, 1)}</Typography>
@@ -171,7 +171,7 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
{!!tracker.stayAligned && (
<div className="flex justify-between">
<Typography color="secondary">
<Typography>
{l10n.getString('widget-imu_visualizer-stay_aligned')}
</Typography>
<StayAlignedInfo color="primary" tracker={tracker} />

View File

@@ -68,9 +68,7 @@ export function OverlayWidget() {
return !loading ? (
<form className="bg-background-60 flex flex-col w-full rounded-md px-2">
<div className="mt-2 px-1">
<Typography color="secondary">
{l10n.getString('widget-overlay')}
</Typography>
<Typography>{l10n.getString('widget-overlay')}</Typography>
</div>
<CheckBox
control={control}

View File

@@ -27,6 +27,14 @@ const hash = (str: string) => {
const firstAsset = (assets: any[], name: string) =>
assets.find((asset: any) => asset.name === name && asset.browser_download_url);
const todaysRange = (deployData: [number, Date][]): number => {
let maxRange = 0;
for (const [range, date] of deployData) {
if (Date.now() >= date.getTime()) maxRange = range;
}
return maxRange;
};
const checkUserCanUpdate = async (url: string, fwVersion: string) => {
if (!url) return true;
const deployDataJson = JSON.parse(
@@ -57,11 +65,7 @@ const checkUserCanUpdate = async (url: string, fwVersion: string) => {
)
return false; // Dates in the wrong order / cancel
const todayUpdateRange = deployData.find(([, date], index) => {
if (index === 0 && Date.now() < date.getTime()) return true;
return Date.now() >= date.getTime();
})?.[0];
const todayUpdateRange = todaysRange(deployData);
if (!todayUpdateRange) return false;
const uniqueUserKey = `${await hostname()}-${await locale()}-${platform()}-${version()}`;

10
gui/src/vite-env.d.ts vendored
View File

@@ -4,14 +4,14 @@
declare const __COMMIT_HASH__: string;
declare const __VERSION_TAG__: string;
declare const __GIT_CLEAN__: boolean;
declare const __ANDROID__:
| {
isThere: () => boolean;
}
| undefined;
interface Window {
readonly isTauri: boolean;
readonly __ANDROID__:
| {
isThere: () => boolean;
}
| undefined;
}
declare module 'tailwind-gradient-mask-image';

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
@@ -26,7 +28,11 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
<service

View File

@@ -8,6 +8,8 @@ import androidx.appcompat.app.AppCompatActivity
import dev.slimevr.Keybinding
import dev.slimevr.VRServer
import dev.slimevr.android.serial.AndroidSerialHandler
import dev.slimevr.android.tracking.trackers.hid.AndroidHIDManager
import dev.slimevr.tracking.trackers.Tracker
import io.eiren.util.logging.LogManager
import io.ktor.http.CacheControl
import io.ktor.http.CacheControl.Visibility
@@ -60,6 +62,15 @@ fun main(activity: AppCompatActivity) {
},
)
vrServer.start()
// Start service for USB HID trackers
val androidHidManager = AndroidHIDManager(
"Sensors HID service",
{ tracker: Tracker -> vrServer.registerTracker(tracker) },
activity,
)
androidHidManager.start()
Keybinding(vrServer)
vrServer.join()
LogManager.closeLogger()

View File

@@ -1,10 +1,13 @@
package dev.slimevr.android.serial
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
@@ -15,10 +18,8 @@ import io.eiren.util.logging.LogManager
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.stream.Stream
import kotlin.concurrent.timerTask
import kotlin.streams.asSequence
import kotlin.streams.asStream
import dev.slimevr.serial.SerialPort as SlimeSerialPort
@@ -43,12 +44,11 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
private var usbIoManager: SerialInputOutputManager? = null
private val listeners: MutableList<SerialListener> = CopyOnWriteArrayList()
private val getDevicesTimer = Timer("GetDevicesTimer")
private var watchingNewDevices = false
private var lastKnownPorts = setOf<SerialPortWrapper>()
private val manager = activity.getSystemService(Context.USB_SERVICE) as UsbManager
private var currentPort: SerialPortWrapper? = null
private var requestingPermission: String = ""
private var readBuffer: StringBuilder = StringBuilder(1024)
override val isConnected: Boolean
get() = currentPort?.port?.isOpen ?: false
@@ -60,37 +60,70 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
.filter { isKnownBoard(it) }
.asStream()
val usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_ATTACHED, UsbManager.ACTION_USB_DEVICE_DETACHED -> {
// Use device from `UsbManager.EXTRA_DEVICE` if this is a problem
detectNewPorts()
}
ACTION_USB_PERMISSION -> {
// TODO: We can probably receive this event in the server to avoid
// polling, but for now we can just ignore it. (Note: This event is
// not currently registered, so it will never fire.)
}
}
}
}
init {
startWatchingNewDevices()
val intentFilter = IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED)
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
// Listen for USB device attach/detach
ContextCompat.registerReceiver(
activity,
usbReceiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED,
)
// Detect initial serial ports
detectNewPorts()
}
private fun getPorts(): List<UsbSerialDriver> = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
private fun startWatchingNewDevices() {
if (watchingNewDevices) return
watchingNewDevices = true
getDevicesTimer.scheduleAtFixedRate(
timerTask {
try {
detectNewPorts()
} catch (t: Throwable) {
LogManager.severe(
"[SerialHandler] Error while watching for new devices, cancelling the \"getDevicesTimer\".",
t,
)
getDevicesTimer.cancel()
}
},
0,
3000,
)
}
private fun onNewDevice(port: SerialPortWrapper) {
// If we missed clearing this on disconnect/close, clear it on discovery
if (requestingPermission == port.portLocation) {
requestingPermission = ""
}
LogManager.info("[SerialHandler] Device added: ${port.descriptivePortName}")
listeners.forEach { it.onNewSerialDevice(port) }
}
private fun onDeviceDel(port: SerialPortWrapper) {
// Remove permission request on disconnect so reconnecting re-requests
if (requestingPermission == port.portLocation) {
requestingPermission = ""
}
// If we're currently using this port, close it
currentPort?.portLocation.let { currentPortLocation ->
if (currentPortLocation == port.portLocation) {
closeSerial()
}
}
// If this port is still open for whatever reason, close it
if (port.port.isOpen) {
port.port.close()
}
LogManager.info("[SerialHandler] Device removed: ${port.descriptivePortName}")
listeners.forEach { it.onSerialDeviceDeleted(port) }
}
@@ -154,9 +187,11 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
flags,
)
if (requestingPermission != newPort.portLocation) {
println("Requesting permission for ${newPort.portLocation}")
LogManager.info("[SerialHandler] Requesting permission for ${newPort.portLocation}")
manager.requestPermission(newPort.port.device, usbPermissionIntent)
requestingPermission = newPort.portLocation
} else {
LogManager.info("[SerialHandler] Already requested permission for ${newPort.portLocation}, skipping")
}
LogManager.warning(
"[SerialHandler] Can't open serial port ${newPort.descriptivePortName}, invalid permissions",
@@ -164,11 +199,13 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
return false
}
// If we have permission, we aren't requesting anymore
requestingPermission = ""
val connection = manager.openDevice(newPort.port.device)
if (connection == null) {
LogManager.warning(
"[SerialHandler] Can't open serial port ${newPort.descriptivePortName}, connection failed",
)
return false
}
@@ -186,7 +223,7 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
@Synchronized
private fun writeSerial(serialText: String, print: Boolean = false) {
try {
usbIoManager?.writeAsync("${serialText}\n".toByteArray())
currentPort?.port?.write("${serialText}\n".toByteArray(), 0)
if (print) {
addLog("-> $serialText\n")
}
@@ -224,6 +261,8 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
usbIoManager?.stop()
usbIoManager = null
currentPort = null
requestingPermission = ""
readBuffer.clear()
} catch (e: Exception) {
LogManager.warning(
"[SerialHandler] Error closing port ${currentPort?.descriptivePortName}",
@@ -233,7 +272,7 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
}
override fun write(buff: ByteArray) {
usbIoManager?.writeAsync(buff)
currentPort?.port?.write(buff, 0)
}
@Synchronized
@@ -251,14 +290,21 @@ class AndroidSerialHandler(val activity: AppCompatActivity) :
override fun onNewData(data: ByteArray?) {
if (data != null) {
val s = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(data)).toString()
addLog(s, false)
// Collect serial in a buffer until newline (or character limit)
// This is somewhat of a workaround for Android serial buffer being smaller
// than on desktop, so we don't read full lines and it causes parsing issues
readBuffer.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(data)))
if (readBuffer.contains('\n') || readBuffer.length >= 1024) {
addLog(readBuffer.toString(), false)
readBuffer.clear()
}
}
}
override fun onRunError(e: java.lang.Exception?) {}
companion object {
private val ACTION_USB_PERMISSION = "dev.slimevr.USB_PERMISSION"
private const val ACTION_USB_PERMISSION = "dev.slimevr.USB_PERMISSION"
}
}

View File

@@ -0,0 +1,93 @@
package dev.slimevr.android.tracking.trackers.hid
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import java.io.Closeable
/**
* A wrapper over Android's [UsbDevice] for HID devices.
*/
class AndroidHIDDevice(hidDevice: UsbDevice, usbManager: UsbManager) : Closeable {
val deviceName = hidDevice.deviceName
val serialNumber = hidDevice.serialNumber
val manufacturerName = hidDevice.manufacturerName
val productName = hidDevice.productName
val hidInterface: UsbInterface
val endpointIn: UsbEndpoint
val endpointOut: UsbEndpoint?
val deviceConnection: UsbDeviceConnection
init {
hidInterface = findHidInterface(hidDevice)!!
val (endpointIn, endpointOut) = findHidIO(hidInterface)
this.endpointIn = endpointIn!!
this.endpointOut = endpointOut
deviceConnection = usbManager.openDevice(hidDevice)!!
deviceConnection.claimInterface(hidInterface, true)
}
override fun close() {
deviceConnection.releaseInterface(hidInterface)
deviceConnection.close()
}
companion object {
/**
* Find the HID interface.
*
* @return
* Return the HID interface if found, otherwise null.
*/
private fun findHidInterface(usbDevice: UsbDevice): UsbInterface? {
val interfaceCount: Int = usbDevice.interfaceCount
for (interfaceIndex in 0 until interfaceCount) {
val usbInterface = usbDevice.getInterface(interfaceIndex)
if (usbInterface.interfaceClass == UsbConstants.USB_CLASS_HID) {
return usbInterface
}
}
return null
}
/**
* Find the HID endpoints.
*
* @return
* Return the HID endpoints if found, otherwise null.
*/
private fun findHidIO(usbInterface: UsbInterface): Pair<UsbEndpoint?, UsbEndpoint?> {
val endpointCount: Int = usbInterface.endpointCount
var usbEndpointIn: UsbEndpoint? = null
var usbEndpointOut: UsbEndpoint? = null
for (endpointIndex in 0 until endpointCount) {
val usbEndpoint = usbInterface.getEndpoint(endpointIndex)
if (usbEndpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT) {
if (usbEndpoint.direction == UsbConstants.USB_DIR_OUT) {
usbEndpointOut = usbEndpoint
} else {
usbEndpointIn = usbEndpoint
}
}
}
return Pair(usbEndpointIn, usbEndpointOut)
}
}
}

View File

@@ -0,0 +1,254 @@
package dev.slimevr.android.tracking.trackers.hid
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.core.content.ContextCompat
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_RECEIVER_PID
import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.HID_TRACKER_RECEIVER_VID
import dev.slimevr.tracking.trackers.hid.HIDCommon.Companion.PACKET_SIZE
import dev.slimevr.tracking.trackers.hid.HIDDevice
import io.eiren.util.logging.LogManager
import java.nio.ByteBuffer
import java.util.function.Consumer
const val ACTION_USB_PERMISSION = "dev.slimevr.USB_PERMISSION"
/**
* Handles Android USB Host HID dongles and receives tracker data from them.
*/
class AndroidHIDManager(
name: String,
private val trackersConsumer: Consumer<Tracker>,
private val context: Context,
) : Thread(name) {
private val devices: MutableList<HIDDevice> = mutableListOf()
private val devicesBySerial: MutableMap<String, MutableList<Int>> = HashMap()
private val devicesByHID: MutableMap<UsbDevice, MutableList<Int>> = HashMap()
private val connByHID: MutableMap<UsbDevice, AndroidHIDDevice> = HashMap()
private val lastDataByHID: MutableMap<UsbDevice, Int> = HashMap()
private val usbManager: UsbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) as UsbDevice?)?.let {
checkConfigureDevice(it, requestPermission = true)
}
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) as UsbDevice?)?.let {
removeDevice(it)
}
}
ACTION_USB_PERMISSION -> {
deviceEnumerate(false)
}
}
}
}
private fun proceedWithDeviceConfiguration(hidDevice: UsbDevice) {
// This is the original logic from checkConfigureDevice after permission is confirmed
LogManager.info("[TrackerServer] USB Permission granted for ${hidDevice.deviceName}. Proceeding with configuration.")
// Close any existing connection (do we still have one?)
this.connByHID[hidDevice]?.close()
// Open new HID connection with USB device
this.connByHID[hidDevice] = AndroidHIDDevice(hidDevice, usbManager)
val serial = hidDevice.serialNumber ?: "Unknown USB Device ${hidDevice.deviceId}"
this.devicesBySerial[serial]?.let {
this.devicesByHID[hidDevice] = it
synchronized(this.devices) {
for (id in it) {
val device = this.devices[id]
for (value in device.trackers.values) {
if (value.status == TrackerStatus.DISCONNECTED) value.status = TrackerStatus.OK
}
}
}
LogManager.info("[TrackerServer] Linked HID device reattached: $serial")
return
}
val list: MutableList<Int> = mutableListOf()
this.devicesBySerial[serial] = list
this.devicesByHID[hidDevice] = list
this.lastDataByHID[hidDevice] = 0 // initialize last data received
LogManager.info("[TrackerServer] (Probably) Compatible HID device detected: $serial")
}
fun checkConfigureDevice(usbDevice: UsbDevice, requestPermission: Boolean = false) {
if (usbDevice.vendorId == HID_TRACKER_RECEIVER_VID && usbDevice.productId == HID_TRACKER_RECEIVER_PID) {
if (usbManager.hasPermission(usbDevice)) {
LogManager.info("[TrackerServer] Already have permission for ${usbDevice.deviceName}")
proceedWithDeviceConfiguration(usbDevice)
} else if (requestPermission) {
LogManager.info("[TrackerServer] Requesting permission for ${usbDevice.deviceName}")
val permissionIntent = PendingIntent.getBroadcast(
context,
0,
Intent(ACTION_USB_PERMISSION).apply { setPackage(context.packageName) }, // Explicitly set package
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
usbManager.requestPermission(usbDevice, permissionIntent)
}
}
}
private fun removeDevice(hidDevice: UsbDevice) {
this.devicesByHID[hidDevice]?.let {
synchronized(this.devices) {
for (id in it) {
val device = this.devices[id]
for (value in device.trackers.values) {
if (value.status == TrackerStatus.OK) {
value.status =
TrackerStatus.DISCONNECTED
}
}
}
}
this.devicesByHID.remove(hidDevice)
val oldConn = this.connByHID.remove(hidDevice)
val serial = oldConn?.serialNumber ?: "Unknown"
oldConn?.close()
LogManager.info("[TrackerServer] Linked HID device removed: $serial")
}
}
private fun dataRead() {
synchronized(devicesByHID) {
var devicesPresent = false
var devicesDataReceived = false
val q = intArrayOf(0, 0, 0, 0)
val a = intArrayOf(0, 0, 0)
val m = intArrayOf(0, 0, 0)
for ((hidDevice, deviceList) in devicesByHID) {
val dataReceived = ByteArray(64)
val conn = connByHID[hidDevice]!!
val dataRead = conn.deviceConnection.bulkTransfer(conn.endpointIn, dataReceived, dataReceived.size, 0)
// LogManager.info("[TrackerServer] HID data read ($dataRead bytes): ${dataReceived.contentToString()}")
devicesPresent = true // Even if the device has no data
if (dataRead > 0) {
// Process data
// The data is always received as 64 bytes, this check no longer works
if (dataRead % PACKET_SIZE != 0) {
LogManager.info("[TrackerServer] Malformed HID packet, ignoring")
continue // Don't continue with this data
}
devicesDataReceived = true // Data is received and is valid (not malformed)
lastDataByHID[hidDevice] = 0 // reset last data received
val packetCount = dataRead / PACKET_SIZE
var i = 0
while (i < packetCount * PACKET_SIZE) {
// Common packet data
val packetType = dataReceived[i].toUByte().toInt()
val id = dataReceived[i + 1].toUByte().toInt()
val deviceId = id
// Register device
if (packetType == 255) { // device register packet from receiver
val buffer = ByteBuffer.wrap(dataReceived, i + 2, 8)
buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
val addr = buffer.getLong() and 0xFFFFFFFFFFFF
val deviceName = String.format("%012X", addr)
HIDCommon.deviceIdLookup(devices, hidDevice.serialNumber, deviceId, deviceName, deviceList) // register device
// server wants tracker to be unique, so use combination of hid serial and full id
i += PACKET_SIZE
continue
}
val device: HIDDevice? = HIDCommon.deviceIdLookup(devices, hidDevice.serialNumber, deviceId, null, deviceList)
if (device == null) { // not registered yet
i += PACKET_SIZE
continue
}
HIDCommon.processPacket(dataReceived, i, packetType, device, q, a, m, trackersConsumer)
i += PACKET_SIZE
}
// LogManager.info("[TrackerServer] HID received $packetCount tracker packets")
} else {
lastDataByHID[hidDevice] = lastDataByHID[hidDevice]!! + 1 // increment last data received
}
}
if (!devicesPresent) {
sleep(10) // No hid device, "empty loop" so sleep to save the poor cpu
} else if (!devicesDataReceived) {
sleep(1) // read has no timeout, no data also causes an "empty loop"
}
}
}
private fun deviceEnumerate(requestPermission: Boolean = false) {
val hidDeviceList: MutableList<UsbDevice> = usbManager.deviceList.values.filter {
it.vendorId == HID_TRACKER_RECEIVER_VID && it.productId == HID_TRACKER_RECEIVER_PID
}.toMutableList()
synchronized(devicesByHID) {
// Work on devicesByHid and add/remove as necessary
val removeList: MutableList<UsbDevice> = devicesByHID.keys.toMutableList()
removeList.removeAll(hidDeviceList)
for (device in removeList) {
removeDevice(device)
}
hidDeviceList.removeAll(devicesByHID.keys) // addList
for (device in hidDeviceList) {
// This will handle permission check/request
checkConfigureDevice(device, requestPermission)
}
}
}
override fun run() {
val intentFilter = IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED)
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
intentFilter.addAction(ACTION_USB_PERMISSION)
// Listen for USB device attach/detach
ContextCompat.registerReceiver(
context,
usbReceiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED,
)
// Enumerate existing devices
deviceEnumerate(true)
// Data read loop
while (true) {
try {
sleep(0) // Possible performance impact
} catch (e: InterruptedException) {
currentThread().interrupt()
break
}
dataRead() // not in try catch?
}
}
fun getDevices(): List<Device> = devices
companion object {
private const val resetSourceName = "TrackerServer"
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device vendor-id="4617" product-id="30352" />
</resources>

View File

@@ -34,7 +34,6 @@ import solarxr_protocol.rpc.*
import kotlin.io.path.Path
class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeader>() {
private var currTransactionId: Long = 0
private val mainScope = CoroutineScope(SupervisorJob())
init {
@@ -238,7 +237,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val localIp = getLocalIp() ?: return
val response = ServerInfosResponse
.createServerInfosResponse(fbb, fbb.createString(localIp))
val outbound = this.createRPCMessage(fbb, RpcMessage.ServerInfosResponse, response)
val outbound = this.createRPCMessage(fbb, RpcMessage.ServerInfosResponse, response, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -251,7 +250,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val config = api.server.configManager.vrConfig.overlay
val response = OverlayDisplayModeResponse
.createOverlayDisplayModeResponse(fbb, config.isVisible, config.isMirrored)
val outbound = this.createRPCMessage(fbb, RpcMessage.OverlayDisplayModeResponse, response)
val outbound = this.createRPCMessage(fbb, RpcMessage.OverlayDisplayModeResponse, response, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -283,7 +282,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
// might not be a good idea maybe let the client ask again
val fbb = FlatBufferBuilder(300)
val config = createSkeletonConfig(fbb, api.server.humanPoseManager)
val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config)
val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -297,7 +296,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val fbb = FlatBufferBuilder(300)
val config = createSkeletonConfig(fbb, api.server.humanPoseManager)
val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config)
val outbound = this.createRPCMessage(fbb, RpcMessage.SkeletonConfigResponse, config, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -332,7 +331,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val fbb = FlatBufferBuilder(40)
val status = RecordBVHStatus
.createRecordBVHStatus(fbb, api.server.bvhRecorder.isRecording)
val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status)
val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -343,7 +342,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val fbb = FlatBufferBuilder(40)
val status = RecordBVHStatus
.createRecordBVHStatus(fbb, api.server.bvhRecorder.isRecording)
val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status)
val outbound = this.createRPCMessage(fbb, RpcMessage.RecordBVHStatus, status, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -495,13 +494,16 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
}
}
fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int): Int {
@JvmOverloads
fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int, respondTo: RpcMessageHeader? = null): Int {
val data = IntArray(1)
RpcMessageHeader.startRpcMessageHeader(fbb)
RpcMessageHeader.addMessage(fbb, messageOffset)
RpcMessageHeader.addMessageType(fbb, messageType)
RpcMessageHeader.addTxId(fbb, TransactionId.createTransactionId(fbb, currTransactionId++))
respondTo?.txId()?.let { txId ->
RpcMessageHeader.addTxId(fbb, TransactionId.createTransactionId(fbb, txId.id()))
}
data[0] = RpcMessageHeader.endRpcMessageHeader(fbb)
val messages = MessageBundle.createRpcMsgsVector(fbb, data)
@@ -525,7 +527,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
val response = StatusSystemResponseT()
response.currentStatuses = statuses
val offset = StatusSystemResponse.pack(fbb, response)
val outbound = this.createRPCMessage(fbb, RpcMessage.StatusSystemResponse, offset)
val outbound = this.createRPCMessage(fbb, RpcMessage.StatusSystemResponse, offset, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
@@ -557,7 +559,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
0f,
)
}
fbb.finish(createRPCMessage(fbb, RpcMessage.HeightResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.HeightResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
}
@@ -572,7 +574,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
0,
api.server.configManager.vrConfig.server.useMagnetometerOnAllTrackers,
)
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
return
}
@@ -584,7 +586,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
trackerId,
tracker.config.shouldHaveMagEnabled == true,
)
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
}
@@ -604,7 +606,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
0,
api.server.configManager.vrConfig.server.useMagnetometerOnAllTrackers,
)
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
}
return
@@ -623,7 +625,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
trackerId,
state,
)
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
return
}
@@ -640,7 +642,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
trackerId,
state,
)
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response))
fbb.finish(createRPCMessage(fbb, RpcMessage.MagToggleResponse, response, messageHeader))
conn.send(fbb.dataBuffer())
}
}
@@ -660,7 +662,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
configManager.saveConfig()
sendSettingsChangedResponse(conn)
sendSettingsChangedResponse(conn, messageHeader)
}
private fun onDetectStayAlignedRelaxedPoseRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
@@ -692,7 +694,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
LogManager.info("[detectStayAlignedRelaxedPose] pose=$pose $relaxedPose")
sendSettingsChangedResponse(conn)
sendSettingsChangedResponse(conn, messageHeader)
}
private fun onResetStayAlignedRelaxedPoseRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
@@ -720,13 +722,13 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
LogManager.info("[resetStayAlignedRelaxedPose] pose=$pose")
sendSettingsChangedResponse(conn)
sendSettingsChangedResponse(conn, messageHeader)
}
fun sendSettingsChangedResponse(conn: GenericConnection) {
fun sendSettingsChangedResponse(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
val fbb = FlatBufferBuilder(32)
val settings = RPCSettingsBuilder.createSettingsResponse(fbb, api.server)
val outbound = createRPCMessage(fbb, RpcMessage.SettingsResponse, settings)
val outbound = createRPCMessage(fbb, RpcMessage.SettingsResponse, settings, messageHeader)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}

View File

@@ -39,7 +39,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
}
fun onSettingsRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
rpcHandler.sendSettingsChangedResponse(conn)
rpcHandler.sendSettingsChangedResponse(conn, messageHeader)
}
fun onChangeSettingsRequest(conn: GenericConnection?, messageHeader: RpcMessageHeader) {

View File

@@ -28,16 +28,16 @@ class RPCTrackingPause(private val rpcHandler: RPCHandler, private val api: Prot
}
}
private fun getPauseStateResponse(trackingPaused: Boolean): ByteBuffer {
private fun getPauseStateResponse(trackingPaused: Boolean, messageHeader: RpcMessageHeader? = null): ByteBuffer {
val fbb = FlatBufferBuilder(32)
val state = TrackingPauseStateResponse.createTrackingPauseStateResponse(fbb, trackingPaused)
val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.TrackingPauseStateResponse, state)
val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.TrackingPauseStateResponse, state, messageHeader)
fbb.finish(outbound)
return fbb.dataBuffer()
}
private fun onTrackingPauseStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
conn.send(getPauseStateResponse(currentPauseState))
conn.send(getPauseStateResponse(currentPauseState, messageHeader))
}
override fun onTrackingPause(trackingPaused: Boolean) {

View File

@@ -52,11 +52,13 @@ class Constraint(
if (constraintType == ConstraintType.COMPLETE) return thisBone.getGlobalRotation()
// If there is no parent and this is not a complete constraint accept the rotation as is.
if (thisBone.parent == null) return rotation
// TODO: This was changed due to a race condition with the RPC thread, see
// https://github.com/SlimeVR/SlimeVR-Server/issues/1534 for more information.
val parent = thisBone.parent ?: return rotation
val localRotation = getLocalRotation(rotation, thisBone)
val localRotation = getLocalRotation(rotation, thisBone, parent)
val constrainedRotation = constraintFunction(localRotation, swingRad, twistRad, allowedDeviationRad)
return getWorldRotationFromLocal(constrainedRotation, thisBone)
return getWorldRotationFromLocal(constrainedRotation, thisBone, parent)
}
/**
@@ -90,14 +92,12 @@ class Constraint(
ConstraintType.LOOSE_HINGE -> looseHingeConstraint
}
private fun getLocalRotation(rotation: Quaternion, thisBone: Bone): Quaternion {
val parent = thisBone.parent!!
private fun getLocalRotation(rotation: Quaternion, thisBone: Bone, parent: Bone): Quaternion {
val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset
return (parent.getGlobalRotation() * localRotationOffset).inv() * rotation
}
private fun getWorldRotationFromLocal(rotation: Quaternion, thisBone: Bone): Quaternion {
val parent = thisBone.parent!!
private fun getWorldRotationFromLocal(rotation: Quaternion, thisBone: Bone, parent: Bone): Quaternion {
val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset
return (parent.getGlobalRotation() * localRotationOffset * rotation).unit()
}

View File

@@ -650,26 +650,30 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
var weightR = getFootPlantWeight(rightFootPosition)
// if foot trackers exist add to the weights
val leftFootYaw = isolateYaw(leftFootRotation)
if (leftFootTracker) {
weightL *= getRotationalDistanceToPlant(
leftFootRotation,
leftFootYaw,
)
}
val rightFootYaw = isolateYaw(rightFootRotation)
if (rightFootTracker) {
weightR *= getRotationalDistanceToPlant(
rightFootRotation,
rightFootYaw,
)
}
// perform the correction
leftFootRotation = leftFootRotation
.interpR(
isolateYaw(leftFootRotation),
leftFootYaw,
weightL * masterWeightL,
)
rightFootRotation = rightFootRotation
.interpR(
isolateYaw(rightFootRotation),
rightFootYaw,
weightR * masterWeightR,
)
}
@@ -805,8 +809,7 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
}
// returns the amount to slerp for foot plant when foot trackers are active
private fun getRotationalDistanceToPlant(footRot: Quaternion): Float {
val footRotYaw: Quaternion = isolateYaw(footRot)
private fun getRotationalDistanceToPlant(footRot: Quaternion, footRotYaw: Quaternion): Float {
var angle = footRot.angleToR(footRotYaw)
angle = (angle / (2 * Math.PI)).toFloat()
angle = FastMath.clamp(
@@ -1005,12 +1008,9 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
}
}
// remove the x and z components of the given quaternion
private fun isolateYaw(quaternion: Quaternion): Quaternion = Quaternion(
quaternion.w,
0f,
quaternion.y,
0f,
// isolate the euler yaw component of a given quaternion
private fun isolateYaw(quaternion: Quaternion): Quaternion = Quaternion.rotationAroundYAxis(
quaternion.toEulerAngles(EulerOrder.YZX).y,
)
// return a quaternion that has been rotated by the new pitch amount

View File

@@ -0,0 +1,325 @@
package dev.slimevr.tracking.trackers.hid
import com.jme3.math.FastMath
import dev.slimevr.VRServer
import dev.slimevr.tracking.trackers.Tracker
import dev.slimevr.tracking.trackers.TrackerStatus
import dev.slimevr.tracking.trackers.udp.BoardType
import dev.slimevr.tracking.trackers.udp.IMUType
import dev.slimevr.tracking.trackers.udp.MCUType
import dev.slimevr.tracking.trackers.udp.MagnetometerStatus
import io.eiren.util.logging.LogManager
import io.github.axisangles.ktmath.Quaternion
import io.github.axisangles.ktmath.Quaternion.Companion.fromRotationVector
import io.github.axisangles.ktmath.Vector3
import java.nio.ByteBuffer
import java.util.function.Consumer
import kotlin.collections.set
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
/**
* A collection of shared HID functions between OS specific HID implementations.
*/
class HIDCommon {
companion object {
const val HID_TRACKER_RECEIVER_VID = 0x1209
const val HID_TRACKER_RECEIVER_PID = 0x7690
const val PACKET_SIZE = 16
private val AXES_OFFSET = fromRotationVector(-FastMath.HALF_PI, 0f, 0f)
fun deviceIdLookup(
hidDevices: MutableList<HIDDevice>,
hidSerialNumber: String?,
deviceId: Int,
deviceName: String? = null,
deviceList: MutableList<Int>,
): HIDDevice? {
synchronized(hidDevices) {
deviceList.map { hidDevices[it] }.find { it.hidId == deviceId }?.let { return it }
if (deviceName == null) { // not registered yet
return null
}
val device = HIDDevice(deviceId)
// server wants tracker to be unique, so use combination of hid serial and full id // TODO: use the tracker "address" instead
// TODO: the server should not setup any device, only when the receiver associates the id with the tracker "address" and sends this packet (0xff?) which it will do occasionally
// device.name = hidDevice.serialNumber ?: "Unknown HID Device"
// device.name += "-$deviceId"
device.name = deviceName
device.manufacturer = "HID Device" // TODO:
// device.manufacturer = hidDevice.manufacturer ?: "HID Device"
// device.hardwareIdentifier = hidDevice.serialNumber // hardwareIdentifier is not used to identify the tracker, so also display the receiver serial
// device.hardwareIdentifier += "-$deviceId/$deviceName" // receiver serial + assigned id in receiver + device address
device.hardwareIdentifier = deviceName // the rest of identifier wont fit in gui
hidDevices.add(device)
deviceList.add(hidDevices.size - 1)
VRServer.instance.deviceManager.addDevice(device) // actually add device to the server
LogManager
.info(
"[TrackerServer] Added device $deviceName for ${hidSerialNumber ?: "Unknown HID Device"}, id $deviceId",
)
return device
}
}
private fun setUpSensor(
device: HIDDevice,
trackerId: Int,
sensorType: IMUType,
sensorStatus: TrackerStatus,
magStatus: MagnetometerStatus,
trackersConsumer: Consumer<Tracker>,
) {
// LogManager.info("[TrackerServer] Sensor $trackerId for ${device.name}, status $sensorStatus")
var imuTracker = device.getTracker(trackerId)
if (imuTracker == null) {
var formattedHWID = device.hardwareIdentifier.replace(":", "").takeLast(5)
imuTracker = Tracker(
device,
VRServer.getNextLocalTrackerId(),
device.name + "/" + trackerId,
"Tracker $formattedHWID",
null,
trackerNum = trackerId,
hasRotation = true,
hasAcceleration = true,
userEditable = true,
imuType = sensorType,
allowFiltering = true,
needsReset = true,
needsMounting = true,
usesTimeout = false,
magStatus = magStatus,
)
// usesTimeout false because HID trackers aren't "Disconnected" unless receiver is physically removed probably
// TODO: Could tracker maybe use "Timed out" status without marking as disconnecting?
// TODO: can be marked as "Disconnected" by timeout if the tracker has enabled activity timeouts
device.trackers[trackerId] = imuTracker
trackersConsumer.accept(imuTracker)
imuTracker.status = sensorStatus
LogManager
.info(
"[TrackerServer] Added sensor $trackerId for ${device.name}, type $sensorType",
)
}
}
fun processPacket(
dataReceived: ByteArray,
i: Int,
packetType: Int,
device: HIDDevice,
q: IntArray,
a: IntArray,
m: IntArray,
trackersConsumer: Consumer<Tracker>,
) {
val trackerId = 0 // no concept of extensions
// Register tracker
if (packetType == 0) { // Tracker register packet (device info)
val imu_id = dataReceived[i + 8].toUByte().toInt()
val mag_id = dataReceived[i + 9].toUByte().toInt()
val sensorType = IMUType.getById(imu_id.toUInt())
// only able to register magnetometer status, not magnetometer type
val magStatus = MagnetometerStatus.getById(mag_id.toUByte())
if (sensorType != null && magStatus != null) {
setUpSensor(device, trackerId, sensorType, TrackerStatus.OK, magStatus, trackersConsumer)
}
}
val tracker: Tracker? = device.getTracker(trackerId)
if (tracker == null) { // not registered yet
return
}
// Packet data
var batt: Int? = null
var batt_v: Int? = null
var temp: Int? = null
var brd_id: Int? = null
var mcu_id: Int? = null
// var imu_id: Int? = null
// var mag_id: Int? = null
var fw_date: Int? = null
var fw_major: Int? = null
var fw_minor: Int? = null
var fw_patch: Int? = null
var svr_status: Int? = null
// var status: Int? = null // raw status from tracker
var rssi: Int? = null
// Tracker packets
when (packetType) {
0 -> { // device info
batt = dataReceived[i + 2].toUByte().toInt()
batt_v = dataReceived[i + 3].toUByte().toInt()
temp = dataReceived[i + 4].toUByte().toInt()
brd_id = dataReceived[i + 5].toUByte().toInt()
mcu_id = dataReceived[i + 6].toUByte().toInt()
// imu_id = dataReceived[i + 8].toUByte().toInt()
// mag_id = dataReceived[i + 9].toUByte().toInt()
// ushort little endian
fw_date = dataReceived[i + 11].toUByte().toInt() shl 8 or dataReceived[i + 10].toUByte().toInt()
fw_major = dataReceived[i + 12].toUByte().toInt()
fw_minor = dataReceived[i + 13].toUByte().toInt()
fw_patch = dataReceived[i + 14].toUByte().toInt()
rssi = dataReceived[i + 15].toUByte().toInt()
}
1 -> { // full precision quat and accel, no extra data
// Q15: 1 is represented as 0x7FFF, -1 as 0x8000
// The sender can use integer saturation to avoid overflow
for (j in 0..3) { // quat received as fixed Q15
// Q15 as short little endian
q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt()
}
for (j in 0..2) { // accel received as fixed 7, in m/s^2
// Q7 as short little endian
a[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt()
}
}
2 -> { // reduced precision quat and accel with data
batt = dataReceived[i + 2].toUByte().toInt()
batt_v = dataReceived[i + 3].toUByte().toInt()
temp = dataReceived[i + 4].toUByte().toInt()
// quaternion is quantized as exponential map
// X = 10 bits, Y/Z = 11 bits
val buffer = ByteBuffer.wrap(dataReceived, i + 5, 4)
buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
val q_buf = buffer.getInt().toUInt()
q[0] = (q_buf and 1023u).toInt()
q[1] = (q_buf shr 10 and 2047u).toInt()
q[2] = (q_buf shr 21 and 2047u).toInt()
for (j in 0..2) { // accel received as fixed 7, in m/s^2
// Q7 as short little endian
a[j] = dataReceived[i + 9 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 9 + j * 2].toUByte().toInt()
}
rssi = dataReceived[i + 15].toUByte().toInt()
}
3 -> { // status
svr_status = dataReceived[i + 2].toUByte().toInt()
// status = dataReceived[i + 3].toUByte().toInt()
rssi = dataReceived[i + 15].toUByte().toInt()
}
4 -> { // full precision quat and mag, no extra data
for (j in 0..3) { // quat received as fixed Q15
// Q15 as short little endian
q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt()
}
for (j in 0..2) { // mag received as fixed 10, in gauss
// Q10 as short little endian
m[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt()
}
}
else -> {
}
}
// Assign data
if (batt != null) {
tracker.batteryLevel = if (batt == 128) 1f else (batt and 127).toFloat()
}
// Server still won't display battery at 0% at all
if (batt_v != null) {
tracker.batteryVoltage = (batt_v.toFloat() + 245f) / 100f
}
if (temp != null) {
tracker.temperature = if (temp > 0) temp.toFloat() / 2f - 39f else null
}
// Range 1 - 255 -> -38.5 - +88.5 C
if (brd_id != null) {
val boardType = BoardType.getById(brd_id.toUInt())
if (boardType != null) {
device.boardType = boardType!!
}
}
if (mcu_id != null) {
val mcuType = MCUType.getById(mcu_id.toUInt())
if (mcuType != null) {
device.mcuType = mcuType!!
}
}
if (fw_date != null && fw_major != null && fw_minor != null && fw_patch != null) {
val firmwareYear = 2020 + (fw_date shr 9 and 127)
val firmwareMonth = fw_date shr 5 and 15
val firmwareDay = fw_date and 31
val firmwareDate = String.format("%04d-%02d-%02d", firmwareYear, firmwareMonth, firmwareDay)
device.firmwareVersion = "$fw_major.$fw_minor.$fw_patch (Build $firmwareDate)"
}
if (svr_status != null) {
val status = TrackerStatus.getById(svr_status)
if (status != null) {
tracker.status = status!!
}
}
if (rssi != null) {
tracker.signalStrength = -rssi
}
// Assign rotation and acceleration
if (packetType == 1 || packetType == 4) {
// The data comes in the same order as in the UDP protocol
// x y z w -> w x y z
var rot = Quaternion(q[3].toFloat(), q[0].toFloat(), q[1].toFloat(), q[2].toFloat())
val scaleRot = 1 / (1 shl 15).toFloat() // compile time evaluation
rot = AXES_OFFSET.times(scaleRot).times(rot) // no division
tracker.setRotation(rot)
}
if (packetType == 2) {
val v = floatArrayOf(q[0].toFloat(), q[1].toFloat(), q[2].toFloat()) // used q array for quantized data
v[0] /= (1 shl 10).toFloat()
v[1] /= (1 shl 11).toFloat()
v[2] /= (1 shl 11).toFloat()
for (i in 0..2) {
v[i] = v[i] * 2 - 1
}
// http://marc-b-reynolds.github.io/quaternions/2017/05/02/QuatQuantPart1.html#fnref:pos:3
// https://github.com/Marc-B-Reynolds/Stand-alone-junk/blob/559bd78893a3a95cdee1845834c632141b945a45/src/Posts/quatquant0.c#L898
val d = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
val invSqrtD = 1 / sqrt(d + 1e-6f)
val a = (PI.toFloat() / 2) * d * invSqrtD
val s = sin(a)
val k = s * invSqrtD
var rot = Quaternion(cos(a), k * v[0], k * v[1], k * v[2])
rot = AXES_OFFSET.times(rot) // no division
tracker.setRotation(rot)
}
if (packetType == 1 || packetType == 2) {
// Acceleration is in local device frame
// On flat surface / face up:
// Right side of the device is +X
// Front side (facing up) is +Z
// Mounted on body / standing up:
// Top side of the device is +Y
// Front side (facing out) is +Z
val scaleAccel = 1 / (1 shl 7).toFloat() // compile time evaluation
val acceleration = Vector3(a[0].toFloat(), a[1].toFloat(), a[2].toFloat()).times(scaleAccel) // no division
tracker.setAcceleration(acceleration)
}
if (packetType == 4) {
// Magnetometer is in local device frame
// On flat surface / face up:
// Right side of the device is +X
// Front side (facing up) is +Z
// Mounted on body / standing up:
// Top side of the device is +Y
// Front side (facing out) is +Z
val scaleMag = 1000 / (1 shl 10).toFloat() // compile time evaluation, and change gauss to milligauss
val magnetometer = Vector3(m[0].toFloat(), m[1].toFloat(), m[2].toFloat()).times(scaleMag) // no division
tracker.setMagVector(magnetometer)
}
if (packetType == 1 || packetType == 2 || packetType == 4) {
tracker.dataTick() // only data tick if there is rotation data
}
}
}
}

View File

@@ -1,4 +1,4 @@
package dev.slimevr.desktop.tracking.trackers.hid
package dev.slimevr.tracking.trackers.hid
import dev.slimevr.tracking.trackers.Device
import dev.slimevr.tracking.trackers.Tracker

View File

@@ -13,7 +13,7 @@ import dev.slimevr.desktop.platform.linux.UnixSocketBridge
import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge
import dev.slimevr.desktop.platform.windows.WindowsNamedPipeBridge
import dev.slimevr.desktop.serial.DesktopSerialHandler
import dev.slimevr.desktop.tracking.trackers.hid.TrackersHID
import dev.slimevr.desktop.tracking.trackers.hid.DesktopHIDManager
import dev.slimevr.tracking.trackers.Tracker
import io.eiren.util.OperatingSystem
import io.eiren.util.collections.FastList
@@ -132,7 +132,7 @@ fun main(args: Array<String>) {
NetworkProfileChecker(vrServer)
// Start service for USB HID trackers
TrackersHID(
DesktopHIDManager(
"Sensors HID service",
) { tracker: Tracker -> vrServer.registerTracker(tracker) }

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