Реалізація VR-контролерів через Bluetooth у мобільному додатку

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Реалізація VR-контролерів через Bluetooth у мобільному додатку
Середній
~3-5 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Реалізація VR-контролерів через Bluetooth у мобільному додатку

Mobile VR з Bluetooth-контролерами — це Cardboard/Daydream-еpоха або сучасні рішення типу Pico G3 з 3DoF-трекінгом. У обох випадках завдання одне: отримувати дані IMU (акселерометр, гіроскоп, магнітометр) та кнопки з контролера по BLE з мінімальною латентністю, переводити в позицію/ориєнтацію у 3D-просторі та передавати в рендеринг рушія.

GATT-профіль BLE-контролера

Більшість VR-контролерів реалізують стандартний HID over GATT профіль або кастомний GATT-сервіс для IMU-даних. Для кастомних — потрібна документація виробника з UUID характеристик.

Типова структура GATT для VR-контролера:

  • Service UUID 00001812-0000-1000-8000-00805f9b34fb (HID Service) або кастомний
  • Report Characteristic — вхідні дані: кнопки + IMU (notify)
  • Battery Service 0000180f-0000-1000-8000-00805f9b34fb — рівень заряду (read/notify)
class VRControllerGattClient(private val context: Context) {
    private var bluetoothGatt: BluetoothGatt? = null
    private val CONTROLLER_SERVICE_UUID = UUID.fromString("YOUR-CONTROLLER-UUID")
    private val IMU_CHARACTERISTIC_UUID = UUID.fromString("YOUR-IMU-CHAR-UUID")

    fun connect(device: BluetoothDevice) {
        // TRANSPORT_LE — явно вказуємо BLE, не класичний BT
        bluetoothGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
                gatt.discoverServices()
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            val service = gatt.getService(CONTROLLER_SERVICE_UUID) ?: return
            val imuChar = service.getCharacteristic(IMU_CHARACTERISTIC_UUID) ?: return
            gatt.setCharacteristicNotification(imuChar, true)

            // Включаємо Client Characteristic Configuration Descriptor
            val descriptor = imuChar.getDescriptor(
                UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
            )
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            gatt.writeDescriptor(descriptor)
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray
        ) {
            parseControllerData(value)
        }
    }
}

requestConnectionPriority(CONNECTION_PRIORITY_HIGH) — переводит BLE connection interval з default 45ms на 7.5ms. Критично для VR: при 45ms затримка введення ощущається як дискомфорт, при 7.5ms — незамітна.

Парсинг IMU-даних та sensor fusion

Дані з MEMS-гіроскопа та акселерометра — сирі показання у одиницях виробника. Потрібна калібровка та sensor fusion для отримання стабільної кватернионної ориєнтації.

data class ControllerState(
    val gyroX: Float, val gyroY: Float, val gyroZ: Float, // рад/с
    val accelX: Float, val accelY: Float, val accelZ: Float, // м/с²
    val buttons: Int, // битмаска кнопок
    val trigger: Float, // аналоговий тригер 0..1
    val timestamp: Long
)

fun parseControllerData(raw: ByteArray): ControllerState {
    val buffer = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN)
    return ControllerState(
        gyroX = buffer.short.toFloat() / 32768f * GYRO_SCALE, // GYRO_SCALE у рад/с
        gyroY = buffer.short.toFloat() / 32768f * GYRO_SCALE,
        gyroZ = buffer.short.toFloat() / 32768f * GYRO_SCALE,
        accelX = buffer.short.toFloat() / 32768f * ACCEL_SCALE,
        accelY = buffer.short.toFloat() / 32768f * ACCEL_SCALE,
        accelZ = buffer.short.toFloat() / 32768f * ACCEL_SCALE,
        buttons = buffer.short.toInt(),
        trigger = (buffer.byte.toInt() and 0xFF) / 255f,
        timestamp = SystemClock.elapsedRealtimeNanos()
    )
}

Для ориєнтації — complementary filter (швидко) або Madgwick/Mahony AHRS (точніше):

class MadgwickFilter(private val beta: Float = 0.1f) {
    private var q = floatArrayOf(1f, 0f, 0f, 0f) // кватернион ориєнтації

    fun update(gx: Float, gy: Float, gz: Float,
               ax: Float, ay: Float, az: Float, dt: Float) {
        // Madgwick AHRS algorithm
        // Нормалізуємо акселерометр
        val norm = sqrt(ax * ax + ay * ay + az * az)
        if (norm == 0f) return
        // ... повна реалізація алгоритму
        // Результат: q[0..3] — кватернион поточної ориєнтації
    }

    fun getQuaternion() = Quaternion(q[0], q[1], q[2], q[3])
}

beta = 0.1f — компроміс між швидкістю реакції та фільтрацією шуму. При швидких рухах збільшувати до 0.3, при статиці — зменшувати до 0.01.

Інтеграція з VR-рендерингом

На Android — передача даних контролера в Unity через AndroidJavaClass або безпосередньо в OpenXR через XR_EXT_hand_tracking-сумісний плагін. Для Godot — GodotAndroidPlugin з exposed методами.

Типова помилка: передавати дані контролера прямо з BLE callback-треду в рендер-тред. Потрібен thread-safe буфер:

// Atomic reference для останнього стану контролера
private val latestState = AtomicReference<ControllerState?>()

override fun onCharacteristicChanged(..., value: ByteArray) {
    latestState.set(parseControllerData(value))
}

// З рендер-треду (кожний кадр)
fun pollControllerState(): ControllerState? = latestState.getAndSet(null)

Строки

BLE-підключення до існуючого VR-контролера з готовою GATT-документацією, парсинг IMU, інтеграція у Unity/Godot: 3–5 днів. Розробка з реверс-інжинирингом GATT-протоколу невідомого контролера + кастомний sensor fusion: 1–2 тижні.