Merge pull request 'Rewrite entire codebase into Go' (#2) from go-rewrite into main

Reviewed-on: #2
This commit is contained in:
Tera << 8 2025-06-25 21:49:50 -04:00
commit 5ea88e8c4a
61 changed files with 5786 additions and 1094 deletions

293
.gitignore vendored
View file

@ -1,281 +1,32 @@
# ---> Python # ---> Go
# Byte-compiled / optimized / DLL files # If you prefer the allow list template instead of the deny list, see community template:
__pycache__/ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
*.py[cod] #
*$py.class # Binaries for programs and plugins
*.exe
# C extensions *.exe~
*.dll
*.so *.so
*.dylib
# Distribution / packaging # Test binary, built with `go test -c`
.Python *.test
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller # Output of the go coverage tool, specifically when used with LiteIDE
# Usually these files are written by a python script from a template *.out
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs # Dependency directories (remove the comment below to include it)
pip-log.txt # vendor/
pip-delete-this-directory.txt
# Unit test / coverage reports # Go workspace file
htmlcov/ go.work
.tox/ go.work.sum
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations # env file
*.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 .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
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# ---> C++
# 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
drivers # artifacts
uxr

9
.gitmodules vendored
View file

@ -1,9 +0,0 @@
[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

13
.zed/settings.json Normal file
View file

@ -0,0 +1,13 @@
// 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"]
}
}
}
}

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
APP_DIR := ./app
OUTPUT := uxr
TAGS := xreal noaudio drm drm_leasing drm_disable_input
.PHONY: all build clean
all: build
build:
go build -v -tags '$(TAGS)' -o $(OUTPUT) $(APP_DIR)
clean:
rm -f $(OUTPUT)

View file

@ -22,41 +22,10 @@ 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.
First, install the runtime dependencies. For Debian-based distros, the dependency list should be: `python3 python3-pip` 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`
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` If you're using Nix/NixOS, all you need to do is use `nix-shell` to enter the development environment.
If you're using Nix/NixOS, use the `nix-shell` to enter the development environment. ## Building
After that, create a virtual environment for Python (done automatically in Nix): `python3 -m venv .venv`. Then, activate it: `source .venv/bin/activate` Just run `make` in the root directory.
Finally, install the Python dependencies: `pip install -r requirements.txt`
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`

80
app/config/config.go Normal file
View file

@ -0,0 +1,80 @@
package config
import _ "embed"
//go:embed default_config.yml
var InitialConfig []byte
type DisplayConfig struct {
Angle *int `yaml:"angle"`
FOV *int `yaml:"fov"`
Spacing *int `yaml:"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 getPtrToBool(bool bool) *bool {
return &bool
}
var DefaultConfig = &Config{
DisplayConfig: DisplayConfig{
Angle: getPtrToInt(45),
FOV: getPtrToInt(45),
Spacing: getPtrToInt(1),
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
}
}

View file

@ -0,0 +1,18 @@
# __ __ ___ __ ____
# / / / /___ ________ ____ _/ / |/ // __ \
# / / / / __ \/ ___/ _ \/ __ `/ /| // /_/ /
# / /_/ / / / / / / __/ /_/ / // |/ _, _/
# \____/_/ /_/_/ \___/\__,_/_//_/|_/_/ |_|
#
# 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: 1 # Spacing between 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.

View file

@ -0,0 +1,72 @@
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)
}

View file

@ -0,0 +1,127 @@
//go:build linux
// +build linux
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
}

View file

@ -0,0 +1,21 @@
//go:build darwin
// +build darwin
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")
}

View file

@ -0,0 +1,21 @@
//go:build windows
// +build windows
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")
}

14
app/edidtools/quirks.go Normal file
View file

@ -0,0 +1,14 @@
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,
},
},
}

20
app/edidtools/struct.go Normal file
View file

@ -0,0 +1,20 @@
package edidtools
type DisplayQuirks struct {
MaxWidth int
MaxHeight int
MaxRefreshRate int
SensorInitDelay int
ZVectorDisabled bool
}
type DisplayMetadata struct {
EDID []byte
DeviceVendor string
DeviceQuirks DisplayQuirks
MaxWidth int
MaxHeight int
MaxRefreshRate int
LinuxDRMCard string
LinuxDRMConnector string
}

219
app/main.go Normal file
View file

@ -0,0 +1,219 @@
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 * 2))
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 := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
displayMetadata := &renderer.EvdiDisplayMetadata{
EvdiNode: openedDevice,
Rect: displayRect,
Buffer: displayBuffer,
ShouldRequestUpdate: true,
}
displayMetadata.EventContext = &libevdi.EvdiEventContext{
UpdateReadyHandler: func(bufferToBeUpdated int) {
displayMetadata.IsUpdateReady = true
},
}
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())
}
}

254
app/renderer/renderer.go Normal file
View file

@ -0,0 +1,254 @@
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
}
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 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(45.0)
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,
)
// Disable front and back face culling. It caused issues involving the entire virtual display dissappearing
// If this issue still happens I *am* going to cry
rl.DisableBackfaceCulling()
rl.DisableDepthTest()
coreMesh := rl.GenMeshPlane(findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize), verticalSize, 1, 1)
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))
for i, card := range evdiCards {
image := rl.NewImage(card.Buffer.Buffer, int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), 1, rl.UncompressedR8g8b8a8)
texture := rl.LoadTextureFromImage(image)
model := rl.LoadModelFromMesh(coreMesh)
rl.SetMaterialTexture(model.Materials, rl.MapAlbedo, texture)
rects[i] = &TextureModelPair{
Texture: texture,
Model: model,
}
}
eventTimeoutDuration := 0 * time.Millisecond
for !rl.WindowShouldClose() {
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)
}
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())
break
}
if ready {
if err := card.EvdiNode.HandleEvents(card.EventContext); err != nil {
log.Errorf("Failed to handle display events: %s", err.Error())
break
}
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)
}
rl.DrawModelEx(
rect.Model,
rl.Vector3{
X: 0,
Y: verticalSize / 2,
Z: 0,
},
// rotate around X to make it vertical
rl.Vector3{
X: 1,
Y: 0,
Z: 0,
},
90,
rl.Vector3{
X: 1,
Y: 1,
Z: 1,
},
rl.White,
)
break
}
rl.EndMode3D()
rl.EndDrawing()
}
log.Info("Goodbye!")
rl.CloseWindow()
}

14
app/renderer/struct.go Normal file
View file

@ -0,0 +1,14 @@
package renderer
import (
"git.terah.dev/imterah/goevdi/libevdi"
)
type EvdiDisplayMetadata struct {
EvdiNode *libevdi.EvdiNode
Rect *libevdi.EvdiDisplayRect
Buffer *libevdi.EvdiBuffer
EventContext *libevdi.EvdiEventContext
ShouldRequestUpdate bool
IsUpdateReady bool
}

23
ardriver/ardriver.go Normal file
View file

@ -0,0 +1,23 @@
package ardriver
import (
"fmt"
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
"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
}
return nil, fmt.Errorf("failed to initialize any device")
}

View file

@ -0,0 +1,22 @@
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)
}

32
ardriver/xreal/Fusion.h Normal file
View file

@ -0,0 +1,32 @@
/**
* @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

510
ardriver/xreal/FusionAhrs.c Normal file
View file

@ -0,0 +1,510 @@
/**
* @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

111
ardriver/xreal/FusionAhrs.h Normal file
View file

@ -0,0 +1,111 @@
/**
* @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

187
ardriver/xreal/FusionAxes.h Normal file
View file

@ -0,0 +1,187 @@
/**
* @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

View file

@ -0,0 +1,44 @@
/**
* @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

View file

@ -0,0 +1,49 @@
/**
* @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

View file

@ -0,0 +1,25 @@
/**
* @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

View file

@ -0,0 +1,25 @@
/**
* @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

481
ardriver/xreal/FusionMath.h Normal file
View file

@ -0,0 +1,481 @@
/**
* @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

View file

@ -0,0 +1,77 @@
/**
* @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

View file

@ -0,0 +1,40 @@
/**
* @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

5
ardriver/xreal/README.md Normal file
View file

@ -0,0 +1,5 @@
# Xreal driver
## Attribution
- [Xreal driver](https://gitlab.com/TheJackiMonster/nrealAirLinuxDriver)
- [Fusion](https://github.com/xioTechnologies/Fusion)

102
ardriver/xreal/crc32.c Normal file
View file

@ -0,0 +1,102 @@
//
// 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;
}

40
ardriver/xreal/crc32.h Normal file
View file

@ -0,0 +1,40 @@
#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

50
ardriver/xreal/device.c Normal file
View file

@ -0,0 +1,50 @@
//
// 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();
}
}

46
ardriver/xreal/device.h Normal file
View file

@ -0,0 +1,46 @@
#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

1272
ardriver/xreal/device_imu.c Normal file

File diff suppressed because it is too large Load diff

227
ardriver/xreal/device_imu.h Normal file
View file

@ -0,0 +1,227 @@
#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

731
ardriver/xreal/device_mcu.c Normal file
View file

@ -0,0 +1,731 @@
//
// 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;
}

212
ardriver/xreal/device_mcu.h Normal file
View file

@ -0,0 +1,212 @@
#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

7
ardriver/xreal/go_ffi.c Normal file
View file

@ -0,0 +1,7 @@
#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);
}

3
ardriver/xreal/go_ffi.h Normal file
View file

@ -0,0 +1,3 @@
#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);

108
ardriver/xreal/hid_ids.c Normal file
View file

@ -0,0 +1,108 @@
//
// 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;
}
}

54
ardriver/xreal/hid_ids.h Normal file
View file

@ -0,0 +1,54 @@
#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

134
ardriver/xreal/xreal.go Normal file
View file

@ -0,0 +1,134 @@
//go:build xreal
// +build xreal
package xreal
// #cgo CFLAGS: -w -I./Fusion/
// #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 (
IsXrealEnabled = true
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
}
func New() (*XrealDevice, error) {
device := &XrealDevice{}
err := device.Initialize()
if err != nil {
return nil, err
}
return device, nil
}

View file

@ -0,0 +1,7 @@
//go:build xreal && !xreal_debug_logging
// +build xreal,!xreal_debug_logging
package xreal
// #cgo CFLAGS: -DNDEBUG
import "C"

View file

@ -0,0 +1,44 @@
//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) error {
return fmt.Errorf("xreal is not enabled")
}
func New() (*XrealDevice, error) {
return nil, fmt.Errorf("xreal is not enabled")
}

102
edidpatcher/edidpatcher.go Normal file
View file

@ -0,0 +1,102 @@
package edidpatcher
import (
"fmt"
"github.com/charmbracelet/log"
"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 {
log.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
}

34
go.mod Normal file
View file

@ -0,0 +1,34 @@
module git.terah.dev/UnrealXR/unrealxr
go 1.24.3
require (
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9
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/google/uuid v1.6.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/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
go.sum Normal file
View file

@ -0,0 +1,65 @@
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 v1.14.11-0.20250626004148-bdbef2a68ff9 h1:TYcPZ62CR3keYf/dE9KyV5X5krh+riDyZ3fnhkeSRyA=
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9/go.mod h1:4scjAuFakx/2gTRSeCtTNHnj1v9FdF3XiOMmWsz4FDs=
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=

View file

@ -1,10 +0,0 @@
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")

View file

@ -1,86 +0,0 @@
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

View file

@ -1,84 +0,0 @@
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)

View file

@ -1,145 +0,0 @@
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()

View file

@ -1,13 +0,0 @@
# 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
View file

@ -1,188 +0,0 @@
#!/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 +0,0 @@
Subproject commit 3673a4b34d386921fc323ddbd2ef0e000022e2d4

@ -1 +0,0 @@
Subproject commit 36ad789c9b5893653c523d4af6e24120ec19ab33

@ -1 +0,0 @@
Subproject commit 7c982c0d4e2773739f00eb91a2d2cbe87eb7c76d

235
render.py
View file

@ -1,235 +0,0 @@
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()

View file

@ -1,7 +0,0 @@
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

View file

@ -3,9 +3,12 @@
}: 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
@ -37,11 +40,5 @@
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"
if [ ! -d ".venv" ]; then
python3 -m venv .venv
fi
source .venv/bin/activate
''; '';
} }

View file

@ -1 +1 @@
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