Compare commits
No commits in common. "main" and "legacy-python" have entirely different histories.
main
...
legacy-pyt
73 changed files with 1093 additions and 6084 deletions
291
.gitignore
vendored
291
.gitignore
vendored
|
@ -1,32 +1,281 @@
|
||||||
# ---> Go
|
# ---> Python
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# Byte-compiled / optimized / DLL files
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
__pycache__/
|
||||||
#
|
*.py[cod]
|
||||||
# Binaries for programs and plugins
|
*$py.class
|
||||||
*.exe
|
|
||||||
*.exe~
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# ---> C
|
||||||
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
|
# Object files
|
||||||
|
*.o
|
||||||
|
*.ko
|
||||||
|
*.obj
|
||||||
|
*.elf
|
||||||
|
|
||||||
|
# Linker output
|
||||||
|
*.ilk
|
||||||
|
*.map
|
||||||
|
*.exp
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
*.lib
|
||||||
|
*.a
|
||||||
|
*.la
|
||||||
|
*.lo
|
||||||
|
|
||||||
|
# Shared objects (inc. Windows DLLs)
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
|
*.so.*
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Executables
|
||||||
*.test
|
*.exe
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
*.out
|
||||||
|
*.app
|
||||||
|
*.i*86
|
||||||
|
*.x86_64
|
||||||
|
*.hex
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Debug files
|
||||||
# vendor/
|
*.dSYM/
|
||||||
|
*.su
|
||||||
|
*.idb
|
||||||
|
*.pdb
|
||||||
|
|
||||||
# Go workspace file
|
# Kernel Module Compile Results
|
||||||
go.work
|
*.mod*
|
||||||
go.work.sum
|
*.cmd
|
||||||
|
.tmp_versions/
|
||||||
|
modules.order
|
||||||
|
Module.symvers
|
||||||
|
Mkfile.old
|
||||||
|
dkms.conf
|
||||||
|
|
||||||
# env file
|
# ---> C++
|
||||||
.env
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
|
# Compiled Object files
|
||||||
|
*.slo
|
||||||
|
*.lo
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Compiled Dynamic libraries
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Fortran module files
|
||||||
|
*.mod
|
||||||
|
*.smod
|
||||||
|
|
||||||
|
# Compiled Static libraries
|
||||||
|
*.lai
|
||||||
|
*.la
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.app
|
||||||
|
|
||||||
# ---> UnrealXR
|
# ---> UnrealXR
|
||||||
# development dirs
|
|
||||||
data
|
data
|
||||||
# artifacts
|
drivers
|
||||||
uxr
|
|
||||||
|
|
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[submodule "modules/raylib-python-cffi"]
|
||||||
|
path = modules/raylib-python-cffi
|
||||||
|
url = https://git.terah.dev/UnrealXR/raylib-python-cffi.git
|
||||||
|
[submodule "modules/evdi"]
|
||||||
|
path = modules/evdi
|
||||||
|
url = https://github.com/DisplayLink/evdi.git
|
||||||
|
[submodule "modules/nreal-driver"]
|
||||||
|
path = modules/nreal-driver
|
||||||
|
url = https://git.terah.dev/UnrealXR/nrealAirLinuxDriver.git
|
|
@ -1,13 +0,0 @@
|
||||||
// Folder-specific settings
|
|
||||||
//
|
|
||||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
|
||||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
|
||||||
{
|
|
||||||
"lsp": {
|
|
||||||
"gopls": {
|
|
||||||
"initialization_options": {
|
|
||||||
"buildFlags": ["-tags=xreal,noaudio,drm,drm_leasing,drm_disable_input"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
23
HACKING.md
23
HACKING.md
|
@ -1,23 +0,0 @@
|
||||||
# UnrealXR Development Documentation
|
|
||||||
|
|
||||||
## Dummies and You
|
|
||||||
|
|
||||||
The dummy/development build generally isn't recommended for use if you're developing headset or OS drivers. It's intended for development on the 3D environment itself only (ie. virtual display positioning, widgets, etc.)
|
|
||||||
|
|
||||||
To use it, all you need to do is run the Makefile with the `dev` target:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make dev
|
|
||||||
```
|
|
||||||
|
|
||||||
In the code, this mode is typically referred to as "dummy mode" (`dummy_ar.go`) or "fake mode" (ie. `patching_tools_fake_patching.go`).
|
|
||||||
|
|
||||||
To control the X11 window, you can use the arrow keys to move the camera around. You **cannot** use the mouse due to the nature of this application. If you use the mouse for this, it would make development really difficult as you're using virtual displays and you need your mouse to interact with the desktop.
|
|
||||||
|
|
||||||
If you're adding new headset drivers, but you still want to test in a window, you still can. It's not the dummy mode, but you can compile UnrealXR without the DRM flags (`drm drm_leasing drm_disable_input`) manually. This may differ in the future, but for now, the command is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd app; go build -v -tags "headset_driver_goes_here noaudio" -o ../uxr .; cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `headset_driver_goes_here` with the name of your headset driver. For example, `xreal` is the Xreal driver.
|
|
16
Makefile
16
Makefile
|
@ -1,16 +0,0 @@
|
||||||
APP_DIR := ./app
|
|
||||||
OUTPUT := ./uxr
|
|
||||||
TAGS := xreal noaudio drm drm_leasing drm_disable_input
|
|
||||||
|
|
||||||
.PHONY: all build clean
|
|
||||||
|
|
||||||
all: build
|
|
||||||
|
|
||||||
build:
|
|
||||||
cd $(APP_DIR) && go build -v -tags '$(TAGS)' -o ../$(OUTPUT) .
|
|
||||||
|
|
||||||
dev:
|
|
||||||
cd $(APP_DIR) && go build -v -tags 'noaudio dummy_ar fake_edid_patching' -o ../$(OUTPUT) .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(OUTPUT)
|
|
43
README.md
43
README.md
|
@ -1,8 +1,4 @@
|
||||||
# UnrealXR
|
# UnrealXR
|
||||||
[](https://git.terah.dev/UnrealXR/unrealxr/actions)
|
|
||||||
[](https://godoc.org/git.terah.dev/UnrealXR/unrealxr)
|
|
||||||
[](https://goreportcard.com/report/git.terah.dev/UnrealXR/unrealxr/app)
|
|
||||||
[](https://git.terah.dev/imterah/goevdi/src/branch/main/app/LICENSE)
|
|
||||||
|
|
||||||
UnrealXR is a spatial multi-display renderer for the Xreal line of devices, enabling immersive, simultaneous viewing of multiple desktops and applications in 3D space.
|
UnrealXR is a spatial multi-display renderer for the Xreal line of devices, enabling immersive, simultaneous viewing of multiple desktops and applications in 3D space.
|
||||||
|
|
||||||
|
@ -26,14 +22,41 @@ UnrealXR is a spatial multi-display renderer for the Xreal line of devices, enab
|
||||||
|
|
||||||
Before anything, this depends on the `evdi` Linux kernel module. This is packaged in Debian-based distributions as `evdi-dkms`. If you already have DisplayLink drivers installed for their devices, you likely do not need to do this step. After installing this, please reboot your computer.
|
Before anything, this depends on the `evdi` Linux kernel module. This is packaged in Debian-based distributions as `evdi-dkms`. If you already have DisplayLink drivers installed for their devices, you likely do not need to do this step. After installing this, please reboot your computer.
|
||||||
|
|
||||||
You'll need to install build dependencies after this. For Debian-based distros, the dependency list should be: `git golang build-essential libdrm libdrm-dev linux-headers-$(uname -r) cmake clang-tools pkg-config libwayland-client++1 libgl1-mesa-dev libglu1-mesa-dev libwayland-dev libxkbcommon-dev libhidapi-dev libjson-c-dev libudev-dev libusb-1.0-0 libusb-1.0-0-dev libopencv-dev`
|
First, install the runtime dependencies. For Debian-based distros, the dependency list should be: `python3 python3-pip`
|
||||||
|
|
||||||
If you're using Nix/NixOS, all you need to do is use `nix-shell` to enter the development environment.
|
You'll need to install build dependencies after this. For Debian-based distros, the dependency list should be: `git build-essential libdrm libdrm-dev linux-headers-$(uname -r) cmake clang-tools pkg-config libwayland-client++1 libgl1-mesa-dev libglu1-mesa-dev libwayland-dev libxkbcommon-dev libhidapi-dev libjson-c-dev libudev-dev libusb-1.0-0 libusb-1.0-0-dev libopencv-dev`
|
||||||
|
|
||||||
## Building
|
If you're using Nix/NixOS, use the `nix-shell` to enter the development environment.
|
||||||
|
|
||||||
Just run `make` in the root directory.
|
After that, create a virtual environment for Python (done automatically in Nix): `python3 -m venv .venv`. Then, activate it: `source .venv/bin/activate`
|
||||||
|
|
||||||
## Development Guide
|
Finally, install the Python dependencies: `pip install -r requirements.txt`
|
||||||
|
|
||||||
See [HACKING.md](https://git.terah.dev/UnrealXR/unrealxr/src/branch/main/HACKING.md).
|
From there, you need to follow all the below steps if applicable to your current platform.
|
||||||
|
.
|
||||||
|
### Building patched `raylib` and `PyRay` (all platforms)
|
||||||
|
|
||||||
|
1. First, you need to build the native version of raylib. To do that, go inside the `modules/raylib-python-cffi/raylib-c` directory.
|
||||||
|
2. Then, make the build directories and go into them: `mkdir -p build/out; cd build`
|
||||||
|
3. Configure raylib: `cmake -DCUSTOMIZE_BUILD=ON -DSUPPORT_FILEFORMAT_JPG=ON -DSUPPORT_FILEFORMAT_FLAC=ON -DWITH_PIC=ON -DCMAKE_BUILD_TYPE=Release -DPLATFORM=DRM -DENABLE_WAYLAND_DRM_LEASING=ON -DSUPPORT_CLIPBOARD_IMAGE=ON -DBUILD_EXAMPLES=OFF -DSUPPORT_SSH_KEYBOARD_RPI=OFF -DDISABLE_EVDEV_INPUT=ON -DCMAKE_INSTALL_PREFIX:PATH=$PWD/out ..`
|
||||||
|
4. Finally, build and install raylib: `make install -j$(nproc)`
|
||||||
|
5. After that, you need to build the Python bindings. To do that, go to the `modules/raylib-python-cffi` directory. Assuming you did everything correctly, you should be able to go 2 directories back (`../..`) to get there.
|
||||||
|
6. If you're on normal Linux and are not using Nix, do this command to build the package: `PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$PWD/raylib-c/build/out/lib64/pkgconfig/" ENABLE_WAYLAND_DRM_LEASING=YES RAYLIB_PLATFORM=DRM python3 setup.py bdist_wheel`
|
||||||
|
7. If you are using Nix/NixOS, do this command to build the package: `PKG_CONFIG_PATH_FOR_TARGET="$PKG_CONFIG_PATH_FOR_TARGET:$PWD/raylib-c/build/out/lib64/pkgconfig/" ENABLE_WAYLAND_DRM_LEASING=YES RAYLIB_PLATFORM=DRM python3 setup.py bdist_wheel`
|
||||||
|
8. Finally, install the package: `pip install dist/*.whl`
|
||||||
|
|
||||||
|
### Building `PyEvdi` (Linux)
|
||||||
|
|
||||||
|
1. First, build the original libevdi. To start that, go inside the `modules/evdi/library` directory.
|
||||||
|
2. Then, build `libevdi`: `make -j$(nproc)`
|
||||||
|
3. After that, you need to build the Python bindings. To do that, go to the `modules/evdi/pyevdi` directory. Assuming you did everything correctly, you should be able to go a directory back (`../pyevdi`) to get there.
|
||||||
|
4. Then, build `PyEvdi`: `make -j$(nproc); make install`
|
||||||
|
|
||||||
|
### Building `nreal-driver` (all platforms)
|
||||||
|
|
||||||
|
1. First, create the `drivers` directory in the project root.
|
||||||
|
2. Then, go inside the `modules/nreal-driver` directory.
|
||||||
|
3. After that, make the build directories and go into them: `mkdir -p build/out; cd build`
|
||||||
|
4. Configure nreal-driver: `cmake -DCMAKE_BUILD_TYPE=Release ..`
|
||||||
|
5. Build the driver: `make -j$(nproc)`
|
||||||
|
6. Move the driver to the correct directory: `mv xrealAirLinuxDriver ../../../drivers/xreal_ar_driver`
|
||||||
|
|
11
app/LICENSE
11
app/LICENSE
|
@ -1,11 +0,0 @@
|
||||||
Copyright (c) 2025 imterah.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
@ -1,88 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import _ "embed"
|
|
||||||
|
|
||||||
//go:embed default_config.yml
|
|
||||||
var InitialConfig []byte
|
|
||||||
|
|
||||||
type DisplayConfig struct {
|
|
||||||
Angle *int `yaml:"angle"`
|
|
||||||
FOV *int `yaml:"fov"`
|
|
||||||
Spacing *float32 `yaml:"spacing"`
|
|
||||||
RadiusMultiplier *float32 `yaml:"circle_radius_multiplier"`
|
|
||||||
UseCircularSpacing *bool `yaml:"use_circular_spacing"`
|
|
||||||
Count *int `yaml:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppOverrides struct {
|
|
||||||
AllowUnsupportedDevices *bool `yaml:"allow_unsupported_devices"`
|
|
||||||
OverrideWidth *int `yaml:"width"`
|
|
||||||
OverrideHeight *int `yaml:"height"`
|
|
||||||
OverrideRefreshRate *int `yaml:"refresh_rate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
DisplayConfig DisplayConfig `yaml:"display"`
|
|
||||||
Overrides AppOverrides `yaml:"overrides"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPtrToInt(int int) *int {
|
|
||||||
return &int
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPtrToFloat32(float32 float32) *float32 {
|
|
||||||
return &float32
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPtrToBool(bool bool) *bool {
|
|
||||||
return &bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultConfig = &Config{
|
|
||||||
DisplayConfig: DisplayConfig{
|
|
||||||
Angle: getPtrToInt(45),
|
|
||||||
FOV: getPtrToInt(45),
|
|
||||||
Spacing: getPtrToFloat32(0.5),
|
|
||||||
RadiusMultiplier: getPtrToFloat32(2),
|
|
||||||
UseCircularSpacing: getPtrToBool(true),
|
|
||||||
Count: getPtrToInt(3),
|
|
||||||
},
|
|
||||||
Overrides: AppOverrides{
|
|
||||||
AllowUnsupportedDevices: getPtrToBool(false),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitializePotentiallyMissingConfigValues(config *Config) {
|
|
||||||
// TODO: is there a better way to do this?
|
|
||||||
if config.DisplayConfig.Angle == nil {
|
|
||||||
config.DisplayConfig.Angle = DefaultConfig.DisplayConfig.Angle
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.DisplayConfig.FOV == nil {
|
|
||||||
config.DisplayConfig.FOV = DefaultConfig.DisplayConfig.FOV
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.DisplayConfig.Spacing == nil {
|
|
||||||
config.DisplayConfig.Spacing = DefaultConfig.DisplayConfig.Spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.DisplayConfig.Count == nil {
|
|
||||||
config.DisplayConfig.Count = DefaultConfig.DisplayConfig.Count
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Overrides.AllowUnsupportedDevices == nil {
|
|
||||||
config.Overrides.AllowUnsupportedDevices = DefaultConfig.Overrides.AllowUnsupportedDevices
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Overrides.OverrideWidth == nil {
|
|
||||||
config.Overrides.OverrideWidth = DefaultConfig.Overrides.OverrideWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Overrides.OverrideHeight == nil {
|
|
||||||
config.Overrides.OverrideHeight = DefaultConfig.Overrides.OverrideHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Overrides.OverrideRefreshRate == nil {
|
|
||||||
config.Overrides.OverrideRefreshRate = DefaultConfig.Overrides.OverrideRefreshRate
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
# __ __ ___ __ ____
|
|
||||||
# / / / /___ ________ ____ _/ / |/ // __ \
|
|
||||||
# / / / / __ \/ ___/ _ \/ __ `/ /| // /_/ /
|
|
||||||
# / /_/ / / / / / / __/ /_/ / // |/ _, _/
|
|
||||||
# \____/_/ /_/_/ \___/\__,_/_//_/|_/_/ |_|
|
|
||||||
#
|
|
||||||
# Welcome to UnrealXR! This is the configuration file to configure various UnrealXR settings.
|
|
||||||
|
|
||||||
display:
|
|
||||||
angle: 45 # Angle of the virtual displays
|
|
||||||
fov: 45 # FOV of the 3D camera
|
|
||||||
spacing: 0.5 # Raw spacing between virtual displays. Does not use circles in the layout. Purely flat plane.
|
|
||||||
circle_radius_multiplier: 2 # Multiplier for the radius of the circle used to calculate the spacing between virtual displays. "Rounded" plane of sorts.
|
|
||||||
use_circular_spacing: true # If true, uses a circular layout for the virtual displays.
|
|
||||||
count: 3 # Count of virtual displays
|
|
||||||
overrides:
|
|
||||||
allow_unsupported_devices: false # If true, allows unsupported devices to be used as long as they're a compatible vendor (Xreal)
|
|
||||||
# width: 1920 # If set, overrides the width of the screen and virtual displays. This does not do any overclocking.
|
|
||||||
# height: 1080 # If set, overrides the height of the screen and virtual displays. This does not do any overclocking.
|
|
||||||
# refresh_rate: 120 # If set, overrides the refresh rate of the screen and the maximum refresh rate of the virtual displays. This does not do any overclocking.
|
|
Binary file not shown.
|
@ -1,72 +0,0 @@
|
||||||
package edidtools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
edidparser "github.com/anoopengineer/edidparser/edid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseEDID(rawEDIDFile []byte, allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
|
||||||
parsedEDID, err := edidparser.NewEdid(rawEDIDFile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse EDID file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for manufacturer, manufacturerSupportedDevices := range QuirksRegistry {
|
|
||||||
if parsedEDID.ManufacturerId == manufacturer {
|
|
||||||
if deviceQuirks, ok := manufacturerSupportedDevices[parsedEDID.MonitorName]; ok || allowUnsupportedDevices {
|
|
||||||
maxWidth := 0
|
|
||||||
maxHeight := 0
|
|
||||||
maxRefreshRate := 0
|
|
||||||
|
|
||||||
for _, resolution := range parsedEDID.DetailedTimingDescriptors {
|
|
||||||
if int(resolution.HorizontalActive) > maxWidth && int(resolution.VerticalActive) > maxHeight {
|
|
||||||
maxWidth = int(resolution.HorizontalActive)
|
|
||||||
maxHeight = int(resolution.VerticalActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert pixel clock to refresh rate
|
|
||||||
// Refresh Rate = Pixel Clock / ((Horizontal Active + Horizontal Blanking) * (Vertical Active + Vertical Blanking))
|
|
||||||
hTotal := int(resolution.HorizontalActive + resolution.HorizontalBlanking)
|
|
||||||
vTotal := int(resolution.VerticalActive + resolution.VerticalBlanking)
|
|
||||||
refreshRate := int(int(resolution.PixelClock*1000) / (hTotal * vTotal))
|
|
||||||
|
|
||||||
if refreshRate > maxRefreshRate {
|
|
||||||
maxRefreshRate = refreshRate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxWidth == 0 || maxHeight == 0 {
|
|
||||||
if deviceQuirks.MaxWidth == 0 || deviceQuirks.MaxHeight == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to determine maximum resolution for monitor '%s'", parsedEDID.MonitorName)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxWidth = deviceQuirks.MaxWidth
|
|
||||||
maxHeight = deviceQuirks.MaxHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxRefreshRate == 0 {
|
|
||||||
if deviceQuirks.MaxRefreshRate == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to determine maximum refresh rate for monitor '%s'", parsedEDID.MonitorName)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxRefreshRate = deviceQuirks.MaxRefreshRate
|
|
||||||
}
|
|
||||||
|
|
||||||
displayMetadata := &DisplayMetadata{
|
|
||||||
EDID: rawEDIDFile,
|
|
||||||
DeviceVendor: parsedEDID.ManufacturerId,
|
|
||||||
DeviceQuirks: deviceQuirks,
|
|
||||||
MaxWidth: maxWidth,
|
|
||||||
MaxHeight: maxHeight,
|
|
||||||
MaxRefreshRate: maxRefreshRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayMetadata, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to match manufacturer for monitor vendor: '%s'", parsedEDID.ManufacturerId)
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
//go:build fake_edid_patching
|
|
||||||
// +build fake_edid_patching
|
|
||||||
|
|
||||||
package edidtools
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed bin/xreal-air-edid.bin
|
|
||||||
var edidFirmware []byte
|
|
||||||
|
|
||||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
|
||||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
|
||||||
log.Warn("Not actually fetching EDID firmware in fake patching build -- using embedded firmware")
|
|
||||||
parsedEDID, err := ParseEDID(edidFirmware, allowUnsupportedDevices)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse embedded EDID firmware: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedEDID.DeviceQuirks.ZVectorDisabled = false
|
|
||||||
parsedEDID.DeviceQuirks.SensorInitDelay = 0
|
|
||||||
parsedEDID.DeviceQuirks.UsesMouseMovement = true
|
|
||||||
|
|
||||||
return parsedEDID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads custom firmware for a supported XR glass device
|
|
||||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
|
||||||
log.Warn("Not actually patching EDID firmware in fake patching build -- ignoring")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unloads custom firmware for a supported XR glass device
|
|
||||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
|
||||||
log.Warn("Not actually unloading EDID firmware in fake patching build -- ignoring")
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
//go:build linux && !fake_edid_patching
|
|
||||||
// +build linux,!fake_edid_patching
|
|
||||||
|
|
||||||
package edidtools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
|
||||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
|
||||||
// Implementation goes here
|
|
||||||
pciDeviceCommand, err := exec.Command("lspci").Output()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute lspci command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pciDevices := strings.Split(string(pciDeviceCommand), "\n")
|
|
||||||
pciDevices = pciDevices[:len(pciDevices)-1]
|
|
||||||
|
|
||||||
vgaDevices := []string{}
|
|
||||||
|
|
||||||
for _, pciDevice := range pciDevices {
|
|
||||||
if strings.Contains(pciDevice, "VGA compatible controller:") {
|
|
||||||
vgaDevices = append(vgaDevices, pciDevice[:strings.Index(pciDevice, " ")])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, vgaDevice := range vgaDevices {
|
|
||||||
cardDevices, err := os.ReadDir("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read directory for device '%s': %w", vgaDevice, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cardDevice := range cardDevices {
|
|
||||||
if !strings.Contains(cardDevice.Name(), "card") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
monitors, err := os.ReadDir("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/" + cardDevice.Name())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read directory for card device '%s': %w", cardDevice.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, monitor := range monitors {
|
|
||||||
if !strings.Contains(monitor.Name(), cardDevice.Name()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
rawEDIDFile, err := os.ReadFile("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/" + cardDevice.Name() + "/" + monitor.Name() + "/edid")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read EDID file for monitor '%s': %w", monitor.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rawEDIDFile) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedEDID, err := ParseEDID(rawEDIDFile, allowUnsupportedDevices)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if !strings.HasPrefix(err.Error(), "failed to match manufacturer for monitor vendor") {
|
|
||||||
log.Warnf("Failed to parse EDID for monitor '%s': %s", monitor.Name(), err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parsedEDID.LinuxDRMCard = cardDevice.Name()
|
|
||||||
parsedEDID.LinuxDRMConnector = strings.Replace(monitor.Name(), cardDevice.Name()+"-", "", 1)
|
|
||||||
|
|
||||||
return parsedEDID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("could not find supported device! Check if the XR device is plugged in. If it is plugged in and working correctly, check the README or open an issue.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads custom firmware for a supported XR glass device
|
|
||||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
|
||||||
if displayMetadata.LinuxDRMCard == "" || displayMetadata.LinuxDRMConnector == "" {
|
|
||||||
return fmt.Errorf("missing Linux DRM card or connector information")
|
|
||||||
}
|
|
||||||
|
|
||||||
drmFile, err := os.OpenFile("/sys/kernel/debug/dri/"+strings.Replace(displayMetadata.LinuxDRMCard, "card", "", 1)+"/"+displayMetadata.LinuxDRMConnector+"/edid_override", os.O_WRONLY, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open EDID override file for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer drmFile.Close()
|
|
||||||
|
|
||||||
if _, err := drmFile.Write(edidFirmware); err != nil {
|
|
||||||
return fmt.Errorf("failed to write EDID firmware for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unloads custom firmware for a supported XR glass device
|
|
||||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
|
||||||
if displayMetadata.LinuxDRMCard == "" || displayMetadata.LinuxDRMConnector == "" {
|
|
||||||
return fmt.Errorf("missing Linux DRM card or connector information")
|
|
||||||
}
|
|
||||||
|
|
||||||
drmFile, err := os.OpenFile("/sys/kernel/debug/dri/"+strings.Replace(displayMetadata.LinuxDRMCard, "card", "", 1)+"/"+displayMetadata.LinuxDRMConnector+"/edid_override", os.O_WRONLY, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open EDID override file for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer drmFile.Close()
|
|
||||||
|
|
||||||
if _, err := drmFile.Write([]byte("reset")); err != nil {
|
|
||||||
return fmt.Errorf("failed to unload EDID firmware for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
//go:build darwin && !fake_edid_patching
|
|
||||||
// +build darwin,!fake_edid_patching
|
|
||||||
|
|
||||||
package edidtools
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
|
||||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
|
||||||
return nil, fmt.Errorf("automatic fetching of EDID data is not supported on macOS")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads custom firmware for a supported XR glass device
|
|
||||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
|
||||||
return fmt.Errorf("loading custom EDID firmware is not supported on macOS")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unloads custom firmware for a supported XR glass device
|
|
||||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
|
||||||
return fmt.Errorf("unloading custom EDID firmware is not supported on macOS")
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
//go:build windows && !fake_edid_patching
|
|
||||||
// +build windows,!fake_edid_patching
|
|
||||||
|
|
||||||
package edidtools
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
|
||||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
|
||||||
return nil, fmt.Errorf("automatic fetching of EDID data is not supported on Windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads custom firmware for a supported XR glass device
|
|
||||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
|
||||||
return fmt.Errorf("loading custom EDID firmware is not supported on Windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unloads custom firmware for a supported XR glass device
|
|
||||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
|
||||||
return fmt.Errorf("unloading custom EDID firmware is not supported on Windows")
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package edidtools
|
|
||||||
|
|
||||||
// Vendor and devices names sourced from "https://uefi.org/uefi-pnp-export"
|
|
||||||
var QuirksRegistry = map[string]map[string]DisplayQuirks{
|
|
||||||
"MRG": {
|
|
||||||
"Air": {
|
|
||||||
MaxWidth: 1920,
|
|
||||||
MaxHeight: 1080,
|
|
||||||
MaxRefreshRate: 120,
|
|
||||||
SensorInitDelay: 10,
|
|
||||||
ZVectorDisabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package edidtools
|
|
||||||
|
|
||||||
type DisplayQuirks struct {
|
|
||||||
MaxWidth int
|
|
||||||
MaxHeight int
|
|
||||||
MaxRefreshRate int
|
|
||||||
SensorInitDelay int
|
|
||||||
ZVectorDisabled bool
|
|
||||||
UsesMouseMovement bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayMetadata struct {
|
|
||||||
EDID []byte
|
|
||||||
DeviceVendor string
|
|
||||||
DeviceQuirks DisplayQuirks
|
|
||||||
MaxWidth int
|
|
||||||
MaxHeight int
|
|
||||||
MaxRefreshRate int
|
|
||||||
LinuxDRMCard string
|
|
||||||
LinuxDRMConnector string
|
|
||||||
}
|
|
40
app/go.mod
40
app/go.mod
|
@ -1,40 +0,0 @@
|
||||||
module git.terah.dev/UnrealXR/unrealxr/app
|
|
||||||
|
|
||||||
go 1.24.3
|
|
||||||
|
|
||||||
replace git.terah.dev/UnrealXR/unrealxr/ardriver => ../ardriver
|
|
||||||
|
|
||||||
replace git.terah.dev/UnrealXR/unrealxr/edidpatcher => ../edidpatcher
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1
|
|
||||||
git.terah.dev/UnrealXR/unrealxr/ardriver v0.0.0-00010101000000-000000000000
|
|
||||||
git.terah.dev/UnrealXR/unrealxr/edidpatcher v0.0.0-00010101000000-000000000000
|
|
||||||
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10
|
|
||||||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0
|
|
||||||
github.com/charmbracelet/log v0.4.2
|
|
||||||
github.com/goccy/go-yaml v1.18.0
|
|
||||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
|
|
||||||
github.com/tebeka/atexit v0.3.0
|
|
||||||
github.com/urfave/cli/v3 v3.3.8
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
|
||||||
)
|
|
65
app/go.sum
65
app/go.sum
|
@ -1,65 +0,0 @@
|
||||||
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1 h1:cO/veKc8mon3Zlueq1sEc047B04fk/hoBJSn+btMLDI=
|
|
||||||
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1/go.mod h1:ZRirF2UuVWSbl2ux7oyHwXcinni9msejCvtIsXbT8yY=
|
|
||||||
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10 h1:M+Wja0b6Ks3mZXMGoBs3KZXsKRRWolUelITLQ/kaIFw=
|
|
||||||
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10/go.mod h1:7zdodqq+tECNHTljD5l7x0PvcGglLCa+mD2eQj0vkEg=
|
|
||||||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 h1:rTfysyBCL7LPbq9GFpQbllvKT8vEI93lQUwksMMxHMI=
|
|
||||||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0/go.mod h1:fEt61NePh3ZMxA+g3iC4CaGzY9lEsHRUkYJY2x0lBAw=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
|
|
||||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tebeka/atexit v0.3.0 h1:jleL99H7Ywt80oJKR+VWmJNnezcCOG0CuzcN3CIpsdI=
|
|
||||||
github.com/tebeka/atexit v0.3.0/go.mod h1:WJmSUSmMT7WoR7etUOaGBVXk+f5/ZJ+67qwuedq7Fbs=
|
|
||||||
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
|
|
||||||
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
220
app/main.go
220
app/main.go
|
@ -1,220 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
libconfig "git.terah.dev/UnrealXR/unrealxr/app/config"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/app/edidtools"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/app/renderer"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/edidpatcher"
|
|
||||||
"git.terah.dev/imterah/goevdi/libevdi"
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
"github.com/kirsle/configdir"
|
|
||||||
"github.com/tebeka/atexit"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
|
|
||||||
rl "git.terah.dev/UnrealXR/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mainEntrypoint(context.Context, *cli.Command) error {
|
|
||||||
// Allow for clean exits
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-c
|
|
||||||
log.Info("Exiting...")
|
|
||||||
atexit.Exit(1)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// TODO: add built-in privesc
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
return fmt.Errorf("this program must be run as root")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Initializing UnrealXR")
|
|
||||||
|
|
||||||
// Allow for overriding the config directory
|
|
||||||
configDir := os.Getenv("UNREALXR_CONFIG_PATH")
|
|
||||||
|
|
||||||
if configDir == "" {
|
|
||||||
configDir = configdir.LocalConfig("unrealxr")
|
|
||||||
err := configdir.MakePath(configDir)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to ensure config directory exists: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := os.Stat(path.Join(configDir, "config.yml"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Creating default config file")
|
|
||||||
err := os.WriteFile(path.Join(configDir, "config.yml"), libconfig.InitialConfig, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create initial config file: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and parse the config file
|
|
||||||
configBytes, err := os.ReadFile(path.Join(configDir, "config.yml"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &libconfig.Config{}
|
|
||||||
err = yaml.Unmarshal(configBytes, config)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libconfig.InitializePotentiallyMissingConfigValues(config)
|
|
||||||
log.Debug("Attempting to read display EDID file and fetch metadata")
|
|
||||||
|
|
||||||
displayMetadata, err := edidtools.FetchXRGlassEDID(*config.Overrides.AllowUnsupportedDevices)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch EDID or get metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("Got EDID file and metadata")
|
|
||||||
log.Debug("Patching EDID firmware to be specialized")
|
|
||||||
|
|
||||||
patchedFirmware, err := edidpatcher.PatchEDIDToBeSpecialized(displayMetadata.EDID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to patch EDID firmware: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Uploading patched EDID firmware")
|
|
||||||
err = edidtools.LoadCustomEDIDFirmware(displayMetadata, patchedFirmware)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to upload patched EDID firmware: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
atexit.Register(func() {
|
|
||||||
err := edidtools.UnloadCustomEDIDFirmware(displayMetadata)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to unload custom EDID firmware: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Please unplug and plug in your XR device to restore it back to normal settings.")
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Print("Press the Enter key to continue loading after you unplug and plug in your XR device.")
|
|
||||||
bufio.NewReader(os.Stdin).ReadBytes('\n') // Wait for Enter key press before continuing
|
|
||||||
|
|
||||||
log.Info("Initializing XR headset")
|
|
||||||
rl.SetTargetFPS(int32(displayMetadata.MaxRefreshRate))
|
|
||||||
rl.InitWindow(int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), "UnrealXR")
|
|
||||||
|
|
||||||
atexit.Register(func() {
|
|
||||||
rl.CloseWindow()
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Info("Initializing virtual displays")
|
|
||||||
|
|
||||||
libevdi.SetupLogger(&libevdi.EvdiLogger{
|
|
||||||
Log: func(msg string) {
|
|
||||||
log.Debugf("EVDI: %s", msg)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
evdiCards := make([]*renderer.EvdiDisplayMetadata, *config.DisplayConfig.Count)
|
|
||||||
|
|
||||||
for currentDisplay := range *config.DisplayConfig.Count {
|
|
||||||
openedDevice, err := libevdi.Open(nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to open EVDI device: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
openedDevice.Connect(displayMetadata.EDID, uint(displayMetadata.MaxWidth), uint(displayMetadata.MaxHeight), uint(displayMetadata.MaxRefreshRate))
|
|
||||||
|
|
||||||
atexit.Register(func() {
|
|
||||||
openedDevice.Disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
displayRect := &libevdi.EvdiDisplayRect{
|
|
||||||
X1: 0,
|
|
||||||
Y1: 0,
|
|
||||||
X2: displayMetadata.MaxWidth,
|
|
||||||
Y2: displayMetadata.MaxHeight,
|
|
||||||
}
|
|
||||||
|
|
||||||
displayBuffer, err := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create buffer for display %d: %s", currentDisplay, err.Error())
|
|
||||||
atexit.Exit(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
displayMetadata := &renderer.EvdiDisplayMetadata{
|
|
||||||
EvdiNode: openedDevice,
|
|
||||||
Rect: displayRect,
|
|
||||||
Buffer: displayBuffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
displayMetadata.EventContext = &libevdi.EvdiEventContext{}
|
|
||||||
openedDevice.RegisterEventHandler(displayMetadata.EventContext)
|
|
||||||
|
|
||||||
evdiCards[currentDisplay] = displayMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// HACK: sometimes the buffer doesn't get initialized properly if we don't wait a bit...
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
|
|
||||||
log.Info("Initialized displays. Entering rendering loop")
|
|
||||||
renderer.EnterRenderLoop(config, displayMetadata, evdiCards)
|
|
||||||
|
|
||||||
atexit.Exit(0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
logLevel := os.Getenv("UNREALXR_LOG_LEVEL")
|
|
||||||
|
|
||||||
if logLevel != "" {
|
|
||||||
switch logLevel {
|
|
||||||
case "debug":
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
case "info":
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
|
|
||||||
case "warn":
|
|
||||||
log.SetLevel(log.WarnLevel)
|
|
||||||
|
|
||||||
case "error":
|
|
||||||
log.SetLevel(log.ErrorLevel)
|
|
||||||
|
|
||||||
case "fatal":
|
|
||||||
log.SetLevel(log.FatalLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the CLI
|
|
||||||
cmd := &cli.Command{
|
|
||||||
Name: "unrealxr",
|
|
||||||
Usage: "A spatial multi-display renderer for XR devices",
|
|
||||||
Action: mainEntrypoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
|
||||||
log.Fatalf("Fatal error during execution: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,319 +0,0 @@
|
||||||
package renderer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image/color"
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
libconfig "git.terah.dev/UnrealXR/unrealxr/app/config"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/app/edidtools"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver"
|
|
||||||
arcommons "git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
"github.com/tebeka/atexit"
|
|
||||||
|
|
||||||
rl "git.terah.dev/UnrealXR/raylib-go/raylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TextureModelPair struct {
|
|
||||||
Texture rl.Texture2D
|
|
||||||
Model rl.Model
|
|
||||||
CurrentAngle float32
|
|
||||||
CurrentDisplaySpacing float32
|
|
||||||
}
|
|
||||||
|
|
||||||
func findMaxVerticalSize(fovyDeg float32, distance float32) float32 {
|
|
||||||
fovyRad := float64(fovyDeg * math.Pi / 180.0)
|
|
||||||
return 2 * distance * float32(math.Tan(fovyRad/2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func findOptimalHorizontalRes(verticalDisplayRes float32, horizontalDisplayRes float32, verticalSize float32) float32 {
|
|
||||||
aspectRatio := horizontalDisplayRes / verticalDisplayRes
|
|
||||||
horizontalSize := verticalSize * aspectRatio
|
|
||||||
|
|
||||||
return horizontalSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func findHfovFromVfov(vfovDeg, w, h float64) float64 {
|
|
||||||
vfovRad := vfovDeg * math.Pi / 180
|
|
||||||
|
|
||||||
ar := w / h
|
|
||||||
hfovRad := 2 * math.Atan(math.Tan(vfovRad/2)*ar)
|
|
||||||
|
|
||||||
return hfovRad * 180 / math.Pi
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.DisplayMetadata, evdiCards []*EvdiDisplayMetadata) {
|
|
||||||
log.Info("Initializing AR driver")
|
|
||||||
headset, err := ardriver.GetDevice()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get device: %s", err.Error())
|
|
||||||
atexit.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Initialized")
|
|
||||||
|
|
||||||
var (
|
|
||||||
currentPitch float32
|
|
||||||
previousPitch float32
|
|
||||||
currentYaw float32
|
|
||||||
previousYaw float32
|
|
||||||
currentRoll float32
|
|
||||||
previousRoll float32
|
|
||||||
|
|
||||||
hasGottenPitchCallbackBefore bool
|
|
||||||
hasGottenYawCallbackBefore bool
|
|
||||||
hasGottenRollCallbackBefore bool
|
|
||||||
)
|
|
||||||
|
|
||||||
arEventListner := &arcommons.AREventListener{
|
|
||||||
PitchCallback: func(newPitch float32) {
|
|
||||||
if !hasGottenPitchCallbackBefore {
|
|
||||||
hasGottenPitchCallbackBefore = true
|
|
||||||
currentPitch = newPitch
|
|
||||||
previousPitch = newPitch
|
|
||||||
} else {
|
|
||||||
previousPitch = currentPitch
|
|
||||||
currentPitch = newPitch
|
|
||||||
}
|
|
||||||
},
|
|
||||||
YawCallback: func(newYaw float32) {
|
|
||||||
if !hasGottenYawCallbackBefore {
|
|
||||||
hasGottenYawCallbackBefore = true
|
|
||||||
currentYaw = newYaw
|
|
||||||
previousYaw = newYaw
|
|
||||||
} else {
|
|
||||||
previousYaw = currentYaw
|
|
||||||
currentYaw = newYaw
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RollCallback: func(newRoll float32) {
|
|
||||||
if !hasGottenRollCallbackBefore {
|
|
||||||
hasGottenRollCallbackBefore = true
|
|
||||||
currentRoll = newRoll
|
|
||||||
previousRoll = newRoll
|
|
||||||
} else {
|
|
||||||
previousRoll = currentRoll
|
|
||||||
currentRoll = newRoll
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if headset.IsPollingLibrary() {
|
|
||||||
log.Error("Connected AR headset requires polling but polling is not implemented in the renderer!")
|
|
||||||
atexit.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
headset.RegisterEventListeners(arEventListner)
|
|
||||||
|
|
||||||
fovY := float32(*config.DisplayConfig.FOV)
|
|
||||||
fovX := findHfovFromVfov(float64(fovY), float64(displayMetadata.MaxWidth), float64(displayMetadata.MaxHeight))
|
|
||||||
|
|
||||||
verticalSize := findMaxVerticalSize(fovY, 5.0)
|
|
||||||
|
|
||||||
camera := rl.NewCamera3D(
|
|
||||||
rl.Vector3{
|
|
||||||
X: 0.0,
|
|
||||||
Y: verticalSize / 2,
|
|
||||||
Z: 5.0,
|
|
||||||
},
|
|
||||||
rl.Vector3{
|
|
||||||
X: 0.0,
|
|
||||||
Y: verticalSize / 2,
|
|
||||||
Z: 0.0,
|
|
||||||
},
|
|
||||||
rl.Vector3{
|
|
||||||
X: 0.0,
|
|
||||||
Y: 1.0,
|
|
||||||
Z: 0.0,
|
|
||||||
},
|
|
||||||
fovY,
|
|
||||||
rl.CameraPerspective,
|
|
||||||
)
|
|
||||||
|
|
||||||
horizontalSize := findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize)
|
|
||||||
coreMesh := rl.GenMeshPlane(horizontalSize, verticalSize, 1, 1)
|
|
||||||
|
|
||||||
var radius float32
|
|
||||||
|
|
||||||
if *config.DisplayConfig.UseCircularSpacing == true {
|
|
||||||
radiusX := (horizontalSize / 2) / float32(math.Tan((float64(fovX)*math.Pi/180.0)/2))
|
|
||||||
radiusY := (verticalSize / 2) / float32(math.Tan((float64(fovY)*math.Pi/180.0)/2))
|
|
||||||
|
|
||||||
if radiusY > radiusX {
|
|
||||||
radius = radiusY
|
|
||||||
} else {
|
|
||||||
radius = radiusX
|
|
||||||
}
|
|
||||||
|
|
||||||
radius *= *config.DisplayConfig.RadiusMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
movementVector := rl.Vector3{
|
|
||||||
X: 0.0,
|
|
||||||
Y: 0.0,
|
|
||||||
Z: 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
lookVector := rl.Vector3{
|
|
||||||
X: 0.0,
|
|
||||||
Y: 0.0,
|
|
||||||
Z: 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
hasZVectorDisabledQuirk := false
|
|
||||||
hasSensorInitDelayQuirk := false
|
|
||||||
sensorInitStartTime := time.Now()
|
|
||||||
|
|
||||||
if displayMetadata.DeviceQuirks.ZVectorDisabled {
|
|
||||||
log.Warn("QUIRK: The Z vector has been disabled for your specific device")
|
|
||||||
hasZVectorDisabledQuirk = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if displayMetadata.DeviceQuirks.SensorInitDelay != 0 {
|
|
||||||
log.Warnf("QUIRK: Waiting %d second(s) before reading sensors", displayMetadata.DeviceQuirks.SensorInitDelay)
|
|
||||||
log.Warn("|| MOVEMENT WILL NOT BE OPERATIONAL DURING THIS TIME. ||")
|
|
||||||
hasSensorInitDelayQuirk = true
|
|
||||||
}
|
|
||||||
|
|
||||||
rects := make([]*TextureModelPair, len(evdiCards))
|
|
||||||
|
|
||||||
displayAngle := float32(*config.DisplayConfig.Angle)
|
|
||||||
displaySpacing := *config.DisplayConfig.Spacing + horizontalSize
|
|
||||||
|
|
||||||
highestPossibleAngleOnBothSides := float32((*config.DisplayConfig.Count)-1) * displayAngle
|
|
||||||
highestPossibleDisplaySpacingOnBothSides := float32((*config.DisplayConfig.Count)-1) * displaySpacing
|
|
||||||
|
|
||||||
for i, card := range evdiCards {
|
|
||||||
currentAngle := (-highestPossibleAngleOnBothSides) + (displayAngle * float32(i+1))
|
|
||||||
currentDisplaySpacing := (-highestPossibleDisplaySpacingOnBothSides) + (displaySpacing * float32(i+1))
|
|
||||||
|
|
||||||
log.Debugf("display #%d: currentAngle=%f, currentDisplaySpacing=%f", i, currentAngle, currentDisplaySpacing)
|
|
||||||
|
|
||||||
image := rl.NewImage(card.Buffer.Buffer, int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), 1, rl.UncompressedR8g8b8a8)
|
|
||||||
|
|
||||||
texture := rl.LoadTextureFromImage(image)
|
|
||||||
model := rl.LoadModelFromMesh(coreMesh)
|
|
||||||
|
|
||||||
// spin up/down
|
|
||||||
pitchRad := float32(-90 * rl.Deg2rad)
|
|
||||||
// spin left/right
|
|
||||||
yawRad := currentAngle * rl.Deg2rad
|
|
||||||
|
|
||||||
rotX := rl.MatrixRotateX(pitchRad)
|
|
||||||
rotY := rl.MatrixRotateY(yawRad)
|
|
||||||
|
|
||||||
transform := rl.MatrixMultiply(rotX, rotY)
|
|
||||||
model.Transform = transform
|
|
||||||
|
|
||||||
rl.SetMaterialTexture(model.Materials, rl.MapAlbedo, texture)
|
|
||||||
|
|
||||||
rects[i] = &TextureModelPair{
|
|
||||||
Texture: texture,
|
|
||||||
Model: model,
|
|
||||||
CurrentAngle: currentAngle,
|
|
||||||
CurrentDisplaySpacing: currentDisplaySpacing,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventTimeoutDuration := 0 * time.Millisecond
|
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
|
||||||
if !displayMetadata.DeviceQuirks.UsesMouseMovement {
|
|
||||||
if hasSensorInitDelayQuirk {
|
|
||||||
if time.Now().Sub(sensorInitStartTime) > time.Duration(displayMetadata.DeviceQuirks.SensorInitDelay)*time.Second {
|
|
||||||
log.Info("Movement is now enabled.")
|
|
||||||
hasSensorInitDelayQuirk = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lookVector.X = (currentYaw - previousYaw) * 6.5
|
|
||||||
lookVector.Y = -(currentPitch - previousPitch) * 6.5
|
|
||||||
|
|
||||||
if !hasZVectorDisabledQuirk {
|
|
||||||
lookVector.Z = (currentRoll - previousRoll) * 6.5
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.UpdateCameraPro(&camera, movementVector, lookVector, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rl.UpdateCamera(&camera, rl.CameraFirstPerson)
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.BeginDrawing()
|
|
||||||
rl.ClearBackground(rl.Black)
|
|
||||||
rl.BeginMode3D(camera)
|
|
||||||
|
|
||||||
for rectPos, rect := range rects {
|
|
||||||
card := evdiCards[rectPos]
|
|
||||||
|
|
||||||
ready, err := card.EvdiNode.WaitUntilEventsAreReadyToHandle(eventTimeoutDuration)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to wait for display events: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ready {
|
|
||||||
if err := card.EvdiNode.HandleEvents(card.EventContext); err != nil {
|
|
||||||
log.Errorf("Failed to handle display events: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
card.EvdiNode.GrabPixels(card.Rect)
|
|
||||||
|
|
||||||
pixels := unsafe.Slice(
|
|
||||||
(*color.RGBA)(unsafe.Pointer(&card.Buffer.Buffer[0])),
|
|
||||||
len(card.Buffer.Buffer)/4,
|
|
||||||
)
|
|
||||||
|
|
||||||
rl.UpdateTexture(rect.Texture, pixels)
|
|
||||||
card.EvdiNode.RequestUpdate(card.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
worldPos := rl.Vector3{
|
|
||||||
X: 0,
|
|
||||||
Y: verticalSize / 2,
|
|
||||||
Z: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
if *config.DisplayConfig.UseCircularSpacing == true {
|
|
||||||
yawRad := float32(rl.Deg2rad * rect.CurrentAngle)
|
|
||||||
|
|
||||||
// WTF?
|
|
||||||
posX := float32(math.Sin(float64(yawRad))) * radius
|
|
||||||
posZ := -float32(math.Cos(float64(yawRad))) * radius
|
|
||||||
|
|
||||||
worldPos.X = posX
|
|
||||||
worldPos.Z = posZ + radius
|
|
||||||
} else {
|
|
||||||
worldPos.X = rect.CurrentDisplaySpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.DrawModelEx(
|
|
||||||
rect.Model,
|
|
||||||
worldPos,
|
|
||||||
// rotate around X to make it vertical
|
|
||||||
rl.Vector3{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
Z: 0,
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
rl.Vector3{
|
|
||||||
X: 1,
|
|
||||||
Y: 1,
|
|
||||||
Z: 1,
|
|
||||||
},
|
|
||||||
rl.White,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.EndMode3D()
|
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Goodbye!")
|
|
||||||
rl.CloseWindow()
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package renderer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.terah.dev/imterah/goevdi/libevdi"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EvdiDisplayMetadata struct {
|
|
||||||
EvdiNode *libevdi.EvdiNode
|
|
||||||
Rect *libevdi.EvdiDisplayRect
|
|
||||||
Buffer *libevdi.EvdiBuffer
|
|
||||||
EventContext *libevdi.EvdiEventContext
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package ardriver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/dummy"
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/xreal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetDevice() (commons.ARDevice, error) {
|
|
||||||
if xreal.IsXrealEnabled {
|
|
||||||
device, err := xreal.New()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to initialize xreal device: %w\n", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return device, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if dummy.IsDummyDeviceEnabled {
|
|
||||||
device, err := dummy.New()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to initialize dummy device: %w\n", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return device, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to initialize any device")
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package commons
|
|
||||||
|
|
||||||
type AREventListener struct {
|
|
||||||
PitchCallback func(float32)
|
|
||||||
YawCallback func(float32)
|
|
||||||
RollCallback func(float32)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ARDevice interface {
|
|
||||||
// Initializes the AR device's sensors.
|
|
||||||
Initialize() error
|
|
||||||
// Ends the AR device's sensors.
|
|
||||||
End() error
|
|
||||||
// Polls the AR device's sensors.
|
|
||||||
Poll() error
|
|
||||||
// Checks if the underlying AR library is polling-based.
|
|
||||||
IsPollingLibrary() bool
|
|
||||||
// Checks if the underlying AR library is event-based.
|
|
||||||
IsEventBasedLibrary() bool
|
|
||||||
// Registers event listeners for the AR device.
|
|
||||||
RegisterEventListeners(eventListener *AREventListener)
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
//go:build dummy_ar
|
|
||||||
// +build dummy_ar
|
|
||||||
|
|
||||||
package dummy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
)
|
|
||||||
|
|
||||||
var IsDummyDeviceEnabled = true
|
|
||||||
|
|
||||||
// Implements commons.ARDevice
|
|
||||||
type DummyDevice struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) Initialize() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) End() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) IsPollingLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) IsEventBasedLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) Poll() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) RegisterEventListeners(*commons.AREventListener) {}
|
|
||||||
|
|
||||||
func New() (*DummyDevice, error) {
|
|
||||||
return &DummyDevice{}, nil
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
//go:build !dummy_ar
|
|
||||||
// +build !dummy_ar
|
|
||||||
|
|
||||||
package dummy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
)
|
|
||||||
|
|
||||||
var IsDummyDeviceEnabled = false
|
|
||||||
|
|
||||||
// Implements commons.ARDevice
|
|
||||||
type DummyDevice struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) Initialize() error {
|
|
||||||
return fmt.Errorf("dummy device is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) End() error {
|
|
||||||
return fmt.Errorf("dummy device is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) IsPollingLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) IsEventBasedLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) Poll() error {
|
|
||||||
return fmt.Errorf("dummy device is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *DummyDevice) RegisterEventListeners(*commons.AREventListener) {}
|
|
||||||
|
|
||||||
func New() (*DummyDevice, error) {
|
|
||||||
return nil, fmt.Errorf("dummy device is not enabled")
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
module git.terah.dev/UnrealXR/unrealxr/ardriver
|
|
||||||
|
|
||||||
go 1.24.3
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Xreal driver
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
- [Xreal driver](https://gitlab.com/TheJackiMonster/nrealAirLinuxDriver)
|
|
||||||
- [Fusion](https://github.com/xioTechnologies/Fusion)
|
|
|
@ -1,28 +0,0 @@
|
||||||
//go:build xreal
|
|
||||||
// +build xreal
|
|
||||||
|
|
||||||
package xreal
|
|
||||||
|
|
||||||
import (
|
|
||||||
xreal "git.terah.dev/UnrealXR/unrealxr/ardriver/xreal/xrealsrc"
|
|
||||||
)
|
|
||||||
|
|
||||||
var IsXrealEnabled = true
|
|
||||||
|
|
||||||
type XrealDevice struct {
|
|
||||||
*xreal.XrealDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() (*XrealDevice, error) {
|
|
||||||
device := &XrealDevice{
|
|
||||||
XrealDevice: &xreal.XrealDevice{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := device.Initialize()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return device, nil
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
//go:build !xreal
|
|
||||||
// +build !xreal
|
|
||||||
|
|
||||||
package xreal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
)
|
|
||||||
|
|
||||||
var IsXrealEnabled = false
|
|
||||||
|
|
||||||
// Implements commons.ARDevice
|
|
||||||
type XrealDevice struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) Initialize() error {
|
|
||||||
return fmt.Errorf("xreal is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) End() error {
|
|
||||||
return fmt.Errorf("xreal is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) IsPollingLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) IsEventBasedLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) Poll() error {
|
|
||||||
return fmt.Errorf("xreal is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) RegisterEventListeners(*commons.AREventListener) {}
|
|
||||||
|
|
||||||
func New() (*XrealDevice, error) {
|
|
||||||
return nil, fmt.Errorf("xreal is not enabled")
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/**
|
|
||||||
* @file Fusion.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Main header file for the Fusion library. This is the only file that
|
|
||||||
* needs to be included when using the library.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_H
|
|
||||||
#define FUSION_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "FusionAhrs.h"
|
|
||||||
#include "FusionAxes.h"
|
|
||||||
#include "FusionCalibration.h"
|
|
||||||
#include "FusionCompass.h"
|
|
||||||
#include "FusionConvention.h"
|
|
||||||
#include "FusionMath.h"
|
|
||||||
#include "FusionOffset.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,510 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionAhrs.c
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer
|
|
||||||
* measurements into a single measurement of orientation relative to the Earth.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include <float.h>
|
|
||||||
#include "FusionAhrs.h"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initial gain used during the initialisation.
|
|
||||||
*/
|
|
||||||
#define INITIAL_GAIN (10.0f)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialisation period in seconds.
|
|
||||||
*/
|
|
||||||
#define INITIALISATION_PERIOD (3.0f)
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Function declarations
|
|
||||||
|
|
||||||
static inline FusionVector HalfGravity(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference);
|
|
||||||
|
|
||||||
static inline int Clamp(const int value, const int min, const int max);
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialises the AHRS algorithm structure.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
*/
|
|
||||||
void FusionAhrsInitialise(FusionAhrs *const ahrs) {
|
|
||||||
const FusionAhrsSettings settings = {
|
|
||||||
.convention = FusionConventionNwu,
|
|
||||||
.gain = 0.5f,
|
|
||||||
.gyroscopeRange = 0.0f,
|
|
||||||
.accelerationRejection = 90.0f,
|
|
||||||
.magneticRejection = 90.0f,
|
|
||||||
.recoveryTriggerPeriod = 0,
|
|
||||||
};
|
|
||||||
FusionAhrsSetSettings(ahrs, &settings);
|
|
||||||
FusionAhrsReset(ahrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Resets the AHRS algorithm. This is equivalent to reinitialising the
|
|
||||||
* algorithm while maintaining the current settings.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
*/
|
|
||||||
void FusionAhrsReset(FusionAhrs *const ahrs) {
|
|
||||||
ahrs->quaternion = FUSION_IDENTITY_QUATERNION;
|
|
||||||
ahrs->accelerometer = FUSION_VECTOR_ZERO;
|
|
||||||
ahrs->initialising = true;
|
|
||||||
ahrs->rampedGain = INITIAL_GAIN;
|
|
||||||
ahrs->angularRateRecovery = false;
|
|
||||||
ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO;
|
|
||||||
ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO;
|
|
||||||
ahrs->accelerometerIgnored = false;
|
|
||||||
ahrs->accelerationRecoveryTrigger = 0;
|
|
||||||
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
ahrs->magnetometerIgnored = false;
|
|
||||||
ahrs->magneticRecoveryTrigger = 0;
|
|
||||||
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the AHRS algorithm settings.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param settings Settings.
|
|
||||||
*/
|
|
||||||
void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) {
|
|
||||||
ahrs->settings.convention = settings->convention;
|
|
||||||
ahrs->settings.gain = settings->gain;
|
|
||||||
ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange;
|
|
||||||
ahrs->settings.accelerationRejection = settings->accelerationRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2);
|
|
||||||
ahrs->settings.magneticRejection = settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2);
|
|
||||||
ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod;
|
|
||||||
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
if ((settings->gain == 0.0f) || (settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero
|
|
||||||
ahrs->settings.accelerationRejection = FLT_MAX;
|
|
||||||
ahrs->settings.magneticRejection = FLT_MAX;
|
|
||||||
}
|
|
||||||
if (ahrs->initialising == false) {
|
|
||||||
ahrs->rampedGain = ahrs->settings.gain;
|
|
||||||
}
|
|
||||||
ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and
|
|
||||||
* magnetometer measurements.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param gyroscope Gyroscope measurement in degrees per second.
|
|
||||||
* @param accelerometer Accelerometer measurement in g.
|
|
||||||
* @param magnetometer Magnetometer measurement in arbitrary units.
|
|
||||||
* @param deltaTime Delta time in seconds.
|
|
||||||
*/
|
|
||||||
void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, const float deltaTime) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
|
|
||||||
// Store accelerometer
|
|
||||||
ahrs->accelerometer = accelerometer;
|
|
||||||
|
|
||||||
// Reinitialise if gyroscope range exceeded
|
|
||||||
if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) {
|
|
||||||
const FusionQuaternion quaternion = ahrs->quaternion;
|
|
||||||
FusionAhrsReset(ahrs);
|
|
||||||
ahrs->quaternion = quaternion;
|
|
||||||
ahrs->angularRateRecovery = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ramp down gain during initialisation
|
|
||||||
if (ahrs->initialising) {
|
|
||||||
ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime;
|
|
||||||
if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) {
|
|
||||||
ahrs->rampedGain = ahrs->settings.gain;
|
|
||||||
ahrs->initialising = false;
|
|
||||||
ahrs->angularRateRecovery = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate direction of gravity indicated by algorithm
|
|
||||||
const FusionVector halfGravity = HalfGravity(ahrs);
|
|
||||||
|
|
||||||
// Calculate accelerometer feedback
|
|
||||||
FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO;
|
|
||||||
ahrs->accelerometerIgnored = true;
|
|
||||||
if (FusionVectorIsZero(accelerometer) == false) {
|
|
||||||
|
|
||||||
// Calculate accelerometer feedback scaled by 0.5
|
|
||||||
ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity);
|
|
||||||
|
|
||||||
// Don't ignore accelerometer if acceleration error below threshold
|
|
||||||
if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) {
|
|
||||||
ahrs->accelerometerIgnored = false;
|
|
||||||
ahrs->accelerationRecoveryTrigger -= 9;
|
|
||||||
} else {
|
|
||||||
ahrs->accelerationRecoveryTrigger += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't ignore accelerometer during acceleration recovery
|
|
||||||
if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) {
|
|
||||||
ahrs->accelerationRecoveryTimeout = 0;
|
|
||||||
ahrs->accelerometerIgnored = false;
|
|
||||||
} else {
|
|
||||||
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
}
|
|
||||||
ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod);
|
|
||||||
|
|
||||||
// Apply accelerometer feedback
|
|
||||||
if (ahrs->accelerometerIgnored == false) {
|
|
||||||
halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate magnetometer feedback
|
|
||||||
FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO;
|
|
||||||
ahrs->magnetometerIgnored = true;
|
|
||||||
if (FusionVectorIsZero(magnetometer) == false) {
|
|
||||||
|
|
||||||
// Calculate direction of magnetic field indicated by algorithm
|
|
||||||
const FusionVector halfMagnetic = HalfMagnetic(ahrs);
|
|
||||||
|
|
||||||
// Calculate magnetometer feedback scaled by 0.5
|
|
||||||
ahrs->halfMagnetometerFeedback = Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic);
|
|
||||||
|
|
||||||
// Don't ignore magnetometer if magnetic error below threshold
|
|
||||||
if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) {
|
|
||||||
ahrs->magnetometerIgnored = false;
|
|
||||||
ahrs->magneticRecoveryTrigger -= 9;
|
|
||||||
} else {
|
|
||||||
ahrs->magneticRecoveryTrigger += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't ignore magnetometer during magnetic recovery
|
|
||||||
if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) {
|
|
||||||
ahrs->magneticRecoveryTimeout = 0;
|
|
||||||
ahrs->magnetometerIgnored = false;
|
|
||||||
} else {
|
|
||||||
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
|
|
||||||
}
|
|
||||||
ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod);
|
|
||||||
|
|
||||||
// Apply magnetometer feedback
|
|
||||||
if (ahrs->magnetometerIgnored == false) {
|
|
||||||
halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert gyroscope to radians per second scaled by 0.5
|
|
||||||
const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f));
|
|
||||||
|
|
||||||
// Apply feedback to gyroscope
|
|
||||||
const FusionVector adjustedHalfGyroscope = FusionVectorAdd(halfGyroscope, FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain));
|
|
||||||
|
|
||||||
// Integrate rate of change of quaternion
|
|
||||||
ahrs->quaternion = FusionQuaternionAdd(ahrs->quaternion, FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime)));
|
|
||||||
|
|
||||||
// Normalise quaternion
|
|
||||||
ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion);
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the direction of gravity scaled by 0.5.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Direction of gravity scaled by 0.5.
|
|
||||||
*/
|
|
||||||
static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
switch (ahrs->settings.convention) {
|
|
||||||
case FusionConventionNwu:
|
|
||||||
case FusionConventionEnu: {
|
|
||||||
const FusionVector halfGravity = {.axis = {
|
|
||||||
.x = Q.x * Q.z - Q.w * Q.y,
|
|
||||||
.y = Q.y * Q.z + Q.w * Q.x,
|
|
||||||
.z = Q.w * Q.w - 0.5f + Q.z * Q.z,
|
|
||||||
}}; // third column of transposed rotation matrix scaled by 0.5
|
|
||||||
return halfGravity;
|
|
||||||
}
|
|
||||||
case FusionConventionNed: {
|
|
||||||
const FusionVector halfGravity = {.axis = {
|
|
||||||
.x = Q.w * Q.y - Q.x * Q.z,
|
|
||||||
.y = -1.0f * (Q.y * Q.z + Q.w * Q.x),
|
|
||||||
.z = 0.5f - Q.w * Q.w - Q.z * Q.z,
|
|
||||||
}}; // third column of transposed rotation matrix scaled by -0.5
|
|
||||||
return halfGravity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FUSION_VECTOR_ZERO; // avoid compiler warning
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the direction of the magnetic field scaled by 0.5.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Direction of the magnetic field scaled by 0.5.
|
|
||||||
*/
|
|
||||||
static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
switch (ahrs->settings.convention) {
|
|
||||||
case FusionConventionNwu: {
|
|
||||||
const FusionVector halfMagnetic = {.axis = {
|
|
||||||
.x = Q.x * Q.y + Q.w * Q.z,
|
|
||||||
.y = Q.w * Q.w - 0.5f + Q.y * Q.y,
|
|
||||||
.z = Q.y * Q.z - Q.w * Q.x,
|
|
||||||
}}; // second column of transposed rotation matrix scaled by 0.5
|
|
||||||
return halfMagnetic;
|
|
||||||
}
|
|
||||||
case FusionConventionEnu: {
|
|
||||||
const FusionVector halfMagnetic = {.axis = {
|
|
||||||
.x = 0.5f - Q.w * Q.w - Q.x * Q.x,
|
|
||||||
.y = Q.w * Q.z - Q.x * Q.y,
|
|
||||||
.z = -1.0f * (Q.x * Q.z + Q.w * Q.y),
|
|
||||||
}}; // first column of transposed rotation matrix scaled by -0.5
|
|
||||||
return halfMagnetic;
|
|
||||||
}
|
|
||||||
case FusionConventionNed: {
|
|
||||||
const FusionVector halfMagnetic = {.axis = {
|
|
||||||
.x = -1.0f * (Q.x * Q.y + Q.w * Q.z),
|
|
||||||
.y = 0.5f - Q.w * Q.w - Q.y * Q.y,
|
|
||||||
.z = Q.w * Q.x - Q.y * Q.z,
|
|
||||||
}}; // second column of transposed rotation matrix scaled by -0.5
|
|
||||||
return halfMagnetic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FUSION_VECTOR_ZERO; // avoid compiler warning
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the feedback.
|
|
||||||
* @param sensor Sensor.
|
|
||||||
* @param reference Reference.
|
|
||||||
* @return Feedback.
|
|
||||||
*/
|
|
||||||
static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference) {
|
|
||||||
if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees
|
|
||||||
return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference));
|
|
||||||
}
|
|
||||||
return FusionVectorCrossProduct(sensor, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a value limited to maximum and minimum.
|
|
||||||
* @param value Value.
|
|
||||||
* @param min Minimum value.
|
|
||||||
* @param max Maximum value.
|
|
||||||
* @return Value limited to maximum and minimum.
|
|
||||||
*/
|
|
||||||
static inline int Clamp(const int value, const int min, const int max) {
|
|
||||||
if (value < min) {
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
if (value > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Updates the AHRS algorithm using the gyroscope and accelerometer
|
|
||||||
* measurements only.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param gyroscope Gyroscope measurement in degrees per second.
|
|
||||||
* @param accelerometer Accelerometer measurement in g.
|
|
||||||
* @param deltaTime Delta time in seconds.
|
|
||||||
*/
|
|
||||||
void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime) {
|
|
||||||
|
|
||||||
// Update AHRS algorithm
|
|
||||||
FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime);
|
|
||||||
|
|
||||||
// Zero heading during initialisation
|
|
||||||
if (ahrs->initialising) {
|
|
||||||
FusionAhrsSetHeading(ahrs, 0.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and
|
|
||||||
* heading measurements.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param gyroscope Gyroscope measurement in degrees per second.
|
|
||||||
* @param accelerometer Accelerometer measurement in g.
|
|
||||||
* @param heading Heading measurement in degrees.
|
|
||||||
* @param deltaTime Delta time in seconds.
|
|
||||||
*/
|
|
||||||
void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, const float deltaTime) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
|
|
||||||
// Calculate roll
|
|
||||||
const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x);
|
|
||||||
|
|
||||||
// Calculate magnetometer
|
|
||||||
const float headingRadians = FusionDegreesToRadians(heading);
|
|
||||||
const float sinHeadingRadians = sinf(headingRadians);
|
|
||||||
const FusionVector magnetometer = {.axis = {
|
|
||||||
.x = cosf(headingRadians),
|
|
||||||
.y = -1.0f * cosf(roll) * sinHeadingRadians,
|
|
||||||
.z = sinHeadingRadians * sinf(roll),
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Update AHRS algorithm
|
|
||||||
FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime);
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the quaternion describing the sensor relative to the Earth.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Quaternion describing the sensor relative to the Earth.
|
|
||||||
*/
|
|
||||||
FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) {
|
|
||||||
return ahrs->quaternion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the quaternion describing the sensor relative to the Earth.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param quaternion Quaternion describing the sensor relative to the Earth.
|
|
||||||
*/
|
|
||||||
void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion) {
|
|
||||||
ahrs->quaternion = quaternion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the direction of gravity in the sensor coordinate frame.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Direction of gravity in the sensor coordinate frame.
|
|
||||||
*/
|
|
||||||
FusionVector FusionAhrsGetGravity(const FusionAhrs *const ahrs) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
const FusionVector gravity = {.axis = {
|
|
||||||
.x = 2.0f * (Q.x * Q.z - Q.w * Q.y),
|
|
||||||
.y = 2.0f * (Q.y * Q.z + Q.w * Q.x),
|
|
||||||
.z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z),
|
|
||||||
}}; // third column of transposed rotation matrix
|
|
||||||
return gravity;
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the linear acceleration measurement equal to the accelerometer
|
|
||||||
* measurement with gravity removed.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Linear acceleration measurement in g.
|
|
||||||
*/
|
|
||||||
FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) {
|
|
||||||
switch (ahrs->settings.convention) {
|
|
||||||
case FusionConventionNwu:
|
|
||||||
case FusionConventionEnu: {
|
|
||||||
return FusionVectorSubtract(ahrs->accelerometer, FusionAhrsGetGravity(ahrs));
|
|
||||||
}
|
|
||||||
case FusionConventionNed: {
|
|
||||||
return FusionVectorAdd(ahrs->accelerometer, FusionAhrsGetGravity(ahrs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FUSION_VECTOR_ZERO; // avoid compiler warning
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the Earth acceleration measurement equal to accelerometer
|
|
||||||
* measurement in the Earth coordinate frame with gravity removed.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return Earth acceleration measurement in g.
|
|
||||||
*/
|
|
||||||
FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
#define A ahrs->accelerometer.axis
|
|
||||||
|
|
||||||
// Calculate accelerometer measurement in the Earth coordinate frame
|
|
||||||
const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations
|
|
||||||
const float qwqx = Q.w * Q.x;
|
|
||||||
const float qwqy = Q.w * Q.y;
|
|
||||||
const float qwqz = Q.w * Q.z;
|
|
||||||
const float qxqy = Q.x * Q.y;
|
|
||||||
const float qxqz = Q.x * Q.z;
|
|
||||||
const float qyqz = Q.y * Q.z;
|
|
||||||
FusionVector accelerometer = {.axis = {
|
|
||||||
.x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z),
|
|
||||||
.y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z),
|
|
||||||
.z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z),
|
|
||||||
}}; // rotation matrix multiplied with the accelerometer
|
|
||||||
|
|
||||||
// Remove gravity from accelerometer measurement
|
|
||||||
switch (ahrs->settings.convention) {
|
|
||||||
case FusionConventionNwu:
|
|
||||||
case FusionConventionEnu:
|
|
||||||
accelerometer.axis.z -= 1.0f;
|
|
||||||
break;
|
|
||||||
case FusionConventionNed:
|
|
||||||
accelerometer.axis.z += 1.0f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return accelerometer;
|
|
||||||
#undef Q
|
|
||||||
#undef A
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the AHRS algorithm internal states.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return AHRS algorithm internal states.
|
|
||||||
*/
|
|
||||||
FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) {
|
|
||||||
const FusionAhrsInternalStates internalStates = {
|
|
||||||
.accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))),
|
|
||||||
.accelerometerIgnored = ahrs->accelerometerIgnored,
|
|
||||||
.accelerationRecoveryTrigger = ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float) ahrs->accelerationRecoveryTrigger / (float) ahrs->settings.recoveryTriggerPeriod,
|
|
||||||
.magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))),
|
|
||||||
.magnetometerIgnored = ahrs->magnetometerIgnored,
|
|
||||||
.magneticRecoveryTrigger = ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float) ahrs->magneticRecoveryTrigger / (float) ahrs->settings.recoveryTriggerPeriod,
|
|
||||||
};
|
|
||||||
return internalStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the AHRS algorithm flags.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @return AHRS algorithm flags.
|
|
||||||
*/
|
|
||||||
FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) {
|
|
||||||
const FusionAhrsFlags flags = {
|
|
||||||
.initialising = ahrs->initialising,
|
|
||||||
.angularRateRecovery = ahrs->angularRateRecovery,
|
|
||||||
.accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout,
|
|
||||||
.magneticRecovery= ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout,
|
|
||||||
};
|
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sets the heading of the orientation measurement provided by the AHRS
|
|
||||||
* algorithm. This function can be used to reset drift in heading when the AHRS
|
|
||||||
* algorithm is being used without a magnetometer.
|
|
||||||
* @param ahrs AHRS algorithm structure.
|
|
||||||
* @param heading Heading angle in degrees.
|
|
||||||
*/
|
|
||||||
void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) {
|
|
||||||
#define Q ahrs->quaternion.element
|
|
||||||
const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z);
|
|
||||||
const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading));
|
|
||||||
const FusionQuaternion rotation = {.element = {
|
|
||||||
.w = cosf(halfYawMinusHeading),
|
|
||||||
.x = 0.0f,
|
|
||||||
.y = 0.0f,
|
|
||||||
.z = -1.0f * sinf(halfYawMinusHeading),
|
|
||||||
}};
|
|
||||||
ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion);
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,111 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionAhrs.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer
|
|
||||||
* measurements into a single measurement of orientation relative to the Earth.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_AHRS_H
|
|
||||||
#define FUSION_AHRS_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionConvention.h"
|
|
||||||
#include "FusionMath.h"
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AHRS algorithm settings.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
FusionConvention convention;
|
|
||||||
float gain;
|
|
||||||
float gyroscopeRange;
|
|
||||||
float accelerationRejection;
|
|
||||||
float magneticRejection;
|
|
||||||
unsigned int recoveryTriggerPeriod;
|
|
||||||
} FusionAhrsSettings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AHRS algorithm structure. Structure members are used internally and
|
|
||||||
* must not be accessed by the application.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
FusionAhrsSettings settings;
|
|
||||||
FusionQuaternion quaternion;
|
|
||||||
FusionVector accelerometer;
|
|
||||||
bool initialising;
|
|
||||||
float rampedGain;
|
|
||||||
float rampedGainStep;
|
|
||||||
bool angularRateRecovery;
|
|
||||||
FusionVector halfAccelerometerFeedback;
|
|
||||||
FusionVector halfMagnetometerFeedback;
|
|
||||||
bool accelerometerIgnored;
|
|
||||||
int accelerationRecoveryTrigger;
|
|
||||||
int accelerationRecoveryTimeout;
|
|
||||||
bool magnetometerIgnored;
|
|
||||||
int magneticRecoveryTrigger;
|
|
||||||
int magneticRecoveryTimeout;
|
|
||||||
} FusionAhrs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AHRS algorithm internal states.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
float accelerationError;
|
|
||||||
bool accelerometerIgnored;
|
|
||||||
float accelerationRecoveryTrigger;
|
|
||||||
float magneticError;
|
|
||||||
bool magnetometerIgnored;
|
|
||||||
float magneticRecoveryTrigger;
|
|
||||||
} FusionAhrsInternalStates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AHRS algorithm flags.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
bool initialising;
|
|
||||||
bool angularRateRecovery;
|
|
||||||
bool accelerationRecovery;
|
|
||||||
bool magneticRecovery;
|
|
||||||
} FusionAhrsFlags;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Function declarations
|
|
||||||
|
|
||||||
void FusionAhrsInitialise(FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
void FusionAhrsReset(FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings);
|
|
||||||
|
|
||||||
void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, const float deltaTime);
|
|
||||||
|
|
||||||
void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime);
|
|
||||||
|
|
||||||
void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, const float deltaTime);
|
|
||||||
|
|
||||||
FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion);
|
|
||||||
|
|
||||||
FusionVector FusionAhrsGetGravity(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs);
|
|
||||||
|
|
||||||
void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,187 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionAxes.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Swaps sensor axes for alignment with the body axes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_AXES_H
|
|
||||||
#define FUSION_AXES_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionMath.h"
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Axes alignment describing the sensor axes relative to the body axes.
|
|
||||||
* For example, if the body X axis is aligned with the sensor Y axis and the
|
|
||||||
* body Y axis is aligned with sensor X axis but pointing the opposite direction
|
|
||||||
* then alignment is +Y-X+Z.
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */
|
|
||||||
FusionAxesAlignmentPXNZPY, /* +X-Z+Y */
|
|
||||||
FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */
|
|
||||||
FusionAxesAlignmentPXPZNY, /* +X+Z-Y */
|
|
||||||
FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */
|
|
||||||
FusionAxesAlignmentNXPZPY, /* -X+Z+Y */
|
|
||||||
FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */
|
|
||||||
FusionAxesAlignmentNXNZNY, /* -X-Z-Y */
|
|
||||||
FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */
|
|
||||||
FusionAxesAlignmentPYNZNX, /* +Y-Z-X */
|
|
||||||
FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */
|
|
||||||
FusionAxesAlignmentPYPZPX, /* +Y+Z+X */
|
|
||||||
FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */
|
|
||||||
FusionAxesAlignmentNYNZPX, /* -Y-Z+X */
|
|
||||||
FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */
|
|
||||||
FusionAxesAlignmentNYPZNX, /* -Y+Z-X */
|
|
||||||
FusionAxesAlignmentPZPYNX, /* +Z+Y-X */
|
|
||||||
FusionAxesAlignmentPZPXPY, /* +Z+X+Y */
|
|
||||||
FusionAxesAlignmentPZNYPX, /* +Z-Y+X */
|
|
||||||
FusionAxesAlignmentPZNXNY, /* +Z-X-Y */
|
|
||||||
FusionAxesAlignmentNZPYPX, /* -Z+Y+X */
|
|
||||||
FusionAxesAlignmentNZNXPY, /* -Z-X+Y */
|
|
||||||
FusionAxesAlignmentNZNYNX, /* -Z-Y-X */
|
|
||||||
FusionAxesAlignmentNZPXNY, /* -Z+X-Y */
|
|
||||||
} FusionAxesAlignment;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Swaps sensor axes for alignment with the body axes.
|
|
||||||
* @param sensor Sensor axes.
|
|
||||||
* @param alignment Axes alignment.
|
|
||||||
* @return Sensor axes aligned with the body axes.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) {
|
|
||||||
FusionVector result;
|
|
||||||
switch (alignment) {
|
|
||||||
case FusionAxesAlignmentPXPYPZ:
|
|
||||||
break;
|
|
||||||
case FusionAxesAlignmentPXNZPY:
|
|
||||||
result.axis.x = +sensor.axis.x;
|
|
||||||
result.axis.y = -sensor.axis.z;
|
|
||||||
result.axis.z = +sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPXNYNZ:
|
|
||||||
result.axis.x = +sensor.axis.x;
|
|
||||||
result.axis.y = -sensor.axis.y;
|
|
||||||
result.axis.z = -sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPXPZNY:
|
|
||||||
result.axis.x = +sensor.axis.x;
|
|
||||||
result.axis.y = +sensor.axis.z;
|
|
||||||
result.axis.z = -sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNXPYNZ:
|
|
||||||
result.axis.x = -sensor.axis.x;
|
|
||||||
result.axis.y = +sensor.axis.y;
|
|
||||||
result.axis.z = -sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNXPZPY:
|
|
||||||
result.axis.x = -sensor.axis.x;
|
|
||||||
result.axis.y = +sensor.axis.z;
|
|
||||||
result.axis.z = +sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNXNYPZ:
|
|
||||||
result.axis.x = -sensor.axis.x;
|
|
||||||
result.axis.y = -sensor.axis.y;
|
|
||||||
result.axis.z = +sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNXNZNY:
|
|
||||||
result.axis.x = -sensor.axis.x;
|
|
||||||
result.axis.y = -sensor.axis.z;
|
|
||||||
result.axis.z = -sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPYNXPZ:
|
|
||||||
result.axis.x = +sensor.axis.y;
|
|
||||||
result.axis.y = -sensor.axis.x;
|
|
||||||
result.axis.z = +sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPYNZNX:
|
|
||||||
result.axis.x = +sensor.axis.y;
|
|
||||||
result.axis.y = -sensor.axis.z;
|
|
||||||
result.axis.z = -sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPYPXNZ:
|
|
||||||
result.axis.x = +sensor.axis.y;
|
|
||||||
result.axis.y = +sensor.axis.x;
|
|
||||||
result.axis.z = -sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPYPZPX:
|
|
||||||
result.axis.x = +sensor.axis.y;
|
|
||||||
result.axis.y = +sensor.axis.z;
|
|
||||||
result.axis.z = +sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNYPXPZ:
|
|
||||||
result.axis.x = -sensor.axis.y;
|
|
||||||
result.axis.y = +sensor.axis.x;
|
|
||||||
result.axis.z = +sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNYNZPX:
|
|
||||||
result.axis.x = -sensor.axis.y;
|
|
||||||
result.axis.y = -sensor.axis.z;
|
|
||||||
result.axis.z = +sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNYNXNZ:
|
|
||||||
result.axis.x = -sensor.axis.y;
|
|
||||||
result.axis.y = -sensor.axis.x;
|
|
||||||
result.axis.z = -sensor.axis.z;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNYPZNX:
|
|
||||||
result.axis.x = -sensor.axis.y;
|
|
||||||
result.axis.y = +sensor.axis.z;
|
|
||||||
result.axis.z = -sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPZPYNX:
|
|
||||||
result.axis.x = +sensor.axis.z;
|
|
||||||
result.axis.y = +sensor.axis.y;
|
|
||||||
result.axis.z = -sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPZPXPY:
|
|
||||||
result.axis.x = +sensor.axis.z;
|
|
||||||
result.axis.y = +sensor.axis.x;
|
|
||||||
result.axis.z = +sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPZNYPX:
|
|
||||||
result.axis.x = +sensor.axis.z;
|
|
||||||
result.axis.y = -sensor.axis.y;
|
|
||||||
result.axis.z = +sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentPZNXNY:
|
|
||||||
result.axis.x = +sensor.axis.z;
|
|
||||||
result.axis.y = -sensor.axis.x;
|
|
||||||
result.axis.z = -sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNZPYPX:
|
|
||||||
result.axis.x = -sensor.axis.z;
|
|
||||||
result.axis.y = +sensor.axis.y;
|
|
||||||
result.axis.z = +sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNZNXPY:
|
|
||||||
result.axis.x = -sensor.axis.z;
|
|
||||||
result.axis.y = -sensor.axis.x;
|
|
||||||
result.axis.z = +sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNZNYNX:
|
|
||||||
result.axis.x = -sensor.axis.z;
|
|
||||||
result.axis.y = -sensor.axis.y;
|
|
||||||
result.axis.z = -sensor.axis.x;
|
|
||||||
return result;
|
|
||||||
case FusionAxesAlignmentNZPXNY:
|
|
||||||
result.axis.x = -sensor.axis.z;
|
|
||||||
result.axis.y = +sensor.axis.x;
|
|
||||||
result.axis.z = -sensor.axis.y;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return sensor; // avoid compiler warning
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,44 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionCalibration.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Gyroscope, accelerometer, and magnetometer calibration models.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_CALIBRATION_H
|
|
||||||
#define FUSION_CALIBRATION_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionMath.h"
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Gyroscope and accelerometer calibration model.
|
|
||||||
* @param uncalibrated Uncalibrated measurement.
|
|
||||||
* @param misalignment Misalignment matrix.
|
|
||||||
* @param sensitivity Sensitivity.
|
|
||||||
* @param offset Offset.
|
|
||||||
* @return Calibrated measurement.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, const FusionVector sensitivity, const FusionVector offset) {
|
|
||||||
return FusionMatrixMultiplyVector(misalignment, FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Magnetometer calibration model.
|
|
||||||
* @param uncalibrated Uncalibrated measurement.
|
|
||||||
* @param softIronMatrix Soft-iron matrix.
|
|
||||||
* @param hardIronOffset Hard-iron offset.
|
|
||||||
* @return Calibrated measurement.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix, const FusionVector hardIronOffset) {
|
|
||||||
return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,49 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionCompass.c
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Tilt-compensated compass to calculate the magnetic heading using
|
|
||||||
* accelerometer and magnetometer measurements.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionAxes.h"
|
|
||||||
#include "FusionCompass.h"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculates the magnetic heading.
|
|
||||||
* @param convention Earth axes convention.
|
|
||||||
* @param accelerometer Accelerometer measurement in any calibrated units.
|
|
||||||
* @param magnetometer Magnetometer measurement in any calibrated units.
|
|
||||||
* @return Heading angle in degrees.
|
|
||||||
*/
|
|
||||||
float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer) {
|
|
||||||
switch (convention) {
|
|
||||||
case FusionConventionNwu: {
|
|
||||||
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer));
|
|
||||||
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer));
|
|
||||||
return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x));
|
|
||||||
}
|
|
||||||
case FusionConventionEnu: {
|
|
||||||
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer));
|
|
||||||
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer));
|
|
||||||
const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f);
|
|
||||||
return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x));
|
|
||||||
}
|
|
||||||
case FusionConventionNed: {
|
|
||||||
const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f);
|
|
||||||
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer));
|
|
||||||
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up));
|
|
||||||
return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0; // avoid compiler warning
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionCompass.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Tilt-compensated compass to calculate the magnetic heading using
|
|
||||||
* accelerometer and magnetometer measurements.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_COMPASS_H
|
|
||||||
#define FUSION_COMPASS_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionConvention.h"
|
|
||||||
#include "FusionMath.h"
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Function declarations
|
|
||||||
|
|
||||||
float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionConvention.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Earth axes convention.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_CONVENTION_H
|
|
||||||
#define FUSION_CONVENTION_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Earth axes convention.
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
FusionConventionNwu, /* North-West-Up */
|
|
||||||
FusionConventionEnu, /* East-North-Up */
|
|
||||||
FusionConventionNed, /* North-East-Down */
|
|
||||||
} FusionConvention;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,481 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionMath.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Math library.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_MATH_H
|
|
||||||
#define FUSION_MATH_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 3D vector.
|
|
||||||
*/
|
|
||||||
typedef union {
|
|
||||||
float array[3];
|
|
||||||
|
|
||||||
struct {
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
float z;
|
|
||||||
} axis;
|
|
||||||
} FusionVector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Quaternion.
|
|
||||||
*/
|
|
||||||
typedef union {
|
|
||||||
float array[4];
|
|
||||||
|
|
||||||
struct {
|
|
||||||
float w;
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
float z;
|
|
||||||
} element;
|
|
||||||
} FusionQuaternion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 3x3 matrix in row-major order.
|
|
||||||
* See http://en.wikipedia.org/wiki/Row-major_order
|
|
||||||
*/
|
|
||||||
typedef union {
|
|
||||||
float array[3][3];
|
|
||||||
|
|
||||||
struct {
|
|
||||||
float xx;
|
|
||||||
float xy;
|
|
||||||
float xz;
|
|
||||||
float yx;
|
|
||||||
float yy;
|
|
||||||
float yz;
|
|
||||||
float zx;
|
|
||||||
float zy;
|
|
||||||
float zz;
|
|
||||||
} element;
|
|
||||||
} FusionMatrix;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Euler angles. Roll, pitch, and yaw correspond to rotations around
|
|
||||||
* X, Y, and Z respectively.
|
|
||||||
*/
|
|
||||||
typedef union {
|
|
||||||
float array[3];
|
|
||||||
|
|
||||||
struct {
|
|
||||||
float roll;
|
|
||||||
float pitch;
|
|
||||||
float yaw;
|
|
||||||
} angle;
|
|
||||||
} FusionEuler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Vector of zeros.
|
|
||||||
*/
|
|
||||||
#define FUSION_VECTOR_ZERO ((FusionVector){ .array = {0.0f, 0.0f, 0.0f} })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Vector of ones.
|
|
||||||
*/
|
|
||||||
#define FUSION_VECTOR_ONES ((FusionVector){ .array = {1.0f, 1.0f, 1.0f} })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Identity quaternion.
|
|
||||||
*/
|
|
||||||
#define FUSION_IDENTITY_QUATERNION ((FusionQuaternion){ .array = {1.0f, 0.0f, 0.0f, 0.0f} })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Identity matrix.
|
|
||||||
*/
|
|
||||||
#define FUSION_IDENTITY_MATRIX ((FusionMatrix){ .array = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}} })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Euler angles of zero.
|
|
||||||
*/
|
|
||||||
#define FUSION_EULER_ZERO ((FusionEuler){ .array = {0.0f, 0.0f, 0.0f} })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Pi. May not be defined in math.h.
|
|
||||||
*/
|
|
||||||
#ifndef M_PI
|
|
||||||
#define M_PI (3.14159265358979323846)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Include this definition or add as a preprocessor definition to use
|
|
||||||
* normal square root operations.
|
|
||||||
*/
|
|
||||||
//#define FUSION_USE_NORMAL_SQRT
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Degrees and radians conversion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts degrees to radians.
|
|
||||||
* @param degrees Degrees.
|
|
||||||
* @return Radians.
|
|
||||||
*/
|
|
||||||
static inline float FusionDegreesToRadians(const float degrees) {
|
|
||||||
return degrees * ((float) M_PI / 180.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts radians to degrees.
|
|
||||||
* @param radians Radians.
|
|
||||||
* @return Degrees.
|
|
||||||
*/
|
|
||||||
static inline float FusionRadiansToDegrees(const float radians) {
|
|
||||||
return radians * (180.0f / (float) M_PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Arc sine
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the arc sine of the value.
|
|
||||||
* @param value Value.
|
|
||||||
* @return Arc sine of the value.
|
|
||||||
*/
|
|
||||||
static inline float FusionAsin(const float value) {
|
|
||||||
if (value <= -1.0f) {
|
|
||||||
return (float) M_PI / -2.0f;
|
|
||||||
}
|
|
||||||
if (value >= 1.0f) {
|
|
||||||
return (float) M_PI / 2.0f;
|
|
||||||
}
|
|
||||||
return asinf(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Fast inverse square root
|
|
||||||
|
|
||||||
#ifndef FUSION_USE_NORMAL_SQRT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculates the reciprocal of the square root.
|
|
||||||
* See https://pizer.wordpress.com/2008/10/12/fast-inverse-square-root/
|
|
||||||
* @param x Operand.
|
|
||||||
* @return Reciprocal of the square root of x.
|
|
||||||
*/
|
|
||||||
static inline float FusionFastInverseSqrt(const float x) {
|
|
||||||
|
|
||||||
typedef union {
|
|
||||||
float f;
|
|
||||||
int32_t i;
|
|
||||||
} Union32;
|
|
||||||
|
|
||||||
Union32 union32 = {.f = x};
|
|
||||||
union32.i = 0x5F1F1412 - (union32.i >> 1);
|
|
||||||
return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Vector operations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns true if the vector is zero.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return True if the vector is zero.
|
|
||||||
*/
|
|
||||||
static inline bool FusionVectorIsZero(const FusionVector vector) {
|
|
||||||
return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the sum of two vectors.
|
|
||||||
* @param vectorA Vector A.
|
|
||||||
* @param vectorB Vector B.
|
|
||||||
* @return Sum of two vectors.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) {
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = vectorA.axis.x + vectorB.axis.x,
|
|
||||||
.y = vectorA.axis.y + vectorB.axis.y,
|
|
||||||
.z = vectorA.axis.z + vectorB.axis.z,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns vector B subtracted from vector A.
|
|
||||||
* @param vectorA Vector A.
|
|
||||||
* @param vectorB Vector B.
|
|
||||||
* @return Vector B subtracted from vector A.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) {
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = vectorA.axis.x - vectorB.axis.x,
|
|
||||||
.y = vectorA.axis.y - vectorB.axis.y,
|
|
||||||
.z = vectorA.axis.z - vectorB.axis.z,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the sum of the elements.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Sum of the elements.
|
|
||||||
*/
|
|
||||||
static inline float FusionVectorSum(const FusionVector vector) {
|
|
||||||
return vector.axis.x + vector.axis.y + vector.axis.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the multiplication of a vector by a scalar.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @param scalar Scalar.
|
|
||||||
* @return Multiplication of a vector by a scalar.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) {
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = vector.axis.x * scalar,
|
|
||||||
.y = vector.axis.y * scalar,
|
|
||||||
.z = vector.axis.z * scalar,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculates the Hadamard product (element-wise multiplication).
|
|
||||||
* @param vectorA Vector A.
|
|
||||||
* @param vectorB Vector B.
|
|
||||||
* @return Hadamard product.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) {
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = vectorA.axis.x * vectorB.axis.x,
|
|
||||||
.y = vectorA.axis.y * vectorB.axis.y,
|
|
||||||
.z = vectorA.axis.z * vectorB.axis.z,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the cross product.
|
|
||||||
* @param vectorA Vector A.
|
|
||||||
* @param vectorB Vector B.
|
|
||||||
* @return Cross product.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) {
|
|
||||||
#define A vectorA.axis
|
|
||||||
#define B vectorB.axis
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = A.y * B.z - A.z * B.y,
|
|
||||||
.y = A.z * B.x - A.x * B.z,
|
|
||||||
.z = A.x * B.y - A.y * B.x,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
#undef A
|
|
||||||
#undef B
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the dot product.
|
|
||||||
* @param vectorA Vector A.
|
|
||||||
* @param vectorB Vector B.
|
|
||||||
* @return Dot product.
|
|
||||||
*/
|
|
||||||
static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) {
|
|
||||||
return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the vector magnitude squared.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Vector magnitude squared.
|
|
||||||
*/
|
|
||||||
static inline float FusionVectorMagnitudeSquared(const FusionVector vector) {
|
|
||||||
return FusionVectorSum(FusionVectorHadamardProduct(vector, vector));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the vector magnitude.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Vector magnitude.
|
|
||||||
*/
|
|
||||||
static inline float FusionVectorMagnitude(const FusionVector vector) {
|
|
||||||
return sqrtf(FusionVectorMagnitudeSquared(vector));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the normalised vector.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Normalised vector.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionVectorNormalise(const FusionVector vector) {
|
|
||||||
#ifdef FUSION_USE_NORMAL_SQRT
|
|
||||||
const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector));
|
|
||||||
#else
|
|
||||||
const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector));
|
|
||||||
#endif
|
|
||||||
return FusionVectorMultiplyScalar(vector, magnitudeReciprocal);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Quaternion operations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the sum of two quaternions.
|
|
||||||
* @param quaternionA Quaternion A.
|
|
||||||
* @param quaternionB Quaternion B.
|
|
||||||
* @return Sum of two quaternions.
|
|
||||||
*/
|
|
||||||
static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) {
|
|
||||||
const FusionQuaternion result = {.element = {
|
|
||||||
.w = quaternionA.element.w + quaternionB.element.w,
|
|
||||||
.x = quaternionA.element.x + quaternionB.element.x,
|
|
||||||
.y = quaternionA.element.y + quaternionB.element.y,
|
|
||||||
.z = quaternionA.element.z + quaternionB.element.z,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the multiplication of two quaternions.
|
|
||||||
* @param quaternionA Quaternion A (to be post-multiplied).
|
|
||||||
* @param quaternionB Quaternion B (to be pre-multiplied).
|
|
||||||
* @return Multiplication of two quaternions.
|
|
||||||
*/
|
|
||||||
static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) {
|
|
||||||
#define A quaternionA.element
|
|
||||||
#define B quaternionB.element
|
|
||||||
const FusionQuaternion result = {.element = {
|
|
||||||
.w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z,
|
|
||||||
.x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y,
|
|
||||||
.y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x,
|
|
||||||
.z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
#undef A
|
|
||||||
#undef B
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the multiplication of a quaternion with a vector. This is a
|
|
||||||
* normal quaternion multiplication where the vector is treated a
|
|
||||||
* quaternion with a W element value of zero. The quaternion is post-
|
|
||||||
* multiplied by the vector.
|
|
||||||
* @param quaternion Quaternion.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Multiplication of a quaternion with a vector.
|
|
||||||
*/
|
|
||||||
static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) {
|
|
||||||
#define Q quaternion.element
|
|
||||||
#define V vector.axis
|
|
||||||
const FusionQuaternion result = {.element = {
|
|
||||||
.w = -Q.x * V.x - Q.y * V.y - Q.z * V.z,
|
|
||||||
.x = Q.w * V.x + Q.y * V.z - Q.z * V.y,
|
|
||||||
.y = Q.w * V.y - Q.x * V.z + Q.z * V.x,
|
|
||||||
.z = Q.w * V.z + Q.x * V.y - Q.y * V.x,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
#undef Q
|
|
||||||
#undef V
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the normalised quaternion.
|
|
||||||
* @param quaternion Quaternion.
|
|
||||||
* @return Normalised quaternion.
|
|
||||||
*/
|
|
||||||
static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) {
|
|
||||||
#define Q quaternion.element
|
|
||||||
#ifdef FUSION_USE_NORMAL_SQRT
|
|
||||||
const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z);
|
|
||||||
#else
|
|
||||||
const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z);
|
|
||||||
#endif
|
|
||||||
const FusionQuaternion result = {.element = {
|
|
||||||
.w = Q.w * magnitudeReciprocal,
|
|
||||||
.x = Q.x * magnitudeReciprocal,
|
|
||||||
.y = Q.y * magnitudeReciprocal,
|
|
||||||
.z = Q.z * magnitudeReciprocal,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Matrix operations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the multiplication of a matrix with a vector.
|
|
||||||
* @param matrix Matrix.
|
|
||||||
* @param vector Vector.
|
|
||||||
* @return Multiplication of a matrix with a vector.
|
|
||||||
*/
|
|
||||||
static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) {
|
|
||||||
#define R matrix.element
|
|
||||||
const FusionVector result = {.axis = {
|
|
||||||
.x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z,
|
|
||||||
.y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z,
|
|
||||||
.z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z,
|
|
||||||
}};
|
|
||||||
return result;
|
|
||||||
#undef R
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Inline functions - Conversion operations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a quaternion to a rotation matrix.
|
|
||||||
* @param quaternion Quaternion.
|
|
||||||
* @return Rotation matrix.
|
|
||||||
*/
|
|
||||||
static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) {
|
|
||||||
#define Q quaternion.element
|
|
||||||
const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations
|
|
||||||
const float qwqx = Q.w * Q.x;
|
|
||||||
const float qwqy = Q.w * Q.y;
|
|
||||||
const float qwqz = Q.w * Q.z;
|
|
||||||
const float qxqy = Q.x * Q.y;
|
|
||||||
const float qxqz = Q.x * Q.z;
|
|
||||||
const float qyqz = Q.y * Q.z;
|
|
||||||
const FusionMatrix matrix = {.element = {
|
|
||||||
.xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x),
|
|
||||||
.xy = 2.0f * (qxqy - qwqz),
|
|
||||||
.xz = 2.0f * (qxqz + qwqy),
|
|
||||||
.yx = 2.0f * (qxqy + qwqz),
|
|
||||||
.yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y),
|
|
||||||
.yz = 2.0f * (qyqz - qwqx),
|
|
||||||
.zx = 2.0f * (qxqz - qwqy),
|
|
||||||
.zy = 2.0f * (qyqz + qwqx),
|
|
||||||
.zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z),
|
|
||||||
}};
|
|
||||||
return matrix;
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a quaternion to ZYX Euler angles in degrees.
|
|
||||||
* @param quaternion Quaternion.
|
|
||||||
* @return Euler angles in degrees.
|
|
||||||
*/
|
|
||||||
static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) {
|
|
||||||
#define Q quaternion.element
|
|
||||||
const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations
|
|
||||||
const FusionEuler euler = {.angle = {
|
|
||||||
.roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)),
|
|
||||||
.pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))),
|
|
||||||
.yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)),
|
|
||||||
}};
|
|
||||||
return euler;
|
|
||||||
#undef Q
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,77 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionOffset.c
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Gyroscope offset correction algorithm for run-time calibration of the
|
|
||||||
* gyroscope offset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionOffset.h"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Cutoff frequency in Hz.
|
|
||||||
*/
|
|
||||||
#define CUTOFF_FREQUENCY (0.02f)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Timeout in seconds.
|
|
||||||
*/
|
|
||||||
#define TIMEOUT (5)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Threshold in degrees per second.
|
|
||||||
*/
|
|
||||||
#define THRESHOLD (3.0f)
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialises the gyroscope offset algorithm.
|
|
||||||
* @param offset Gyroscope offset algorithm structure.
|
|
||||||
* @param sampleRate Sample rate in Hz.
|
|
||||||
*/
|
|
||||||
void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) {
|
|
||||||
offset->filterCoefficient = 2.0f * (float) M_PI * CUTOFF_FREQUENCY * (1.0f / (float) sampleRate);
|
|
||||||
offset->timeout = TIMEOUT * sampleRate;
|
|
||||||
offset->timer = 0;
|
|
||||||
offset->gyroscopeOffset = FUSION_VECTOR_ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Updates the gyroscope offset algorithm and returns the corrected
|
|
||||||
* gyroscope measurement.
|
|
||||||
* @param offset Gyroscope offset algorithm structure.
|
|
||||||
* @param gyroscope Gyroscope measurement in degrees per second.
|
|
||||||
* @return Corrected gyroscope measurement in degrees per second.
|
|
||||||
*/
|
|
||||||
FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) {
|
|
||||||
|
|
||||||
// Subtract offset from gyroscope measurement
|
|
||||||
gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset);
|
|
||||||
|
|
||||||
// Reset timer if gyroscope not stationary
|
|
||||||
if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) {
|
|
||||||
offset->timer = 0;
|
|
||||||
return gyroscope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment timer while gyroscope stationary
|
|
||||||
if (offset->timer < offset->timeout) {
|
|
||||||
offset->timer++;
|
|
||||||
return gyroscope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust offset if timer has elapsed
|
|
||||||
offset->gyroscopeOffset = FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient));
|
|
||||||
return gyroscope;
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,40 +0,0 @@
|
||||||
/**
|
|
||||||
* @file FusionOffset.h
|
|
||||||
* @author Seb Madgwick
|
|
||||||
* @brief Gyroscope offset correction algorithm for run-time calibration of the
|
|
||||||
* gyroscope offset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FUSION_OFFSET_H
|
|
||||||
#define FUSION_OFFSET_H
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Includes
|
|
||||||
|
|
||||||
#include "FusionMath.h"
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Gyroscope offset algorithm structure. Structure members are used
|
|
||||||
* internally and must not be accessed by the application.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
float filterCoefficient;
|
|
||||||
unsigned int timeout;
|
|
||||||
unsigned int timer;
|
|
||||||
FusionVector gyroscopeOffset;
|
|
||||||
} FusionOffset;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Function declarations
|
|
||||||
|
|
||||||
void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate);
|
|
||||||
|
|
||||||
FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// End of file
|
|
|
@ -1,102 +0,0 @@
|
||||||
//
|
|
||||||
// Created by thejackimonster on 21.04.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2024 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "crc32.h"
|
|
||||||
|
|
||||||
const uint32_t crc32_table [256] = {
|
|
||||||
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
|
|
||||||
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
|
|
||||||
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
|
||||||
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
|
|
||||||
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
|
|
||||||
0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
|
|
||||||
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
|
|
||||||
0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
|
|
||||||
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
|
|
||||||
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
|
|
||||||
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
|
|
||||||
0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
|
|
||||||
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
|
|
||||||
0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
|
|
||||||
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
|
|
||||||
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
|
|
||||||
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
|
|
||||||
0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
|
|
||||||
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
|
|
||||||
0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
|
|
||||||
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
|
|
||||||
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
|
|
||||||
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
|
|
||||||
0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
|
|
||||||
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
|
|
||||||
0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
|
|
||||||
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
|
|
||||||
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
|
|
||||||
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
|
|
||||||
0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
|
|
||||||
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
|
|
||||||
0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
|
|
||||||
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
|
|
||||||
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
|
|
||||||
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
|
|
||||||
0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
|
|
||||||
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
|
|
||||||
0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
|
|
||||||
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
|
|
||||||
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
|
|
||||||
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
|
|
||||||
0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
|
|
||||||
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
|
|
||||||
0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
|
|
||||||
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
|
|
||||||
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
|
|
||||||
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
|
|
||||||
0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
|
|
||||||
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
|
|
||||||
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
|
|
||||||
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
|
|
||||||
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
|
|
||||||
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
|
|
||||||
0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
|
|
||||||
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
|
|
||||||
0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
|
|
||||||
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
|
|
||||||
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
|
|
||||||
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
|
|
||||||
0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
|
|
||||||
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
|
|
||||||
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
|
|
||||||
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
|
||||||
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t crc32_checksum(const uint8_t* buf, uint32_t len) {
|
|
||||||
uint32_t CRC32_data = 0xFFFFFFFF;
|
|
||||||
for (uint32_t i = 0; i < len; i++) {
|
|
||||||
const uint8_t t = (CRC32_data ^ buf[i]) & 0xFF;
|
|
||||||
CRC32_data = ((CRC32_data >> 8) & 0xFFFFFF) ^ crc32_table[t];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ~CRC32_data;
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
#pragma once
|
|
||||||
//
|
|
||||||
// Created by thejackimonster on 21.04.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
uint32_t crc32_checksum(const uint8_t* buf, uint32_t len);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} // extern "C"
|
|
||||||
#endif
|
|
|
@ -1,50 +0,0 @@
|
||||||
//
|
|
||||||
// Created by thejackimonster on 19.12.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "device.h"
|
|
||||||
|
|
||||||
#include <hidapi/hidapi.h>
|
|
||||||
|
|
||||||
static size_t hid_device_counter = 0;
|
|
||||||
|
|
||||||
bool device_init() {
|
|
||||||
if ((!hid_device_counter) && (0 != hid_init())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hid_device_counter++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void device_exit() {
|
|
||||||
if (!hid_device_counter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hid_device_counter--;
|
|
||||||
|
|
||||||
if (0 == hid_device_counter) {
|
|
||||||
hid_exit();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
#pragma once
|
|
||||||
//
|
|
||||||
// Created by thejackimonster on 19.12.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdbool.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool device_init();
|
|
||||||
|
|
||||||
void device_exit();
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} // extern "C"
|
|
||||||
#endif
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,227 +0,0 @@
|
||||||
#pragma once
|
|
||||||
//
|
|
||||||
// Created by thejackimonster on 30.03.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2025 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdbool.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define DEVICE_IMU_MSG_GET_CAL_DATA_LENGTH 0x14
|
|
||||||
#define DEVICE_IMU_MSG_CAL_DATA_GET_NEXT_SEGMENT 0x15
|
|
||||||
#define DEVICE_IMU_MSG_ALLOCATE_CAL_DATA_BUFFER 0x16
|
|
||||||
#define DEVICE_IMU_MSG_WRITE_CAL_DATA_SEGMENT 0x17
|
|
||||||
#define DEVICE_IMU_MSG_FREE_CAL_BUFFER 0x18
|
|
||||||
#define DEVICE_IMU_MSG_START_IMU_DATA 0x19
|
|
||||||
#define DEVICE_IMU_MSG_GET_STATIC_ID 0x1A
|
|
||||||
#define DEVICE_IMU_MSG_UNKNOWN 0x1D
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
enum device_imu_error_t {
|
|
||||||
DEVICE_IMU_ERROR_NO_ERROR = 0,
|
|
||||||
DEVICE_IMU_ERROR_NO_DEVICE = 1,
|
|
||||||
DEVICE_IMU_ERROR_NO_HANDLE = 2,
|
|
||||||
DEVICE_IMU_ERROR_NO_ALLOCATION = 3,
|
|
||||||
DEVICE_IMU_ERROR_WRONG_SIZE = 4,
|
|
||||||
DEVICE_IMU_ERROR_FILE_NOT_OPEN = 5,
|
|
||||||
DEVICE_IMU_ERROR_FILE_NOT_CLOSED = 6,
|
|
||||||
DEVICE_IMU_ERROR_LOADING_FAILED = 7,
|
|
||||||
DEVICE_IMU_ERROR_SAVING_FAILED = 8,
|
|
||||||
DEVICE_IMU_ERROR_UNPLUGGED = 9,
|
|
||||||
DEVICE_IMU_ERROR_UNEXPECTED = 10,
|
|
||||||
DEVICE_IMU_ERROR_WRONG_SIGNATURE = 11,
|
|
||||||
DEVICE_IMU_ERROR_INVALID_VALUE = 12,
|
|
||||||
DEVICE_IMU_ERROR_NOT_INITIALIZED = 13,
|
|
||||||
DEVICE_IMU_ERROR_PAYLOAD_FAILED = 14,
|
|
||||||
DEVICE_IMU_ERROR_UNKNOWN = 15,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct __attribute__((__packed__)) device_imu_packet_t {
|
|
||||||
uint8_t signature [2];
|
|
||||||
uint8_t temperature [2];
|
|
||||||
uint64_t timestamp;
|
|
||||||
uint8_t angular_multiplier [2];
|
|
||||||
uint8_t angular_divisor [4];
|
|
||||||
uint8_t angular_velocity_x [3];
|
|
||||||
uint8_t angular_velocity_y [3];
|
|
||||||
uint8_t angular_velocity_z [3];
|
|
||||||
uint8_t acceleration_multiplier [2];
|
|
||||||
uint8_t acceleration_divisor [4];
|
|
||||||
uint8_t acceleration_x [3];
|
|
||||||
uint8_t acceleration_y [3];
|
|
||||||
uint8_t acceleration_z [3];
|
|
||||||
uint8_t magnetic_multiplier [2];
|
|
||||||
uint8_t magnetic_divisor [4];
|
|
||||||
uint8_t magnetic_x [2];
|
|
||||||
uint8_t magnetic_y [2];
|
|
||||||
uint8_t magnetic_z [2];
|
|
||||||
uint32_t checksum;
|
|
||||||
uint8_t _padding [6];
|
|
||||||
};
|
|
||||||
|
|
||||||
enum device_imu_event_t {
|
|
||||||
DEVICE_IMU_EVENT_UNKNOWN = 0,
|
|
||||||
DEVICE_IMU_EVENT_INIT = 1,
|
|
||||||
DEVICE_IMU_EVENT_UPDATE = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_ahrs_t;
|
|
||||||
struct device_imu_camera_sensor_t;
|
|
||||||
struct device_imu_camera_t;
|
|
||||||
struct device_imu_camera_calibration_t;
|
|
||||||
struct device_imu_calibration_t;
|
|
||||||
|
|
||||||
struct device_imu_vec2_t {
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_vec3_t {
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
float z;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_quat_t {
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
float z;
|
|
||||||
float w;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_euler_t {
|
|
||||||
float roll;
|
|
||||||
float pitch;
|
|
||||||
float yaw;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_mat3x3_t {
|
|
||||||
float m [9];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct device_imu_size_t {
|
|
||||||
uint16_t width;
|
|
||||||
uint16_t height;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef enum device_imu_error_t device_imu_error_type;
|
|
||||||
typedef struct device_imu_packet_t device_imu_packet_type;
|
|
||||||
typedef enum device_imu_event_t device_imu_event_type;
|
|
||||||
|
|
||||||
typedef struct device_imu_ahrs_t device_imu_ahrs_type;
|
|
||||||
|
|
||||||
typedef struct device_imu_camera_sensor_t device_imu_camera_sensor_type;
|
|
||||||
typedef struct device_imu_camera_t device_imu_camera_type;
|
|
||||||
typedef struct device_imu_camera_calibration_t device_imu_camera_calibration_type;
|
|
||||||
typedef struct device_imu_calibration_t device_imu_calibration_type;
|
|
||||||
|
|
||||||
typedef struct device_imu_vec2_t device_imu_vec2_type;
|
|
||||||
typedef struct device_imu_vec3_t device_imu_vec3_type;
|
|
||||||
typedef struct device_imu_quat_t device_imu_quat_type;
|
|
||||||
typedef struct device_imu_euler_t device_imu_euler_type;
|
|
||||||
typedef struct device_imu_mat3x3_t device_imu_mat3x3_type;
|
|
||||||
typedef struct device_imu_size_t device_imu_size_type;
|
|
||||||
|
|
||||||
typedef void (*device_imu_event_callback)(
|
|
||||||
uint64_t timestamp,
|
|
||||||
device_imu_event_type event,
|
|
||||||
const device_imu_ahrs_type* ahrs
|
|
||||||
);
|
|
||||||
|
|
||||||
struct device_imu_t {
|
|
||||||
uint16_t vendor_id;
|
|
||||||
uint16_t product_id;
|
|
||||||
|
|
||||||
void* handle;
|
|
||||||
uint16_t max_payload_size;
|
|
||||||
|
|
||||||
uint32_t static_id;
|
|
||||||
|
|
||||||
uint64_t last_timestamp;
|
|
||||||
float temperature; // (in °C)
|
|
||||||
|
|
||||||
void* offset;
|
|
||||||
device_imu_ahrs_type* ahrs;
|
|
||||||
|
|
||||||
device_imu_event_callback callback;
|
|
||||||
device_imu_calibration_type* calibration;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct device_imu_t device_imu_type;
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_open(device_imu_type* device, device_imu_event_callback callback);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_reset_calibration(device_imu_type* device);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_load_calibration(device_imu_type* device, const char* path);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_save_calibration(device_imu_type* device, const char* path);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_clear(device_imu_type* device);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_calibrate(device_imu_type* device, uint32_t iterations, bool gyro, bool accel, bool magnet);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_read(device_imu_type* device, int timeout);
|
|
||||||
|
|
||||||
device_imu_vec3_type device_imu_get_earth_acceleration(const device_imu_ahrs_type* ahrs);
|
|
||||||
|
|
||||||
device_imu_vec3_type device_imu_get_linear_acceleration(const device_imu_ahrs_type* ahrs);
|
|
||||||
|
|
||||||
device_imu_quat_type device_imu_get_orientation(const device_imu_ahrs_type* ahrs);
|
|
||||||
|
|
||||||
device_imu_euler_type device_imu_get_euler(device_imu_quat_type quat);
|
|
||||||
|
|
||||||
uint32_t device_imu_get_num_of_cameras(device_imu_type *device);
|
|
||||||
|
|
||||||
const device_imu_camera_type* device_imu_get_camera(const device_imu_type *device, uint32_t index);
|
|
||||||
|
|
||||||
uint32_t device_imu_camera_get_num_of_sensors(const device_imu_camera_type *camera);
|
|
||||||
|
|
||||||
const device_imu_camera_sensor_type* device_imu_camera_get_sensor(const device_imu_camera_type *camera, uint32_t index);
|
|
||||||
|
|
||||||
device_imu_mat3x3_type device_imu_sensor_get_rotation(const device_imu_camera_sensor_type *sensor);
|
|
||||||
|
|
||||||
device_imu_vec3_type device_imu_sensor_get_position(const device_imu_camera_sensor_type *sensor);
|
|
||||||
|
|
||||||
device_imu_size_type device_imu_sensor_get_resolution(const device_imu_camera_sensor_type *sensor);
|
|
||||||
|
|
||||||
device_imu_vec2_type device_imu_sensor_get_cc(const device_imu_camera_sensor_type *sensor);
|
|
||||||
|
|
||||||
device_imu_vec2_type device_imu_sensor_get_fc(const device_imu_camera_sensor_type *sensor);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_sensor_get_kc(const device_imu_camera_sensor_type *sensor, uint32_t *num_kc, float *kc);
|
|
||||||
|
|
||||||
device_imu_error_type device_imu_close(device_imu_type* device);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} // extern "C"
|
|
||||||
#endif
|
|
|
@ -1,731 +0,0 @@
|
||||||
//
|
|
||||||
// Created by thejackimonster on 29.03.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2024 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "device_mcu.h"
|
|
||||||
#include "device.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
#include <hidapi/hidapi.h>
|
|
||||||
|
|
||||||
#include "crc32.h"
|
|
||||||
#include "hid_ids.h"
|
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
#define device_mcu_error(msg) fprintf(stderr, "ERROR: %s\n", msg)
|
|
||||||
#else
|
|
||||||
#define device_mcu_error(msg) (0)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define device_mcu_warning(msg) device_mcu_error(msg)
|
|
||||||
|
|
||||||
#define MAX_PACKET_SIZE 64
|
|
||||||
#define PACKET_HEAD 0xFD
|
|
||||||
|
|
||||||
static bool send_payload(device_mcu_type* device, uint8_t size, const uint8_t* payload) {
|
|
||||||
int payload_size = size;
|
|
||||||
if (payload_size > MAX_PACKET_SIZE) {
|
|
||||||
payload_size = MAX_PACKET_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
int transferred = hid_write(device->handle, payload, payload_size);
|
|
||||||
|
|
||||||
if (transferred != payload_size) {
|
|
||||||
device_mcu_error("Sending payload failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (transferred == size);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool recv_payload(device_mcu_type* device, uint8_t size, uint8_t* payload) {
|
|
||||||
int payload_size = size;
|
|
||||||
if (payload_size > MAX_PACKET_SIZE) {
|
|
||||||
payload_size = MAX_PACKET_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
int transferred = hid_read(device->handle, payload, payload_size);
|
|
||||||
|
|
||||||
if (transferred >= payload_size) {
|
|
||||||
transferred = payload_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferred == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferred != payload_size) {
|
|
||||||
device_mcu_error("Receiving payload failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (transferred == size);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool send_payload_action(device_mcu_type* device, uint16_t msgid, uint8_t len, const uint8_t* data) {
|
|
||||||
static device_mcu_packet_type packet;
|
|
||||||
|
|
||||||
const uint16_t packet_len = 17 + len;
|
|
||||||
const uint16_t payload_len = 5 + packet_len;
|
|
||||||
|
|
||||||
packet.head = PACKET_HEAD;
|
|
||||||
packet.length = htole16(packet_len);
|
|
||||||
packet.timestamp = htole64(0);
|
|
||||||
packet.msgid = htole16(msgid);
|
|
||||||
memset(packet.reserved, 0, 5);
|
|
||||||
|
|
||||||
memcpy(packet.data, data, len);
|
|
||||||
packet.checksum = htole32(
|
|
||||||
crc32_checksum(
|
|
||||||
(const uint8_t*) (&packet.length),
|
|
||||||
packet.length
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return send_payload(device, payload_len, (uint8_t*) (&packet));
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool recv_payload_msg(device_mcu_type* device, uint16_t msgid, uint8_t len, uint8_t* data) {
|
|
||||||
static device_mcu_packet_type packet;
|
|
||||||
|
|
||||||
packet.head = 0;
|
|
||||||
packet.length = 0;
|
|
||||||
packet.msgid = 0;
|
|
||||||
|
|
||||||
const uint16_t packet_len = 18 + len;
|
|
||||||
const uint16_t payload_len = 5 + packet_len;
|
|
||||||
|
|
||||||
if (!recv_payload(device, payload_len, (uint8_t*) (&packet))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packet.head != PACKET_HEAD) {
|
|
||||||
device_mcu_error("Invalid payload received");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (le16toh(packet.msgid) != msgid) {
|
|
||||||
device_mcu_error("Unexpected payload received");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t status = packet.data[0];
|
|
||||||
|
|
||||||
if (status != 0) {
|
|
||||||
device_mcu_error("Payload status failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint16_t data_len = le16toh(packet.length) - 18;
|
|
||||||
|
|
||||||
if (len <= data_len) {
|
|
||||||
memcpy(data, packet.data + 1, len);
|
|
||||||
} else {
|
|
||||||
memcpy(data, packet.data + 1, data_len);
|
|
||||||
memset(data + data_len, 0, len - data_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool do_payload_action(device_mcu_type* device, uint16_t msgid, uint8_t len, const uint8_t* data) {
|
|
||||||
if (!send_payload_action(device, msgid, len, data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint16_t attempts_per_second = (device->active? 60 : 1);
|
|
||||||
|
|
||||||
uint16_t attempts = attempts_per_second * 5;
|
|
||||||
|
|
||||||
while (attempts > 0) {
|
|
||||||
if (recv_payload_msg(device, msgid, 0, NULL)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_open(device_mcu_type* device, device_mcu_event_callback callback) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(device, 0, sizeof(device_mcu_type));
|
|
||||||
device->vendor_id = xreal_vendor_id;
|
|
||||||
device->product_id = 0;
|
|
||||||
device->callback = callback;
|
|
||||||
|
|
||||||
if (!device_init()) {
|
|
||||||
device_mcu_error("Not initialized");
|
|
||||||
return DEVICE_MCU_ERROR_NOT_INITIALIZED;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct hid_device_info* info = hid_enumerate(
|
|
||||||
device->vendor_id,
|
|
||||||
device->product_id
|
|
||||||
);
|
|
||||||
|
|
||||||
struct hid_device_info* it = info;
|
|
||||||
while (it) {
|
|
||||||
int interface_id = xreal_mcu_interface_id(it->product_id);
|
|
||||||
if (interface_id != -1 && it->interface_number == interface_id) {
|
|
||||||
#ifndef NDEBUG
|
|
||||||
printf("Found MCU device with product_id 0x%x on interface %d\n", it->product_id, interface_id);
|
|
||||||
#endif
|
|
||||||
device->product_id = it->product_id;
|
|
||||||
device->handle = hid_open_path(it->path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
it = it->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
hid_free_enumeration(info);
|
|
||||||
|
|
||||||
if (!device->handle) {
|
|
||||||
device_mcu_error("No handle");
|
|
||||||
return DEVICE_MCU_ERROR_NO_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_clear(device);
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_ACTIVATION_TIME, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting activation time failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t activated;
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_ACTIVATION_TIME, 1, &activated)) {
|
|
||||||
device_mcu_error("Receiving activation time failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->activated = (activated != 0);
|
|
||||||
|
|
||||||
if (!device->activated) {
|
|
||||||
device_mcu_warning("Device is not activated");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_MCU_APP_FW_VERSION, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting current MCU app firmware version");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_MCU_APP_FW_VERSION, 41, (uint8_t*) device->mcu_app_fw_version)) {
|
|
||||||
device_mcu_error("Receiving current MCU app firmware version failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_DP7911_FW_VERSION, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting current DP firmware version");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_DP7911_FW_VERSION, 41, (uint8_t*) device->dp_fw_version)) {
|
|
||||||
device_mcu_error("Receiving current DP firmware version failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_DSP_APP_FW_VERSION, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting current DSP app firmware version");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_DSP_APP_FW_VERSION, 41, (uint8_t*) device->dsp_fw_version)) {
|
|
||||||
device_mcu_error("Receiving current DSP app firmware version failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
printf("MCU: %s\n", device->mcu_app_fw_version);
|
|
||||||
printf("DP: %s\n", device->dp_fw_version);
|
|
||||||
printf("DSP: %s\n", device->dsp_fw_version);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_BRIGHTNESS, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting initial brightness failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_BRIGHTNESS, 1, &device->brightness)) {
|
|
||||||
device_mcu_error("Receiving initial brightness failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_DISP_MODE, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting display mode failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_DISP_MODE, 1, &device->disp_mode)) {
|
|
||||||
device_mcu_error("Receiving display mode failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
printf("Brightness: %d\n", device->brightness);
|
|
||||||
printf("Disp-Mode: %d\n", device->disp_mode);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void device_mcu_callback(device_mcu_type* device,
|
|
||||||
uint64_t timestamp,
|
|
||||||
device_mcu_event_type event,
|
|
||||||
uint8_t brightness,
|
|
||||||
const char* msg) {
|
|
||||||
if (!device->callback) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->callback(timestamp, event, brightness, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_clear(device_mcu_type* device) {
|
|
||||||
return device_mcu_read(device, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_read(device_mcu_type* device, int timeout) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device->handle) {
|
|
||||||
device_mcu_error("No handle");
|
|
||||||
return DEVICE_MCU_ERROR_NO_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MAX_PACKET_SIZE != sizeof(device_mcu_packet_type)) {
|
|
||||||
device_mcu_error("Not proper size");
|
|
||||||
return DEVICE_MCU_ERROR_WRONG_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_packet_type packet;
|
|
||||||
memset(&packet, 0, sizeof(device_mcu_packet_type));
|
|
||||||
|
|
||||||
int transferred = hid_read_timeout(
|
|
||||||
device->handle,
|
|
||||||
(uint8_t*) &packet,
|
|
||||||
MAX_PACKET_SIZE,
|
|
||||||
timeout
|
|
||||||
);
|
|
||||||
|
|
||||||
if (transferred == -1) {
|
|
||||||
device_mcu_error("Device may be unplugged");
|
|
||||||
return DEVICE_MCU_ERROR_UNPLUGGED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferred == 0) {
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MAX_PACKET_SIZE != transferred) {
|
|
||||||
device_mcu_error("Unexpected packet size");
|
|
||||||
return DEVICE_MCU_ERROR_UNEXPECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packet.head != PACKET_HEAD) {
|
|
||||||
device_mcu_error("Wrong packet head");
|
|
||||||
return DEVICE_MCU_ERROR_WRONG_HEAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint32_t timestamp = le32toh(packet.timestamp);
|
|
||||||
const uint16_t msgid = le16toh(packet.msgid);
|
|
||||||
const uint16_t length = le16toh(packet.length);
|
|
||||||
|
|
||||||
const size_t data_len = (size_t) &(packet.data) - (size_t) &(packet.length);
|
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
printf("MSG: %d = %04x (%d)\n", msgid, msgid, length);
|
|
||||||
|
|
||||||
if (length > 11) {
|
|
||||||
for (int i = 0; i < length - 11; i++) {
|
|
||||||
printf("%02x ", packet.data[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
switch (msgid) {
|
|
||||||
case DEVICE_MCU_MSG_P_START_HEARTBEAT: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DEVICE_MCU_MSG_P_DISPLAY_TOGGLED: {
|
|
||||||
const uint8_t value = packet.data[0];
|
|
||||||
|
|
||||||
device->active = value;
|
|
||||||
|
|
||||||
if (device->active) {
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_ON,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_OFF,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DEVICE_MCU_MSG_P_BUTTON_PRESSED: {
|
|
||||||
const uint8_t phys_button = packet.data[0];
|
|
||||||
const uint8_t virt_button = packet.data[4];
|
|
||||||
const uint8_t value = packet.data[8];
|
|
||||||
|
|
||||||
switch (virt_button) {
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_DISPLAY_TOGGLE:
|
|
||||||
device->active = value;
|
|
||||||
|
|
||||||
if (device->active) {
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_ON,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_OFF,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_BRIGHTNESS_UP:
|
|
||||||
device->brightness = value;
|
|
||||||
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_BRIGHTNESS_UP,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_BRIGHTNESS_DOWN:
|
|
||||||
device->brightness = value;
|
|
||||||
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_BRIGHTNESS_DOWN,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_UP:
|
|
||||||
if (device->control_mode == DEVICE_MCU_CONTROL_MODE_VOLUME)
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_VOLUME_UP,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_DOWN:
|
|
||||||
if (device->control_mode == DEVICE_MCU_CONTROL_MODE_VOLUME)
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_VOLUME_DOWN,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_MODE_2D:
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_DISPLAY_MODE_2D,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_MODE_3D:
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_DISPLAY_MODE_3D,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_BLEND_CYCLE:
|
|
||||||
device->blend_state = value;
|
|
||||||
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_BLEND_CYCLE,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DEVICE_MCU_BUTTON_VIRT_CONTROL_TOGGLE:
|
|
||||||
device->control_mode = value;
|
|
||||||
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_CONTROL_TOGGLE,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DEVICE_MCU_MSG_P_ASYNC_TEXT_LOG: {
|
|
||||||
const char* text = packet.text;
|
|
||||||
const size_t text_len = strlen(text);
|
|
||||||
|
|
||||||
device->active = true;
|
|
||||||
|
|
||||||
if (data_len + text_len != length) {
|
|
||||||
device_mcu_error("Not matching length");
|
|
||||||
return DEVICE_MCU_ERROR_INVALID_LENGTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_MESSAGE,
|
|
||||||
device->brightness,
|
|
||||||
text
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DEVICE_MCU_MSG_P_END_HEARTBEAT: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
device_mcu_callback(
|
|
||||||
device,
|
|
||||||
timestamp,
|
|
||||||
DEVICE_MCU_EVENT_UNKNOWN,
|
|
||||||
device->brightness,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_poll_display_mode(device_mcu_type* device) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device->handle) {
|
|
||||||
device_mcu_error("No handle");
|
|
||||||
return DEVICE_MCU_ERROR_NO_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_DISP_MODE, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting display mode failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_DISP_MODE, 1, &device->disp_mode)) {
|
|
||||||
device_mcu_error("Receiving display mode failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_update_display_mode(device_mcu_type* device) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device->handle) {
|
|
||||||
device_mcu_error("No handle");
|
|
||||||
return DEVICE_MCU_ERROR_NO_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!do_payload_action(device, DEVICE_MCU_MSG_W_DISP_MODE, 1, &device->disp_mode)) {
|
|
||||||
device_mcu_error("Sending display mode failed");
|
|
||||||
return DEVICE_MCU_ERROR_PAYLOAD_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_update_firmware(device_mcu_type* device, const char* path) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device->handle) {
|
|
||||||
device_mcu_error("No handle");
|
|
||||||
return DEVICE_MCU_ERROR_NO_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device->activated) {
|
|
||||||
device_mcu_error("Device is not activated");
|
|
||||||
return DEVICE_MCU_ERROR_NO_ACTIVATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t firmware_len = 0;
|
|
||||||
uint8_t* firmware = NULL;
|
|
||||||
|
|
||||||
FILE* file = fopen(path, "rb");
|
|
||||||
bool result = DEVICE_MCU_ERROR_UNKNOWN;
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
fseek(file, 0, SEEK_END);
|
|
||||||
firmware_len = ftell(file);
|
|
||||||
|
|
||||||
if (firmware_len > 0) {
|
|
||||||
firmware = (uint8_t*) malloc(firmware_len);
|
|
||||||
fread(firmware, sizeof(uint8_t), firmware_len, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_clear(device);
|
|
||||||
|
|
||||||
printf("Prepare upload: %lu\n", firmware_len);
|
|
||||||
|
|
||||||
if (!do_payload_action(device, DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_PREPARE, 0, NULL)) {
|
|
||||||
device_mcu_error("Failed preparing the device for MCU firmware update!\n");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!do_payload_action(device, DEVICE_MCU_MSG_W_MCU_APP_JUMP_TO_BOOT, 0, NULL)) {
|
|
||||||
device_mcu_error("Failed mcu app jumping to boot");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!send_payload_action(device, DEVICE_MCU_MSG_R_ACTIVATION_TIME, 0, NULL)) {
|
|
||||||
device_mcu_error("Requesting activation time failed");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t activated;
|
|
||||||
if (!recv_payload_msg(device, DEVICE_MCU_MSG_R_ACTIVATION_TIME, 1, &activated)) {
|
|
||||||
device_mcu_error("Receiving activation time failed");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activated) {
|
|
||||||
device_mcu_error("Device is not activated");
|
|
||||||
goto jump_to_app;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t offset = 0;
|
|
||||||
|
|
||||||
while (offset < firmware_len) {
|
|
||||||
const size_t remaining = firmware_len - offset;
|
|
||||||
|
|
||||||
printf("Upload: %lu / %lu\n", offset, firmware_len);
|
|
||||||
|
|
||||||
uint8_t len;
|
|
||||||
uint16_t msgid;
|
|
||||||
if (offset == 0) {
|
|
||||||
len = remaining > 24? 24 : remaining;
|
|
||||||
msgid = DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_START;
|
|
||||||
} else {
|
|
||||||
len = remaining > 42? 42 : remaining;
|
|
||||||
msgid = DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_TRANSMIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!do_payload_action(device, msgid, len, firmware + offset)) {
|
|
||||||
device_mcu_error("Failed sending firmware upload");
|
|
||||||
goto jump_to_app;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Finish upload");
|
|
||||||
|
|
||||||
if (!do_payload_action(device, DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_FINISH, 0, NULL)) {
|
|
||||||
device_mcu_error("Failed finishing firmware upload");
|
|
||||||
goto jump_to_app;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
|
|
||||||
jump_to_app:
|
|
||||||
if (!do_payload_action(device, DEVICE_MCU_MSG_W_BOOT_JUMP_TO_APP, 0, NULL)) {
|
|
||||||
device_mcu_error("Failed boot jumping back to app");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
if (firmware) {
|
|
||||||
free(firmware);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_close(device_mcu_type* device) {
|
|
||||||
if (!device) {
|
|
||||||
device_mcu_error("No device");
|
|
||||||
return DEVICE_MCU_ERROR_NO_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device->handle) {
|
|
||||||
hid_close(device->handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(device, 0, sizeof(device_mcu_type));
|
|
||||||
device_exit();
|
|
||||||
|
|
||||||
return DEVICE_MCU_ERROR_NO_ERROR;
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
#pragma once
|
|
||||||
//
|
|
||||||
// Created by thejackimonster on 29.03.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2024 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdbool.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_R_BRIGHTNESS 0x03
|
|
||||||
#define DEVICE_MCU_MSG_W_BRIGHTNESS 0x04
|
|
||||||
#define DEVICE_MCU_MSG_R_DISP_MODE 0x07
|
|
||||||
#define DEVICE_MCU_MSG_W_DISP_MODE 0x08
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_R_GLASSID 0x15
|
|
||||||
#define DEVICE_MCU_MSG_R_DP7911_FW_VERSION 0x16
|
|
||||||
#define DEVICE_MCU_MSG_R_DSP_VERSION 0x18
|
|
||||||
#define DEVICE_MCU_MSG_W_CANCEL_ACTIVATION 0x19
|
|
||||||
#define DEVICE_MCU_MSG_P_HEARTBEAT 0x1A
|
|
||||||
#define DEVICE_MCU_MSG_W_SLEEP_TIME 0x1E
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_R_DSP_APP_FW_VERSION 0x21
|
|
||||||
#define DEVICE_MCU_MSG_R_MCU_APP_FW_VERSION 0x26
|
|
||||||
#define DEVICE_MCU_MSG_R_ACTIVATION_TIME 0x29
|
|
||||||
#define DEVICE_MCU_MSG_W_ACTIVATION_TIME 0x2A
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_R_DP7911_FW_IS_UPDATE 0x3C
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_DP 0x3D
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_PREPARE 0x3E
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_START 0x3F
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_TRANSMIT 0x40
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_MCU_APP_FW_FINISH 0x41
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_JUMP_TO_APP 0x42
|
|
||||||
#define DEVICE_MCU_MSG_W_MCU_APP_JUMP_TO_BOOT 0x44
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_DSP_APP_FW_PREPARE 0x45
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_DSP_APP_FW_START 0x46
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_DSP_APP_FW_TRANSMIT 0x47
|
|
||||||
#define DEVICE_MCU_MSG_W_UPDATE_DSP_APP_FW_FINISH 0x48
|
|
||||||
#define DEVICE_MCU_MSG_R_IS_NEED_UPGRADE_DSP_FW 0x49
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_W_FORCE_UPGRADE_DSP_FW 0x69
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_MODE 0x1100
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_CONFIRM 0x1101
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_PREPARE 0x1102
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_START 0x1103
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_TRANSMIT 0x1104
|
|
||||||
#define DEVICE_MCU_MSG_W_BOOT_UPDATE_FINISH 0x1105
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_P_START_HEARTBEAT 0x6c02
|
|
||||||
#define DEVICE_MCU_MSG_P_DISPLAY_TOGGLED 0x6C04
|
|
||||||
#define DEVICE_MCU_MSG_P_BUTTON_PRESSED 0x6C05
|
|
||||||
#define DEVICE_MCU_MSG_P_END_HEARTBEAT 0x6c12
|
|
||||||
#define DEVICE_MCU_MSG_P_ASYNC_TEXT_LOG 0x6c09
|
|
||||||
|
|
||||||
#define DEVICE_MCU_MSG_E_DSP_ONE_PACKGE_WRITE_FINISH 0x6C0E
|
|
||||||
#define DEVICE_MCU_MSG_E_DSP_UPDATE_PROGRES 0x6C10
|
|
||||||
#define DEVICE_MCU_MSG_E_DSP_UPDATE_ENDING 0x6C11
|
|
||||||
|
|
||||||
#define DEVICE_MCU_BUTTON_PHYS_DISPLAY_TOGGLE 0x1
|
|
||||||
#define DEVICE_MCU_BUTTON_PHYS_BRIGHTNESS_UP 0x2
|
|
||||||
#define DEVICE_MCU_BUTTON_PHYS_BRIGHTNESS_DOWN 0x3
|
|
||||||
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_1920x1080_60 0x1
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_3840x1080_60_SBS 0x3
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_3840x1080_72_SBS 0x4
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_1920x1080_72 0x5
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_1920x1080_60_SBS 0x8
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_3840x1080_90_SBS 0x9
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_1920x1080_90 0xA
|
|
||||||
#define DEVICE_MCU_DISPLAY_MODE_1920x1080_120 0xB
|
|
||||||
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_DISPLAY_TOGGLE 0x1
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_BRIGHTNESS_UP 0x6
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_BRIGHTNESS_DOWN 0x7
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_UP 0x8
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_DOWN 0x9
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_MODE_2D 0xA
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_MODE_3D 0xB
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_BLEND_CYCLE 0xC
|
|
||||||
#define DEVICE_MCU_BUTTON_VIRT_CONTROL_TOGGLE 0xF
|
|
||||||
|
|
||||||
#define DEVICE_MCU_BLEND_STATE_LOW 0x0
|
|
||||||
#define DEVICE_MCU_BLEND_STATE_MEDIUM 0x2
|
|
||||||
#define DEVICE_MCU_BLEND_STATE_FULL 0x3
|
|
||||||
|
|
||||||
#define DEVICE_MCU_CONTROL_MODE_BRIGHTNESS 0x0
|
|
||||||
#define DEVICE_MCU_CONTROL_MODE_VOLUME 0x1
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
enum device_mcu_error_t {
|
|
||||||
DEVICE_MCU_ERROR_NO_ERROR = 0,
|
|
||||||
DEVICE_MCU_ERROR_NO_DEVICE = 1,
|
|
||||||
DEVICE_MCU_ERROR_NO_HANDLE = 2,
|
|
||||||
DEVICE_MCU_ERROR_NO_ACTIVATION = 3,
|
|
||||||
DEVICE_MCU_ERROR_WRONG_SIZE = 4,
|
|
||||||
DEVICE_MCU_ERROR_UNPLUGGED = 5,
|
|
||||||
DEVICE_MCU_ERROR_UNEXPECTED = 6,
|
|
||||||
DEVICE_MCU_ERROR_WRONG_HEAD = 7,
|
|
||||||
DEVICE_MCU_ERROR_INVALID_LENGTH = 8,
|
|
||||||
DEVICE_MCU_ERROR_NOT_INITIALIZED = 9,
|
|
||||||
DEVICE_MCU_ERROR_PAYLOAD_FAILED = 10,
|
|
||||||
DEVICE_MCU_ERROR_UNKNOWN = 11,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct __attribute__((__packed__)) device_mcu_packet_t {
|
|
||||||
uint8_t head;
|
|
||||||
uint32_t checksum;
|
|
||||||
uint16_t length;
|
|
||||||
uint64_t timestamp;
|
|
||||||
uint16_t msgid;
|
|
||||||
uint8_t reserved [5];
|
|
||||||
union {
|
|
||||||
char text [42];
|
|
||||||
uint8_t data [42];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
enum device_mcu_event_t {
|
|
||||||
DEVICE_MCU_EVENT_UNKNOWN = 0,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_ON = 1,
|
|
||||||
DEVICE_MCU_EVENT_SCREEN_OFF = 2,
|
|
||||||
DEVICE_MCU_EVENT_BRIGHTNESS_UP = 3,
|
|
||||||
DEVICE_MCU_EVENT_BRIGHTNESS_DOWN = 4,
|
|
||||||
DEVICE_MCU_EVENT_MESSAGE = 5,
|
|
||||||
DEVICE_MCU_EVENT_DISPLAY_MODE_2D = 6,
|
|
||||||
DEVICE_MCU_EVENT_DISPLAY_MODE_3D = 7,
|
|
||||||
DEVICE_MCU_EVENT_BLEND_CYCLE = 8,
|
|
||||||
DEVICE_MCU_EVENT_CONTROL_TOGGLE = 9,
|
|
||||||
DEVICE_MCU_EVENT_VOLUME_UP = 10,
|
|
||||||
DEVICE_MCU_EVENT_VOLUME_DOWN = 11,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef enum device_mcu_error_t device_mcu_error_type;
|
|
||||||
typedef struct device_mcu_packet_t device_mcu_packet_type;
|
|
||||||
typedef enum device_mcu_event_t device_mcu_event_type;
|
|
||||||
typedef void (*device_mcu_event_callback)(
|
|
||||||
uint64_t timestamp,
|
|
||||||
device_mcu_event_type event,
|
|
||||||
uint8_t brightness,
|
|
||||||
const char* msg
|
|
||||||
);
|
|
||||||
|
|
||||||
struct device_mcu_t {
|
|
||||||
uint16_t vendor_id;
|
|
||||||
uint16_t product_id;
|
|
||||||
|
|
||||||
void* handle;
|
|
||||||
|
|
||||||
bool activated;
|
|
||||||
char mcu_app_fw_version [42];
|
|
||||||
char dp_fw_version [42];
|
|
||||||
char dsp_fw_version [42];
|
|
||||||
|
|
||||||
bool active;
|
|
||||||
uint8_t brightness;
|
|
||||||
uint8_t disp_mode;
|
|
||||||
uint8_t blend_state;
|
|
||||||
uint8_t control_mode;
|
|
||||||
|
|
||||||
device_mcu_event_callback callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct device_mcu_t device_mcu_type;
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_open(device_mcu_type* device, device_mcu_event_callback callback);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_clear(device_mcu_type* device);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_read(device_mcu_type* device, int timeout);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_poll_display_mode(device_mcu_type* device);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_update_display_mode(device_mcu_type* device);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_update_firmware(device_mcu_type* device, const char* path);
|
|
||||||
|
|
||||||
device_mcu_error_type device_mcu_close(device_mcu_type* device);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} // extern "C"
|
|
||||||
#endif
|
|
|
@ -1,7 +0,0 @@
|
||||||
#include "device_imu.h"
|
|
||||||
|
|
||||||
extern void goIMUEventHandler(uint64_t, device_imu_event_type, device_imu_ahrs_type*);
|
|
||||||
|
|
||||||
void imuEventHandler(uint64_t timestamp, device_imu_event_type event, const device_imu_ahrs_type* ahrs) {
|
|
||||||
goIMUEventHandler(timestamp, event, (device_imu_ahrs_type*)ahrs);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
#include "device_imu.h"
|
|
||||||
extern void goIMUEventHandler(uint64_t, device_imu_event_type, device_imu_ahrs_type*);
|
|
||||||
void imuEventHandler(uint64_t timestamp, device_imu_event_type event, const device_imu_ahrs_type* ahrs);
|
|
|
@ -1,108 +0,0 @@
|
||||||
//
|
|
||||||
// Created by wheaney on 12.11.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2024 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "hid_ids.h"
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdbool.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const uint16_t xreal_vendor_id = 0x3318;
|
|
||||||
const uint16_t xreal_product_ids[NUM_SUPPORTED_PRODUCTS] = {
|
|
||||||
0x0424, // XREAL Air
|
|
||||||
0x0428, // XREAL Air 2
|
|
||||||
0x0432, // XREAL Air 2 Pro
|
|
||||||
0x0426 // XREAL Air 2 Ultra
|
|
||||||
};
|
|
||||||
|
|
||||||
const int xreal_imu_interface_ids[NUM_SUPPORTED_PRODUCTS] = {
|
|
||||||
3, // XREAL Air
|
|
||||||
3, // XREAL Air 2
|
|
||||||
3, // XREAL Air 2 Pro
|
|
||||||
2 // XREAL Air 2 Ultra
|
|
||||||
};
|
|
||||||
|
|
||||||
const int xreal_mcu_interface_ids[NUM_SUPPORTED_PRODUCTS] = {
|
|
||||||
4, // XREAL Air
|
|
||||||
4, // XREAL Air 2
|
|
||||||
4, // XREAL Air 2 Pro
|
|
||||||
0 // XREAL Air 2 Ultra MCU
|
|
||||||
};
|
|
||||||
|
|
||||||
const uint16_t xreal_imu_max_payload_sizes[NUM_SUPPORTED_PRODUCTS] = {
|
|
||||||
64, // XREAL Air
|
|
||||||
64, // XREAL Air 2
|
|
||||||
64, // XREAL Air 2 Pro
|
|
||||||
512 // XREAL Air 2 Ultra
|
|
||||||
};
|
|
||||||
|
|
||||||
static int xreal_product_index(uint16_t product_id) {
|
|
||||||
for (int i = 0; i < NUM_SUPPORTED_PRODUCTS; i++) {
|
|
||||||
if (xreal_product_ids[i] == product_id) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_xreal_product_id(uint16_t product_id) {
|
|
||||||
return xreal_product_index(product_id) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int xreal_imu_interface_id(uint16_t product_id) {
|
|
||||||
const int index = xreal_product_index(product_id);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
return xreal_imu_interface_ids[index];
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int xreal_mcu_interface_id(uint16_t product_id) {
|
|
||||||
const int index = xreal_product_index(product_id);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
return xreal_mcu_interface_ids[index];
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t xreal_imu_max_payload_size(uint16_t product_id) {
|
|
||||||
const int index = xreal_product_index(product_id);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
return xreal_imu_max_payload_sizes[index];
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
#pragma once
|
|
||||||
//
|
|
||||||
// Created by wheaney on 20.11.23.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2023-2024 thejackimonster. All rights reserved.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdbool.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef __cplusplus
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
|
||||||
#include <cstdint>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define NUM_SUPPORTED_PRODUCTS 4
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
extern const uint16_t xreal_vendor_id;
|
|
||||||
extern const uint16_t xreal_product_ids [NUM_SUPPORTED_PRODUCTS];
|
|
||||||
|
|
||||||
bool is_xreal_product_id(uint16_t product_id);
|
|
||||||
|
|
||||||
int xreal_imu_interface_id(uint16_t product_id);
|
|
||||||
int xreal_mcu_interface_id(uint16_t product_id);
|
|
||||||
|
|
||||||
uint16_t xreal_imu_max_payload_size(uint16_t product_id);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} // extern "C"
|
|
||||||
#endif
|
|
|
@ -1,122 +0,0 @@
|
||||||
//go:build xreal
|
|
||||||
// +build xreal
|
|
||||||
|
|
||||||
package xreal
|
|
||||||
|
|
||||||
// #cgo CFLAGS: -w
|
|
||||||
// #cgo pkg-config: json-c libusb-1.0 hidapi-libusb
|
|
||||||
// #include "go_ffi.h"
|
|
||||||
// #include "device.h"
|
|
||||||
// #include "device_imu.h"
|
|
||||||
// #include "device_mcu.h"
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
deviceEventHandlerMutex = sync.Mutex{}
|
|
||||||
deviceEventListener *commons.AREventListener
|
|
||||||
)
|
|
||||||
|
|
||||||
//export goIMUEventHandler
|
|
||||||
func goIMUEventHandler(_ C.uint64_t, event_type C.device_imu_event_type, ahrs *C.struct_device_imu_ahrs_t) {
|
|
||||||
if deviceEventListener == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if event_type != C.DEVICE_IMU_EVENT_UPDATE {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orientation := C.device_imu_get_orientation(ahrs)
|
|
||||||
euler := C.device_imu_get_euler(orientation)
|
|
||||||
|
|
||||||
deviceEventListener.PitchCallback(float32(euler.pitch))
|
|
||||||
deviceEventListener.RollCallback(float32(euler.roll))
|
|
||||||
deviceEventListener.YawCallback(float32(euler.yaw))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements commons.ARDevice
|
|
||||||
type XrealDevice struct {
|
|
||||||
eventListener *commons.AREventListener
|
|
||||||
imuDevice *C.struct_device_imu_t
|
|
||||||
deviceIsOpen bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) Initialize() error {
|
|
||||||
if device.deviceIsOpen {
|
|
||||||
return fmt.Errorf("device is already open")
|
|
||||||
}
|
|
||||||
|
|
||||||
device.imuDevice = &C.struct_device_imu_t{}
|
|
||||||
|
|
||||||
// (*[0]byte) is a FUBAR way to cast a pointer to a function, but unsafe.Pointer doesn't work:
|
|
||||||
// cannot use unsafe.Pointer(_Cgo_ptr(_Cfpvar_fp_imuEventHandler)) (value of type unsafe.Pointer) as *[0]byte value in variable declaration
|
|
||||||
if C.DEVICE_IMU_ERROR_NO_ERROR != C.device_imu_open(device.imuDevice, (*[0]byte)(C.imuEventHandler)) {
|
|
||||||
return fmt.Errorf("failed to open IMU device")
|
|
||||||
}
|
|
||||||
|
|
||||||
C.device_imu_clear(device.imuDevice)
|
|
||||||
C.device_imu_calibrate(device.imuDevice, 1000, true, true, false)
|
|
||||||
|
|
||||||
device.deviceIsOpen = true
|
|
||||||
|
|
||||||
// let's hope this doesn't cause race conditions
|
|
||||||
go func() {
|
|
||||||
for device.eventListener == nil {
|
|
||||||
time.Sleep(time.Millisecond * 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
if !device.deviceIsOpen {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// I'm sorry.
|
|
||||||
deviceEventHandlerMutex.Lock()
|
|
||||||
deviceEventListener = device.eventListener
|
|
||||||
status := C.device_imu_read(device.imuDevice, -1)
|
|
||||||
deviceEventHandlerMutex.Unlock()
|
|
||||||
|
|
||||||
if status != C.DEVICE_IMU_ERROR_NO_ERROR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
device.deviceIsOpen = false
|
|
||||||
C.device_imu_close(device.imuDevice)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) End() error {
|
|
||||||
if !device.deviceIsOpen {
|
|
||||||
return fmt.Errorf("device is not open")
|
|
||||||
}
|
|
||||||
|
|
||||||
C.device_imu_close(device.imuDevice)
|
|
||||||
device.deviceIsOpen = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) IsPollingLibrary() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) IsEventBasedLibrary() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) Poll() error {
|
|
||||||
return fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (device *XrealDevice) RegisterEventListeners(listener *commons.AREventListener) {
|
|
||||||
device.eventListener = listener
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
//go:build xreal && !xreal_debug_logging
|
|
||||||
// +build xreal,!xreal_debug_logging
|
|
||||||
|
|
||||||
package xreal
|
|
||||||
|
|
||||||
// #cgo CFLAGS: -DNDEBUG
|
|
||||||
import "C"
|
|
|
@ -1,11 +0,0 @@
|
||||||
Copyright (c) 2025 imterah.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
@ -1,101 +0,0 @@
|
||||||
package edidpatcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculates a checksum for a given EDID block (base EDID, extension blocks, etc.)
|
|
||||||
func CalculateEDIDChecksum(edidBlock []byte) byte {
|
|
||||||
sum := 0
|
|
||||||
|
|
||||||
for _, value := range edidBlock[:len(edidBlock)-1] {
|
|
||||||
sum += int(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return byte((-sum) & 0xFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
var MSFTPayloadSize = byte(22 + 4)
|
|
||||||
|
|
||||||
// Patch a given EDID to be a "specialized display", allowing for the display to be used by third-party window-managers/compositors/applications directly.
|
|
||||||
func PatchEDIDToBeSpecialized(edid []byte) ([]byte, error) {
|
|
||||||
newEDID := make([]byte, len(edid))
|
|
||||||
copy(newEDID, edid)
|
|
||||||
|
|
||||||
isAnEnhancedEDID := len(newEDID) > 128
|
|
||||||
|
|
||||||
foundExtensionBase := 0
|
|
||||||
extensionBaseExists := false
|
|
||||||
|
|
||||||
// Find an appropriate extension base
|
|
||||||
if isAnEnhancedEDID {
|
|
||||||
for currentExtensionPosition := 128; currentExtensionPosition < len(newEDID); currentExtensionPosition += 128 {
|
|
||||||
if newEDID[currentExtensionPosition] != 0x02 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if newEDID[currentExtensionPosition+1] != 0x03 {
|
|
||||||
fmt.Println("WARN: Incompatible version detected for ANSI CTA data section in EDID")
|
|
||||||
}
|
|
||||||
|
|
||||||
foundExtensionBase = currentExtensionPosition
|
|
||||||
extensionBaseExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if foundExtensionBase == 0 {
|
|
||||||
foundExtensionBase = len(newEDID)
|
|
||||||
newEDID = append(newEDID, make([]byte, 128)...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foundExtensionBase = 128
|
|
||||||
newEDID = append(newEDID, make([]byte, 128)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
newEDID[foundExtensionBase+2] = MSFTPayloadSize
|
|
||||||
|
|
||||||
if !extensionBaseExists {
|
|
||||||
// Add another extension to the original EDID
|
|
||||||
if newEDID[126] == 255 {
|
|
||||||
return nil, fmt.Errorf("EDID extension block limit reached, but we need to add another extension")
|
|
||||||
}
|
|
||||||
|
|
||||||
newEDID[126] += 1
|
|
||||||
newEDID[127] = CalculateEDIDChecksum(newEDID[:128])
|
|
||||||
|
|
||||||
newEDID[foundExtensionBase] = 0x02
|
|
||||||
newEDID[foundExtensionBase+1] = 0x03
|
|
||||||
newEDID[foundExtensionBase+3] = 0x00
|
|
||||||
} else {
|
|
||||||
if newEDID[foundExtensionBase+2] != MSFTPayloadSize && newEDID[foundExtensionBase+2] != 0 {
|
|
||||||
currentBase := newEDID[foundExtensionBase+2]
|
|
||||||
|
|
||||||
copy(newEDID[foundExtensionBase+4:foundExtensionBase+int(currentBase)-1], make([]byte, int(currentBase)-1))
|
|
||||||
copy(newEDID[foundExtensionBase+int(MSFTPayloadSize):foundExtensionBase+127], newEDID[foundExtensionBase+int(currentBase):foundExtensionBase+127])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedUUID := uuid.New()
|
|
||||||
uuidBytes, err := generatedUUID.MarshalBinary()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal UUID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implemented using https://learn.microsoft.com/en-us/windows-hardware/drivers/display/specialized-monitors-edid-extension
|
|
||||||
// VST & Length
|
|
||||||
newEDID[foundExtensionBase+4] = 0x3<<5 | 0x15 // 0x3: vendor specific tag; 0x15: length
|
|
||||||
// Assigned IEEE OUI
|
|
||||||
newEDID[foundExtensionBase+5] = 0x5C
|
|
||||||
newEDID[foundExtensionBase+6] = 0x12
|
|
||||||
newEDID[foundExtensionBase+7] = 0xCA
|
|
||||||
// Actual data
|
|
||||||
newEDID[foundExtensionBase+8] = 0x2 // Using version 0x2 for better compatibility
|
|
||||||
newEDID[foundExtensionBase+9] = 0x7 // Using VR tag for better compatibility even though it probably doesn't matter
|
|
||||||
copy(newEDID[foundExtensionBase+10:foundExtensionBase+10+16], uuidBytes)
|
|
||||||
|
|
||||||
newEDID[foundExtensionBase+127] = CalculateEDIDChecksum(newEDID[foundExtensionBase : foundExtensionBase+127])
|
|
||||||
|
|
||||||
return newEDID, nil
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
module git.terah.dev/UnrealXR/unrealxr/edidpatcher
|
|
||||||
|
|
||||||
go 1.24.3
|
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0
|
|
|
@ -1,2 +0,0 @@
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
10
libunreal/__init__.py
Normal file
10
libunreal/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from libunreal.supported_devices import *
|
||||||
|
from libunreal.mcu_driver import *
|
||||||
|
from libunreal.edid import *
|
||||||
|
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
if platform == "linux" or platform == "linux2":
|
||||||
|
from libunreal.linux import *
|
||||||
|
else:
|
||||||
|
raise OSError("Unsupported operating system")
|
86
libunreal/edid.py
Normal file
86
libunreal/edid.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnrealXRDisplayMetadata:
|
||||||
|
edid: bytes
|
||||||
|
device_vendor: str
|
||||||
|
device_quirks: dict[str, str | int]
|
||||||
|
max_width: int
|
||||||
|
max_height: int
|
||||||
|
max_refresh_rate: int
|
||||||
|
linux_drm_card: str
|
||||||
|
linux_drm_connector: str
|
||||||
|
|
||||||
|
calculated_msft_payload_size = 22+4
|
||||||
|
|
||||||
|
def calculate_checksum(block):
|
||||||
|
checksum = (-sum(block[:-1])) & 0xFF
|
||||||
|
return checksum
|
||||||
|
|
||||||
|
def patch_edid_to_be_specialized(edid_data: bytes | bytearray) -> bytes | bytearray:
|
||||||
|
mutable_edid = bytearray(edid_data)
|
||||||
|
is_enhanced_mode = len(edid_data) > 128
|
||||||
|
|
||||||
|
found_extension_base = 0
|
||||||
|
extension_base_existed = False
|
||||||
|
|
||||||
|
if is_enhanced_mode:
|
||||||
|
for i in range(128, len(edid_data), 128):
|
||||||
|
if edid_data[i] == 0x02:
|
||||||
|
logger.warning("Detected existing ANSI CTA data section. Patching in place but untested! Please report any issues you discover")
|
||||||
|
|
||||||
|
if edid_data[i+1] != 0x03:
|
||||||
|
logger.warning("Incompatible version detected for ANSI CTA data section in EDID")
|
||||||
|
|
||||||
|
found_extension_base = i
|
||||||
|
extension_base_existed = True
|
||||||
|
|
||||||
|
if found_extension_base == 0:
|
||||||
|
found_extension_base = len(edid_data)
|
||||||
|
mutable_edid.extend([0]*128)
|
||||||
|
else:
|
||||||
|
mutable_edid.extend([0]*128)
|
||||||
|
found_extension_base = 128
|
||||||
|
|
||||||
|
generated_uuid = uuid.uuid4()
|
||||||
|
|
||||||
|
mutable_edid[found_extension_base] = 0x02
|
||||||
|
mutable_edid[found_extension_base+1] = 0x03
|
||||||
|
|
||||||
|
if extension_base_existed and mutable_edid[found_extension_base+2] != calculated_msft_payload_size and mutable_edid[found_extension_base+2] != 0:
|
||||||
|
# We try our best to move our data into place
|
||||||
|
current_base = mutable_edid[found_extension_base+2]
|
||||||
|
mutable_edid[found_extension_base+2] = calculated_msft_payload_size+1
|
||||||
|
|
||||||
|
mutable_edid[found_extension_base+4:found_extension_base+current_base-1] = [0]*(current_base-1)
|
||||||
|
mutable_edid[found_extension_base+calculated_msft_payload_size:found_extension_base+127] = mutable_edid[found_extension_base+current_base:found_extension_base+127]
|
||||||
|
else:
|
||||||
|
mutable_edid[found_extension_base+2] = calculated_msft_payload_size
|
||||||
|
|
||||||
|
if not extension_base_existed:
|
||||||
|
mutable_edid[126] += 1
|
||||||
|
mutable_edid[127] = calculate_checksum(mutable_edid[:128])
|
||||||
|
|
||||||
|
mutable_edid[found_extension_base+3] = 0 # We don't know any of these properties
|
||||||
|
|
||||||
|
# Implemented using https://learn.microsoft.com/en-us/windows-hardware/drivers/display/specialized-monitors-edid-extension
|
||||||
|
# VST & Length
|
||||||
|
mutable_edid[found_extension_base+4] = 0x3 << 5 | 0x15 # 0x3: vendor specific tag; 0x15: length
|
||||||
|
# Assigned IEEE OUI
|
||||||
|
mutable_edid[found_extension_base+5] = 0x5C
|
||||||
|
mutable_edid[found_extension_base+6] = 0x12
|
||||||
|
mutable_edid[found_extension_base+7] = 0xCA
|
||||||
|
# Actual data
|
||||||
|
mutable_edid[found_extension_base+8] = 0x2 # Using version 0x2 for better compatibility
|
||||||
|
mutable_edid[found_extension_base+9] = 0x7 # Using VR tag for better compatibility even though it probably doesn't matter
|
||||||
|
mutable_edid[found_extension_base+10:found_extension_base+10+16] = generated_uuid.bytes
|
||||||
|
|
||||||
|
mutable_edid[found_extension_base+127] = calculate_checksum(mutable_edid[found_extension_base:found_extension_base+127])
|
||||||
|
|
||||||
|
if isinstance(edid_data, bytes):
|
||||||
|
return bytes(mutable_edid)
|
||||||
|
else:
|
||||||
|
return mutable_edid
|
84
libunreal/linux/__init__.py
Normal file
84
libunreal/linux/__init__.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
from libunreal.supported_devices import supported_devices
|
||||||
|
from libunreal.edid import UnrealXRDisplayMetadata
|
||||||
|
import pyedid
|
||||||
|
|
||||||
|
def upload_new_device_edid(display_spec: UnrealXRDisplayMetadata, edid: bytes | bytearray):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetch_xr_glass_edid(allow_unsupported_devices) -> UnrealXRDisplayMetadata:
|
||||||
|
# Scan for all VGA devices and their IDs
|
||||||
|
pci_device_comand = subprocess.run(["lspci"], capture_output=True)
|
||||||
|
|
||||||
|
if pci_device_comand.returncode != 0:
|
||||||
|
raise OSError("Failed to scan PCI devices")
|
||||||
|
|
||||||
|
pci_devices: list[str] = pci_device_comand.stdout.decode("utf-8").split("\n")
|
||||||
|
pci_devices = pci_devices[:-1]
|
||||||
|
|
||||||
|
vga_devices: list[str] = []
|
||||||
|
|
||||||
|
for pci_device in pci_devices:
|
||||||
|
if "VGA compatible controller:" in pci_device:
|
||||||
|
vga_devices.append(pci_device[:pci_device.index(" ")])
|
||||||
|
|
||||||
|
# Attempt to find any XR glasses
|
||||||
|
for vga_device in vga_devices:
|
||||||
|
card_devices = list(os.listdir(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/"))
|
||||||
|
|
||||||
|
for card_device in card_devices:
|
||||||
|
if "card" not in card_device:
|
||||||
|
continue
|
||||||
|
|
||||||
|
monitors = list(os.listdir(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/{card_device}/"))
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
if card_device not in monitor:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/{card_device}/{monitor}/edid", "rb") as edid:
|
||||||
|
raw_edid_file = edid.read()
|
||||||
|
|
||||||
|
if len(raw_edid_file) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
edid = pyedid.parse_edid(raw_edid_file)
|
||||||
|
|
||||||
|
for manufacturer, manufacturer_supported_devices in supported_devices.items():
|
||||||
|
if edid.manufacturer_pnp_id == manufacturer and (edid.name in manufacturer_supported_devices or allow_unsupported_devices):
|
||||||
|
max_width = 0
|
||||||
|
max_height = 0
|
||||||
|
max_refresh = 0
|
||||||
|
|
||||||
|
for resolution in edid.resolutions:
|
||||||
|
if resolution[0] > max_width and resolution[1] > max_height:
|
||||||
|
max_width = resolution[0]
|
||||||
|
max_height = resolution[1]
|
||||||
|
|
||||||
|
max_refresh = max(max_refresh, int(resolution[2]))
|
||||||
|
|
||||||
|
if max_width == 0 or max_height == 0:
|
||||||
|
if "max_width" not in manufacturer_supported_devices[edid.name] or "max_height" not in manufacturer_supported_devices[edid.name]:
|
||||||
|
raise ValueError("Couldn't determine maximum width and height, and the maximum width and height isn't defined in the device quirks section")
|
||||||
|
|
||||||
|
max_width = int(manufacturer_supported_devices[edid.name]["max_width"])
|
||||||
|
max_height = int(manufacturer_supported_devices[edid.name]["max_height"])
|
||||||
|
|
||||||
|
if max_refresh == 0:
|
||||||
|
if "max_refresh" not in manufacturer_supported_devices[edid.name]:
|
||||||
|
raise ValueError("Couldn't determine maximum refresh rate, and the maximum refresh rate isn't defined in the device quirks section")
|
||||||
|
|
||||||
|
max_refresh = int(manufacturer_supported_devices[edid.name]["max_refresh"])
|
||||||
|
|
||||||
|
return UnrealXRDisplayMetadata(raw_edid_file, edid.manufacturer_pnp_id, manufacturer_supported_devices[edid.name], max_width, max_height, max_refresh, card_device, monitor.replace(f"{card_device}-", ""))
|
||||||
|
|
||||||
|
raise ValueError("Could not find supported device. Check if the device is plugged in. If it is plugged in and working correctly, check the README or open an issue.")
|
||||||
|
|
||||||
|
def upload_edid_firmware(display: UnrealXRDisplayMetadata, fw: bytes | bytearray):
|
||||||
|
if display.linux_drm_connector == "" or display.linux_drm_card == "":
|
||||||
|
raise ValueError("Linux DRM connector and/or Linux DRM card not specified!")
|
||||||
|
|
||||||
|
with open(f"/sys/kernel/debug/dri/{display.linux_drm_card.replace("card", "")}/{display.linux_drm_connector}/edid_override", "wb") as kernel_edid:
|
||||||
|
kernel_edid.write(fw)
|
145
libunreal/mcu_driver.py
Normal file
145
libunreal/mcu_driver.py
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from os import path, environ
|
||||||
|
from typing import Callable
|
||||||
|
from loguru import logger
|
||||||
|
from shutil import which
|
||||||
|
from time import sleep
|
||||||
|
from enum import Enum
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import signal
|
||||||
|
import atexit
|
||||||
|
import os
|
||||||
|
|
||||||
|
class MCUCommandTypes(Enum):
|
||||||
|
ROLL = 0
|
||||||
|
PITCH = 1
|
||||||
|
YAW = 2
|
||||||
|
GENERIC_MESSAGE = 3
|
||||||
|
BRIGHTNESS_UP = 4
|
||||||
|
BRIGHTNESS_DOWN = 5
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCUCallbackWrapper:
|
||||||
|
OnRollUpdate: Callable[[float], None]
|
||||||
|
OnPitchUpdate: Callable[[float], None]
|
||||||
|
OnYawUpdate: Callable[[float], None]
|
||||||
|
OnTextMessageRecieved: Callable[[str], None]
|
||||||
|
OnBrightnessUp: Callable[[int], None]
|
||||||
|
OnBrightnessDown: Callable[[int], None]
|
||||||
|
|
||||||
|
vendor_to_driver_table: dict[str, str] = {
|
||||||
|
"MRG": "xreal_ar_driver",
|
||||||
|
}
|
||||||
|
|
||||||
|
def find_executable_path_from_driver_name(driver_name) -> str:
|
||||||
|
# First try the normal driver path
|
||||||
|
try:
|
||||||
|
driver_path = path.join("drivers", driver_name)
|
||||||
|
|
||||||
|
file = open(driver_path)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return driver_path
|
||||||
|
except OSError:
|
||||||
|
# Then search the system path
|
||||||
|
driver_path = which(driver_name)
|
||||||
|
|
||||||
|
if driver_path == "":
|
||||||
|
raise OSError("Could not find driver executable in driver directory or in PATH")
|
||||||
|
|
||||||
|
return driver_path
|
||||||
|
|
||||||
|
def start_mcu_event_listener(driver_vendor: str, events: MCUCallbackWrapper):
|
||||||
|
driver_executable = find_executable_path_from_driver_name(vendor_to_driver_table[driver_vendor])
|
||||||
|
|
||||||
|
created_temp_dir = TemporaryDirectory()
|
||||||
|
sock_path = path.join(created_temp_dir.name, "mcu_socket")
|
||||||
|
|
||||||
|
def on_socket_event(sock: socket.socket):
|
||||||
|
while True:
|
||||||
|
message_type = sock.recv(1)
|
||||||
|
|
||||||
|
if message_type[0] == MCUCommandTypes.ROLL.value:
|
||||||
|
roll_data = sock.recv(4)
|
||||||
|
roll_value = struct.unpack("!f", roll_data)[0]
|
||||||
|
|
||||||
|
if not isinstance(roll_value, float):
|
||||||
|
logger.warning("Expected roll value to be a float but got other type instead")
|
||||||
|
continue
|
||||||
|
|
||||||
|
events.OnRollUpdate(roll_value)
|
||||||
|
elif message_type[0] == MCUCommandTypes.PITCH.value:
|
||||||
|
pitch_data = sock.recv(4)
|
||||||
|
pitch_value = struct.unpack("!f", pitch_data)[0]
|
||||||
|
|
||||||
|
if not isinstance(pitch_value, float):
|
||||||
|
logger.warning("Expected pitch value to be a float but got other type instead")
|
||||||
|
continue
|
||||||
|
|
||||||
|
events.OnPitchUpdate(pitch_value)
|
||||||
|
elif message_type[0] == MCUCommandTypes.YAW.value:
|
||||||
|
yaw_data = sock.recv(4)
|
||||||
|
yaw_value = struct.unpack("!f", yaw_data)[0]
|
||||||
|
|
||||||
|
if not isinstance(yaw_value, float):
|
||||||
|
logger.warning("Expected yaw value to be a float but got other type instead")
|
||||||
|
continue
|
||||||
|
|
||||||
|
events.OnYawUpdate(yaw_value)
|
||||||
|
elif message_type[0] == MCUCommandTypes.GENERIC_MESSAGE.value:
|
||||||
|
length_bytes = sock.recv(4)
|
||||||
|
|
||||||
|
msg_len = struct.unpack("!I", length_bytes)[0]
|
||||||
|
msg_bytes = sock.recv(msg_len)
|
||||||
|
|
||||||
|
msg = msg_bytes.decode("utf-8", errors="replace")
|
||||||
|
events.OnTextMessageRecieved(msg)
|
||||||
|
elif message_type[0] == MCUCommandTypes.BRIGHTNESS_UP.value:
|
||||||
|
brightness_bytes = sock.recv(1)
|
||||||
|
events.OnBrightnessUp(int.from_bytes(brightness_bytes, byteorder='big'))
|
||||||
|
elif message_type[0] == MCUCommandTypes.BRIGHTNESS_DOWN.value:
|
||||||
|
brightness_bytes = sock.recv(1)
|
||||||
|
events.OnBrightnessDown(int.from_bytes(brightness_bytes, byteorder='big'))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown message type recieved: {str(message_type[0])}")
|
||||||
|
|
||||||
|
def start_socket_handout():
|
||||||
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
server.bind(sock_path)
|
||||||
|
server.listen(1)
|
||||||
|
|
||||||
|
sock: socket.socket | None = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
sock, _ = server.accept()
|
||||||
|
|
||||||
|
threaded_connection_processing = threading.Thread(target=on_socket_event, args=(sock,), daemon=True)
|
||||||
|
threaded_connection_processing.start()
|
||||||
|
|
||||||
|
created_temp_dir.cleanup()
|
||||||
|
|
||||||
|
def spawn_child_process():
|
||||||
|
custom_env = environ.copy()
|
||||||
|
custom_env["UNREALXR_NREAL_DRIVER_SOCK"] = sock_path
|
||||||
|
|
||||||
|
process = subprocess.Popen([driver_executable], env=custom_env)
|
||||||
|
|
||||||
|
def kill_child():
|
||||||
|
if process.pid is None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.kill(process.pid, signal.SIGTERM)
|
||||||
|
|
||||||
|
atexit.register(kill_child)
|
||||||
|
|
||||||
|
threaded_socket_handout = threading.Thread(target=start_socket_handout, daemon=True)
|
||||||
|
threaded_socket_handout.start()
|
||||||
|
|
||||||
|
sleep(0.01) # Give the socket server time to initialize
|
||||||
|
|
||||||
|
threaded_child_process = threading.Thread(target=spawn_child_process, daemon=True)
|
||||||
|
threaded_child_process.start()
|
13
libunreal/supported_devices.py
Normal file
13
libunreal/supported_devices.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Sourced from "https://uefi.org/uefi-pnp-export"
|
||||||
|
supported_devices: dict[str, dict[str, dict[str, str | int]]] = {
|
||||||
|
"MRG": {
|
||||||
|
# Quirks section
|
||||||
|
"Air": {
|
||||||
|
"max_width": 1920,
|
||||||
|
"max_height": 1080,
|
||||||
|
"max_refresh": 120,
|
||||||
|
"sensor_init_delay": 10,
|
||||||
|
"z_vector_disabled": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
188
main.py
Executable file
188
main.py
Executable file
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from sys import platform
|
||||||
|
import logging
|
||||||
|
import atexit
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Silence pyray init messages
|
||||||
|
raylib_python_logger = logging.getLogger("raylib")
|
||||||
|
raylib_python_logger.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
from platformdirs import user_data_dir, user_config_dir
|
||||||
|
from loguru import logger
|
||||||
|
import PyEvdi
|
||||||
|
import pyedid
|
||||||
|
import pyray
|
||||||
|
import time
|
||||||
|
|
||||||
|
from render import render_loop
|
||||||
|
import libunreal
|
||||||
|
|
||||||
|
default_configuration: dict[str, str | int] = {
|
||||||
|
"display_angle": 45,
|
||||||
|
"display_spacing": 1,
|
||||||
|
"display_count": 3,
|
||||||
|
"allow_unsupported_devices": False,
|
||||||
|
"allow_unsupported_vendors": False,
|
||||||
|
"override_default_edid": False,
|
||||||
|
"edid_override_path": "/file/here",
|
||||||
|
"override_width": 0,
|
||||||
|
"override_height": 0,
|
||||||
|
"override_refresh_rate": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize_configuration():
|
||||||
|
pass
|
||||||
|
|
||||||
|
used_cards: list[int] = []
|
||||||
|
|
||||||
|
def find_suitable_evdi_card() -> int:
|
||||||
|
for i in range(20):
|
||||||
|
if PyEvdi.check_device(i) == PyEvdi.AVAILABLE and i not in used_cards:
|
||||||
|
used_cards.append(i)
|
||||||
|
return i
|
||||||
|
|
||||||
|
PyEvdi.add_device()
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
if PyEvdi.check_device(i) == PyEvdi.AVAILABLE and i not in used_cards:
|
||||||
|
used_cards.append(i)
|
||||||
|
return i
|
||||||
|
|
||||||
|
raise ValueError("Failed to allocate virtual display device")
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
|
def main():
|
||||||
|
configuration = {}
|
||||||
|
|
||||||
|
logger.info("Loading configuration")
|
||||||
|
config_dir = os.environ["UNREALXR_CONFIG_PATH"] if "UNREALXR_CONFIG_PATH" in os.environ else ""
|
||||||
|
data_dir = os.environ["UNREALXR_DATA_PATH"] if "UNREALXR_DATA_PATH" in os.environ else ""
|
||||||
|
|
||||||
|
# Use OS defaults if we weren't overriden in env
|
||||||
|
if config_dir == "":
|
||||||
|
config_dir = user_config_dir("UnrealXR", "Tera")
|
||||||
|
|
||||||
|
if data_dir == "":
|
||||||
|
data_dir = user_data_dir("UnrealXR", "Tera")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat(data_dir)
|
||||||
|
except OSError:
|
||||||
|
os.makedirs(data_dir)
|
||||||
|
|
||||||
|
# Read config and create it if it doesn't exist
|
||||||
|
config_path = os.path.join(config_dir, "config.json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat(config_path)
|
||||||
|
|
||||||
|
with open(config_path, "r") as config_file:
|
||||||
|
configuration = json.load(config_file)
|
||||||
|
except OSError:
|
||||||
|
try:
|
||||||
|
os.makedirs(config_dir)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open(config_path, "w") as config_file:
|
||||||
|
json.dump(default_configuration, config_file, indent=4)
|
||||||
|
|
||||||
|
configuration = default_configuration
|
||||||
|
|
||||||
|
# Set unbound values (ie. if user is using an older version)
|
||||||
|
for key, default_value in default_configuration.items():
|
||||||
|
if key not in configuration:
|
||||||
|
# Add quotes if we're a string
|
||||||
|
value = default_value
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = f'"{value}"'
|
||||||
|
|
||||||
|
logger.warning(f"Setting unbound key '{key}' with default value {value}. You might want to define this! If not, this warning can be safely ignored.")
|
||||||
|
configuration[key] = default_value
|
||||||
|
|
||||||
|
# Initialize logging to files
|
||||||
|
logger.add(os.path.join(data_dir, "unrealxr.log"), format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}")
|
||||||
|
logger.info("Loaded configuration")
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
raise OSError("You are not running as root! Running as root is necessary to talk to the EVDI service")
|
||||||
|
|
||||||
|
# Get the display EDID
|
||||||
|
logger.info("Attempting to read display EDID file")
|
||||||
|
edid: libunreal.UnrealXRDisplayMetadata | None = None
|
||||||
|
|
||||||
|
if configuration["override_default_edid"] or configuration["allow_unsupported_vendors"]:
|
||||||
|
# We need to parse it to get the maximum width, height, and refresh rate for EVDI's calculations
|
||||||
|
with open(configuration["edid_override_path"], "rb") as edid_file:
|
||||||
|
edid_file = edid_file.read()
|
||||||
|
parsed_edid_file = pyedid.parse_edid(edid_file)
|
||||||
|
|
||||||
|
max_width = int(configuration["override_width"])
|
||||||
|
max_height = int(configuration["override_height"])
|
||||||
|
max_refresh = int(configuration["override_refresh_rate"])
|
||||||
|
|
||||||
|
if configuration["override_width"] == 0 or configuration["override_height"] == 0 or configuration["override_refresh_rate"] == 0:
|
||||||
|
for resolution in parsed_edid_file.resolutions:
|
||||||
|
if configuration["override_width"] == 0 or configuration["override_height"] == 0:
|
||||||
|
if resolution[0] > max_width and resolution[1] > max_height:
|
||||||
|
max_width = resolution[0]
|
||||||
|
max_height = resolution[1]
|
||||||
|
|
||||||
|
if configuration["override_refresh_rate"] == 0:
|
||||||
|
max_refresh = max(max_refresh, int(resolution[2]))
|
||||||
|
|
||||||
|
if max_width == 0 or max_height == 0:
|
||||||
|
raise ValueError("Could not determine maximum width and/or height from EDID file, and the width and/or height overrides aren't set!")
|
||||||
|
|
||||||
|
if max_refresh == 0:
|
||||||
|
raise ValueError("Could not determine maximum refresh rate from EDID file, and the refresh rate overrides aren't set!")
|
||||||
|
|
||||||
|
edid = libunreal.UnrealXRDisplayMetadata(edid_file, parsed_edid_file.name if parsed_edid_file.name else "", {}, max_width, max_height, max_refresh, "", "")
|
||||||
|
else:
|
||||||
|
edid = libunreal.fetch_xr_glass_edid(configuration["allow_unsupported_devices"])
|
||||||
|
|
||||||
|
assert(edid is not None)
|
||||||
|
logger.info("Got EDID file")
|
||||||
|
|
||||||
|
if platform == "linux" or platform == "linux2":
|
||||||
|
# TODO: implement EDID patching for overridden displays
|
||||||
|
logger.info("Patching EDID firmware")
|
||||||
|
patched_edid = libunreal.patch_edid_to_be_specialized(edid.edid)
|
||||||
|
libunreal.upload_edid_firmware(edid, patched_edid)
|
||||||
|
|
||||||
|
def unload_custom_fw():
|
||||||
|
with open(f"/sys/kernel/debug/dri/{edid.linux_drm_card.replace("card", "")}/{edid.linux_drm_connector}/edid_override", "w") as kernel_edid:
|
||||||
|
kernel_edid.write("reset")
|
||||||
|
|
||||||
|
logger.info("Please unplug and plug in your XR device to restore it back to normal settings.")
|
||||||
|
|
||||||
|
atexit.register(unload_custom_fw)
|
||||||
|
input("Press the Enter key to continue loading after you unplug and plug in your XR device.")
|
||||||
|
|
||||||
|
# Raylib gets confused if there's multiple dri devices so we initialize the window before anything
|
||||||
|
logger.info("Initializing XR headset")
|
||||||
|
pyray.set_target_fps(edid.max_refresh_rate*2) # we need more headroom...
|
||||||
|
pyray.init_window(edid.max_width, edid.max_height, "UnrealXR")
|
||||||
|
|
||||||
|
logger.info("Initializing virtual displays")
|
||||||
|
cards = []
|
||||||
|
|
||||||
|
for i in range(int(configuration["display_count"])):
|
||||||
|
suitable_card_id = find_suitable_evdi_card()
|
||||||
|
card = PyEvdi.Card(suitable_card_id)
|
||||||
|
card.connect(edid.edid, len(edid.edid), edid.max_width*edid.max_height, edid.max_width*edid.max_height*edid.max_refresh_rate)
|
||||||
|
cards.append(card)
|
||||||
|
|
||||||
|
logger.debug(f"Initialized card #{str(i+1)}")
|
||||||
|
|
||||||
|
atexit.register(lambda: card.close())
|
||||||
|
|
||||||
|
logger.info("Initialized displays. Entering rendering loop")
|
||||||
|
render_loop(edid, configuration, cards)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Welcome to UnrealXR!\n")
|
||||||
|
main()
|
1
modules/evdi
Submodule
1
modules/evdi
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 3673a4b34d386921fc323ddbd2ef0e000022e2d4
|
1
modules/nreal-driver
Submodule
1
modules/nreal-driver
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 36ad789c9b5893653c523d4af6e24120ec19ab33
|
1
modules/raylib-python-cffi
Submodule
1
modules/raylib-python-cffi
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7c982c0d4e2773739f00eb91a2d2cbe87eb7c76d
|
235
render.py
Normal file
235
render.py
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from io import BufferedWriter
|
||||||
|
from sys import int_info
|
||||||
|
from typing import Union
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from raylib import rl
|
||||||
|
import PyEvdi
|
||||||
|
import pyray
|
||||||
|
|
||||||
|
from libunreal import UnrealXRDisplayMetadata, MCUCallbackWrapper, start_mcu_event_listener
|
||||||
|
|
||||||
|
vertical_size = 0.0
|
||||||
|
horizontal_sizing_constant = 1
|
||||||
|
|
||||||
|
previous_pitch = 0.0
|
||||||
|
previous_yaw = 0.0
|
||||||
|
previous_roll = 0.0
|
||||||
|
|
||||||
|
current_pitch = 0.0
|
||||||
|
current_yaw = 0.0
|
||||||
|
current_roll = 0.0
|
||||||
|
|
||||||
|
has_gotten_pitch_callback_before = False
|
||||||
|
has_gotten_yaw_callback_before = False
|
||||||
|
has_gotten_roll_callback_before = False
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RectMetadata:
|
||||||
|
card: PyEvdi.Card
|
||||||
|
buffer_ptr: pyray.ffi.CData | None
|
||||||
|
texture: Union[pyray.Texture, None]
|
||||||
|
model: Union[pyray.Model, None]
|
||||||
|
angle: int
|
||||||
|
relative_position: int
|
||||||
|
|
||||||
|
def pitch_callback(new_pitch: float):
|
||||||
|
global current_pitch
|
||||||
|
global previous_pitch
|
||||||
|
global has_gotten_pitch_callback_before
|
||||||
|
|
||||||
|
if not has_gotten_pitch_callback_before:
|
||||||
|
has_gotten_pitch_callback_before = True
|
||||||
|
previous_pitch = new_pitch
|
||||||
|
current_pitch = new_pitch
|
||||||
|
else:
|
||||||
|
previous_pitch = current_pitch
|
||||||
|
current_pitch = new_pitch
|
||||||
|
|
||||||
|
def yaw_callback(new_yaw: float):
|
||||||
|
global current_yaw
|
||||||
|
global previous_yaw
|
||||||
|
global has_gotten_yaw_callback_before
|
||||||
|
|
||||||
|
if not has_gotten_yaw_callback_before:
|
||||||
|
has_gotten_yaw_callback_before = True
|
||||||
|
previous_yaw = new_yaw
|
||||||
|
current_yaw = new_yaw
|
||||||
|
else:
|
||||||
|
previous_yaw = current_yaw
|
||||||
|
current_yaw = new_yaw
|
||||||
|
|
||||||
|
def roll_callback(new_roll: float):
|
||||||
|
global current_roll
|
||||||
|
global previous_roll
|
||||||
|
global has_gotten_roll_callback_before
|
||||||
|
|
||||||
|
if not has_gotten_roll_callback_before:
|
||||||
|
has_gotten_roll_callback_before = True
|
||||||
|
previous_roll = new_roll
|
||||||
|
roll = new_roll
|
||||||
|
else:
|
||||||
|
previous_roll = current_roll
|
||||||
|
current_roll = new_roll
|
||||||
|
|
||||||
|
def text_message(message: str):
|
||||||
|
logger.debug(f"Got message from AR's MCU: {message}")
|
||||||
|
|
||||||
|
def stub_brightness_function(brightness: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def find_max_vertical_size(fovy_deg: float, distance: float) -> float:
|
||||||
|
fovy_rad = math.radians(fovy_deg)
|
||||||
|
return 2 * distance * math.tan(fovy_rad / 2)
|
||||||
|
|
||||||
|
def find_optimal_horizonal_res(vertical_display_res: int, horizontal_display_res: int) -> float:
|
||||||
|
aspect_ratio = horizontal_display_res/vertical_display_res
|
||||||
|
horizontal_size = vertical_size * aspect_ratio
|
||||||
|
horizontal_size = horizontal_size * horizontal_sizing_constant
|
||||||
|
|
||||||
|
return horizontal_size
|
||||||
|
|
||||||
|
def render_loop(display_metadata: UnrealXRDisplayMetadata, config: dict[str, str | int], cards: list[PyEvdi.Card]):
|
||||||
|
global vertical_size
|
||||||
|
global core_mesh
|
||||||
|
logger.info("Starting sensor event listener")
|
||||||
|
|
||||||
|
mcu_callbacks = MCUCallbackWrapper(roll_callback, pitch_callback, yaw_callback, text_message, stub_brightness_function, stub_brightness_function)
|
||||||
|
start_mcu_event_listener(display_metadata.device_vendor, mcu_callbacks)
|
||||||
|
|
||||||
|
logger.info("Beginning sensor initialization. Awaiting first sensor update")
|
||||||
|
|
||||||
|
while (not has_gotten_pitch_callback_before) or (not has_gotten_yaw_callback_before) or (not has_gotten_roll_callback_before):
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
logger.info("Initialized sensors")
|
||||||
|
|
||||||
|
camera = pyray.Camera3D()
|
||||||
|
camera.fovy = 45.0
|
||||||
|
|
||||||
|
vertical_size = find_max_vertical_size(camera.fovy, 5.0)
|
||||||
|
|
||||||
|
camera.position = pyray.Vector3(0.0, vertical_size/2, 5.0)
|
||||||
|
camera.target = pyray.Vector3(0.0, vertical_size/2, 0.0)
|
||||||
|
camera.up = pyray.Vector3(0.0, 1.0, 0.0)
|
||||||
|
camera.projection = pyray.CameraProjection.CAMERA_PERSPECTIVE
|
||||||
|
|
||||||
|
core_mesh = pyray.gen_mesh_plane(find_optimal_horizonal_res(display_metadata.max_height, display_metadata.max_width), vertical_size, 1, 1)
|
||||||
|
|
||||||
|
movement_vector = pyray.Vector3()
|
||||||
|
look_vector = pyray.Vector3()
|
||||||
|
|
||||||
|
has_z_vector_disabled_quirk = False
|
||||||
|
has_sensor_init_delay_quirk = False
|
||||||
|
sensor_init_start_time = time.time()
|
||||||
|
|
||||||
|
if "z_vector_disabled" in display_metadata.device_quirks and bool(display_metadata.device_quirks["z_vector_disabled"]):
|
||||||
|
logger.warning("QUIRK: The Z vector has been disabled for your specific device")
|
||||||
|
has_z_vector_disabled_quirk = True
|
||||||
|
|
||||||
|
if "sensor_init_delay" in display_metadata.device_quirks:
|
||||||
|
logger.warning(f"QUIRK: Waiting {str(display_metadata.device_quirks["sensor_init_delay"])} second(s) before reading sensors")
|
||||||
|
logger.warning("|| MOVEMENT WILL NOT BE OPERATIONAL DURING THIS TIME. ||")
|
||||||
|
sensor_init_start_time = time.time()
|
||||||
|
has_sensor_init_delay_quirk = True
|
||||||
|
|
||||||
|
rects: list[RectMetadata] = []
|
||||||
|
|
||||||
|
if int(config["display_count"]) >= 2:
|
||||||
|
display_angle = int(config["display_angle"])
|
||||||
|
display_spacing = int(config["display_spacing"])
|
||||||
|
total_displays = int(config["display_count"])
|
||||||
|
|
||||||
|
highest_possible_angle_on_both_sides = (total_displays-1)*display_angle
|
||||||
|
highest_possible_pixel_spacing_on_both_sides = (total_displays-1)*display_spacing
|
||||||
|
|
||||||
|
for i in range(total_displays):
|
||||||
|
current_angle = (-highest_possible_angle_on_both_sides)+(display_angle*i)
|
||||||
|
current_display_spacing = (-highest_possible_pixel_spacing_on_both_sides)+(display_spacing*i)
|
||||||
|
|
||||||
|
rect_metadata = RectMetadata(cards[i], None, None, None, current_angle, current_display_spacing)
|
||||||
|
|
||||||
|
has_acquired_fb = False
|
||||||
|
|
||||||
|
def fb_acquire_handler(evdi_buffer: PyEvdi.Buffer):
|
||||||
|
nonlocal has_acquired_fb
|
||||||
|
|
||||||
|
if has_acquired_fb:
|
||||||
|
return
|
||||||
|
|
||||||
|
has_acquired_fb = True
|
||||||
|
logger.info(f"Acquired buffer for card #{i+1} with ID {evdi_buffer.id}")
|
||||||
|
|
||||||
|
address = ctypes.pythonapi.PyCapsule_GetPointer
|
||||||
|
address.restype = ctypes.c_void_p
|
||||||
|
address.argtypes = [ctypes.py_object, ctypes.c_char_p]
|
||||||
|
|
||||||
|
buffer_void_ptr = address(evdi_buffer.bytes, None)
|
||||||
|
rect_metadata.buffer_ptr = pyray.ffi.cast("void *", buffer_void_ptr)
|
||||||
|
|
||||||
|
pyray_image = pyray.Image()
|
||||||
|
|
||||||
|
pyray_image.data = rect_metadata.buffer_ptr
|
||||||
|
pyray_image.width = display_metadata.max_width
|
||||||
|
pyray_image.height = display_metadata.max_height
|
||||||
|
pyray_image.mipmaps = 1
|
||||||
|
pyray_image.format = pyray.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||||
|
|
||||||
|
rect_metadata.texture = pyray.load_texture_from_image(pyray_image)
|
||||||
|
rect_metadata.model = pyray.load_model_from_mesh(core_mesh)
|
||||||
|
|
||||||
|
pyray.set_material_texture(rect_metadata.model.materials[0], pyray.MaterialMapIndex.MATERIAL_MAP_ALBEDO, rect_metadata.texture)
|
||||||
|
|
||||||
|
cards[i].acquire_framebuffer_handler = fb_acquire_handler
|
||||||
|
cards[i].handle_events(1000)
|
||||||
|
|
||||||
|
rects.append(rect_metadata)
|
||||||
|
|
||||||
|
while not pyray.window_should_close():
|
||||||
|
if has_sensor_init_delay_quirk:
|
||||||
|
if time.time() - sensor_init_start_time >= int(display_metadata.device_quirks["sensor_init_delay"]):
|
||||||
|
# Unset the quirk state
|
||||||
|
logger.info("Movement is now enabled.")
|
||||||
|
has_sensor_init_delay_quirk = False
|
||||||
|
else:
|
||||||
|
look_vector.x = (current_yaw-previous_yaw)*6.5
|
||||||
|
look_vector.y = -(current_pitch-previous_pitch)*6.5
|
||||||
|
|
||||||
|
if not has_z_vector_disabled_quirk:
|
||||||
|
look_vector.z = (current_roll-previous_roll)*6.5
|
||||||
|
|
||||||
|
pyray.update_camera_pro(camera, movement_vector, look_vector, 0.0)
|
||||||
|
|
||||||
|
pyray.begin_drawing()
|
||||||
|
pyray.clear_background(pyray.BLACK)
|
||||||
|
pyray.begin_mode_3d(camera)
|
||||||
|
|
||||||
|
for rect_count in range(len(rects)):
|
||||||
|
rect = rects[rect_count]
|
||||||
|
|
||||||
|
if rect.buffer_ptr is None or rect.texture is None or rect.model is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cards[rect_count].handle_events(1)
|
||||||
|
pyray.update_texture(rect.texture, rect.buffer_ptr)
|
||||||
|
|
||||||
|
pyray.draw_model_ex(
|
||||||
|
rect.model,
|
||||||
|
pyray.Vector3(0, vertical_size/2, 0),
|
||||||
|
pyray.Vector3(1, 0, 0), # rotate around X to make it vertical
|
||||||
|
90,
|
||||||
|
pyray.Vector3(1, 1, 1),
|
||||||
|
pyray.WHITE
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
pyray.end_mode_3d()
|
||||||
|
pyray.end_drawing()
|
||||||
|
|
||||||
|
logger.info("Goodbye!")
|
||||||
|
pyray.close_window()
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
setuptools==80.9.0
|
||||||
|
pybind11==2.13.6
|
||||||
|
pyedid==1.0.3
|
||||||
|
loguru==0.7.3
|
||||||
|
platformdirs==4.3.8
|
||||||
|
cffi=1.17.1
|
||||||
|
wheel=0.45.1
|
17
shell.nix
17
shell.nix
|
@ -3,12 +3,9 @@
|
||||||
}: pkgs.mkShell {
|
}: pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Runtime dependencies
|
# Runtime dependencies
|
||||||
|
python3
|
||||||
pciutils
|
pciutils
|
||||||
|
|
||||||
# UnrealXR build dependencies
|
|
||||||
go
|
|
||||||
gopls
|
|
||||||
|
|
||||||
# evdi build dependencies
|
# evdi build dependencies
|
||||||
libdrm
|
libdrm
|
||||||
linuxHeaders
|
linuxHeaders
|
||||||
|
@ -27,13 +24,12 @@
|
||||||
xorg.libXinerama
|
xorg.libXinerama
|
||||||
xorg.libX11
|
xorg.libX11
|
||||||
|
|
||||||
# raylib X11 build dependencies (for dev-only non-XR builds)
|
|
||||||
libxkbcommon
|
|
||||||
|
|
||||||
# nreal driver build dependencies
|
# nreal driver build dependencies
|
||||||
hidapi
|
hidapi
|
||||||
json_c
|
json_c
|
||||||
|
udev
|
||||||
libusb1
|
libusb1
|
||||||
|
opencv
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
@ -41,6 +37,11 @@
|
||||||
mkdir -p "$PWD/data/config" "$PWD/data/data"
|
mkdir -p "$PWD/data/config" "$PWD/data/data"
|
||||||
export UNREALXR_CONFIG_PATH="$PWD/data/config"
|
export UNREALXR_CONFIG_PATH="$PWD/data/config"
|
||||||
export UNREALXR_DATA_PATH="$PWD/data/data"
|
export UNREALXR_DATA_PATH="$PWD/data/data"
|
||||||
export UNREALXR_LOG_LEVEL="debug"
|
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
3
unrealxr
3
unrealxr
|
@ -1,2 +1 @@
|
||||||
#!/usr/bin/env bash
|
sudo LD_LIBRARY_PATH="$LD_LIBRARY_PATH" UNREALXR_CONFIG_PATH="$UNREALXR_CONFIG_PATH" UNREALXR_DATA_PATH="$UNREALXR_DATA_PATH" WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" XDG_RUNTIME_DIR="/user/run/0" python3 main.py
|
||||||
sudo UNREALXR_LOG_LEVEL="$UNREALXR_LOG_LEVEL" LD_LIBRARY_PATH="$LD_LIBRARY_PATH" UNREALXR_CONFIG_PATH="$UNREALXR_CONFIG_PATH" UNREALXR_DATA_PATH="$UNREALXR_DATA_PATH" WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" XDG_RUNTIME_DIR="/user/run/0" ./uxr
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue