godice_universal_ble 0.1.0 copy "godice_universal_ble: ^0.1.0" to clipboard
godice_universal_ble: ^0.1.0 copied to clipboard

GoDice transport and scanner for Flutter built on universal_ble. Pair it with package:godice to connect to GoDice smart dice on Android, iOS, macOS, Windows, Linux and Web.

example/lib/main.dart

// Example app for godice_universal_ble.
//
// Scans for GoDice, connects to the one you tap, shows the rolled value and
// lets you switch die type, read battery/colour and control the LEDs. Every
// roll is also printed with debugPrint so it shows up in `flutter run` logs.
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:godice/godice.dart';
import 'package:godice_universal_ble/godice_universal_ble.dart';

void main() => runApp(const GoDiceExampleApp());

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

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'GoDice',
    theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
    darkTheme: ThemeData(
      colorSchemeSeed: Colors.indigo,
      brightness: Brightness.dark,
      useMaterial3: true,
    ),
    home: const HomePage(),
  );
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final GoDiceScanner _scanner = GoDiceScanner(manageScan: true);
  final Map<String, BleDevice> _found = <String, BleDevice>{};
  StreamSubscription<BleDevice>? _scanSub;
  String? _error;

  GoDice? _die;
  StreamSubscription<GoDiceMessage>? _messageSub;
  StreamSubscription<bool>? _linkSub;
  bool _connecting = false;
  bool _ledsOn = false;
  PositionMessage? _lastPosition;
  bool _rolling = false;
  int? _battery;

  bool get _scanning => _scanSub != null;

  @override
  void dispose() {
    _scanSub?.cancel();
    _disconnect();
    super.dispose();
  }

  void _toggleScan() {
    if (_scanning) {
      _scanSub?.cancel();
      setState(() => _scanSub = null);
      return;
    }
    setState(() {
      _error = null;
      _found.clear();
    });
    _scanSub = _scanner.discover().listen(
      (d) => setState(() => _found[d.deviceId] = d),
      onError: (Object e) => setState(() {
        _error = e.toString();
        _scanSub = null;
      }),
      onDone: () => setState(() => _scanSub = null),
    );
    setState(() {});
  }

  Future<void> _connect(BleDevice device) async {
    _scanSub?.cancel();
    _scanSub = null;
    final die = GoDiceUniversalBle.fromDevice(device);
    setState(() {
      _connecting = true;
      _error = null;
      _die = die;
    });
    try {
      _linkSub = die.connectionState.listen((up) {
        if (!up && mounted) {
          debugPrint('GoDice ${die.name} disconnected');
          _disconnect();
        }
      });
      _messageSub = die.messages.listen(_onMessage);
      await die.connect();
      debugPrint('Connected to ${die.name}');
      _battery = await die.getBatteryLevel();
      await die.getColor();
      await die.setLeds(die.color?.rgb ?? RgbColor.white);
      _ledsOn = true;
    } on Object catch (e) {
      debugPrint('Connect failed: $e');
      _error = e.toString();
      await _disconnect();
    } finally {
      if (mounted) setState(() => _connecting = false);
    }
  }

  void _onMessage(GoDiceMessage message) {
    switch (message) {
      case RollStartMessage():
        debugPrint('rolling...');
        setState(() => _rolling = true);
      case PositionMessage():
        debugPrint(
          '${message.kind.name} ${message.value} '
          '(${message.dieType.name}, xyz ${message.xyz})',
        );
        setState(() {
          _rolling = false;
          _lastPosition = message;
        });
      case BatteryLevelMessage(:final level):
        setState(() => _battery = level);
      case DiceColorMessage() || UnknownMessage():
        setState(() {});
    }
  }

  Future<void> _disconnect() async {
    final die = _die;
    await _messageSub?.cancel();
    await _linkSub?.cancel();
    _messageSub = null;
    _linkSub = null;
    if (die != null) {
      if (die.isConnected) {
        try {
          await die.ledsOff();
        } on Object {
          // Link already gone.
        }
      }
      await die.dispose();
    }
    if (mounted) {
      setState(() {
        _die = null;
        _lastPosition = null;
        _rolling = false;
        _battery = null;
        _ledsOn = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final die = _die;
    return Scaffold(
      appBar: AppBar(
        title: Text(die == null ? 'GoDice' : die.name ?? 'GoDice'),
        actions: <Widget>[
          if (die != null)
            IconButton(
              tooltip: 'Disconnect',
              icon: const Icon(Icons.bluetooth_disabled),
              onPressed: _disconnect,
            ),
        ],
      ),
      body: die == null ? _buildScanView() : _buildDieView(die),
    );
  }

  Widget _buildScanView() {
    final devices = _found.values.toList()
      ..sort((a, b) => (b.rssi ?? -999).compareTo(a.rssi ?? -999));
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(16),
          child: FilledButton.icon(
            onPressed: _connecting ? null : _toggleScan,
            icon: Icon(_scanning ? Icons.stop : Icons.search),
            label: Text(_scanning ? 'Stop scanning' : 'Scan for dice'),
          ),
        ),
        if (_error != null)
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(
              _error!,
              style: TextStyle(color: Theme.of(context).colorScheme.error),
            ),
          ),
        if (_scanning) const LinearProgressIndicator(),
        if (_connecting)
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('Connecting...'),
          ),
        Expanded(
          child: devices.isEmpty
              ? const Center(child: Text('Shake a die to wake it up.'))
              : ListView(
                  children: <Widget>[
                    for (final d in devices)
                      ListTile(
                        leading: _ColorDot(
                          GoDiceDeviceName.tryParse(d.name)?.color,
                        ),
                        title: Text(d.name ?? d.deviceId),
                        subtitle: Text('${d.rssi ?? '?'} dBm  ${d.deviceId}'),
                        onTap: _connecting ? null : () => _connect(d),
                      ),
                  ],
                ),
        ),
      ],
    );
  }

  Widget _buildDieView(GoDice die) {
    final position = _lastPosition;
    final theme = Theme.of(context);
    final String big;
    if (_rolling) {
      big = '…';
    } else if (position == null) {
      big = '?';
    } else if (die.dieType == DieType.d10x) {
      big = position.value.toString().padLeft(2, '0');
    } else {
      big = '${position.value}';
    }
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: <Widget>[
          Wrap(
            spacing: 16,
            runSpacing: 8,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: <Widget>[
              _ColorDot(die.color, label: die.color?.name ?? 'colour ?'),
              Text(_battery == null ? 'battery ?' : 'battery $_battery%'),
              DropdownButton<DieType>(
                value: die.dieType,
                onChanged: (t) => setState(() => die.dieType = t!),
                items: <DropdownMenuItem<DieType>>[
                  for (final t in DieType.values)
                    DropdownMenuItem<DieType>(
                      value: t,
                      child: Text(t.name.toUpperCase()),
                    ),
                ],
              ),
            ],
          ),
          Expanded(
            child: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text(
                    big,
                    style: theme.textTheme.displayLarge?.copyWith(
                      fontSize: 140,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Text(
                    _rolling
                        ? 'rolling'
                        : position == null
                        ? 'Roll the die'
                        : switch (position.kind) {
                            StabilityKind.stable => 'rolled',
                            StabilityKind.fakeStable => 'paused mid-roll',
                            StabilityKind.tiltStable => 'tilted',
                            StabilityKind.moveStable => 'moved, not rolled',
                          },
                    style: theme.textTheme.titleMedium,
                  ),
                  if (position != null)
                    Text(
                      'xyz ${position.xyz}',
                      style: theme.textTheme.bodySmall,
                    ),
                ],
              ),
            ),
          ),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              OutlinedButton(
                onPressed: () => unawaited(
                  die.getBatteryLevel().catchError((Object _) => _battery ?? 0),
                ),
                child: const Text('Battery'),
              ),
              OutlinedButton(
                onPressed: () {
                  _ledsOn = !_ledsOn;
                  unawaited(
                    _ledsOn
                        ? die.setLeds(die.color?.rgb ?? RgbColor.white)
                        : die.ledsOff(),
                  );
                  setState(() {});
                },
                child: Text(_ledsOn ? 'LEDs off' : 'LEDs on'),
              ),
              OutlinedButton(
                onPressed: () => unawaited(
                  die.pulseLed(color: die.color?.rgb ?? RgbColor.white),
                ),
                child: const Text('Pulse'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _ColorDot extends StatelessWidget {
  const _ColorDot(this.color, {this.label});

  final DiceColor? color;
  final String? label;

  @override
  Widget build(BuildContext context) {
    final rgb = color?.rgb;
    final dot = Container(
      width: 20,
      height: 20,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: rgb == null
            ? Colors.grey
            : color == DiceColor.black
            ? Colors.black
            : Color.fromARGB(255, rgb.r, rgb.g, rgb.b),
        border: Border.all(color: Colors.grey.shade600),
      ),
    );
    if (label == null) return dot;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[dot, const SizedBox(width: 8), Text(label!)],
    );
  }
}
1
likes
160
points
77
downloads

Documentation

API reference

Publisher

verified publisherskystoneapps.com

Weekly Downloads

GoDice transport and scanner for Flutter built on universal_ble. Pair it with package:godice to connect to GoDice smart dice on Android, iOS, macOS, Windows, Linux and Web.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

flutter, godice, universal_ble

More

Packages that depend on godice_universal_ble