Commit 531a4d4e authored by akari's avatar akari

feat: 添加蓝牙,MQTT连接

parent b56b4a34
# FVM Version Cache
.fvm/
.DS_Store
# claude code
.claude/
......@@ -9,6 +10,9 @@
.dart_tool/
build/
.flutter-plugins
.flutter-plugins-dependencies
third_party/
pubspec.lock
......
{
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
"java.configuration.updateBuildConfiguration": "disabled"
}
\ No newline at end of file
"java.configuration.updateBuildConfiguration": "disabled",
"files.exclude": {
"**/.dart_tool": true,
"**/.gradle": true,
"**/.idea": true,
"**/build": true,
"**/third_party": true,
"**/*.g.dart": true,
"**/*.gr.dart": true,
"**/*.freezed.dart": true,
"**/*.mocks.dart": true
},
"search.exclude": {
"**/.dart_tool": true,
"**/.gradle": true,
"**/.idea": true,
"**/build": true,
"**/third_party": true,
"**/*.g.dart": true,
"**/*.gr.dart": true,
"**/*.freezed.dart": true,
"**/*.mocks.dart": true
},
"files.watcherExclude": {
"**/.dart_tool/**": true,
"**/.gradle/**": true,
"**/.idea/**": true,
"**/build/**": true,
"**/third_party/**": true
}
}
......@@ -9,6 +9,16 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.gr.dart"
- "**/*.freezed.dart"
- "**/*.mocks.dart"
- ".dart_tool/**"
- "build/**"
- "third_party/**"
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
......
......@@ -4,6 +4,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- BLE 蓝牙扫描/连接权限 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- USB 摄像头权限 -->
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
......
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-22.0.1.jdk/Contents/Home
org.gradle.java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
......@@ -11,6 +11,9 @@ class UserModel extends Equatable {
final dynamic hospitalId;
final String? hospitalName;
// ignore: non_constant_identifier_names
static String? MAC;
const UserModel({
required this.id,
this.nickname,
......
This diff is collapsed.
export 'bluetooth_manager.dart';
export 'mcu_report.dart';
This diff is collapsed.
export 'laki_mqtt_client.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
enum LakiMqttConnectionState {
disconnected,
connecting,
connected,
failed,
}
class LakiMqttConfig {
const LakiMqttConfig({
required this.host,
required this.clientId,
this.port = 1883,
this.username,
this.password,
this.useSsl = false,
this.useWebSocket = false,
this.webSocketProtocols,
this.keepAliveSeconds = 30,
this.connectTimeoutMs = 5000,
this.logging = false,
});
/// TODO: 替换为腾讯云 IoT MQTT broker 地址、端口、clientId 和鉴权信息。
const LakiMqttConfig.placeholder()
: host = '',
port = 1883,
clientId = '',
username = null,
password = null,
useSsl = false,
useWebSocket = false,
webSocketProtocols = null,
keepAliveSeconds = 30,
connectTimeoutMs = 5000,
logging = false;
final String host;
final int port;
final String clientId;
final String? username;
final String? password;
final bool useSsl;
final bool useWebSocket;
final List<String>? webSocketProtocols;
final int keepAliveSeconds;
final int connectTimeoutMs;
final bool logging;
bool get isConfigured => host.isNotEmpty && clientId.isNotEmpty;
}
class LakiMqttTopics {
const LakiMqttTopics._();
static String icuUp(String deviceSn) => 'icu/$deviceSn/up';
static String icuTo(String deviceSn) => 'icu/$deviceSn/to';
static String icuPetBind(String deviceSn) => 'icu/$deviceSn/pet/bind';
static String cameraTo(String cabinSn) => 'camera/$cabinSn/to';
static const String clientConnected = r'$events/client_connected';
static const String clientDisconnected = r'$events/client_disconnected';
static const String messageDelivered = r'$events/message_delivered';
}
class LakiMqttMessage {
const LakiMqttMessage({
required this.topic,
required this.payload,
this.json,
});
final String topic;
final String payload;
final Map<String, dynamic>? json;
}
class LakiMqttClient {
LakiMqttClient({required LakiMqttConfig config}) : _config = config;
final LakiMqttConfig _config;
MqttServerClient? _client;
StreamSubscription<List<MqttReceivedMessage<MqttMessage>>>? _updatesSub;
final _stateController =
StreamController<LakiMqttConnectionState>.broadcast();
final _messageController = StreamController<LakiMqttMessage>.broadcast();
final _errorController = StreamController<String>.broadcast();
Stream<LakiMqttConnectionState> get stateStream => _stateController.stream;
Stream<LakiMqttMessage> get messageStream => _messageController.stream;
Stream<String> get errorStream => _errorController.stream;
bool get isConnected =>
_client?.connectionStatus?.state == MqttConnectionState.connected;
Future<void> connect() async {
if (!_config.isConfigured) {
throw StateError('MQTT 连接配置未填写,请先配置 host 和 clientId');
}
if (isConnected) {
return;
}
_emitState(LakiMqttConnectionState.connecting);
final client = MqttServerClient(_config.host, _config.clientId);
client.port = _config.port;
client.secure = _config.useSsl;
client.useWebSocket = _config.useWebSocket;
client.keepAlivePeriod = _config.keepAliveSeconds;
client.connectTimeoutPeriod = _config.connectTimeoutMs;
client.onConnected = () => _emitState(LakiMqttConnectionState.connected);
client.onDisconnected =
() => _emitState(LakiMqttConnectionState.disconnected);
client.onSubscribed = (topic) => _emitError('MQTT 已订阅: $topic');
client.onSubscribeFail = (topic) => _emitError('MQTT 订阅失败: $topic');
client.pongCallback = () {};
final webSocketProtocols = _config.webSocketProtocols;
if (webSocketProtocols != null) {
client.websocketProtocols = webSocketProtocols;
}
client.logging(on: _config.logging);
client.setProtocolV311();
client.connectionMessage = MqttConnectMessage()
.withClientIdentifier(_config.clientId)
.startClean()
.withWillQos(MqttQos.atLeastOnce);
try {
await client.connect(_config.username, _config.password);
} on NoConnectionException catch (error) {
client.disconnect();
_emitState(LakiMqttConnectionState.failed);
_emitError('MQTT 连接失败: $error');
rethrow;
} on SocketException catch (error) {
client.disconnect();
_emitState(LakiMqttConnectionState.failed);
_emitError('MQTT Socket 连接失败: $error');
rethrow;
}
if (client.connectionStatus?.state != MqttConnectionState.connected) {
final status = client.connectionStatus;
client.disconnect();
_emitState(LakiMqttConnectionState.failed);
throw StateError('MQTT 连接未成功: $status');
}
_client = client;
_listenUpdates(client);
_emitState(LakiMqttConnectionState.connected);
}
void subscribe(String topic, {MqttQos qos = MqttQos.atLeastOnce}) {
_requireConnected().subscribe(topic, qos);
}
void unsubscribe(String topic) {
_requireConnected().unsubscribe(topic);
}
void subscribeIcuCommands(String deviceSn) {
subscribe(LakiMqttTopics.icuTo(deviceSn));
}
void subscribePetBind(String deviceSn) {
subscribe(LakiMqttTopics.icuPetBind(deviceSn));
}
int publishJson(
String topic,
Map<String, dynamic> payload, {
MqttQos qos = MqttQos.atLeastOnce,
bool retain = false,
}) {
return publishString(
topic,
jsonEncode(payload),
qos: qos,
retain: retain,
);
}
int publishString(
String topic,
String payload, {
MqttQos qos = MqttQos.atLeastOnce,
bool retain = false,
}) {
final builder = MqttClientPayloadBuilder()..addString(payload);
return _requireConnected().publishMessage(
topic,
qos,
builder.payload!,
retain: retain,
);
}
int publishMonitorReport(
McuReport report, {
DateTime? date,
MqttQos qos = MqttQos.atLeastOnce,
}) {
if (report.sn.isEmpty) {
throw ArgumentError.value(report.sn, 'report.sn', '设备 SN 不能为空');
}
return publishJson(
LakiMqttTopics.icuUp(report.sn),
report.toMonitorPayload(date: date),
qos: qos,
);
}
int publishCommandResponse({
required String deviceSn,
required String eventId,
required String refEventId,
required int status,
required String message,
MqttQos qos = MqttQos.atLeastOnce,
}) {
return publishJson(
LakiMqttTopics.icuUp(deviceSn),
{
'eventId': eventId,
'refEventId': refEventId,
'deviceType': 'ICU',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'type': 'cmdResponse',
'data': {
'status': status,
'msg': message,
},
},
qos: qos,
);
}
Future<void> disconnect() async {
await _updatesSub?.cancel();
_updatesSub = null;
_client?.disconnect();
_client = null;
_emitState(LakiMqttConnectionState.disconnected);
}
Future<void> dispose() async {
await disconnect();
await _stateController.close();
await _messageController.close();
await _errorController.close();
}
void _listenUpdates(MqttServerClient client) {
_updatesSub?.cancel();
_updatesSub = client.updates?.listen((messages) {
for (final item in messages) {
final payload = item.payload;
if (payload is! MqttPublishMessage) {
continue;
}
final payloadText = MqttPublishPayload.bytesToStringAsString(
payload.payload.message,
);
_messageController.add(
LakiMqttMessage(
topic: item.topic,
payload: payloadText,
json: _tryDecodeJson(payloadText),
),
);
}
});
}
MqttServerClient _requireConnected() {
final client = _client;
if (client == null || !isConnected) {
throw StateError('MQTT 未连接');
}
return client;
}
void _emitState(LakiMqttConnectionState state) {
if (!_stateController.isClosed) {
_stateController.add(state);
}
}
void _emitError(String message) {
if (!_errorController.isClosed) {
_errorController.add(message);
}
}
Map<String, dynamic>? _tryDecodeJson(String payload) {
try {
final decoded = jsonDecode(payload);
return decoded is Map<String, dynamic> ? decoded : null;
} catch (_) {
return null;
}
}
}
......@@ -9,7 +9,11 @@ class StorageService {
static const String _keyRememberSmsCode = 'remember_sms_code';
static const String _keyRememberEnabled = 'remember_enabled';
static const String _keyUserInfo = 'user_info';
static const String _keyUserMac = 'user_mac';
static const String _keyClientId = 'client_id';
static const String _keyBoundBluetoothDeviceId = 'bound_bluetooth_device_id';
static const String _keyBoundBluetoothDeviceName =
'bound_bluetooth_device_name';
final FlutterSecureStorage _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(
......@@ -76,6 +80,7 @@ class StorageService {
Future<UserModel?> getUserInfo() async {
final value = await _storage.read(key: _keyUserInfo);
UserModel.MAC = await getUserMac();
if (value == null || value.isEmpty) return null;
final parts = value.split('|');
if (parts.length < 9) return null;
......@@ -96,6 +101,22 @@ class StorageService {
await _storage.delete(key: _keyUserInfo);
}
Future<void> saveUserMac(String mac) async {
UserModel.MAC = mac;
await _storage.write(key: _keyUserMac, value: mac);
}
Future<String?> getUserMac() async {
final mac = await _storage.read(key: _keyUserMac);
UserModel.MAC = mac;
return mac;
}
Future<void> deleteUserMac() async {
UserModel.MAC = null;
await _storage.delete(key: _keyUserMac);
}
// ==================== ClientId 相关 ====================
Future<void> saveClientId(String clientId) async {
......@@ -110,6 +131,30 @@ class StorageService {
await _storage.delete(key: _keyClientId);
}
// ==================== 蓝牙绑定相关 ====================
Future<void> saveBoundBluetoothDevice({
required String deviceId,
required String deviceName,
}) async {
await _storage.write(key: _keyBoundBluetoothDeviceId, value: deviceId);
await _storage.write(key: _keyBoundBluetoothDeviceName, value: deviceName);
}
Future<Map<String, String?>> getBoundBluetoothDevice() async {
final deviceId = await _storage.read(key: _keyBoundBluetoothDeviceId);
final deviceName = await _storage.read(key: _keyBoundBluetoothDeviceName);
return {
'deviceId': deviceId,
'deviceName': deviceName,
};
}
Future<void> deleteBoundBluetoothDevice() async {
await _storage.delete(key: _keyBoundBluetoothDeviceId);
await _storage.delete(key: _keyBoundBluetoothDeviceName);
}
// ==================== 记住密码相关 ====================
Future<void> saveRememberCredentials({
......
......@@ -6,21 +6,25 @@ import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/p2p_video_service.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:laki_icu_app/utils/storage/storage_service.dart';
import 'home_index_state.dart';
/// 设备凭据 —— Mock 阶段使用占位值,后续接口接入后替换
/// TODO: 从舱详情接口获取真实的 cameraSn 和 wifiPwd
const _mockDeviceNo = 'MOCK_DEVICE_SN';
const _mockDevicePassword = 'MOCK_WIFI_PWD';
class HomeIndexCubit extends Cubit<HomeIndexState> {
final MonitoringService _monitoringService;
final WebrtcService _webrtcService;
final P2pVideoService _p2pVideoService;
final StorageService _storageService;
final BleBluetoothManager _bluetoothManager;
final McuReportFrameDecoder _mcuReportDecoder;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
StreamSubscription<P2pServiceState>? _p2pStateSub;
StreamSubscription<List<BluetoothScanDevice>>? _bluetoothScanSub;
StreamSubscription<BluetoothConnectionStatus>? _bluetoothStateSub;
StreamSubscription<String>? _bluetoothMessageSub;
StreamSubscription<BluetoothDataPacket>? _bluetoothDataSub;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
......@@ -30,9 +34,14 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
: _monitoringService = MonitoringService(),
_webrtcService = WebrtcService(),
_p2pVideoService = P2pVideoService(),
_storageService = StorageService(),
_bluetoothManager = BleBluetoothManager(),
_mcuReportDecoder = McuReportFrameDecoder(),
super(const HomeIndexState()) {
_listenWebrtcState();
_listenP2pState();
_listenBluetoothState();
_loadBoundBluetoothDevice();
loadData();
}
......@@ -49,11 +58,69 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
_p2pStateSub = _p2pVideoService.stateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(
isP2pConnected: state == P2pServiceState.connected,
));
isP2pConnected: state == P2pServiceState.connected,
));
});
}
/// 监听蓝牙扫描、连接和数据状态
void _listenBluetoothState() {
_bluetoothScanSub = _bluetoothManager.scanDevicesStream.listen((devices) {
if (isClosed) return;
emit(state.copyWith(bluetoothDevices: devices));
});
_bluetoothStateSub = _bluetoothManager.stateStream.listen((status) {
if (isClosed) return;
emit(state.copyWith(bluetoothConnectionStatus: status));
});
_bluetoothMessageSub = _bluetoothManager.messageStream.listen((message) {
if (isClosed) return;
emit(state.copyWith(bluetoothMessage: message));
});
_bluetoothDataSub = _bluetoothManager.dataStream.listen((packet) {
if (isClosed) return;
try {
final reports = _mcuReportDecoder.addBytes(packet.bytes);
final latestReport = reports.isEmpty ? null : reports.last;
emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex,
latestMcuReport: latestReport,
bluetoothMessage: latestReport == null
? state.bluetoothMessage
: '蓝牙数据解析成功: ${latestReport.sn.isEmpty ? latestReport.head : latestReport.sn}',
));
} catch (e) {
emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex,
bluetoothMessage: '蓝牙数据解析失败: $e',
));
}
});
}
Future<void> _loadBoundBluetoothDevice() async {
final boundDevice = await _storageService.getBoundBluetoothDevice();
if (isClosed) return;
final deviceId = boundDevice['deviceId'];
if (deviceId != null && deviceId.isNotEmpty) {
final userMac = await _storageService.getUserMac();
if (userMac == null || userMac.isEmpty) {
await _storageService.saveUserMac(deviceId);
}
} else {
await _storageService.getUserMac();
}
if (isClosed) return;
emit(state.copyWith(
boundBluetoothDeviceId:
deviceId == null || deviceId.isEmpty ? null : deviceId,
boundBluetoothDeviceName: boundDevice['deviceName'],
));
}
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, error: null));
try {
......@@ -203,6 +270,81 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
}
}
// ==================== 蓝牙扫描/绑定 ====================
Future<void> scanBluetoothDevices() async {
if (state.isBluetoothScanning) return;
emit(state.copyWith(
isBluetoothScanning: true,
bluetoothDevices: const [],
bluetoothMessage: '开始扫描蓝牙设备',
));
try {
await _bluetoothManager.scanForDevices(
namePrefixes: const [],
timeout: const Duration(seconds: 10),
);
} catch (e) {
if (isClosed) return;
emit(state.copyWith(bluetoothMessage: '蓝牙扫描失败: $e'));
} finally {
if (!isClosed) {
emit(state.copyWith(isBluetoothScanning: false));
}
}
}
Future<void> bindBluetoothDevice(BluetoothScanDevice device) async {
if (state.isBluetoothBinding) return;
_mcuReportDecoder.clear();
emit(state.copyWith(
isBluetoothBinding: true,
bindingBluetoothDeviceId: device.remoteId,
bluetoothMessage: '正在绑定蓝牙设备: ${device.name}',
));
try {
await _bluetoothManager.connectToDevice(device);
await _storageService.saveBoundBluetoothDevice(
deviceId: device.remoteId,
deviceName: device.name,
);
await _storageService.saveUserMac(device.remoteId);
if (isClosed) return;
emit(state.copyWith(
isBluetoothBinding: false,
bindingBluetoothDeviceId: '',
boundBluetoothDeviceId: device.remoteId,
boundBluetoothDeviceName: device.name,
bluetoothMessage: '蓝牙设备已绑定: ${device.name}',
));
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
isBluetoothBinding: false,
bindingBluetoothDeviceId: '',
bluetoothMessage: '蓝牙绑定失败: $e',
));
}
}
Future<void> unbindBluetoothDevice() async {
await _bluetoothManager.disconnect();
await _storageService.deleteBoundBluetoothDevice();
await _storageService.deleteUserMac();
_mcuReportDecoder.clear();
if (isClosed) return;
emit(state.copyWith(
clearBoundBluetoothDevice: true,
clearLatestMcuReport: true,
bluetoothMessage: '蓝牙设备已解绑',
latestBluetoothRawHex: '',
));
}
/// 获取 WebRTC 渲染器供 UI 层使用
WebrtcService get webrtcService => _webrtcService;
......@@ -213,8 +355,12 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
Future<void> close() {
_webrtcStateSub?.cancel();
_p2pStateSub?.cancel();
_bluetoothScanSub?.cancel();
_bluetoothStateSub?.cancel();
_bluetoothMessageSub?.cancel();
_bluetoothDataSub?.cancel();
_webrtcService.dispose();
_p2pVideoService.dispose();
return super.close();
return _bluetoothManager.release().then((_) => super.close());
}
}
......@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
enum HomeIndexStatus {
initial,
......@@ -31,6 +32,36 @@ class HomeIndexState extends Equatable {
/// P2P 模式是否已连接
final bool isP2pConnected;
/// 蓝牙扫描中
final bool isBluetoothScanning;
/// 蓝牙绑定/连接处理中
final bool isBluetoothBinding;
/// 正在绑定/连接的蓝牙设备 ID
final String bindingBluetoothDeviceId;
/// 扫描到的蓝牙设备
final List<BluetoothScanDevice> bluetoothDevices;
/// 已绑定蓝牙设备 ID
final String? boundBluetoothDeviceId;
/// 已绑定蓝牙设备名称
final String? boundBluetoothDeviceName;
/// 蓝牙连接状态
final BluetoothConnectionStatus bluetoothConnectionStatus;
/// 蓝牙状态提示
final String? bluetoothMessage;
/// 最近收到的蓝牙原始 Hex 数据
final String? latestBluetoothRawHex;
/// 最近解析出的 MCU 上报数据
final McuReport? latestMcuReport;
const HomeIndexState({
this.status = HomeIndexStatus.initial,
this.isLoading = false,
......@@ -43,6 +74,16 @@ class HomeIndexState extends Equatable {
this.videoStreamMode = VideoStreamMode.webrtc,
this.isSwitchingMode = false,
this.isP2pConnected = false,
this.isBluetoothScanning = false,
this.isBluetoothBinding = false,
this.bindingBluetoothDeviceId = '',
this.bluetoothDevices = const [],
this.boundBluetoothDeviceId,
this.boundBluetoothDeviceName,
this.bluetoothConnectionStatus = BluetoothConnectionStatus.disconnected,
this.bluetoothMessage,
this.latestBluetoothRawHex,
this.latestMcuReport,
});
HomeIndexState copyWith({
......@@ -57,6 +98,18 @@ class HomeIndexState extends Equatable {
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
bool? isP2pConnected,
bool? isBluetoothScanning,
bool? isBluetoothBinding,
String? bindingBluetoothDeviceId,
List<BluetoothScanDevice>? bluetoothDevices,
String? boundBluetoothDeviceId,
String? boundBluetoothDeviceName,
BluetoothConnectionStatus? bluetoothConnectionStatus,
String? bluetoothMessage,
String? latestBluetoothRawHex,
McuReport? latestMcuReport,
bool clearBoundBluetoothDevice = false,
bool clearLatestMcuReport = false,
}) {
return HomeIndexState(
status: status ?? this.status,
......@@ -66,11 +119,28 @@ class HomeIndexState extends Equatable {
patientInfo: patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
menuItems: menuItems ?? this.menuItems,
videoConnectionState:
videoConnectionState ?? this.videoConnectionState,
videoConnectionState: videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
isP2pConnected: isP2pConnected ?? this.isP2pConnected,
isBluetoothScanning: isBluetoothScanning ?? this.isBluetoothScanning,
isBluetoothBinding: isBluetoothBinding ?? this.isBluetoothBinding,
bindingBluetoothDeviceId:
bindingBluetoothDeviceId ?? this.bindingBluetoothDeviceId,
bluetoothDevices: bluetoothDevices ?? this.bluetoothDevices,
boundBluetoothDeviceId: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceId ?? this.boundBluetoothDeviceId,
boundBluetoothDeviceName: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceName ?? this.boundBluetoothDeviceName,
bluetoothConnectionStatus:
bluetoothConnectionStatus ?? this.bluetoothConnectionStatus,
bluetoothMessage: bluetoothMessage ?? this.bluetoothMessage,
latestBluetoothRawHex:
latestBluetoothRawHex ?? this.latestBluetoothRawHex,
latestMcuReport:
clearLatestMcuReport ? null : latestMcuReport ?? this.latestMcuReport,
);
}
......@@ -87,5 +157,15 @@ class HomeIndexState extends Equatable {
videoStreamMode,
isSwitchingMode,
isP2pConnected,
isBluetoothScanning,
isBluetoothBinding,
bindingBluetoothDeviceId,
bluetoothDevices,
boundBluetoothDeviceId,
boundBluetoothDeviceName,
bluetoothConnectionStatus,
bluetoothMessage,
latestBluetoothRawHex,
latestMcuReport,
];
}
......@@ -8,6 +8,7 @@ import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'cubit/home_index_cubit.dart';
import 'cubit/home_index_state.dart';
import 'widgets/alert_info_card.dart';
import 'widgets/bluetooth_bind_dialog.dart';
import 'widgets/metric_card.dart';
import 'widgets/patient_info_card.dart';
import 'widgets/sidebar_menu_item.dart';
......@@ -49,7 +50,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
body: SafeArea(
child: Column(
children: [
_buildAppBar(),
BlocBuilder<HomeIndexCubit, HomeIndexState>(
buildWhen: (previous, current) =>
previous.boundBluetoothDeviceId !=
current.boundBluetoothDeviceId,
builder: (context, state) => _buildAppBar(state),
),
Expanded(
child: BlocBuilder<HomeIndexCubit, HomeIndexState>(
builder: (context, state) {
......@@ -92,7 +98,9 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
}
Widget _buildAppBar() {
Widget _buildAppBar(HomeIndexState state) {
final isBluetoothBound = state.boundBluetoothDeviceId?.isNotEmpty == true;
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
decoration: const BoxDecoration(
......@@ -106,7 +114,8 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
const CircleAvatar(
backgroundColor: Colors.white,
radius: 24,
child: Icon(Icons.local_hospital, color: Color(0xFF1E10B6), size: 28),
child:
Icon(Icons.local_hospital, color: Color(0xFF1E10B6), size: 28),
),
SizedBox(width: 16.w),
Text(
......@@ -118,7 +127,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
),
),
const Spacer(),
_buildAppBarIcon(Icons.bluetooth, 'TSAAS'),
_buildAppBarIcon(
Icons.bluetooth,
'TSAAS',
isActive: isBluetoothBound,
onTap: _showBluetoothDialog,
),
SizedBox(width: 16.w),
_buildAppBarIcon(Icons.description, null),
SizedBox(width: 16.w),
......@@ -128,27 +142,52 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
}
Widget _buildAppBarIcon(IconData icon, String? label) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
border: Border.all(color: Colors.white24, width: 1),
borderRadius: BorderRadius.circular(20.r),
),
child: Row(
children: [
Icon(icon, color: Colors.white, size: 20.w),
if (label != null) ...[
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
Widget _buildAppBarIcon(
IconData icon,
String? label, {
bool isActive = false,
VoidCallback? onTap,
}) {
final foregroundColor = isActive ? const Color(0xFF003A8C) : Colors.white;
final backgroundColor =
isActive ? const Color(0xFF00F6FF) : Colors.transparent;
final borderColor = isActive ? const Color(0xFFB8FFFF) : Colors.white24;
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor, width: 1),
borderRadius: BorderRadius.circular(20.r),
),
child: Row(
children: [
Icon(icon, color: foregroundColor, size: 20.w),
if (label != null) ...[
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 18.sp,
color: foregroundColor,
fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
),
),
),
],
],
],
),
),
);
}
void _showBluetoothDialog() {
showDialog<void>(
context: context,
builder: (_) => BlocProvider.value(
value: _homeIndexCubit,
child: const BluetoothBindDialog(),
),
);
}
......
This diff is collapsed.
......@@ -9,6 +9,7 @@ import device_info_plus
import flutter_image_compress_macos
import flutter_secure_storage_macos
import flutter_webrtc
import reactive_ble_mobile
import share_plus
import shared_preferences_foundation
import url_launcher_macos
......@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
ReactiveBlePlugin.register(with: registry.registrar(forPlugin: "ReactiveBlePlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
......
......@@ -50,16 +50,15 @@ dependencies:
logger: ^2.7.0
fluttertoast: ^9.0.0
permission_handler: ^11.3.1
flutter_usbcamera: ^0.0.1
flutter_usbcamera:
path: ./third_party/flutter_usbcamera
path_provider: ^2.1.2
flutter_webrtc:
path: ./plugins/flutter_webrtc
vsdk:
path: ./plugins/vsdk
# flutter_usbcamera: ^0.0.1
# fl_chart: ^0.71.0
# fl_chart: ^1.1.0
flutter_reactive_ble: ^5.5.0
mqtt_client: ^10.5.1
dev_dependencies:
flutter_test:
sdk: flutter
......@@ -84,6 +83,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
default-flavor: dev
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment