flutter_blue优化(FlutterBlue.instance.scan搜索重复、搜索结果处理、更新之前保存缓存数据、保存连接成功的设备)

本文主要介绍了FlutterBlue库的优化,包括避免扫描重复、处理搜索结果、更新及保存缓存数据,以及如何保存连接成功的设备。同时,还涉及了十进制到十六进制的转换和写入十六进制数据的操作。示例代码如json_scan.dart和index.dart。

1.搜索列表优化(FlutterBlue.instance.scan搜索重复、搜索结果处理、更新之前保存缓存数据)

2.保存连接过的设备

3.十进制转十六进制

4.写入十六进制数据

json_scan.dart 实体类(主要是使用flutter_blue的device保存到本地缓存会有问题)

class ScanResultArr {
  String rssi;
  DeviceItem device;

  ScanResultArr({this.rssi, this.device});

  ScanResultArr.fromJson(Map<String, dynamic> json) {
    rssi = json['rssi'];
    device = json['device'] != null
        ? new DeviceItem.fromJson(json['device'])
        : null;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['rssi'] = this.rssi;
    if (this.device 
“疲劳可见”核心功能代码实现(基于Flutter移动端) 1. 项目初始化与依赖配置(pubspec.yaml) yaml name: fatigue_visible description: 基于数字孪生与生物反馈的运动恢复助手 version: 1.0.0 environment: sdk: '>=3.0.0 <4.0.0' dependencies: flutter: sdk: flutter # 传感器与设备连接 sensors_flutter: ^2.1.1 # 加速度计、陀螺仪 flutter_blue_plus: ^1.13.3 # 蓝牙连接手环 camera: ^0.10.5+5 # 手机摄像头采集PPG信号 # 数据处理与存储 sqflite: ^2.3.0 # 本地数据缓存 shared_preferences: ^2.2.2 # 配置存储 # 3D数字孪生 flutter_3d_obj: ^0.1.1 # 3D模型渲染 # 图表与可视化 fl_chart: ^0.55.2 # 恢复指标图表 # 网络与同步 http: ^1.1.0 # 后端数据同步 ntp: ^2.0.0 # NTP时间同步 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 flutter: uses-material-design: true assets: - assets/3d/human_model.obj # 3D人体模型文件 - assets/3d/human_texture.png # 模型纹理   2. PPG心率与HRV采集(ppg_sensor.dart) dart import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class PpgSensor { late CameraController _controller; late Future<void> _initializeControllerFuture; final StreamController<double> _hrvStreamController = StreamController<double>.broadcast(); final StreamController<int> _heartRateStreamController = StreamController<int>.broadcast(); // 暴露数据流 Stream<double> get hrvStream => _hrvStreamController.stream; Stream<int> get heartRateStream => _heartRateStreamController.stream; // 初始化摄像头(PPG采集用后置摄像头) Future<void> initCamera(List<CameraDescription> cameras) async { _controller = CameraController( cameras.firstWhere((cam) => cam.lensDirection == CameraLensDirection.back), ResolutionPreset.low, // 低分辨率降低功耗 enableAudio: false, ); _initializeControllerFuture = _controller.initialize(); await _initializeControllerFuture; _startPpgDetection(); } // PPG信号采集与处理(滑动窗口滤波+HRV计算) void _startPpgDetection() async { final List<int> ppgValues = []; final List<DateTime> timestamps = []; const windowSize = 30; // 滑动窗口大小 const sampleRate = 30; // 采样率(次/秒) _controller.startImageStream((CameraImage image) async { // 提取亮度值(PPG信号核心) final int avgBrightness = _calculateAvgBrightness(image); ppgValues.add(avgBrightness); timestamps.add(DateTime.now()); // 滑动窗口滤波:保留最新windowSize个数据 if (ppgValues.length > windowSize) { ppgValues.removeAt(0); timestamps.removeAt(0); } // 数据量足够时计算心率与HRV if (ppgValues.length == windowSize) { final int heartRate = _calculateHeartRate(ppgValues, timestamps); final double hrv = _calculateHRV(ppgValues, timestamps); _heartRateStreamController.add(heartRate); _hrvStreamController.add(hrv); } }); } // 计算图像平均亮度 int _calculateAvgBrightness(CameraImage image) { final int width = image.width; final int height = image.height; final Uint8List bytes = image.planes[0].bytes; int sum = 0; for (int i = 0; i < width * height; i++) { sum += bytes[i]; } return sum ~/ (width * height); } // 简单心率计算(基于亮度波动周期) int _calculateHeartRate(List<int> values, List<DateTime> timestamps) { // 找峰值(亮度波动峰值对应心跳) final List<int> peaks = []; for (int i = 1; i < values.length - 1; i++) { if (values[i] > values[i-1] && values[i] > values[i+1]) { peaks.add(i); } } if (peaks.length < 2) return 0; // 峰值不足无法计算 // 计算峰值间隔(秒) final double interval = timestamps[peaks.last].difference(timestamps[peaks.first]).inSeconds / (peaks.length - 1); return (60 / interval).toInt(); // 心率(次/分钟) } // 简单HRV计算(基于心跳间隔标准差) double _calculateHRV(List<int> values, List<DateTime> timestamps) { final List<int> peaks = []; for (int i = 1; i < values.length - 1; i++) { if (values[i] > values[i-1] && values[i] > values[i+1]) { peaks.add(i); } } if (peaks.length < 3) return 0.0; // 需至少3个峰值 // 计算心跳间隔(RR间期) final List<int> rrIntervals = []; for (int i = 1; i < peaks.length; i++) { rrIntervals.add(timestamps[peaks[i]].difference(timestamps[peaks[i-1]]).inMilliseconds); } // 计算标准差(HRV指标之一) final double avg = rrIntervals.reduce((a, b) => a + b) / rrIntervals.length; final double variance = rrIntervals.map((x) => (x - avg) * (x - avg)).reduce((a, b) => a + b) / rrIntervals.length; return sqrt(variance); } // 释放资源 void dispose() { _controller.dispose(); _hrvStreamController.close(); _heartRateStreamController.close(); } }   3. 蓝牙连接手环(bluetooth_service.dart) dart import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:ntp/ntp.dart'; class BluetoothService { final FlutterBluePlus _flutterBlue = FlutterBluePlus.instance; final StreamController<int> _bandHeartRateStream = StreamController<int>.broadcast(); final StreamController<String> _connectionStatusStream = StreamController<String>.broadcast(); Stream<int> get bandHeartRateStream => _bandHeartRateStream.stream; Stream<String> get connectionStatusStream => _connectionStatusStream.stream; // 开始扫描手环(目标设备名称含"SportBand") Future<void> startScan() async { _connectionStatusStream.add("正在扫描手环..."); _flutterBlue.startScan(timeout: const Duration(seconds: 4)); // 监听扫描结果 _flutterBlue.scanResults.listen((List<ScanResult> results) { for (ScanResult result in results) { if (result.device.name.contains("SportBand")) { _flutterBlue.stopScan(); _connectToDevice(result.device); break; } } }); _flutterBlue.isScanning.listen((bool isScanning) { if (!isScanning) { _connectionStatusStream.add("未找到手环,请确保手环蓝牙已开启"); } }); } // 连接手环 Future<void> _connectToDevice(BluetoothDevice device) async { try { await device.connect(); _connectionStatusStream.add("已连接手环: ${device.name}"); _discoverServices(device); } catch (e) { _connectionStatusStream.add("连接失败: $e"); } } // 发现手环服务(心率服务UUID:0000180d-0000-1000-8000-00805f9b34fb) Future<void> _discoverServices(BluetoothDevice device) async { List<BluetoothService> services = await device.discoverServices(); for (BluetoothService service in services) { if (service.uuid.toString() == "0000180d-0000-1000-8000-00805f9b34fb") { // 找到心率服务,监听心率特征值 for (BluetoothCharacteristic char in service.characteristics) { if (char.uuid.toString() == "00002a37-0000-1000-8000-00805f9b34fb") { // 启用通知 await char.setNotifyValue(true); // 监听心率数据(带NTP时间同步) char.value.listen((List<int> value) async { final int heartRate = value[1]; // 解析心率值 final int ntpTime = await NTP.now().then((date) => date.millisecondsSinceEpoch); _bandHeartRateStream.add(heartRate); print("手环心率(NTP时间:$ntpTime): $heartRate"); }); } } } } } // 断开连接 Future<void> disconnect(BluetoothDevice? device) async { if (device != null && device.isConnected) { await device.disconnect(); _connectionStatusStream.add("已断开手环连接"); } } // 释放资源 void dispose() { _bandHeartRateStream.close(); _connectionStatusStream.close(); _flutterBlue.stopScan(); } }   4. 3D数字孪生模型渲染(digital_twin_view.dart) dart import 'package:flutter/material.dart'; import 'package:flutter_3d_obj/flutter_3d_obj.dart'; class DigitalTwinView extends StatefulWidget { final double fatigueLevel; // 疲劳度(0-100,0=无疲劳,100=极度疲劳) final List<String> tiredAreas; // 疲劳区域(如["legs", "shoulders"]) const DigitalTwinView({ super.key, required this.fatigueLevel, required this.tiredAreas, }); @override State<DigitalTwinView> createState() => _DigitalTwinViewState(); } class _DigitalTwinViewState extends State<DigitalTwinView> { late Color _modelColor; // 根据疲劳度更新模型颜色 void _updateModelColor() { if (widget.fatigueLevel < 30) { _modelColor = Colors.green; // 恢复良好:绿色 } else if (widget.fatigueLevel < 70) { _modelColor = Colors.orange; // 轻度疲劳:橙色 } else { _modelColor = Colors.red; // 重度疲劳:红色 } } @override void initState() { super.initState(); _updateModelColor(); } @override void didUpdateWidget(covariant DigitalTwinView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.fatigueLevel != oldWidget.fatigueLevel) { _updateModelColor(); } } @override Widget build(BuildContext context) { return Column( children: [ // 疲劳区域提示 Text( widget.tiredAreas.isEmpty ? "当前无明显疲劳区域" : "疲劳区域:${widget.tiredAreas.join("、")}", style: TextStyle(color: _modelColor, fontSize: 16), ), // 3D模型渲染(占满剩余空间) Expanded( child: Obj3D( size: const Size(double.infinity, double.infinity), path: "assets/3d/human_model.obj", texturePath: "assets/3d/human_texture.png", color: _modelColor.withOpacity(0.8), rotateX: 0.5, // 初始X轴旋转角度 rotateY: 0.5, // 初始Y轴旋转角度 scale: 0.8, // 模型缩放 ), ), // 疲劳度进度条 Padding( padding: const EdgeInsets.all(16.0), child: LinearProgressIndicator( value: widget.fatigueLevel / 100, backgroundColor: Colors.grey[200], color: _modelColor, semanticsLabel: "疲劳度", ), ), ], ); } }   5. 主页面整合(main_page.dart) dart import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; import 'ppg_sensor.dart'; import 'bluetooth_service.dart'; import 'digital_twin_view.dart'; import 'package:fl_chart/fl_chart.dart'; class MainPage extends StatefulWidget { const MainPage({super.key}); @override State<MainPage> createState() => _MainPageState(); } class _MainPageState extends State<MainPage> { late PpgSensor _ppgSensor; late BluetoothService _bluetoothService; List<CameraDescription> _cameras = []; int _heartRate = 0; double _hrv = 0.0; int _bandHeartRate = 0; double _fatigueLevel = 0.0; List<String> _tiredAreas = []; String _bluetoothStatus = "未连接手环"; // 心率历史数据(用于图表) final List<FlSpot> _heartRateHistory = []; @override void initState() { super.initState(); _initServices(); } // 初始化传感器与蓝牙服务 Future<void> _initServices() async { // 初始化摄像头 _cameras = await availableCameras(); _ppgSensor = PpgSensor(); await _ppgSensor.initCamera(_cameras); // 监听PPG数据 _ppgSensor.heartRateStream.listen((int rate) { setState(() { _heartRate = rate; // 更新心率历史(保留最近20个数据点) if (_heartRateHistory.length >= 20) { _heartRateHistory.removeAt(0); } _heartRateHistory.add(FlSpot(_heartRateHistory.length.toDouble(), rate.toDouble())); // 计算疲劳度(简化逻辑:心率>100或HRV<50则疲劳度升高) _calculateFatigueLevel(); }); }); _ppgSensor.hrvStream.listen((double hrv) { setState(() => _hrv = hrv); }); // 初始化蓝牙服务 _bluetoothService = BluetoothService(); _bluetoothService.connectionStatusStream.listen((String status) { setState(() => _bluetoothStatus = status); }); _bluetoothService.bandHeartRateStream.listen((int rate) { setState(() => _bandHeartRate = rate); }); // 自动扫描手环 _bluetoothService.startScan(); } // 计算疲劳度与疲劳区域(简化逻辑) void _calculateFatigueLevel() { double level = 0.0; final List<String> areas = []; // 心率过高(>100):+30疲劳度,标记全身疲劳 if (_heartRate > 100) { level += 30; areas.add("全身"); } // HRV过低(<50):+40疲劳度,标记心脏负荷 if (_hrv < 50) { level += 40; areas.add("心脏"); } // 手环心率与手机心率差异大(>15):+20疲劳度,标记数据异常 if ((_heartRate - _bandHeartRate).abs() > 15 && _bandHeartRate != 0) { level += 20; } setState(() { _fatigueLevel = level.clamp(0, 100); // 限制在0-100 _tiredAreas = areas; }); } // 生成恢复建议 String _getRecoverySuggestion() { if (_fatigueLevel < 30) { return "恢复良好,可进行中低强度训练,建议补充蛋白质(如鸡蛋、牛奶)"; } else if (_fatigueLevel < 70) { return "轻度疲劳,今日建议动态拉伸(腿部+肩部),避免高强度训练,保证7小时睡眠"; } else { return "重度疲劳,需暂停训练,进行15分钟深呼吸放松,补充电解质(如淡盐水),提前休息"; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("疲劳可见 - 运动恢复助手"), centerTitle: true, ), body: Padding( padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 蓝牙状态与核心指标 Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Text(_bluetoothStatus, style: const TextStyle(fontSize: 14)), const SizedBox(height: 16), // 核心指标网格 GridView.count( shrinkWrap: true, crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, children: [ _buildMetricCard("手机心率", "$_heartRate 次/分", Colors.blue), _buildMetricCard("手环心率", _bandHeartRate == 0 ? "未获取" : "$_bandHeartRate 次/分", Colors.green), _buildMetricCard("心率变异性", "$_hrv ms", Colors.purple), _buildMetricCard("疲劳度", "${_fatigueLevel.round()}%", _fatigueLevel < 30 ? Colors.green : _fatigueLevel < 70 ? Colors.orange : Colors.red), ], ), ], ), ), ), const SizedBox(height: 16), // 3D数字孪生 const Text("身体状态可视化", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( height: 300, child: DigitalTwinView( fatigueLevel: _fatigueLevel, tiredAreas: _tiredAreas, ), ), const SizedBox(height: 16), // 心率历史图表 const Text("心率变化趋势", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( height: 200, child: LineChart( LineChartData( minX: 0, maxX: 19, minY: 60, maxY: 120, lineBarsData: [ LineChartBarData( spots: _heartRateHistory, isCurved: true, color: Colors.blue, dotData: const FlDotData(show: false), ), ], titlesData: const FlTitlesData( bottomTitles: AxisTitles( axisName: Text("时间(分钟)"), axisNameSize: 14, ), leftTitles: AxisTitles( axisName: Text("心率(次/分)"), axisNameSize: 14, ), ), ), ), ), const SizedBox(height: 16), // 恢复建议 Card( color: Colors.grey[50], child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("今日恢复建议", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(_getRecoverySuggestion(), style: const TextStyle(fontSize: 16)), ], ), ), ), ], ), ), ), ); } // 构建指标卡片 Widget _buildMetricCard(String title, String value, Color color) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title, style: TextStyle(color: Colors.grey[600], fontSize: 14)), const SizedBox(height: 4), Text(value, style: TextStyle(color: color, fontSize: 18, fontWeight: FontWeight.bold)), ], ); } @override void dispose() { _ppgSensor.dispose(); _bluetoothService.dispose(); super.dispose(); } }   6. 入口文件(main.dart) dart import 'package:flutter/material.dart'; import 'main_page.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '疲劳可见', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MainPage(), debugShowCheckedModeBanner: false, ); } }   运行说明 1. 环境准备: - 安装Flutter SDK(3.0+),配置Android Studio/iOS Xcode开发环境 - 在VS Code中打开项目,执行 flutter pub get 安装依赖 - 准备3D模型文件(human_model.obj)和纹理图(human_texture.png),放入 assets/3d 目录 2. 设备测试: - 连接Android/iOS真机(开启开发者模式),或启动模拟器 - 执行 flutter run ,授予摄像头、蓝牙权限 - 确保手环(名称含"SportBand")蓝牙已开启,APP会自动扫描连接 3. 核心功能验证: - 查看心率、HRV实时数据(手机摄像头需贴紧手指采集PPG信号) - 观察3D模型颜色变化(绿色=良好,橙色=轻度疲劳,红色=重度疲劳) - 查看自动生成的恢复建议,验证疲劳度计算逻辑
10-26
import 'package:flutter/material.dart'; import 'package:wifi_scan/wifi_scan.dart'; import 'package:wifi_iot/wifi_iot.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import '../../../core/localization/app_localizations.dart'; import '../models/wifi.dart'; import '../viewmodels/WiFiListScreen_viewmodel.dart'; class WiFiListScreen extends StatefulWidget { const WiFiListScreen({super.key}); @override WiFiListScreenState createState() => WiFiListScreenState(); } class WiFiListScreenState extends State<WiFiListScreen> { List<WiFiAccessPoint> accessPoints = []; bool isScanning = false; // 模拟已保存的网络数据 List<Map<String, String>> savedNetworks = [ {'ssid': 'GD-10 supreme', 'status': '未连接'}, {'ssid': 'Home-WiFi', 'status': '未连接'}, ]; // 动态的可用网络列表,将从扫描结果更新 List<Map<String, String>> availableNetworks = []; @override void initState() { super.initState(); _checkPermissionsAndStartScan(); _loadSavedNetworks(); } Future<void> _checkPermissionsAndStartScan() async { var status = await Permission.location.request(); if (status.isGranted) { _startScan(); } else { print("Location permission denied"); } } Future<void> _loadSavedNetworks() async { final viewModel = context.read<WiFiViewModel>(); final wifiList = await viewModel.getWiFi(); setState(() { print('加载的已保存网络: $wifiList'); // 正确转换 WiFi 对象为 Map savedNetworks = wifiList.map((wifi) { return { 'ssid': (wifi.ssid ?? '未知网络').toString(), 'status': '未连接', 'bssid': (wifi.bssid ?? '').toString(), 'level': (wifi.level?.toString() ?? '-100').toString(), // 添加信号强度 }; }).toList(); print('转换后的 savedNetworks: $savedNetworks'); }); } Future<void> _startScan() async { setState(() => isScanning = true); // 获取扫描结果 accessPoints = await WiFiScan.instance.getScannedResults() ?? []; setState(() { availableNetworks = accessPoints.map((ap) { return { 'ssid': ap.ssid.isNotEmpty ? ap.ssid : '隐藏网络', 'status': _getConnectionStatus(ap.level), 'level': ap.level.toString(), 'capabilities': ap.capabilities, 'bssid': ap.bssid, 'frequency': ap.frequency.toString(), }; }).toList(); print('完整结果: $availableNetworks'); isScanning = false; }); } // 根据信号强度获取连接状态描述 String _getConnectionStatus(int level) { if (level >= -50) return '信号强'; if (level >= -60) return '信号良好'; if (level >= -70) return '信号一般'; return '信号较弱'; } Future<void> _connectToWiFi(String ssid, String password) async { bool isConnected = await WiFiForIoTPlugin.connect( ssid, password: password, security: NetworkSecurity.WPA, ); if (isConnected) { print("Connected to $ssid"); // 更新连接状态[8](@ref) setState(() { // 更新保存网络的状态 for (var network in savedNetworks) { if (network['ssid'] == ssid) { network['status'] = '已连接'; } } // 更新可用网络的状态 for (var network in availableNetworks) { if (network['ssid'] == ssid) { network['status'] = '已连接'; } } }); } else { print("Failed to connect"); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部状态栏 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '9:30', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ), Row( children: [ Icon(Icons.signal_cellular_alt, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Icon(Icons.wifi, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Icon(Icons.battery_std, size: 16, color: Colors.grey[600]), ], ), ], ), ), // 未连接状态显示 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: const Text( '未连接', style: TextStyle( fontSize: 14, color: Colors.grey, ), ), ), // 设备连接标题和搜索按钮 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '设备连接', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, ), ), ElevatedButton( onPressed: _startScan, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), ), child: isScanning ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation<Color>(Colors.white), ), ) : const Text('搜索'), ), ], ), ), Expanded( child: isScanning ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 已保存网络区域 _buildNetworkSection('已保存网络', savedNetworks), // 可用网络区域(使用真实扫描数据) _buildNetworkSection('可用网络', availableNetworks), ], ), ), ), ], ), ), ); } Widget _buildNetworkSection(String title, List<Map<String, String>> networks) { if (networks.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 分区标题 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black54, ), ), ), // 网络列表[6](@ref) ...networks.map((network) => _buildNetworkItem(network)).toList(), ], ); } Widget _buildNetworkItem(Map<String, String> network) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), leading: _getSignalIcon(int.tryParse(network['level'] ?? '-100') ?? -100), title: Text( network['ssid']!, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black, ), ), subtitle: network['level'] != null ? Text("信号: ${network['level']}dBm") : null, trailing: Text( network['status']!, style: TextStyle( fontSize: 14, color: network['status'] == '已连接' ? Colors.green : Colors.grey, ), ), onTap: () { _showPasswordDialog(network['ssid']!); }, ); } // 根据信号强度显示对应的WiFi图标 Widget _getSignalIcon(int level) { Color color; IconData icon; if (level >= -50) { color = Colors.green; icon = Icons.wifi; } else if (level >= -60) { color = Colors.blue; icon = Icons.wifi; } else if (level >= -70) { color = Colors.orange; icon = Icons.wifi; } else { color = Colors.red; icon = Icons.wifi; } return Icon(icon, color: color, size: 24); } void _showPasswordDialog(String ssid) { TextEditingController passwordController = TextEditingController(); showDialog( context: context, builder: (context) => AlertDialog( title: Text("连接到 $ssid"), content: TextField( controller: passwordController, obscureText: true, decoration: const InputDecoration( labelText: '密码', border: OutlineInputBorder(), hintText: '请输入WiFi密码', ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("取消"), ), ElevatedButton( onPressed: () { Navigator.pop(context); _connectToWiFi(ssid, passwordController.text); }, child: const Text("连接"), ), ], ), ); } }获取已连接的wifi
10-14
import 'package:flutter/material.dart'; import 'package:wifi_scan/wifi_scan.dart'; import 'package:wifi_iot/wifi_iot.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:network_info_plus/network_info_plus.dart'; // 新增:用于获取当前连接WiFi信息 import '../../../core/localization/app_localizations.dart'; import '../models/wifi.dart'; import '../viewmodels/WiFiListScreen_viewmodel.dart'; import '../../../core/network/tcp_client.dart'; import '../widgets/device_connection_dialog.dart'; class WiFiListScreen extends StatefulWidget { const WiFiListScreen({super.key}); @override WiFiListScreenState createState() => WiFiListScreenState(); } class WiFiListScreenState extends State<WiFiListScreen> { List<WiFiAccessPoint> accessPoints = []; bool isScanning = false; late WiFiViewModel viewModel; final NetworkInfo _networkInfo = NetworkInfo(); // 新增:网络信息实例 // 已保存的网络数据 List<Map<String, String>> savedNetworks = []; // 动态的可用网络列表,将从扫描结果更新 List<Map<String, String>> availableNetworks = []; // 创建一个Map,用于存储每个SSID对应的最强信号接入点 Map<String, Map<String, String>> uniqueNetworksMap = {}; String? _currentConnectedSSID; // 新增:当前连接的WiFi SSID String _connectionStatus = '未连接'; // 新增:连接状态文本 @override void initState() { super.initState(); _checkPermissionsAndGetCurrentWiFi(); // 修改:先获取当前连接状态 } // 新增:检查权限并获取当前连接状态 Future<void> _checkPermissionsAndGetCurrentWiFi() async { var status = await Permission.location.request(); if (status.isGranted) { await _getCurrentConnectedWiFi(); // 先获取当前连接状态 _loadSavedNetworks(); _startScan(); } else { print('Location permission denied'); } } // 新增:获取当前设备连接的WiFi信息 Future<void> _getCurrentConnectedWiFi() async { try { // 获取当前连接的WiFi名称 String? wifiName = await _networkInfo.getWifiName(); String? bssid = await _networkInfo.getWifiBSSID(); String? ipAddress = await _networkInfo.getWifiIP(); String? subnetMask = await _networkInfo.getWifiSubmask(); String? gateway = await _networkInfo.getWifiGatewayIP(); if (wifiName != null && wifiName.isNotEmpty && wifiName != 'null') { // 在Android上,WiFi名称可能带有双引号,需要处理 if (wifiName.startsWith('"') && wifiName.endsWith('"')) { wifiName = wifiName.substring(1, wifiName.length - 1); } setState(() { _currentConnectedSSID = wifiName; _connectionStatus = '已连接到 $wifiName'; }); } else { setState(() { _currentConnectedSSID = null; _connectionStatus = '未连接'; }); } } catch (e) { setState(() { _currentConnectedSSID = null; _connectionStatus = '未连接'; }); } } Future<void> _loadSavedNetworks() async { final vm = context.read<WiFiViewModel>(); viewModel = vm; final wifiList = await vm.getWiFi(); setState(() { savedNetworks = wifiList.map((wifi) { final ssid = wifi.ssid ?? '未知网络'; final isConnected = ssid == _currentConnectedSSID; return <String, String>{ 'ssid': ssid, 'status': isConnected ? '已连接' : '未连接', 'bssid': (wifi.bssid ?? '').toString(), 'level': (wifi.level?.toString() ?? '-100').toString(), 'capabilities': wifi.capabilities ?? '', 'passord': wifi.password ?? '', 'frequency': (wifi.frequency?.toString() ?? '0').toString(), }; }).toList(); }); } Future<void> _startScan() async { setState(() => isScanning = true); // 获取扫描结果 accessPoints = await WiFiScan.instance.getScannedResults() ?? []; setState(() { uniqueNetworksMap.clear(); // 清空之前的记录 for (var ap in accessPoints) { String ssid = ap.ssid.isNotEmpty ? ap.ssid : '隐藏网络'; int level = ap.level; // 如果这个SSID还没有记录,或者当前接入点的信号比已记录的强 if (!uniqueNetworksMap.containsKey(ssid) || level > int.parse(uniqueNetworksMap[ssid]!['level']!)) { final isConnected = ssid == _currentConnectedSSID; // 如果是当前已经连接的网络,判断是否已经在已保存列表中 if (isConnected) { final isAlreadySaved = savedNetworks.any( (item) => item['ssid'] == ssid, ); if (!isAlreadySaved) { savedNetworks.add({ 'ssid': ssid, 'status': '已连接', 'bssid': ap.bssid, 'level': level.toString(), 'capabilities': ap.capabilities, 'passord': '', 'frequency': ap.frequency.toString(), }); // 从可用网络中移除 availableNetworks.removeWhere((item) => item['ssid'] == ssid); // 存入数据库 viewModel.insertWiFi( WiFi( ssid: ssid, bssid: ap.bssid ?? '', capabilities: ap.capabilities ?? '', level: level, frequency: ap.frequency ?? 0, password: '', // 因为获取不多到密码,暂时存空 lastConnected: DateTime.now().toIso8601String(), ), ); } continue; // 已处理,跳过后续逻辑 } uniqueNetworksMap[ssid] = { 'ssid': ssid, 'status': isConnected ? '已连接' : '未连接', 'level': level.toString(), 'capabilities': ap.capabilities, 'bssid': ap.bssid, 'frequency': ap.frequency.toString(), }; } } // 将Map的值转换成列表,并过滤掉已保存的网络(除非是当前连接的网络) final savedSSIDs = savedNetworks .map((network) => network['ssid']) .toSet(); availableNetworks = uniqueNetworksMap.values.where((network) { final ssid = network['ssid']!; // 显示未保存的网络,或者是当前连接的网络(即使已保存也要显示在可用网络中) return !savedSSIDs.contains(ssid) || ssid == _currentConnectedSSID; }).toList(); isScanning = false; }); } // 根据信号强度获取连接状态描述 String _getConnectionStatus(int level) { if (level >= -50) return '信号强'; if (level >= -60) return '信号良好'; if (level >= -70) return '信号一般'; return '信号较弱'; } // 修改连接方法,添加网络状态更新逻辑 Future<void> _connectToWiFi( Map<String, String> network, String password, ) async { final ssid = network['ssid']!; final bssid = network['bssid']!; // 尝试连接 final isConnected = await WiFiForIoTPlugin.connect( ssid, password: password, bssid: bssid, security: _getSecurity(network['capabilities'] ?? ''), ); if (isConnected) { // 更新当前连接状态 setState(() { _currentConnectedSSID = ssid; _connectionStatus = '已连接到 $ssid'; }); // 如果这个网络不在已保存列表中,添加它 final isAlreadySaved = savedNetworks.any((item) => item['ssid'] == ssid); // 从可用网络移除 setState(() { availableNetworks.removeWhere((item) => item['bssid'] == bssid); // 更新保存网络状态 savedNetworks = savedNetworks.map((item) { if (item['ssid'] == ssid) { return {...item, 'status': '已连接'}; } else { return {...item, 'status': '未连接'}; } }).toList(); if (!isAlreadySaved) { savedNetworks.add({ 'ssid': ssid, 'status': '已连接', 'bssid': bssid, 'level': network['level'] ?? '-100', 'capabilities': network['capabilities'] ?? '', 'frequency': network['frequency'] ?? '0', }); } }); if (!isAlreadySaved) { // 保存到数据库 await viewModel.insertWiFi( WiFi( ssid: ssid, bssid: bssid, capabilities: network['capabilities'] ?? '', level: int.tryParse(network['level'] ?? '-100') ?? -100, password: password, frequency: int.tryParse(network['frequency'] ?? '0') ?? 0, lastConnected: DateTime.now().toIso8601String(), ), ); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已成功连接到 $ssid'), backgroundColor: Colors.green, ), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('连接 $ssid 失败'), backgroundColor: Colors.red), ); } } } // 安全类型检测 NetworkSecurity _getSecurity(String capabilities) { if (capabilities.contains('WPA2') || capabilities.contains('WPA')) { return NetworkSecurity.WPA; } if (capabilities.contains('WEP')) return NetworkSecurity.WEP; return NetworkSecurity.NONE; } // 新增:断开网络连接 Future<void> _disconnectFromWiFi(String ssid) async { bool isDisconnected = await WiFiForIoTPlugin.disconnect(); if (isDisconnected) { setState(() { _currentConnectedSSID = null; _connectionStatus = '未连接'; // 更新网络状态 for (var network in savedNetworks) { if (network['ssid'] == ssid) { network['status'] = '已保存'; } } }); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('已断开与 $ssid 的连接'))); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 连接状态显示(动态显示当前连接状态) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _connectionStatus, style: TextStyle( fontSize: 14, color: _currentConnectedSSID != null ? Colors.green : Colors.grey, ), ), IconButton( icon: const Icon(Icons.settings_ethernet, size: 32), color: Colors.blue, onPressed: _showDeviceConnectionDialog, ), ], ), ), // 设备连接标题和搜索按钮 // 在设备连接标题区域的Row中替换按钮代码: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '设备连接', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, ), ), IconButton( // 替换为图标按钮 onPressed: _startScan, icon: isScanning ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation<Color>( Colors.blue, ), ), ) : const Icon(Icons.refresh, color: Colors.blue), tooltip: '刷新WiFi列表', // 添加提示文本 ), ], ), ), Expanded( child: isScanning ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 已保存网络区域(包括已连接的) _buildNetworkSection('已保存网络', savedNetworks, true), // 可用网络区域 _buildNetworkSection( '可用网络', availableNetworks, false, ), // 如果没有网络显示空状态 if (savedNetworks.isEmpty && availableNetworks.isEmpty) const Center( child: Padding( padding: EdgeInsets.only(top: 100), child: Text( '未发现网络', style: TextStyle( fontSize: 16, color: Colors.grey, ), ), ), ), ], ), ), ), ], ), ), ); } Widget _buildNetworkSection( String title, List<Map<String, String>> networks, bool isSavedSection, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black54, ), ), ), // 网络列表 ...networks .map((network) => _buildNetworkItem(network, isSavedSection)) .toList(), ], ); } Widget _buildNetworkItem(Map<String, String> network, bool isSavedItem) { final isConnected = network['status'] == '已连接'; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), leading: _getSignalIcon(int.tryParse(network['level'] ?? '-100') ?? -100), title: Text( network['ssid']!, style: TextStyle( fontSize: 16, fontWeight: isConnected ? FontWeight.bold : FontWeight.normal, color: isConnected ? Colors.green : Colors.black, ), ), subtitle: network['level'] != null ? Text( " ${_getConnectionStatus(int.tryParse(network['level'] ?? '-100') ?? -100)}", ) : null, trailing: Text( network['status']!, style: TextStyle( fontSize: 14, color: isConnected ? Colors.green : Colors.grey, ), ), onTap: () { if (isSavedItem && isConnected) { _disconnectFromWiFi(network['ssid']!); } else if (isSavedItem && !isConnected) { if (network['passord'] != null && network['passord']!.isNotEmpty) { _connectToWiFi(network, network['passord']!); } else { _showPasswordDialog(network); } } else if (!isConnected) { _showPasswordDialog(network); } }, ); } // 根据信号强度显示对应的WiFi图标 Widget _getSignalIcon(int level) { Color color; IconData icon; if (level >= -50) { color = Colors.green; icon = Icons.wifi; } else if (level >= -60) { color = Colors.blue; icon = Icons.wifi; } else if (level >= -70) { color = Colors.orange; icon = Icons.wifi; } else { color = Colors.red; icon = Icons.wifi; } return Icon(icon, color: color, size: 24); } void _showPasswordDialog(Map<String, String> network) { TextEditingController passwordController = TextEditingController(); showDialog( context: context, builder: (context) => AlertDialog( titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 10), // 调整标题内边距 contentPadding: const EdgeInsets.symmetric( horizontal: 24, vertical: 10, ), // 调整内容内边距 actionsPadding: const EdgeInsets.fromLTRB(24, 10, 24, 16), // 调整按钮内边距 title: Text( "连接到 ${network['ssid']}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), content: TextField( controller: passwordController, obscureText: true, style: const TextStyle(fontSize: 14), decoration: const InputDecoration( labelText: '密码', labelStyle: TextStyle(fontSize: 14), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(10)), borderSide: BorderSide(color: Colors.grey), ), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), hintText: '请输入WiFi密码', hintStyle: TextStyle(fontSize: 14), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom(foregroundColor: Colors.grey[700]), child: const Text('取消', style: TextStyle(fontSize: 14)), ), ElevatedButton( onPressed: () { Navigator.pop(context); _connectToWiFi(network, passwordController.text); }, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: const Text( '连接', style: TextStyle(fontSize: 14, color: Colors.white), ), ), ], ), ); } void _showDeviceConnectionDialog() { TextEditingController ipController = TextEditingController(); TextEditingController portController = TextEditingController(); showDialog( context: context, builder: (context) => DeviceConnectionDialog( ipController: ipController, portController: portController, onCancel: () { Navigator.pop(context); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('连接已取消'))); }, onConfirm: () { _connectToDevice(ipController.text, portController.text); Navigator.pop(context); }, ), ); } // 设备连接逻辑 void _connectToDevice(String ip, String port) { if (ip.isEmpty || port.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请填写IP地址和端口号'))); return; } viewModel.connectTcp(host: ip, port: int.tryParse(port) ?? 0); } } 改成consumer的方式包裹ui
10-17
评论 17
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值