chore: Remove legacy Python codebase
This commit is contained in:
parent
919d247934
commit
643fd3c61c
14 changed files with 24 additions and 1058 deletions
291
.gitignore
vendored
291
.gitignore
vendored
|
@ -1,281 +1,30 @@
|
|||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# env file
|
||||
.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
|
||||
# development dirs
|
||||
data
|
||||
drivers
|
||||
|
|
9
.gitmodules
vendored
9
.gitmodules
vendored
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
188
main.py
|
@ -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
235
render.py
|
@ -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()
|
|
@ -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
|
11
shell.nix
11
shell.nix
|
@ -3,9 +3,12 @@
|
|||
}: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Runtime dependencies
|
||||
python3
|
||||
pciutils
|
||||
|
||||
# UnrealXR build dependencies
|
||||
go
|
||||
gopls
|
||||
|
||||
# evdi build dependencies
|
||||
libdrm
|
||||
linuxHeaders
|
||||
|
@ -37,11 +40,5 @@
|
|||
mkdir -p "$PWD/data/config" "$PWD/data/data"
|
||||
export UNREALXR_CONFIG_PATH="$PWD/data/config"
|
||||
export UNREALXR_DATA_PATH="$PWD/data/data"
|
||||
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
|
||||
source .venv/bin/activate
|
||||
'';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue