viture_kit 0.1.1 copy "viture_kit: ^0.1.1" to clipboard
viture_kit: ^0.1.1 copied to clipboard

Native Dart FFI bindings for the VITURE XR Glasses SDK.

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:viture_kit/viture_kit.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SensorHomeScreen(),
    );
  }
}

class SensorHomeScreen extends StatefulWidget {
  const SensorHomeScreen({super.key});

  @override
  State<SensorHomeScreen> createState() => _SensorHomeScreenState();
}

class _SensorHomeScreenState extends State<SensorHomeScreen> {
  final VitureKit _vitureKit = VitureKit();

  double _brightness = 0;
  double _volume = 0;

  StreamSubscription<VitureSensorData>? _poseSubscription;

  bool _isBusy = false;
  String? _busyMessage;

  double _pitch = 0.0;
  double _roll = 0.0;
  double _yaw = 0.0;

  static const double pitchThreshold = 15.0;
  static const double yawThreshold = 15.0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _loadInitialValues();
    });
  }

  Future<void> _runWithLoading(
    Future<void> Function() action, {
    String message = 'Please wait…',
  }) async {
    if (_isBusy) return;
    setState(() {
      _isBusy = true;
      _busyMessage = message;
    });
    try {
      await action();
    } finally {
      if (mounted) {
        setState(() {
          _isBusy = false;
          _busyMessage = null;
        });
      }
    }
  }

  Future<void> _loadInitialValues() async {
    await _runWithLoading(() async {
      try {
        final brightness = _vitureKit.getBrightnessLevel();
        final volume = _vitureKit.getVolumeLevel();
        if (!mounted) return;
        setState(() {
          _brightness = brightness.toDouble();
          _volume = volume.toDouble();
        });
      } catch (e) {
        if (!mounted) return;
        _showErrorSnackBar('Failed to connect to device: $e');
      }
    }, message: 'Connecting to device…');
  }

  void _updateBrightness(double value) {
    setState(() => _brightness = value);
    try {
      _vitureKit.setBrightnessLevel(value.toInt());
    } catch (e) {
      _showErrorSnackBar('Failed to set brightness: $e');
    }
  }

  void _updateVolume(double value) {
    setState(() => _volume = value);
    try {
      _vitureKit.setVolumeLevel(value.toInt());
    } catch (e) {
      _showErrorSnackBar('Failed to set volume: $e');
    }
  }

  void _showErrorSnackBar(String message) {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  Future<void> _onToggleChanged(bool enabled) async {
    await _runWithLoading(
      () async {
        try {
          if (enabled) {
            _poseSubscription = _vitureKit.sensorStream.listen(
              (data) {
                if (!mounted) return;
                setState(() {
                  _pitch = -data.pitch;
                  _roll = -data.roll;
                  _yaw = -data.yaw;
                });
              },
              onError: (Object error) {
                if (!mounted) return;
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Head tracking error: $error')),
                );
              },
            );
            await _vitureKit.startHeadTracking();
          } else {
            await _poseSubscription?.cancel();
            _poseSubscription = null;
            await _vitureKit.releaseHeadTracking();
            if (mounted) {
              setState(() {
                _pitch = 0.0;
                _roll = 0.0;
                _yaw = 0.0;
              });
            }
          }
        } catch (e) {
          if (enabled) {
            await _poseSubscription?.cancel();
            _poseSubscription = null;
          }
          if (mounted) {
            ScaffoldMessenger.of(context)
                .showSnackBar(SnackBar(content: Text('Failed: $e')));
          }
        }
      },
      message: enabled ? 'Starting head tracking…' : 'Releasing head tracking…',
    );
  }

  @override
  void dispose() {
    _poseSubscription?.cancel();
    _vitureKit.dispose();
    super.dispose();
  }

  String _getDirectionText(double pitch, double yaw) {
    final isUp = pitch > pitchThreshold;
    final isDown = pitch < -pitchThreshold;
    final isRight = yaw > yawThreshold;
    final isLeft = yaw < -yawThreshold;

    if (!isUp && !isDown && !isRight && !isLeft) {
      return 'Looking straight';
    }

    String vertical = '';
    if (isUp) vertical = 'up';
    if (isDown) vertical = 'down';

    String horizontal = '';
    if (isRight) horizontal = 'right';
    if (isLeft) horizontal = 'left';

    if (vertical.isEmpty) return 'Looking $horizontal';
    if (horizontal.isEmpty) return 'Looking $vertical';

    return 'Looking $horizontal $vertical';
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 15);
    const spacer = SizedBox(height: 12);

    final bool isActive = _vitureKit.isHeadTrackingActive;
    final directionText = _getDirectionText(_pitch, _yaw);

    return Scaffold(
      appBar: AppBar(title: const Text('VITURE Head Tracking')),
      body: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                ElevatedButton(
                  onPressed: _isBusy
                      ? null
                      : () async {
                          await _runWithLoading(() async {
                            final res = VitureKit.fetchHidapiVitureProductIds();
                            debugPrint(res.toString());
                          }, message: 'Reading HID…');
                        },
                  child: const Text('hidapi'),
                ),
                ElevatedButton(
                  onPressed: _isBusy
                      ? null
                      : () async {
                          await _runWithLoading(() async {
                            final res = _vitureKit.getBrightnessLevel();
                            debugPrint(res.toString());
                            if (mounted) {
                              setState(() {
                                _brightness = res.toDouble();
                              });
                            }
                          }, message: 'Reading brightness…');
                        },
                  child: const Text('Get Brightness'),
                ),
                ElevatedButton(
                  onPressed: _isBusy
                      ? null
                      : () async {
                          await _runWithLoading(() async {
                            final res = _vitureKit.getVolumeLevel();
                            debugPrint(res.toString());
                            if (mounted) {
                              setState(() {
                                _volume = res.toDouble();
                              });
                            }
                          }, message: 'Reading volume…');
                        },
                  child: const Text('Get Volume'),
                ),
                Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Brightness: ${_brightness.toInt()}',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    Slider(
                      value: _brightness,
                      min: 0,
                      max: 8,
                      divisions: 8,
                      label: '${_brightness.toInt()}',
                      onChanged: _isBusy ? null : _updateBrightness,
                    ),
                    const SizedBox(height: 16),
                    Text(
                      'Volume: ${_volume.toInt()}',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    Slider(
                      value: _volume,
                      min: 0,
                      max: 8,
                      divisions: 8,
                      label: '${_volume.toInt()}',
                      onChanged: _isBusy ? null : _updateVolume,
                    ),
                  ],
                ),
                Card(
                  elevation: 2,
                  child: SwitchListTile.adaptive(
                    title: const Text('Take head tracking'),
                    subtitle: Text(
                      isActive
                          ? 'Active – SpaceWalker tracking is disabled'
                          : 'Released – SpaceWalker can use tracking',
                    ),
                    value: isActive,
                    onChanged: _isBusy ? null : _onToggleChanged,
                  ),
                ),
                spacer,
                Expanded(
                  child: !isActive
                      ? const Center(
                          child: Text(
                            'Head tracking is released.\n'
                            'SpaceWalker should be able to use the glasses.',
                            textAlign: TextAlign.center,
                          ),
                        )
                      : SingleChildScrollView(
                          child: Column(
                            children: [
                              Card(
                                elevation: 3,
                                child: Padding(
                                  padding: const EdgeInsets.symmetric(
                                    vertical: 16,
                                    horizontal: 20,
                                  ),
                                  child: Text(
                                    directionText,
                                    style: const TextStyle(
                                      fontSize: 24,
                                      fontWeight: FontWeight.bold,
                                    ),
                                    textAlign: TextAlign.center,
                                  ),
                                ),
                              ),
                              spacer,
                              HeadVisualizer(
                                pitch: _pitch,
                                roll: _roll,
                                yaw: _yaw,
                              ),
                              spacer,
                              Card(
                                elevation: 2,
                                child: Padding(
                                  padding: const EdgeInsets.all(12),
                                  child: Text(
                                    'Pitch: ${_pitch.toStringAsFixed(1)}°   '
                                    'Roll: ${_roll.toStringAsFixed(1)}°   '
                                    'Yaw: ${_yaw.toStringAsFixed(1)}°',
                                    style: textStyle,
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                ),
              ],
            ),
          ),
          if (_isBusy)
            Positioned.fill(
              child: ColoredBox(
                color: Colors.black.withValues(alpha: 0.55),
                child: Center(
                  child: Card(
                    elevation: 8,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 32,
                        vertical: 28,
                      ),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          const SizedBox(
                            width: 48,
                            height: 48,
                            child: CircularProgressIndicator(strokeWidth: 3),
                          ),
                          if (_busyMessage != null) ...[
                            const SizedBox(height: 20),
                            Text(
                              _busyMessage!,
                              style: Theme.of(context).textTheme.titleMedium,
                              textAlign: TextAlign.center,
                            ),
                          ],
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class HeadVisualizer extends StatelessWidget {
  final double pitch;
  final double roll;
  final double yaw;

  const HeadVisualizer({
    super.key,
    required this.pitch,
    required this.roll,
    required this.yaw,
  });

  @override
  Widget build(BuildContext context) {
    const double sensitivity = 0.85;

    final double yawRad = -yaw * math.pi / 180.0 * sensitivity;
    final double pitchRad = -pitch * math.pi / 180.0 * sensitivity;
    final double rollRad = roll * math.pi / 180.0 * sensitivity;

    final matrix = Matrix4.identity()
      ..setEntry(3, 2, 0.0015)
      ..rotateY(yawRad)
      ..rotateX(pitchRad)
      ..rotateZ(rollRad);

    return Card(
      elevation: 4,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            const Text(
              'Head',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
            const SizedBox(height: 12),
            SizedBox(
              height: 240,
              width: 240,
              child: Center(
                child: Transform(
                  alignment: Alignment.center,
                  transform: matrix,
                  child: ClipOval(
                    child: Image.network(
                      'https://randomuser.me/api/portraits/men/32.jpg',
                      width: 200,
                      height: 200,
                      fit: BoxFit.cover,
                      loadingBuilder: (context, child, loadingProgress) {
                        if (loadingProgress == null) return child;
                        return Container(
                          width: 200,
                          height: 200,
                          color: Colors.grey.shade200,
                          child: const Center(
                            child: CircularProgressIndicator(strokeWidth: 2),
                          ),
                        );
                      },
                      errorBuilder: (context, error, stackTrace) {
                        debugPrint('Image load error: $error');
                        return Container(
                          width: 200,
                          height: 200,
                          decoration: BoxDecoration(
                            color: const Color(0xFFFFDBAC),
                            shape: BoxShape.circle,
                            border: Border.all(
                              color: Colors.brown.shade400,
                              width: 3,
                            ),
                          ),
                          child: const Icon(
                            Icons.person,
                            size: 110,
                            color: Colors.brown,
                          ),
                        );
                      },
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
0
points
373
downloads

Publisher

unverified uploader

Weekly Downloads

Native Dart FFI bindings for the VITURE XR Glasses SDK.

Homepage
Repository (GitHub)
View/report issues

Topics

#viture #xr #macos #ffi

License

unknown (license)

Dependencies

code_assets, ffi, hidapi, hooks, logging, native_toolchain_c

More

Packages that depend on viture_kit