diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml
index 3e90c7d18e..9d1b711612 100644
--- a/.github/workflows/gradle.yaml
+++ b/.github/workflows/gradle.yaml
@@ -285,7 +285,7 @@ jobs:
./bundle_dmg.sh --volname SlimeVR --icon slimevr 180 170 --app-drop-link 480 170 \
--window-size 660 400 --hide-extension ../macos/SlimeVR.app \
--volicon ../macos/SlimeVR.app/Contents/Resources/icon.icns --skip-jenkins \
- --eula ../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
+ --eula ../../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
- uses: actions/upload-artifact@v4
with:
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..32f5979c17
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+update-notifier=false
diff --git a/Cargo.lock b/Cargo.lock
index 23fc1864de..a3823723de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2018,6 +2018,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "0.4.8"
@@ -3188,7 +3197,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -3827,6 +3836,7 @@ dependencies = [
"discord-sdk",
"flexi_logger",
"glob",
+ "itertools",
"libloading 0.8.5",
"log",
"log-panics",
diff --git a/dev.slimevr.SlimeVR.metainfo.xml b/dev.slimevr.SlimeVR.metainfo.xml
index 141ea2040c..688c848ad4 100644
--- a/dev.slimevr.SlimeVR.metainfo.xml
+++ b/dev.slimevr.SlimeVR.metainfo.xml
@@ -65,6 +65,7 @@ work. If not, see .
+ https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.2https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.3https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.2
diff --git a/flake.nix b/flake.nix
index 809182a6c7..2bdecbac9d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -92,7 +92,7 @@
harfbuzz
libffi
libsoup_3
- openssl
+ openssl.dev
pango
pkg-config
treefmt
diff --git a/gui/.env b/gui/.env
new file mode 100644
index 0000000000..7ce15648fa
--- /dev/null
+++ b/gui/.env
@@ -0,0 +1,8 @@
+VITE_FIRMWARE_TOOL_URL=https://fw-tool-api.slimevr.io
+VITE_FIRMWARE_TOOL_S3_URL=https://fw-tool-bucket.slimevr.io
+FIRMWARE_TOOL_SCHEMA_URL=https://fw-tool-api.slimevr.io/api-json
+
+
+# VITE_FIRMWARE_TOOL_URL=http://localhost:3000
+# VITE_FIRMWARE_TOOL_S3_URL=http://localhost:9000
+# FIRMWARE_TOOL_SCHEMA_URL=http://localhost:3000/api-json
diff --git a/gui/.eslintrc.json b/gui/.eslintrc.json
deleted file mode 100644
index dc120fbd29..0000000000
--- a/gui/.eslintrc.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "env": {
- "browser": true,
- "es2021": true,
- "jest": true
- },
- "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@dword-design/import-alias/recommended"],
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaFeatures": {
- "jsx": true
- },
- "ecmaVersion": "latest",
- "sourceType": "module"
- },
- "plugins": ["react-hooks", "@typescript-eslint"],
- "rules": {
- "react/react-in-jsx-scope": "off",
- "react/prop-types": "off",
- "spaced-comment": "error",
- "quotes": ["error", "single"],
- "no-duplicate-imports": "error",
- "no-inline-styles": "off",
- "@typescript-eslint/no-explicit-any": "off",
- "react/no-unescaped-entities": "off",
- "camelcase": "error",
- "@typescript-eslint/no-unused-vars": [
- "warn",
- {
- "argsIgnorePattern": "^_",
- "varsIgnorePattern": "^_"
- }
- ],
- "@dword-design/import-alias/prefer-alias": [
- "error",
- {
- "alias": {
- "@": "./src/"
- }
- }
- ]
- },
- "settings": {
- "import/resolver": {
- "typescript": {}
- },
- "react": {
- "version": "detect"
- }
- }
-}
diff --git a/gui/.gitignore b/gui/.gitignore
index a1ac58f664..90069ef5a6 100644
--- a/gui/.gitignore
+++ b/gui/.gitignore
@@ -28,6 +28,7 @@ yarn-error.log*
# vite
/dist
/stats.html
+vite.config.ts.timestamp*
# eslint
.eslintcache
diff --git a/gui/.lintstagedrc.mjs b/gui/.lintstagedrc.mjs
index 3f4d417894..cf8c7a097c 100644
--- a/gui/.lintstagedrc.mjs
+++ b/gui/.lintstagedrc.mjs
@@ -1,5 +1,5 @@
export default {
'**/*.{ts,tsx}': () => 'tsc -p tsconfig.json --noEmit',
- '**/*.{js,jsx,ts,tsx}': 'eslint --max-warnings=0 --cache --fix',
+ 'src/**/*.{js,jsx,ts,tsx}': 'eslint --max-warnings=0 --no-warn-ignored --cache --fix',
'**/*.{js,jsx,ts,tsx,css,md,json}': 'prettier --write',
};
diff --git a/gui/eslint.config.js b/gui/eslint.config.js
new file mode 100644
index 0000000000..c4d534218e
--- /dev/null
+++ b/gui/eslint.config.js
@@ -0,0 +1,79 @@
+import { FlatCompat } from '@eslint/eslintrc';
+import eslint from '@eslint/js';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+const compat = new FlatCompat();
+
+export const gui = [
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...compat.extends('plugin:@dword-design/import-alias/recommended'),
+ ...compat.plugins('eslint-plugin-react-hooks'),
+ // Add import-alias rule inside compat because plugin doesn't like flat configs
+ ...compat.config({
+ rules: {
+ '@dword-design/import-alias/prefer-alias': [
+ 'error',
+ {
+ alias: {
+ '@': './src/',
+ },
+ },
+ ],
+ },
+ }),
+ {
+ languageOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ parser: tseslint.parser,
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ globals: {
+ ...globals.browser,
+ ...globals.jest,
+ },
+ },
+ files: ['src/**/*.{js,jsx,ts,tsx,json}'],
+ plugins: {
+ '@typescript-eslint': tseslint.plugin,
+ },
+ rules: {
+ 'react/react-in-jsx-scope': 'off',
+ 'react/prop-types': 'off',
+ 'spaced-comment': 'error',
+ quotes: ['error', 'single'],
+ 'no-duplicate-imports': 'error',
+ 'no-inline-styles': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ 'react/no-unescaped-entities': 'off',
+ camelcase: 'error',
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ ignoreRestSiblings: true,
+ },
+ ],
+ },
+ settings: {
+ 'import/resolver': {
+ typescript: {},
+ },
+ react: {
+ version: 'detect',
+ },
+ },
+ },
+ // Global ignore
+ {
+ ignores: ['**/firmware-tool-api/'],
+ },
+];
+
+export default gui;
diff --git a/gui/openapi-codegen.config.ts b/gui/openapi-codegen.config.ts
new file mode 100644
index 0000000000..3e007342bc
--- /dev/null
+++ b/gui/openapi-codegen.config.ts
@@ -0,0 +1,28 @@
+import {
+ generateSchemaTypes,
+ generateReactQueryComponents,
+} from '@openapi-codegen/typescript';
+import { defineConfig } from '@openapi-codegen/cli';
+import dotenv from 'dotenv';
+
+dotenv.config()
+
+export default defineConfig({
+ firmwareTool: {
+ from: {
+ source: 'url',
+ url: process.env.FIRMWARE_TOOL_SCHEMA_URL ?? 'http://localhost:3000/api-json',
+ },
+ outputDir: 'src/firmware-tool-api',
+ to: async (context) => {
+ const filenamePrefix = 'firmwareTool';
+ const { schemasFiles } = await generateSchemaTypes(context, {
+ filenamePrefix,
+ });
+ await generateReactQueryComponents(context, {
+ filenamePrefix,
+ schemasFiles,
+ });
+ },
+ },
+});
diff --git a/gui/package.json b/gui/package.json
index 58c18e6520..f3a0c160d4 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -2,13 +2,17 @@
"name": "slimevr-ui",
"version": "0.5.1",
"private": true,
+ "type": "module",
"dependencies": {
"@fluent/bundle": "^0.18.0",
"@fluent/react": "^0.15.2",
"@fontsource/poppins": "^5.1.0",
"@formatjs/intl-localematcher": "^0.2.32",
+ "@hookform/resolvers": "^3.6.0",
"@react-three/drei": "^9.114.3",
"@react-three/fiber": "^8.17.10",
+ "@tailwindcss/typography": "^0.5.15",
+ "@tanstack/react-query": "^5.48.0",
"@tauri-apps/api": "^2.0.2",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
@@ -26,15 +30,18 @@
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.53.0",
+ "react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.26.2",
+ "remark-gfm": "^4.0.0",
"semver": "^7.6.3",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.163.0",
"ts-pattern": "^5.4.0",
"typescript": "^5.6.3",
- "use-double-tap": "^1.3.6"
+ "use-double-tap": "^1.3.6",
+ "yup": "^1.4.0"
},
"scripts": {
"start": "vite --force",
@@ -46,10 +53,14 @@
"lint:fix": "tsc --noEmit && eslint --fix --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && pnpm run format",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
"preview-vite": "vite preview",
- "javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class"
+ "javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
+ "gen:javaversion": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
+ "gen:firmware-tool": "openapi-codegen gen firmwareTool"
},
"devDependencies": {
"@dword-design/eslint-plugin-import-alias": "^4.0.9",
+ "@openapi-codegen/cli": "^2.0.2",
+ "@openapi-codegen/typescript": "^8.0.2",
"@tailwindcss/forms": "^0.5.9",
"@tauri-apps/cli": "^2.0.2",
"@types/file-saver": "^2.0.7",
@@ -64,6 +75,7 @@
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
+ "dotenv": "^16.4.5",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-typescript": "^3.6.3",
@@ -71,6 +83,7 @@
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
+ "globals": "^15.10.0",
"prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.4",
@@ -78,6 +91,7 @@
"tailwind-gradient-mask-image": "^1.2.0",
"tailwindcss": "^3.4.13",
"ts-xor": "^1.3.0",
- "vite": "^5.4.8"
+ "vite": "^5.4.8",
+ "typescript-eslint": "^8.8.0"
}
}
diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl
index e95b99a28c..9badaaf3c0 100644
--- a/gui/public/i18n/en/translation.ftl
+++ b/gui/public/i18n/en/translation.ftl
@@ -75,6 +75,19 @@ body_part-RIGHT_LITTLE_PROXIMAL = Right little proximal
body_part-RIGHT_LITTLE_INTERMEDIATE = Right little intermediate
body_part-RIGHT_LITTLE_DISTAL = Right little distal
+## BoardType
+board_type-UNKNOWN = Unknown
+board_type-NODEMCU = NodeMCU
+board_type-CUSTOM = Custom Board
+board_type-WROOM32 = WROOM32
+board_type-WEMOSD1MINI = Wemos D1 Mini
+board_type-TTGO_TBASE = TTGO T-Base
+board_type-ESP01 = ESP-01
+board_type-SLIMEVR = SlimeVR
+board_type-LOLIN_C3_MINI = Lolin C3 Mini
+board_type-BEETLE32C3 = Beetle ESP32-C3
+board_type-ES32C3DEVKITM1 = Espressif ESP32-C3 DevKitM-1
+
## Proportions
skeleton_bone-NONE = None
skeleton_bone-HEAD = Head Shift
@@ -253,6 +266,11 @@ tracker-settings-name_section-label = Tracker name
tracker-settings-forget = Forget tracker
tracker-settings-forget-description = Removes the tracker from the SlimeVR Server and prevent it from connecting to it until the server is restarted. The configuration of the tracker won't be lost.
tracker-settings-forget-label = Forget tracker
+tracker-settings-update-unavailable = Cannot be updated (DIY)
+tracker-settings-update-up_to_date = Up to date
+tracker-settings-update-available = { $versionName } is now available
+tracker-settings-update = Update now
+tracker-settings-update-title = Firmware version
## Tracker part card info
tracker-part_card-no_name = No name
@@ -325,6 +343,7 @@ settings-sidebar-utils = Utilities
settings-sidebar-serial = Serial console
settings-sidebar-appearance = Appearance
settings-sidebar-notifications = Notifications
+settings-sidebar-firmware-tool = DIY Firmware Tool
settings-sidebar-advanced = Advanced
## SteamVR settings
@@ -701,6 +720,7 @@ onboarding-wifi_creds-submit = Submit!
onboarding-wifi_creds-ssid =
.label = Wi-Fi name
.placeholder = Enter Wi-Fi name
+onboarding-wifi_creds-ssid-required = Wi-Fi name is required
onboarding-wifi_creds-password =
.label = Password
.placeholder = Enter password
@@ -750,6 +770,7 @@ onboarding-connect_tracker-issue-serial = I'm having trouble connecting!
onboarding-connect_tracker-usb = USB Tracker
onboarding-connect_tracker-connection_status-none = Looking for trackers
onboarding-connect_tracker-connection_status-serial_init = Connecting to serial device
+onboarding-connect_tracker-connection_status-obtaining_mac_address = Obtaining the tracker mac address
onboarding-connect_tracker-connection_status-provisioning = Sending Wi-Fi credentials
onboarding-connect_tracker-connection_status-connecting = Trying to connect to Wi-Fi
onboarding-connect_tracker-connection_status-looking_for_server = Looking for server
@@ -1115,6 +1136,165 @@ status_system-StatusSteamVRDisconnected = { $type ->
status_system-StatusTrackerError = The { $trackerName } tracker has an error.
status_system-StatusUnassignedHMD = The VR headset should be assigned as a head tracker.
+
+## Firmware tool globals
+firmware_tool-next_step = Next Step
+firmware_tool-previous_step = Previous Step
+firmware_tool-ok = Looks good
+firmware_tool-retry = Retry
+
+firmware_tool-loading = Loading...
+
+## Firmware tool Steps
+firmware_tool = DIY Firmware tool
+firmware_tool-description =
+ Allows you to configure and flash your DIY trackers
+firmware_tool-not_available = Oops, the firmware tool is not available at the moment. Come back later!
+firmware_tool-not_compatible = The firmware tool is not compatible with this version of the server. Please update your server!
+
+firmware_tool-board_step = Select your Board
+firmware_tool-board_step-description = Select one of the boards listed below.
+
+firmware_tool-board_pins_step = Check the pins
+firmware_tool-board_pins_step-description =
+ Please verify that the selected pins are correct.
+ If you followed the SlimeVR documentation the defaults values should be correct
+firmware_tool-board_pins_step-enable_led = Enable LED
+firmware_tool-board_pins_step-led_pin =
+ .label = LED Pin
+ .placeholder = Enter the pin address of the LED
+
+firmware_tool-board_pins_step-battery_type = Select the battery type
+firmware_tool-board_pins_step-battery_type-BAT_EXTERNAL = External battery
+firmware_tool-board_pins_step-battery_type-BAT_INTERNAL = Internal battery
+firmware_tool-board_pins_step-battery_type-BAT_INTERNAL_MCP3021 = Internal MCP3021
+firmware_tool-board_pins_step-battery_type-BAT_MCP3021 = MCP3021
+
+
+firmware_tool-board_pins_step-battery_sensor_pin =
+ .label = Battery sensor Pin
+ .placeholder = Enter the pin address of battery sensor
+firmware_tool-board_pins_step-battery_resistor =
+ .label = Battery Resistor (Ohms)
+ .placeholder = Enter the value of battery resistor
+firmware_tool-board_pins_step-battery_shield_resistor-0 =
+ .label = Battery Shield R1 (Ohms)
+ .placeholder = Enter the value of Battery Shield R1
+firmware_tool-board_pins_step-battery_shield_resistor-1 =
+ .label = Battery Shield R2 (Ohms)
+ .placeholder = Enter the value of Battery Shield R2
+
+firmware_tool-add_imus_step = Declare your IMUs
+firmware_tool-add_imus_step-description =
+ Please add the IMUs that your tracker has
+ If you followed the SlimeVR documentation the defaults values should be correct
+firmware_tool-add_imus_step-imu_type-label = IMU type
+firmware_tool-add_imus_step-imu_type-placeholder = Select the type of IMU
+firmware_tool-add_imus_step-imu_rotation =
+ .label = IMU Rotation (deg)
+ .placeholder = Rotation angle of the IMU
+firmware_tool-add_imus_step-scl_pin =
+ .label = SCL Pin
+ .placeholder = Pin address of SCL
+firmware_tool-add_imus_step-sda_pin =
+ .label = SDA Pin
+ .placeholder = Pin address of SDA
+firmware_tool-add_imus_step-int_pin =
+ .label = INT Pin
+ .placeholder = Pin address of INT
+firmware_tool-add_imus_step-optional_tracker =
+ .label = Optional tracker
+firmware_tool-add_imus_step-show_less = Show Less
+firmware_tool-add_imus_step-show_more = Show More
+firmware_tool-add_imus_step-add_more = Add more IMUs
+
+firmware_tool-select_firmware_step = Select the firmware version
+firmware_tool-select_firmware_step-description =
+ Please choose what version of the firmware you want to use
+firmware_tool-select_firmware_step-show-third-party =
+ .label = Show third party firmwares
+
+firmware_tool-flash_method_step = Flashing Method
+firmware_tool-flash_method_step-description =
+ Please select the flashing method you want to use
+firmware_tool-flash_method_step-ota =
+ .label = OTA
+ .description = Use the over the air method. Your tracker will use the Wi-Fi to update it's firmware. Works only on already setup trackers.
+firmware_tool-flash_method_step-serial =
+ .label = Serial
+ .description = Use a USB cable to update your tracker.
+
+firmware_tool-flashbtn_step = Press the boot btn
+firmware_tool-flashbtn_step-description = Before going into the next step there is a few things you need to do
+
+firmware_tool-flashbtn_step-board_SLIMEVR = Turn off the tracker, remove the case (if any), connect a USB cable to this computer, then do one of the following steps according to your SlimeVR board revision:
+firmware_tool-flashbtn_step-board_SLIMEVR-r11 = Turn on the tracker while shorting the second rectangular FLASH pad from the edge on the top side of the board, and the metal shield of the microcontroller
+firmware_tool-flashbtn_step-board_SLIMEVR-r12 = Turn on the tracker while shorting the circular FLASH pad on the top side of the board, and the metal shield of the microcontroller
+firmware_tool-flashbtn_step-board_SLIMEVR-r14 = Turn on the tracker while pushing in the FLASH button on the top side of the board
+
+firmware_tool-flashbtn_step-board_OTHER = Before flashing you will probably need to put the tracker into bootloader mode.
+ Most of the time it means pressing the boot button on the board before the flashing process starts.
+ If the flashing process timeout at the begining of the flashing it probably means that the tracker was not in bootloader mode
+ Please refer to the flashing instructions of your board to know how to turn on the boatloader mode
+
+
+
+firmware_tool-flash_method_ota-devices = Detected OTA Devices:
+firmware_tool-flash_method_ota-no_devices = There are no boards that can be updated using OTA, make sure you selected the correct board type
+firmware_tool-flash_method_serial-wifi = Wi-Fi Credentials:
+firmware_tool-flash_method_serial-devices-label = Detected Serial Devices:
+firmware_tool-flash_method_serial-devices-placeholder = Select a serial device
+firmware_tool-flash_method_serial-no_devices = There are no compatible serial devices detected, make sure the tracker is plugged in
+
+firmware_tool-build_step = Building
+firmware_tool-build_step-description =
+ The firmware is building, please wait
+
+firmware_tool-flashing_step = Flashing
+firmware_tool-flashing_step-description =
+ Your trackers are flashing, please follow the instructions on the screen
+firmware_tool-flashing_step-warning = Do not unplug or restart the tracker during the upload process unless told to, it may make your board unusable
+firmware_tool-flashing_step-flash_more = Flash more trackers
+firmware_tool-flashing_step-exit = Exit
+
+## firmware tool build status
+firmware_tool-build-CREATING_BUILD_FOLDER = Creating the build folder
+firmware_tool-build-DOWNLOADING_FIRMWARE = Downloading the firmware
+firmware_tool-build-EXTRACTING_FIRMWARE = Extracting the firmware
+firmware_tool-build-SETTING_UP_DEFINES = Configuring the defines
+firmware_tool-build-BUILDING = Building the firmware
+firmware_tool-build-SAVING = Saving the build
+firmware_tool-build-DONE = Build Complete
+firmware_tool-build-ERROR = Unable to build the firmware
+
+## Firmware update status
+firmware_update-status-DOWNLOADING = Downloading the firmware
+firmware_update-status-NEED_MANUAL_REBOOT = Waiting for the user to reboot the tracker
+firmware_update-status-AUTHENTICATING = Authenticating with the mcu
+firmware_update-status-UPLOADING = Uploading the firmware
+firmware_update-status-SYNCING_WITH_MCU = Syncing with the mcu
+firmware_update-status-REBOOTING = Rebooting the tracker
+firmware_update-status-PROVISIONING = Setting Wi-Fi credentials
+firmware_update-status-DONE = Update complete!
+firmware_update-status-ERROR_DEVICE_NOT_FOUND = Could not find the device
+firmware_update-status-ERROR_TIMEOUT = The update process timed out
+firmware_update-status-ERROR_DOWNLOAD_FAILED = Could not download the firmware
+firmware_update-status-ERROR_AUTHENTICATION_FAILED = Could not authenticate with the mcu
+firmware_update-status-ERROR_UPLOAD_FAILED = Could not upload the firmware
+firmware_update-status-ERROR_PROVISIONING_FAILED = Could not set the Wi-Fi credentials
+firmware_update-status-ERROR_UNSUPPORTED_METHOD = The update method is not supported
+firmware_update-status-ERROR_UNKNOWN = Unknown error
+
+## Dedicated Firmware Update Page
+firmware_update-title = Firmware update
+firmware_update-devices = Available Devices
+firmware_update-devices-description = Please select the trackers you want to update to the latest version of SlimeVR firmware
+firmware_update-no_devices = Plase make sure that the trackers you want to update are ON and connected to the Wi-Fi!
+firmware_update-changelog-title = Updating to {$version}
+firmware_update-looking_for_devices = Looking for devices to update...
+firmware_update-retry = Retry
+firmware_update-update = Update Selected Trackers
+
## Tray Menu
tray_menu-show = Show
tray_menu-hide = Hide
diff --git a/gui/public/images/R11_board_reset.webp b/gui/public/images/R11_board_reset.webp
new file mode 100644
index 0000000000..c4870d76bd
Binary files /dev/null and b/gui/public/images/R11_board_reset.webp differ
diff --git a/gui/public/images/R12_board_reset.webp b/gui/public/images/R12_board_reset.webp
new file mode 100644
index 0000000000..12aabb61b5
Binary files /dev/null and b/gui/public/images/R12_board_reset.webp differ
diff --git a/gui/public/images/R14_board_reset_sw.webp b/gui/public/images/R14_board_reset_sw.webp
new file mode 100644
index 0000000000..5c1d89a06d
Binary files /dev/null and b/gui/public/images/R14_board_reset_sw.webp differ
diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml
index 2f6985599f..dfda2bb448 100644
--- a/gui/src-tauri/Cargo.toml
+++ b/gui/src-tauri/Cargo.toml
@@ -53,6 +53,7 @@ rfd = { version = "0.15", features = ["gtk3"], default-features = false }
dirs-next = "2.0.0"
discord-sdk = "0.3.6"
tokio = { version = "1.37.0", features = ["time"] }
+itertools = "0.13.0"
[target.'cfg(windows)'.dependencies]
win32job = "1"
diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs
index 337ca37b59..fda7f594a1 100644
--- a/gui/src-tauri/src/main.rs
+++ b/gui/src-tauri/src/main.rs
@@ -1,4 +1,5 @@
#![cfg_attr(all(not(debug_assertions), windows), windows_subsystem = "windows")]
+use std::env;
use std::panic;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
@@ -68,88 +69,125 @@ fn main() -> Result<()> {
let tauri_context = tauri::generate_context!();
// Set up loggers and global handlers
- let _logger = {
- use flexi_logger::{
- Age, Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode,
- };
- use tauri::Error;
-
- // Based on https://docs.rs/tauri/2.0.0-alpha.10/src/tauri/path/desktop.rs.html#238-256
- #[cfg(target_os = "macos")]
- let path = dirs_next::home_dir().ok_or(Error::UnknownPath).map(|dir| {
- dir.join("Library/Logs")
- .join(&tauri_context.config().identifier)
- });
-
- #[cfg(not(target_os = "macos"))]
- let path = dirs_next::data_dir()
- .ok_or(Error::UnknownPath)
- .map(|dir| dir.join(&tauri_context.config().identifier).join("logs"));
-
- Logger::try_with_env_or_str("info")?
- .log_to_file(
- FileSpec::default().directory(path.expect("We need a log dir")),
- )
- .format_for_files(|w, now, record| {
- util::logger_format(w, now, record, false)
- })
- .format_for_stderr(|w, now, record| {
- util::logger_format(w, now, record, true)
- })
- .rotate(
- Criterion::Age(Age::Day),
- Naming::Timestamps,
- Cleanup::KeepLogFiles(2),
- )
- .duplicate_to_stderr(Duplicate::All)
- .write_mode(WriteMode::BufferAndFlush)
- .start()?
- };
+ let _logger = setup_logger(&tauri_context);
// Ensure child processes die when spawned on windows
// and then check for WebView2's existence
#[cfg(windows)]
- {
- use crate::util::webview2_exists;
- use win32job::{ExtendedLimitInfo, Job};
-
- let mut info = ExtendedLimitInfo::new();
- info.limit_kill_on_job_close();
- let job = Job::create_with_limit_info(&mut info).expect("Failed to create Job");
- job.assign_current_process()
- .expect("Failed to assign current process to Job");
-
- // We don't do anything with the job anymore, but we shouldn't drop it because that would
- // terminate our process tree. So we intentionally leak it instead.
- std::mem::forget(job);
-
- if !webview2_exists() {
- // This makes a dialog appear which let's you press Ok or Cancel
- // If you press Ok it will open the SlimeVR installer documentation
- use rfd::{
- MessageButtons, MessageDialog, MessageDialogResult, MessageLevel,
- };
+ setup_webview2()?;
- let confirm = MessageDialog::new()
- .set_title("SlimeVR")
- .set_description("Couldn't find WebView2 installed. You can install it with the SlimeVR installer")
- .set_buttons(MessageButtons::OkCancel)
- .set_level(MessageLevel::Error)
- .show();
- if confirm == MessageDialogResult::Ok {
- open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
- }
- return Ok(());
- }
- }
+ // Check for environment variables that can affect the server, and if so, warn in log and GUI
+ check_environment_variables();
// Spawn server process
let exit_flag = Arc::new(AtomicBool::new(false));
let backend = Arc::new(Mutex::new(Option::::None));
- let backend_termination = backend.clone();
- let run_path = get_launch_path(cli);
- let server_info = if let Some(p) = run_path {
+ let server_info = execute_server(cli)?;
+ let build_result = setup_tauri(
+ tauri_context,
+ server_info,
+ exit_flag.clone(),
+ backend.clone(),
+ );
+
+ tauri_build_result(build_result, exit_flag, backend);
+
+ Ok(())
+}
+
+fn setup_logger(context: &tauri::Context) -> Result {
+ use flexi_logger::{
+ Age, Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode,
+ };
+ use tauri::Error;
+
+ // Based on https://docs.rs/tauri/2.0.0-alpha.10/src/tauri/path/desktop.rs.html#238-256
+ #[cfg(target_os = "macos")]
+ let path = dirs_next::home_dir()
+ .ok_or(Error::UnknownPath)
+ .map(|dir| dir.join("Library/Logs").join(&context.config().identifier));
+
+ #[cfg(not(target_os = "macos"))]
+ let path = dirs_next::data_dir()
+ .ok_or(Error::UnknownPath)
+ .map(|dir| dir.join(&context.config().identifier).join("logs"));
+
+ Ok(Logger::try_with_env_or_str("info")?
+ .log_to_file(FileSpec::default().directory(path.expect("We need a log dir")))
+ .format_for_files(|w, now, record| util::logger_format(w, now, record, false))
+ .format_for_stderr(|w, now, record| util::logger_format(w, now, record, true))
+ .rotate(
+ Criterion::Age(Age::Day),
+ Naming::Timestamps,
+ Cleanup::KeepLogFiles(2),
+ )
+ .duplicate_to_stderr(Duplicate::All)
+ .write_mode(WriteMode::BufferAndFlush)
+ .start()?)
+}
+
+#[cfg(windows)]
+fn setup_webview2() -> Result<()> {
+ use crate::util::webview2_exists;
+ use win32job::{ExtendedLimitInfo, Job};
+
+ let mut info = ExtendedLimitInfo::new();
+ info.limit_kill_on_job_close();
+ let job = Job::create_with_limit_info(&mut info).expect("Failed to create Job");
+ job.assign_current_process()
+ .expect("Failed to assign current process to Job");
+
+ // We don't do anything with the job anymore, but we shouldn't drop it because that would
+ // terminate our process tree. So we intentionally leak it instead.
+ std::mem::forget(job);
+
+ if !webview2_exists() {
+ // This makes a dialog appear which let's you press Ok or Cancel
+ // If you press Ok it will open the SlimeVR installer documentation
+ use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
+
+ let confirm = MessageDialog::new()
+ .set_title("SlimeVR")
+ .set_description("Couldn't find WebView2 installed. You can install it with the SlimeVR installer")
+ .set_buttons(MessageButtons::OkCancel)
+ .set_level(MessageLevel::Error)
+ .show();
+ if confirm == MessageDialogResult::Ok {
+ open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
+ }
+ }
+ Ok(())
+}
+
+fn check_environment_variables() {
+ use itertools::Itertools;
+ const ENVS_TO_CHECK: &[&str] = &["_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"];
+ let checked_envs = ENVS_TO_CHECK
+ .into_iter()
+ .filter_map(|e| {
+ let Ok(data) = env::var(e) else {
+ return None;
+ };
+ log::warn!("{e} is set to: {data}");
+ Some(e)
+ })
+ .join(", ");
+
+ if !checked_envs.is_empty() {
+ rfd::MessageDialog::new()
+ .set_title("SlimeVR")
+ .set_description(format!("You have environment variables {} set, which may cause the SlimeVR Server to fail to launch properly.", checked_envs))
+ .set_level(rfd::MessageLevel::Warning)
+ .show();
+ }
+}
+
+fn execute_server(
+ cli: Cli,
+) -> Result