simple_ble_plugin 0.0.921 copy "simple_ble_plugin: ^0.0.921" to clipboard
simple_ble_plugin: ^0.0.921 copied to clipboard

simple_ble_plugin

example/lib/main.dart

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
import 'package:simple_ble_plugin/simple_ble_plugin.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _simpleBlePlugin = SimpleBlePlugin();
  List<ScanResult> _scanResults = [];
  bool _isScanning = false;
  StreamSubscription? _scanSubscription;

  @override
  void initState() {
    super.initState();
    _scanSubscription = _simpleBlePlugin.scanResults.listen((result) {
      setState(() {
        final index = _scanResults.indexWhere((r) => r.device.id == result.device.id);
        if (index != -1) {
          _scanResults[index] = result;
        } else {
          _scanResults.add(result);
        }
      });
    });
  }

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

  Future<void> _startScan() async {
    // Request permissions
    Map<Permission, PermissionStatus> statuses = await [
      Permission.bluetooth,
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
      Permission.location,
    ].request();

    // Check if granted (simplified)
    bool granted = true;
    // logic depends on Android version, but generally...
    // just try starting scan

    setState(() {
      _scanResults.clear();
      _isScanning = true;
    });

    try {
      await _simpleBlePlugin.startScan(
        timeout: 10,
        extended: true,
        settings: ScanSettings(
          scanMode: ScanMode.lowLatency,
          allowDuplicates: true,
        ),
      );
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Scan failed: $e")));
        setState(() {
          _isScanning = false;
        });
      }
    }
  }

  Future<void> _stopScan() async {
    await _simpleBlePlugin.stopScan();
    setState(() {
      _isScanning = false;
    });
  }

  void _navigateToDevice(BleDevice device) {
    _stopScan();
    Navigator.of(context).push(MaterialPageRoute(
      builder: (context) => DeviceScreen(device: device),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BLE Example'),
        actions: [
          if (_isScanning)
            IconButton(icon: const Icon(Icons.stop), onPressed: _stopScan)
          else
            IconButton(icon: const Icon(Icons.refresh), onPressed: _startScan),
        ],
      ),
      body: ListView.builder(
        itemCount: _scanResults.length,
        itemBuilder: (context, index) {
          final result = _scanResults[index];
          return ListTile(
            title: Text(result.device.name ?? "Unknown Device"),
            subtitle: Text("${result.device.id}\nRSSI: ${result.rssi}"),
            onTap: () => _navigateToDevice(result.device),
          );
        },
      ),
    );
  }
}

class DeviceScreen extends StatefulWidget {
  final BleDevice device;

  const DeviceScreen({super.key, required this.device});

  @override
  _DeviceScreenState createState() => _DeviceScreenState();
}

class _DeviceScreenState extends State<DeviceScreen> {
  final _blePlugin = SimpleBlePlugin();
  bool _isConnected = false;
  List<BleService> _services = [];
  String? _status;

  @override
  void initState() {
    super.initState();
    _connect();
  }

  void _setStatus(String status) {
    setState(() {
      _status = status;
    });
  }

  Future<void> _connect() async {
    _setStatus("Connecting...");
    try {
      await _blePlugin.connect(widget.device.id);
      if (!mounted) return;
      setState(() {
        _isConnected = true;
      });
      _setStatus("Connected. Discovering services...");
      final services = await _blePlugin.discoverServices(widget.device.id);
      if (!mounted) return;
      setState(() {
        _services = services;
        _status = "Services found: ${services.length}";
      });
    } catch (e) {
      if (!mounted) return;
      _setStatus("Error: $e");
    }
  }

  Future<void> _disconnect() async {
    try {
      await _blePlugin.disconnect(widget.device.id);
      if (!mounted) return;
      setState(() {
        _isConnected = false;
        _services = [];
        _status = "Disconnected";
      });
    } catch(e) {
      _setStatus("Error disconnecting: $e");
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.device.name ?? "Device"),
      ),
      body: Column(
        children: [
          if (_status != null) Padding(padding: const EdgeInsets.all(8.0), child: Text(_status!)),
          Expanded(
            child: ListView.builder(
              itemCount: _services.length,
              itemBuilder: (context, index) {
                final service = _services[index];
                return ExpansionTile(
                  title: Text("Service ${service.uuid}"),
                  children: service.characteristics.map((c) => CharacteristicTile(
                      device: widget.device,
                      service: service,
                      characteristic: c
                  )).toList(),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class CharacteristicTile extends StatefulWidget {
  final BleDevice device;
  final BleService service;
  final BleCharacteristic characteristic;

  const CharacteristicTile({
    required this.device,
    required this.service,
    required this.characteristic
  });

  @override
  _CharacteristicTileState createState() => _CharacteristicTileState();
}

class _CharacteristicTileState extends State<CharacteristicTile> {
  final _blePlugin = SimpleBlePlugin();
  List<int>? _value;
  String? _error;
  StreamSubscription? _subscription;
  bool _isMonitoring = false;

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

  Future<void> _read() async {
    setState(() { _error = null; });
    try {
      final val = await _blePlugin.readCharacteristic(
          widget.device.id,
          widget.service.uuid,
          widget.characteristic.uuid
      );
      if (mounted) setState(() { _value = val; });
    } catch (e) {
      if (mounted) setState(() { _error = e.toString(); });
    }
  }

  Future<void> _write() async {
    // Example: Write 0x01
    setState(() { _error = null; });
    try {
      await _blePlugin.writeCharacteristic(
          widget.device.id,
          widget.service.uuid,
          widget.characteristic.uuid,
          Uint8List.fromList([0x01])
      );
      if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Write success")));
    } catch (e) {
      if (mounted) setState(() { _error = e.toString(); });
    }
  }

  void _toggleMonitor() {
    setState(() { _error = null; });
    if (_isMonitoring) {
      _subscription?.cancel();
      setState(() { _isMonitoring = false; });
    } else {
      try {
        final stream = _blePlugin.monitorCharacteristic(
            widget.device.id,
            widget.service.uuid,
            widget.characteristic.uuid
        );
        _subscription = stream.listen((val) {
          if (mounted) setState(() { _value = val; });
        }, onError: (e) {
          if (mounted) setState(() { _error = e.toString(); _isMonitoring = false; });
        });
        setState(() { _isMonitoring = true; });
      } catch (e) {
        setState(() { _error = e.toString(); });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final props = widget.characteristic.properties;
    final canRead = props.contains("READ");
    final canWrite = props.contains("WRITE") || props.contains("WRITE_NO_RESPONSE");
    final canNotify = props.contains("NOTIFY") || props.contains("INDICATE");

    return ListTile(
      title: Text("Char: ${widget.characteristic.uuid}"),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text("Props: ${props.join(', ')}"),
          if (_value != null) Text("Value: ${_value!}"),
          if (_error != null) Text("Error: $_error", style: TextStyle(color: Colors.red)),
        ],
      ),
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (canRead)
            IconButton(icon: Icon(Icons.download), onPressed: _read),
          if (canWrite)
            IconButton(icon: Icon(Icons.upload), onPressed: _write),
          if (canNotify)
            IconButton(
                icon: Icon(_isMonitoring ? Icons.notifications_active : Icons.notifications_off),
                onPressed: _toggleMonitor
            ),
        ],
      ),
    );
  }
}