Реализация 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
    756
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1054
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    864
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    445

Реализация VR-контроллеров через Bluetooth в мобильном приложении

Mobile VR с Bluetooth-контроллерами — это Cardboard/Daydream-эра или современные решения типа 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 недели.