flutter_elink_ble 0.1.3 copy "flutter_elink_ble: ^0.1.3" to clipboard
flutter_elink_ble: ^0.1.3 copied to clipboard

ElinkThings BLE SDK plugin for scanning, connecting, and processing Elink BLE data.

example/lib/main.dart

import 'dart:async';
import 'dart:io' show Platform;

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

import 'bluetooth_connection_page.dart';
import 'example_time_utils.dart';
import 'scan_page.dart';
import 'wifi_provisioning_page.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const ElinkHomePage(),
    );
  }
}

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

  @override
  State<ElinkHomePage> createState() => _ElinkHomePageState();
}

class _ElinkHomePageState extends State<ElinkHomePage> {
  static const int _maxLogCount = 160;

  /// Android 示例默认请求的 GATT MTU。
  static const int _defaultAndroidMtu = 517;

  final List<StreamSubscription<Object?>> _subscriptions = [];
  final List<String> _logs = <String>[];
  final Set<String> _handshakeStartedRemoteIds = <String>{};
  final Set<String> _handshakeReadyRemoteIds = <String>{};
  final Map<String, ElinkServiceDiscoveredEvent> _handshakeServiceEvents =
      <String, ElinkServiceDiscoveredEvent>{};
  List<ElinkScanResult> _scanResults = const <ElinkScanResult>[];
  ElinkAdapterState _adapterState = ElinkBle.adapterStateNow;
  bool _isScanning = false;
  bool _enableTlvParse = false;
  String? _connectedRemoteId;
  String? _connectedMacAddress;
  String? _bmVersion;

  @override
  void initState() {
    super.initState();
    _subscriptions
      ..add(
        ElinkBle.adapterState.listen((state) {
          setState(() => _adapterState = state);
          _addLog('[adapterState] ${state.name}');
        }),
      )
      ..add(
        ElinkBle.isScanning.listen((isScanning) {
          if (_isScanning == isScanning) {
            return;
          }
          setState(() => _isScanning = isScanning);
          _addLog('[isScanning] $isScanning');
        }),
      )
      ..add(
        ElinkBle.scanResults.listen((results) {
          setState(() => _scanResults = results);
        }),
      )
      ..add(
        ElinkBle.connectionEvents.listen((event) {
          setState(() {
            if (event.connectionState.isConnected) {
              _connectedRemoteId = event.remoteId;
              _connectedMacAddress = _scanResults
                  .where((result) => result.device.remoteId == event.remoteId)
                  .map((result) => result.device.macAddress)
                  .firstWhere(
                    (macAddress) => macAddress.isNotEmpty,
                    orElse: () => '',
                  );
            } else {
              _connectedRemoteId = null;
              _connectedMacAddress = null;
              _handshakeStartedRemoteIds.remove(event.remoteId);
              _handshakeReadyRemoteIds.remove(event.remoteId);
              _handshakeServiceEvents.remove(event.remoteId);
              _bmVersion = null;
            }
          });
          _addLog(
            '[connectionEvents] ${event.remoteId}: '
            '${event.connectionState.name}',
          );
          if (event.connectionState.isConnected) {
            unawaited(_startHandshakeIfReady(event.remoteId));
          }
        }),
      )
      ..add(
        ElinkBle.serviceDiscoveryEvents.listen((event) {
          _addLog(
            '[serviceDiscoveryEvents] ${event.remoteId}: '
            '${event.serviceUuid} '
            '${event.characteristicUuids.map((uuid) => uuid.value).join(",")}',
          );
          if (_hasWriteCharacteristic(event)) {
            _handshakeServiceEvents[event.remoteId] = event;
            unawaited(_startHandshakeIfReady(event.remoteId));
          }
        }),
      )
      ..add(
        ElinkBle.protocolDataPackets.listen((packet) {
          _addLog(
            '[protocolDataPackets] ${packet.remoteId}: '
            '${packet.protocol.name.toUpperCase()} '
            'payload=${ElinkDataProcessor.formatHex(packet.data)}',
          );
          _addProtocolPacketParseLogs(packet);
        }),
      )
      ..add(
        ElinkBle.passthroughDataPackets.listen((packet) {
          _addLog(
            '[passthroughDataPackets] ${packet.remoteId}: '
            '${ElinkDataProcessor.formatHex(packet.data)}',
          );
          _addRawFrameParseLogs(
            source: 'passthroughDataPackets',
            remoteId: packet.remoteId,
            data: packet.data,
          );
        }),
      )
      ..add(
        ElinkBle.characteristicEvents.listen((event) {
          _addLog(
            '[characteristicEvents] ${event.remoteId}: '
            '${event.operation.name} ${event.characteristicUuid} '
            '${ElinkDataProcessor.formatHex(event.data)}',
          );
          unawaited(_handleA7CharacteristicChanged(event));
          _addRawFrameParseLogs(
            source: 'characteristicEvents',
            remoteId: event.remoteId,
            data: event.data,
          );
        }),
      )
      ..add(
        ElinkBle.rssiEvents.listen((event) {
          _addLog('[rssiEvents] ${event.remoteId}: ${event.rssi}');
        }),
      )
      ..add(
        ElinkBle.mtuEvents.listen((event) {
          _addLog(
            '[mtuEvents] ${event.remoteId}: '
            'mtu=${event.mtu} available=${event.availableMtu}',
          );
        }),
      )
      ..add(
        ElinkBle.handshakeEvents.listen((event) {
          if (event.success) {
            _handshakeReadyRemoteIds.add(event.remoteId);
          }
          _addLog('[handshakeEvents] ${event.remoteId}: ${event.success}');
        }),
      )
      ..add(
        ElinkBle.bmVersionEvents.listen((event) {
          if (event.remoteId == _connectedRemoteId) {
            setState(() => _bmVersion = event.version);
          }
          _addLog('[bmVersionEvents] ${event.remoteId}: ${event.version}');
        }),
      )
      ..add(
        ElinkBle.errors.listen((error) {
          _addLog('[errors] $error');
        }),
      );
    unawaited(ElinkBle.refreshAdapterState());
  }

  @override
  void dispose() {
    for (final subscription in _subscriptions) {
      subscription.cancel();
    }
    unawaited(ElinkBle.dispose());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Elink BLE'),
        actions: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Center(child: Text(_adapterState.name)),
          ),
        ],
      ),
      body: _buildCurrentPage(),
    );
  }

  /// Build the current example page from BLE connection state (根据 BLE 连接状态构建当前示例页面).
  Widget _buildCurrentPage() {
    if (_connectedRemoteId == null) {
      return ScanPage(
        adapterState: _adapterState,
        isScanning: _isScanning,
        scanResults: _scanResults,
        onOpenBluetooth: () => unawaited(_openBluetooth()),
        onStartScan: () => unawaited(_startScan()),
        onStopScan: () => unawaited(_stopScan()),
        onConnect: (device) => unawaited(_connect(device)),
      );
    }
    return BluetoothConnectionPage(
      connectedRemoteId: _connectedRemoteId,
      connectedMacAddress: _connectedMacAddress,
      bmVersion: _bmVersion,
      enableTlvParse: _enableTlvParse,
      logs: _logs,
      onDisconnect: () => unawaited(_disconnectCurrent()),
      onGetBmVersion: () => unawaited(_getBmVersion()),
      mtuActionLabel: Platform.isIOS
          ? 'Get iOS MTU'
          : Platform.isAndroid
          ? 'Set MTU 517'
          : 'MTU',
      onMtuAction: () => unawaited(_handleMtu()),
      onOpenWifiProvisioning: _openWifiProvisioning,
      onEnableTlvParseChanged: (value) {
        setState(() => _enableTlvParse = value);
        _addLog('[parseConfig] tlv=$value');
      },
    );
  }

  /// Connect one BLE device after stopping this example scan, and log failures (停止示例页扫描后连接单个 BLE 设备,并记录失败).
  Future<void> _connect(ElinkDevice device) async {
    try {
      _connectedMacAddress = device.macAddress.isEmpty
          ? null
          : device.macAddress;
      if (_isScanning) {
        await ElinkBle.stopScan();
      }
      await ElinkBle.connect(device);
    } catch (error) {
      _addLog('[connect] ${device.remoteId}: $error');
    }
  }

  /// 根据当前平台处理 MTU:Android 请求 517,iOS 查询最大写入长度。
  Future<void> _handleMtu({int androidMtu = _defaultAndroidMtu}) async {
    final remoteId = _connectedRemoteId;
    if (remoteId == null) {
      return;
    }
    try {
      if (Platform.isIOS) {
        final mtu = await ElinkBle.getIosMtu(remoteId);
        _addLog(
          '[getIosMtu] $remoteId: '
          'withoutResponse=${mtu.maxWriteWithoutResponse} '
          'withResponse=${mtu.maxWriteWithResponse}',
        );
        return;
      }
      if (!Platform.isAndroid) {
        _addLog('[mtu] $remoteId: unsupported platform');
        return;
      }
      final requested = await ElinkBle.setAndroidMtu(remoteId, androidMtu);
      _addLog(
        '[setAndroidMtu] $remoteId: '
        'mtu=$androidMtu requested=$requested',
      );
    } catch (error) {
      _addLog('[mtu] $remoteId: $error');
    }
  }

  /// Disconnect the current BLE device and log failures (断开当前 BLE 设备并记录失败).
  Future<void> _disconnectCurrent() async {
    try {
      await ElinkBle.disconnectCurrent();
    } catch (error) {
      _addLog('[disconnectCurrent] $error');
    }
  }

  Future<void> _startScan() async {
    try {
      await ElinkBle.startScan(timeout: const Duration(seconds: 10));
    } catch (error) {
      _addLog(error.toString());
    }
  }

  Future<void> _stopScan() async {
    try {
      await ElinkBle.stopScan();
    } catch (error) {
      _addLog(error.toString());
    }
  }

  /// 请求系统打开蓝牙,并立即刷新一次缓存状态。
  Future<void> _openBluetooth() async {
    try {
      await ElinkBle.openBluetooth();
      await ElinkBle.refreshAdapterState();
    } catch (error) {
      _addLog(error.toString());
    }
  }

  Future<void> _getBmVersion() async {
    final remoteId = _connectedRemoteId;
    if (remoteId == null) {
      return;
    }
    try {
      await ElinkBle.getBmVersion(remoteId);
      _addLog(
        '[tx][getBmVersion] $remoteId: '
        '${ElinkDataProcessor.formatHex(ElinkDataProcessor.getBmVersionPacket())}',
      );
    } catch (error) {
      _addLog('[getBmVersion] $remoteId: $error');
    }
  }

  /// Open the WiFi provisioning page with the connected BLE remoteId (使用当前已连接 BLE remoteId 打开 WiFi 配网页面).
  void _openWifiProvisioning() {
    final remoteId = _connectedRemoteId;
    if (remoteId == null) {
      return;
    }
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (context) => WifiProvisioningPage(initialRemoteId: remoteId),
      ),
    );
  }

  bool _hasWriteCharacteristic(ElinkServiceDiscoveredEvent event) {
    return event.characteristicUuids.any((uuid) {
      return uuid == ElinkGuid.write ||
          uuid == ElinkGuid.writeAndNotify ||
          uuid.value == 'FFE1' ||
          uuid.value == 'FFE3';
    });
  }

  Future<void> _startHandshakeIfReady(String remoteId) async {
    final event = _handshakeServiceEvents[remoteId];
    if (event == null ||
        _connectedRemoteId != remoteId ||
        !_handshakeStartedRemoteIds.add(remoteId)) {
      return;
    }
    try {
      final packet = await ElinkDataProcessor.initHandshake();
      if (packet == null || packet.isEmpty) {
        _addLog('[handshake] $remoteId: init packet is empty');
        return;
      }
      await _writeData(remoteId, packet, source: 'handshake');
      _addRawFrameParseLogs(
        source: 'tx:handshake',
        remoteId: remoteId,
        data: packet,
      );
    } catch (error) {
      _handshakeStartedRemoteIds.remove(remoteId);
      _addLog('[handshake] $remoteId: $error');
    }
  }

  Future<void> _writeData(
    String remoteId,
    Iterable<int> data, {
    required String source,
  }) async {
    final bytes = List<int>.unmodifiable(data);
    await ElinkBle.write(remoteId, bytes);
    _addLog('[tx][$source] $remoteId: ${ElinkDataProcessor.formatHex(bytes)}');
  }

  /// 在 sample 的 characteristic changed 回调中处理完整 A7 原始 frame。
  Future<void> _handleA7CharacteristicChanged(
    ElinkCharacteristicEvent event,
  ) async {
    if (event.operation != ElinkCharacteristicOperation.changed) {
      return;
    }
    final frame = ElinkDataProcessor.tryParseA7Frame(event.data);
    if (frame == null) {
      return;
    }
    final mac = _a7DecryptMac(event.remoteId);
    if (mac == null || mac.isEmpty) {
      _addLog('[a7Decrypt] ${event.remoteId}: missing MAC');
      return;
    }
    try {
      final decrypted = await ElinkDataProcessor.decryptA7Packet(
        mac: mac,
        packet: frame.rawData,
      );
      if (decrypted == null) {
        _addLog('[a7Decrypt] ${event.remoteId}: decrypt result is null');
        return;
      }
      final cidText = frame.cid == null
          ? '-'
          : '0x${frame.cid!.toRadixString(16).padLeft(4, '0').toUpperCase()}';
      _addLog(
        '[a7Decrypt][characteristicEvents] ${event.remoteId}: '
        'cid=$cidText payload=${ElinkDataProcessor.formatHex(decrypted)}',
      );
      _addPayloadParseLog(
        source: 'a7Decrypt:characteristicEvents',
        remoteId: event.remoteId,
        payload: decrypted,
      );
    } catch (error) {
      _addLog('[a7Decrypt] ${event.remoteId}: $error');
    }
  }

  /// 获取 A7 解密需要的 little-endian MAC,优先使用扫描广播数据。
  List<int>? _a7DecryptMac(String remoteId) {
    for (final result in _scanResults) {
      if (result.device.remoteId != remoteId) {
        continue;
      }
      final bleData = result.advertisementData.identity;
      if (!bleData.isEmpty && bleData.mac.any((byte) => byte != 0)) {
        return bleData.mac;
      }
    }
    return _littleEndianMacFromRemoteId(remoteId);
  }

  /// 将 Android 常见 MAC remoteId 转为 SDK 解密所需的 little-endian byte 顺序。
  List<int>? _littleEndianMacFromRemoteId(String remoteId) {
    final hex = remoteId.replaceAll(RegExp(r'[^0-9A-Fa-f]'), '');
    if (hex.length != 12) {
      return null;
    }
    final bytes = <int>[];
    for (var index = 0; index < hex.length; index += 2) {
      final byte = int.tryParse(hex.substring(index, index + 2), radix: 16);
      if (byte == null) {
        return null;
      }
      bytes.add(byte);
    }
    return bytes.reversed.toList(growable: false);
  }

  void _addProtocolPacketParseLogs(ElinkProtocolDataPacket packet) {
    switch (packet.protocol) {
      case ElinkProtocolDataType.a6:
        final frame = ElinkDataProcessor.parseA6Frame(
          ElinkDataProcessor.wrapA6Frame(packet.data),
        );
        _addFrameParseLog(
          source: 'protocolDataPackets',
          remoteId: packet.remoteId,
          frame: frame,
        );
        _addPayloadParseLog(
          source: 'protocolDataPackets',
          remoteId: packet.remoteId,
          payload: frame.payload,
        );
        break;
      case ElinkProtocolDataType.a7:
        final cid = packet.deviceType;
        if (cid == null) {
          _addPayloadParseLog(
            source: 'protocolDataPackets',
            remoteId: packet.remoteId,
            payload: packet.data,
            prefix: 'A7 cid is missing, ',
          );
          return;
        }
        final frame = ElinkDataProcessor.parseA7Frame(
          ElinkDataProcessor.wrapA7Frame(cid: cid, payload: packet.data),
        );
        _addFrameParseLog(
          source: 'protocolDataPackets',
          remoteId: packet.remoteId,
          frame: frame,
        );
        _addPayloadParseLog(
          source: 'protocolDataPackets',
          remoteId: packet.remoteId,
          payload: frame.payload,
        );
        break;
    }
  }

  void _addRawFrameParseLogs({
    required String source,
    required String remoteId,
    required Iterable<int> data,
  }) {
    final frame = ElinkDataProcessor.tryParseProtocolFrame(data.toList());
    if (frame == null) {
      return;
    }
    _addFrameParseLog(source: source, remoteId: remoteId, frame: frame);
    _addPayloadParseLog(
      source: source,
      remoteId: remoteId,
      payload: frame.payload,
    );
  }

  void _addFrameParseLog({
    required String source,
    required String remoteId,
    required ElinkProtocolFrame frame,
  }) {
    final cidText = frame.cid == null
        ? '-'
        : '0x${frame.cid!.toRadixString(16).padLeft(4, '0').toUpperCase()}';
    _addLog(
      '[parse][$source] $remoteId: '
      'protocol=${frame.protocol.name.toUpperCase()} '
      'cid=$cidText len=${frame.payloadLength} '
      'checksum=0x${frame.checksum.toRadixString(16).padLeft(2, '0').toUpperCase()} '
      'payload=${ElinkDataProcessor.formatHex(frame.payload)}',
    );
  }

  void _addPayloadParseLog({
    required String source,
    required String remoteId,
    required Iterable<int> payload,
    String prefix = '',
  }) {
    final payloads = ElinkDataProcessor.tryParsePayload(
      payload.toList(growable: false),
      parseTlv: _enableTlvParse,
    );
    _addLog(
      '[parse][$source] $remoteId: '
      '$prefix${_enableTlvParse ? 'tlv=' : 'payload='}'
      '${_formatPayloads(payloads, parseTlv: _enableTlvParse)}',
    );
  }

  String _formatPayloads(
    List<ElinkPayload>? payloads, {
    required bool parseTlv,
  }) {
    if (payloads == null) {
      return 'invalid';
    }
    if (payloads.isEmpty) {
      return 'empty';
    }
    return payloads
        .map((payload) => _formatPayload(payload, parseTlv))
        .join(' | ');
  }

  String _formatPayload(ElinkPayload payload, bool parseTlv) {
    final data = payload.data.isEmpty
        ? 'empty'
        : ElinkDataProcessor.formatHex(payload.data);
    final length = parseTlv ? ', length: ${payload.length}' : '';
    return '{type: 0x${payload.type.toRadixString(16).padLeft(2, '0').toUpperCase()}'
        '$length, data: $data}';
  }

  void _addLog(String message) {
    if (!mounted) return;
    final timestamp = ExampleTimeUtils.formatTimestamp(DateTime.now());
    setState(() {
      _logs.add('[$timestamp] $message');
      if (_logs.length > _maxLogCount) {
        _logs.removeAt(0);
      }
    });
  }
}
0
likes
0
points
725
downloads

Publisher

unverified uploader

Weekly Downloads

ElinkThings BLE SDK plugin for scanning, connecting, and processing Elink BLE data.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_elink_ble

Packages that implement flutter_elink_ble