Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
laki_icu_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
laki_icu_app
Commits
531a4d4e
Commit
531a4d4e
authored
Jun 21, 2026
by
akari
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 添加蓝牙,MQTT连接
parent
b56b4a34
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
2156 additions
and
42 deletions
+2156
-42
.gitignore
.gitignore
+4
-0
settings.json
.vscode/settings.json
+31
-3
analysis_options.yaml
analysis_options.yaml
+10
-0
AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
+8
-0
gradle.properties
android/gradle.properties
+5
-1
user_model.dart
lib/models/bo/user_model.dart
+3
-0
bluetooth_manager.dart
lib/utils/bluetooth/bluetooth_manager.dart
+463
-0
index.dart
lib/utils/bluetooth/index.dart
+2
-0
mcu_report.dart
lib/utils/bluetooth/mcu_report.dart
+469
-0
index.dart
lib/utils/mqtt/index.dart
+1
-0
laki_mqtt_client.dart
lib/utils/mqtt/laki_mqtt_client.dart
+325
-0
storage_service.dart
lib/utils/storage/storage_service.dart
+45
-0
home_index_cubit.dart
lib/views/home/index/cubit/home_index_cubit.dart
+154
-8
home_index_state.dart
lib/views/home/index/cubit/home_index_state.dart
+82
-2
home_index_view.dart
lib/views/home/index/home_index_view.dart
+62
-23
bluetooth_bind_dialog.dart
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
+485
-0
GeneratedPluginRegistrant.swift
macos/Flutter/GeneratedPluginRegistrant.swift
+2
-0
pubspec.yaml
pubspec.yaml
+5
-5
No files found.
.gitignore
View file @
531a4d4e
# FVM Version Cache
# FVM Version Cache
.fvm/
.fvm/
.DS_Store
# claude code
# claude code
.claude/
.claude/
...
@@ -9,6 +10,9 @@
...
@@ -9,6 +10,9 @@
.dart_tool/
.dart_tool/
build/
build/
.flutter-plugins
.flutter-plugins-dependencies
third_party/
pubspec.lock
pubspec.lock
...
...
.vscode/settings.json
View file @
531a4d4e
{
{
"dart.flutterSdkPath"
:
".fvm/versions/3.41.9"
,
"dart.flutterSdkPath"
:
".fvm/versions/3.41.9"
,
"java.configuration.updateBuildConfiguration"
:
"disabled"
"java.configuration.updateBuildConfiguration"
:
"disabled"
,
}
"files.exclude"
:
{
\ No newline at end of file
"**/.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
}
}
analysis_options.yaml
View file @
531a4d4e
...
@@ -9,6 +9,16 @@
...
@@ -9,6 +9,16 @@
# packages, and plugins designed to encourage good coding practices.
# packages, and plugins designed to encourage good coding practices.
include
:
package:flutter_lints/flutter.yaml
include
:
package:flutter_lints/flutter.yaml
analyzer
:
exclude
:
-
"
**/*.g.dart"
-
"
**/*.gr.dart"
-
"
**/*.freezed.dart"
-
"
**/*.mocks.dart"
-
"
.dart_tool/**"
-
"
build/**"
-
"
third_party/**"
linter
:
linter
:
# The lint rules applied to this project can be customized in the
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
...
...
android/app/src/main/AndroidManifest.xml
View file @
531a4d4e
...
@@ -4,6 +4,14 @@
...
@@ -4,6 +4,14 @@
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.ACCESS_NETWORK_STATE"
/>
<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 摄像头权限 -->
<!-- USB 摄像头权限 -->
<uses-feature
android:name=
"android.hardware.usb.host"
android:required=
"true"
/>
<uses-feature
android:name=
"android.hardware.usb.host"
android:required=
"true"
/>
<uses-permission
android:name=
"android.permission.CAMERA"
/>
<uses-permission
android:name=
"android.permission.CAMERA"
/>
...
...
android/gradle.properties
View file @
531a4d4e
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
org.gradle.jvmargs
=
-Xmx4G
android.useAndroidX
=
true
android.useAndroidX
=
true
android.enableJetifier
=
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
lib/models/bo/user_model.dart
View file @
531a4d4e
...
@@ -11,6 +11,9 @@ class UserModel extends Equatable {
...
@@ -11,6 +11,9 @@ class UserModel extends Equatable {
final
dynamic
hospitalId
;
final
dynamic
hospitalId
;
final
String
?
hospitalName
;
final
String
?
hospitalName
;
// ignore: non_constant_identifier_names
static
String
?
MAC
;
const
UserModel
({
const
UserModel
({
required
this
.
id
,
required
this
.
id
,
this
.
nickname
,
this
.
nickname
,
...
...
lib/utils/bluetooth/bluetooth_manager.dart
0 → 100644
View file @
531a4d4e
import
'dart:async'
;
import
'package:flutter_reactive_ble/flutter_reactive_ble.dart'
;
typedef
BluetoothPayloadParser
<
T
>
=
T
?
Function
(
List
<
int
>
bytes
,
String
rawHex
);
enum
BluetoothConnectionStatus
{
disconnected
,
connecting
,
connectedReady
,
active
,
}
class
BluetoothScanDevice
{
const
BluetoothScanDevice
({
required
this
.
device
,
required
this
.
remoteId
,
required
this
.
name
,
required
this
.
hasName
,
required
this
.
rssi
,
});
final
DiscoveredDevice
device
;
final
String
remoteId
;
final
String
name
;
final
bool
hasName
;
final
int
rssi
;
}
class
BluetoothDataPacket
<
T
>
{
const
BluetoothDataPacket
({
required
this
.
deviceId
,
required
this
.
rawHex
,
required
this
.
bytes
,
this
.
parsedData
,
});
final
String
deviceId
;
final
String
rawHex
;
final
List
<
int
>
bytes
;
final
T
?
parsedData
;
}
class
BleBluetoothManager
<
T
>
{
BleBluetoothManager
({
BluetoothPayloadParser
<
T
>?
parser
,
FlutterReactiveBle
?
ble
,
Uuid
?
serviceUuid
,
Uuid
?
notifyCharacteristicUuid
,
Uuid
?
writeCharacteristicUuid
,
this
.
connectionTimeout
=
const
Duration
(
seconds:
15
),
})
:
_parser
=
parser
,
_ble
=
ble
??
FlutterReactiveBle
(),
serviceUuid
=
serviceUuid
??
Uuid
.
parse
(
'0000FFE0-0000-1000-8000-00805F9B34FB'
),
notifyCharacteristicUuid
=
notifyCharacteristicUuid
??
Uuid
.
parse
(
'0000FFE4-0000-1000-8000-00805F9B34FB'
),
writeCharacteristicUuid
=
writeCharacteristicUuid
??
Uuid
.
parse
(
'0000FFE9-0000-1000-8000-00805F9B34FB'
);
final
BluetoothPayloadParser
<
T
>?
_parser
;
final
FlutterReactiveBle
_ble
;
final
Uuid
serviceUuid
;
final
Uuid
notifyCharacteristicUuid
;
final
Uuid
writeCharacteristicUuid
;
final
Duration
connectionTimeout
;
final
_scanDevicesController
=
StreamController
<
List
<
BluetoothScanDevice
>>.
broadcast
();
final
_dataController
=
StreamController
<
BluetoothDataPacket
<
T
>>.
broadcast
();
final
_stateController
=
StreamController
<
BluetoothConnectionStatus
>.
broadcast
();
final
_messageController
=
StreamController
<
String
>.
broadcast
();
final
_sendResultController
=
StreamController
<
bool
>.
broadcast
();
Stream
<
List
<
BluetoothScanDevice
>>
get
scanDevicesStream
=>
_scanDevicesController
.
stream
;
Stream
<
BluetoothDataPacket
<
T
>>
get
dataStream
=>
_dataController
.
stream
;
Stream
<
BluetoothConnectionStatus
>
get
stateStream
=>
_stateController
.
stream
;
Stream
<
String
>
get
messageStream
=>
_messageController
.
stream
;
Stream
<
bool
>
get
sendResultStream
=>
_sendResultController
.
stream
;
BluetoothConnectionStatus
_status
=
BluetoothConnectionStatus
.
disconnected
;
String
?
_connectedDeviceId
;
QualifiedCharacteristic
?
_notifyCharacteristic
;
QualifiedCharacteristic
?
_writeCharacteristic
;
StreamSubscription
<
DiscoveredDevice
>?
_scanSubscription
;
StreamSubscription
<
ConnectionStateUpdate
>?
_connectionSubscription
;
StreamSubscription
<
List
<
int
>>?
_notifySubscription
;
Timer
?
_scanTimer
;
final
Map
<
String
,
BluetoothScanDevice
>
_scannedDevices
=
{};
BluetoothConnectionStatus
get
status
=>
_status
;
String
?
get
connectedDeviceId
=>
_connectedDeviceId
;
bool
get
isConnected
=>
_status
==
BluetoothConnectionStatus
.
connectedReady
||
_status
==
BluetoothConnectionStatus
.
active
;
bool
get
isActive
=>
_status
==
BluetoothConnectionStatus
.
active
;
List
<
BluetoothScanDevice
>
get
scannedDevices
=>
List
.
unmodifiable
(
_scannedDevices
.
values
.
toList
()
..
sort
(
(
left
,
right
)
{
if
(
left
.
hasName
!=
right
.
hasName
)
{
return
left
.
hasName
?
-
1
:
1
;
}
if
(
left
.
hasName
&&
right
.
hasName
)
{
final
nameCompare
=
left
.
name
.
toUpperCase
().
compareTo
(
right
.
name
.
toUpperCase
(),
);
if
(
nameCompare
!=
0
)
{
return
nameCompare
;
}
}
return
_normalizeRemoteIdForSort
(
left
.
remoteId
,
).
compareTo
(
_normalizeRemoteIdForSort
(
right
.
remoteId
));
},
),
);
Future
<
bool
>
ensureBluetoothReady
()
async
{
await
_ble
.
initialize
();
final
status
=
_ble
.
status
;
if
(
status
==
BleStatus
.
ready
)
{
return
true
;
}
final
nextStatus
=
await
_ble
.
statusStream
.
where
((
value
)
=>
value
!=
BleStatus
.
unknown
)
.
first
.
timeout
(
const
Duration
(
seconds:
2
),
onTimeout:
()
=>
status
);
if
(
nextStatus
!=
BleStatus
.
ready
)
{
_notifyMessage
(
'蓝牙未就绪或未授权:
$nextStatus
'
);
return
false
;
}
return
true
;
}
Future
<
void
>
scanForDevices
({
required
List
<
String
>
namePrefixes
,
Duration
timeout
=
const
Duration
(
seconds:
10
),
})
async
{
if
(!
await
ensureBluetoothReady
())
{
return
;
}
await
stopScan
();
_scannedDevices
.
clear
();
_scanDevicesController
.
add
(
scannedDevices
);
_notifyMessage
(
namePrefixes
.
isEmpty
?
'开始扫描全部蓝牙设备'
:
'开始扫描蓝牙设备:
$namePrefixes
'
);
_scanSubscription
=
_ble
.
scanForDevices
(
withServices:
const
[],
scanMode:
ScanMode
.
lowLatency
,
requireLocationServicesEnabled:
false
,
).
listen
(
(
device
)
{
if
(
namePrefixes
.
isNotEmpty
&&
!
_startsWithAny
(
device
.
name
,
namePrefixes
))
{
return
;
}
final
deviceName
=
device
.
name
.
trim
();
_scannedDevices
[
device
.
id
]
=
BluetoothScanDevice
(
device:
device
,
remoteId:
device
.
id
,
name:
deviceName
.
isEmpty
?
'未知设备'
:
deviceName
,
hasName:
deviceName
.
isNotEmpty
,
rssi:
device
.
rssi
,
);
_scanDevicesController
.
add
(
scannedDevices
);
},
onError:
(
Object
error
)
=>
_notifyMessage
(
'扫描失败:
$error
'
),
);
await
Future
<
void
>.
delayed
(
timeout
);
await
stopScan
();
_scanDevicesController
.
add
(
scannedDevices
);
_notifyMessage
(
'蓝牙扫描结束,共发现
${_scannedDevices.length}
个设备'
);
}
Future
<
void
>
prepareConnection
(
String
targetName
)
async
{
if
(
targetName
.
isEmpty
)
{
_notifyMessage
(
'无效的蓝牙设备名称'
);
return
;
}
if
(
_status
!=
BluetoothConnectionStatus
.
disconnected
)
{
_notifyMessage
(
'蓝牙已在连接或已连接状态,跳过连接'
);
return
;
}
if
(!
await
ensureBluetoothReady
())
{
return
;
}
_setStatus
(
BluetoothConnectionStatus
.
connecting
);
await
stopScan
();
_notifyMessage
(
'开始扫描目标蓝牙设备:
$targetName
'
);
var
found
=
false
;
_scanSubscription
=
_ble
.
scanForDevices
(
withServices:
const
[],
scanMode:
ScanMode
.
lowLatency
,
requireLocationServicesEnabled:
false
,
).
listen
(
(
device
)
async
{
if
(
found
||
!
device
.
name
.
contains
(
targetName
))
{
return
;
}
found
=
true
;
await
stopScan
();
await
connectToDeviceId
(
device
.
id
);
},
onError:
(
Object
error
)
{
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
_notifyMessage
(
'扫描目标设备失败:
$error
'
);
},
);
_scanTimer
=
Timer
(
connectionTimeout
,
()
async
{
if
(!
found
&&
_status
==
BluetoothConnectionStatus
.
connecting
)
{
await
stopScan
();
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
_notifyMessage
(
'未找到目标蓝牙设备:
$targetName
'
);
}
});
}
Future
<
void
>
connectByRemoteId
(
String
remoteId
)
{
return
connectToDeviceId
(
remoteId
);
}
Future
<
void
>
connectToDevice
(
BluetoothScanDevice
device
)
{
return
connectToDeviceId
(
device
.
remoteId
);
}
Future
<
void
>
connectToDeviceId
(
String
deviceId
)
async
{
if
(
deviceId
.
isEmpty
)
{
_notifyMessage
(
'无效的蓝牙设备地址'
);
return
;
}
await
disconnect
();
_connectedDeviceId
=
deviceId
;
_notifyCharacteristic
=
QualifiedCharacteristic
(
deviceId:
deviceId
,
serviceId:
serviceUuid
,
characteristicId:
notifyCharacteristicUuid
,
);
_writeCharacteristic
=
QualifiedCharacteristic
(
deviceId:
deviceId
,
serviceId:
serviceUuid
,
characteristicId:
writeCharacteristicUuid
,
);
_setStatus
(
BluetoothConnectionStatus
.
connecting
);
_notifyMessage
(
'正在连接蓝牙设备:
$deviceId
'
);
final
completer
=
Completer
<
void
>();
_connectionSubscription
=
_ble
.
connectToDevice
(
id:
deviceId
,
servicesWithCharacteristicsToDiscover:
{
serviceUuid:
[
notifyCharacteristicUuid
,
writeCharacteristicUuid
],
},
connectionTimeout:
connectionTimeout
,
)
.
listen
(
(
update
)
async
{
switch
(
update
.
connectionState
)
{
case
DeviceConnectionState
.
connecting
:
_setStatus
(
BluetoothConnectionStatus
.
connecting
);
break
;
case
DeviceConnectionState
.
connected
:
_setStatus
(
BluetoothConnectionStatus
.
connectedReady
);
_notifyMessage
(
'蓝牙连接成功:
$deviceId
'
);
await
_enableNotifications
(
deviceId
);
activateDataSource
();
if
(!
completer
.
isCompleted
)
{
completer
.
complete
();
}
break
;
case
DeviceConnectionState
.
disconnecting
:
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
break
;
case
DeviceConnectionState
.
disconnected
:
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
_notifyMessage
(
'蓝牙连接断开:
$deviceId
'
);
_clearCharacteristics
();
if
(!
completer
.
isCompleted
&&
update
.
failure
!=
null
)
{
completer
.
completeError
(
update
.
failure
!);
}
break
;
}
},
onError:
(
Object
error
)
{
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
_notifyMessage
(
'蓝牙连接失败:
$error
'
);
if
(!
completer
.
isCompleted
)
{
completer
.
completeError
(
error
);
}
},
);
try
{
await
completer
.
future
.
timeout
(
connectionTimeout
);
}
catch
(
_
)
{
await
disconnect
();
rethrow
;
}
}
void
activateDataSource
()
{
if
(
_status
==
BluetoothConnectionStatus
.
connectedReady
||
_status
==
BluetoothConnectionStatus
.
active
)
{
_setStatus
(
BluetoothConnectionStatus
.
active
);
_notifyMessage
(
'蓝牙数据源已激活'
);
}
}
Future
<
void
>
writeData
(
List
<
int
>
data
,
{
bool
withoutResponse
=
false
})
async
{
if
(
data
.
isEmpty
)
{
_notifyMessage
(
'发送数据为空'
);
_sendResultController
.
add
(
false
);
return
;
}
final
characteristic
=
_writeCharacteristic
;
if
(!
isConnected
||
characteristic
==
null
)
{
_notifyMessage
(
'蓝牙未连接或未找到发送特征值,无法发送数据'
);
_sendResultController
.
add
(
false
);
return
;
}
try
{
if
(
withoutResponse
)
{
await
_ble
.
writeCharacteristicWithoutResponse
(
characteristic
,
value:
data
,
);
}
else
{
await
_ble
.
writeCharacteristicWithResponse
(
characteristic
,
value:
data
);
}
_notifyMessage
(
'蓝牙数据发送成功:
${bytesToHex(data)}
'
);
_sendResultController
.
add
(
true
);
}
catch
(
error
)
{
_notifyMessage
(
'蓝牙数据发送失败:
$error
'
);
_sendResultController
.
add
(
false
);
}
}
Future
<
void
>
writeHex
(
String
hex
,
{
bool
withoutResponse
=
false
})
{
return
writeData
(
hexToBytes
(
hex
),
withoutResponse:
withoutResponse
);
}
Future
<
void
>
stopScan
()
async
{
_scanTimer
?.
cancel
();
_scanTimer
=
null
;
await
_scanSubscription
?.
cancel
();
_scanSubscription
=
null
;
}
Future
<
void
>
disconnect
()
async
{
await
stopScan
();
await
_notifySubscription
?.
cancel
();
_notifySubscription
=
null
;
await
_connectionSubscription
?.
cancel
();
_connectionSubscription
=
null
;
_connectedDeviceId
=
null
;
_clearCharacteristics
();
_setStatus
(
BluetoothConnectionStatus
.
disconnected
);
}
Future
<
void
>
release
()
async
{
await
disconnect
();
await
_scanDevicesController
.
close
();
await
_dataController
.
close
();
await
_stateController
.
close
();
await
_messageController
.
close
();
await
_sendResultController
.
close
();
}
Future
<
void
>
_enableNotifications
(
String
deviceId
)
async
{
final
characteristic
=
_notifyCharacteristic
;
if
(
characteristic
==
null
)
{
throw
StateError
(
'接收特征值为空'
);
}
await
_notifySubscription
?.
cancel
();
_notifySubscription
=
_ble
.
subscribeToCharacteristic
(
characteristic
).
listen
(
(
bytes
)
{
if
(!
isActive
||
bytes
.
isEmpty
)
{
return
;
}
final
rawHex
=
bytesToHex
(
bytes
);
final
parsedData
=
_parser
?.
call
(
bytes
,
rawHex
);
_dataController
.
add
(
BluetoothDataPacket
<
T
>(
deviceId:
deviceId
,
rawHex:
rawHex
,
bytes:
List
.
unmodifiable
(
bytes
),
parsedData:
parsedData
,
),
);
},
onError:
(
Object
error
)
=>
_notifyMessage
(
'接收蓝牙数据失败:
$error
'
),
);
}
void
_clearCharacteristics
()
{
_notifyCharacteristic
=
null
;
_writeCharacteristic
=
null
;
}
void
_setStatus
(
BluetoothConnectionStatus
status
)
{
_status
=
status
;
if
(!
_stateController
.
isClosed
)
{
_stateController
.
add
(
status
);
}
}
void
_notifyMessage
(
String
message
)
{
if
(!
_messageController
.
isClosed
)
{
_messageController
.
add
(
message
);
}
}
static
String
bytesToHex
(
List
<
int
>
bytes
)
{
return
bytes
.
map
((
item
)
=>
item
.
toRadixString
(
16
).
padLeft
(
2
,
'0'
))
.
join
()
.
toUpperCase
();
}
static
List
<
int
>
hexToBytes
(
String
hex
)
{
final
clean
=
hex
.
replaceAll
(
RegExp
(
r'\s+'
),
''
);
if
(
clean
.
length
.
isOdd
)
{
throw
FormatException
(
'Hex 字符串长度必须为偶数:
$hex
'
);
}
return
[
for
(
var
index
=
0
;
index
<
clean
.
length
;
index
+=
2
)
int
.
parse
(
clean
.
substring
(
index
,
index
+
2
),
radix:
16
),
];
}
static
bool
_startsWithAny
(
String
value
,
List
<
String
>
prefixes
)
{
return
prefixes
.
any
(
value
.
startsWith
);
}
static
String
_normalizeRemoteIdForSort
(
String
remoteId
)
{
return
remoteId
.
replaceAll
(
RegExp
(
r'[^0-9a-zA-Z]'
),
''
).
toUpperCase
();
}
}
lib/utils/bluetooth/index.dart
0 → 100644
View file @
531a4d4e
export
'bluetooth_manager.dart'
;
export
'mcu_report.dart'
;
lib/utils/bluetooth/mcu_report.dart
0 → 100644
View file @
531a4d4e
import
'dart:convert'
;
const
int
mcuReportBytes
=
145
;
class
HexReader
{
HexReader
(
String
hexData
)
{
final
cleanHex
=
hexData
.
replaceAll
(
RegExp
(
r'\s+'
),
''
).
toUpperCase
();
if
(
cleanHex
.
isEmpty
||
cleanHex
.
length
.
isOdd
||
!
RegExp
(
r'^[0-9A-F]+$'
).
hasMatch
(
cleanHex
))
{
throw
const
FormatException
(
'数据必须是长度为偶数的十六进制字符串'
);
}
data
=
cleanHex
;
}
late
final
String
data
;
int
index
=
0
;
int
get
byteIndex
=>
index
~/
2
;
int
get
remainingBytes
=>
(
data
.
length
-
index
)
~/
2
;
String
readHex
(
int
bytes
)
{
_ensureAvailable
(
bytes
);
final
length
=
bytes
*
2
;
final
value
=
data
.
substring
(
index
,
index
+
length
);
index
+=
length
;
return
value
;
}
int
readUInt8
()
{
return
int
.
parse
(
readHex
(
1
),
radix:
16
);
}
int
readUInt16
()
{
return
int
.
parse
(
readHex
(
2
),
radix:
16
);
}
double
readTenths
()
{
return
readUInt16
()
/
10
;
}
int
readHighByte
()
{
return
readUInt16
()
>>
8
;
}
String
readAscii
(
int
bytes
)
{
final
hex
=
readHex
(
bytes
);
final
values
=
<
int
>[];
for
(
var
index
=
0
;
index
<
hex
.
length
;
index
+=
2
)
{
values
.
add
(
int
.
parse
(
hex
.
substring
(
index
,
index
+
2
),
radix:
16
));
}
return
utf8
.
decode
(
values
,
allowMalformed:
true
).
split
(
'
\
x00'
).
first
.
trim
();
}
void
_ensureAvailable
(
int
bytes
)
{
if
(
bytes
<
0
)
{
throw
ArgumentError
.
value
(
bytes
,
'bytes'
,
'字节数必须是非负整数'
);
}
if
(
remainingBytes
<
bytes
)
{
throw
RangeError
(
'数据不足:需要
$bytes
字节,只剩
$remainingBytes
字节'
);
}
}
}
class
McuReport
{
const
McuReport
({
required
this
.
head
,
required
this
.
direct
,
required
this
.
serviceNum
,
required
this
.
length
,
required
this
.
mainTemperature
,
required
this
.
versionNumber
,
required
this
.
ambientTemperature
,
required
this
.
upperTemperature
,
required
this
.
humidity
,
required
this
.
oxygenConcentration
,
required
this
.
temperatureSetting
,
required
this
.
lowerTemperature
,
required
this
.
humiditySetting
,
required
this
.
oxygenSetting
,
required
this
.
internalExternalCycleState
,
required
this
.
negativeIonSwitch
,
required
this
.
uvLamp
,
required
this
.
inspectionLamp
,
required
this
.
floodLight
,
required
this
.
o2ChangeSlope
,
required
this
.
rightAtomizerTreatmentTime
,
required
this
.
rightInfraredPhysiotherapyTime
,
required
this
.
tempS1
,
required
this
.
tempH1
,
required
this
.
atomizerTreatmentTime
,
required
this
.
infraredPhysiotherapyTime
,
required
this
.
mainTempCorrect
,
required
this
.
auxiliaryTemperatureCorrect
,
required
this
.
o2CorrectStart
,
required
this
.
o2CorrectDiffer
,
required
this
.
humidityCorrect
,
required
this
.
oxygenCorrect
,
required
this
.
eTempCorrect
,
required
this
.
xyyModel
,
required
this
.
unknown
,
required
this
.
wifible
,
required
this
.
levelLight
,
required
this
.
openO2
,
required
this
.
airConditioner
,
required
this
.
co2Correct
,
required
this
.
co2Measurement
,
required
this
.
co2Setting
,
required
this
.
co2FastClearSwitch
,
required
this
.
openO2SettingMax
,
required
this
.
noOpenO2SettingMax
,
required
this
.
windSet
,
required
this
.
rangeData1
,
required
this
.
rangeData2
,
required
this
.
rangeData3
,
required
this
.
co2Safety
,
required
this
.
co2SettingMin
,
required
this
.
blueLight
,
required
this
.
redLight
,
required
this
.
fcSwitch
,
required
this
.
o2SupplyTime
,
required
this
.
totalTime
,
required
this
.
ratioTemp2
,
required
this
.
ratioTemp1
,
required
this
.
sp
,
required
this
.
dp
,
required
this
.
mean
,
required
this
.
pulseRate
,
required
this
.
xyyStatus
,
required
this
.
xyyError1
,
required
this
.
sn
,
required
this
.
topTempCorrect
,
required
this
.
midTempCorrect
,
required
this
.
o2OpenCorrectStart
,
required
this
.
o2OpenCorrectStep
,
required
this
.
o2CorrectMax
,
required
this
.
o2OpenCorrectMax
,
required
this
.
parsedBytes
,
});
final
String
head
;
final
String
direct
;
final
String
serviceNum
;
final
int
length
;
final
double
mainTemperature
;
final
double
versionNumber
;
final
double
ambientTemperature
;
final
double
upperTemperature
;
final
int
humidity
;
final
int
oxygenConcentration
;
final
double
temperatureSetting
;
final
double
lowerTemperature
;
final
int
humiditySetting
;
final
int
oxygenSetting
;
final
int
internalExternalCycleState
;
final
int
negativeIonSwitch
;
final
int
uvLamp
;
final
int
inspectionLamp
;
final
int
floodLight
;
final
int
o2ChangeSlope
;
final
int
rightAtomizerTreatmentTime
;
final
int
rightInfraredPhysiotherapyTime
;
final
double
tempS1
;
final
int
tempH1
;
final
int
atomizerTreatmentTime
;
final
int
infraredPhysiotherapyTime
;
final
int
mainTempCorrect
;
final
int
auxiliaryTemperatureCorrect
;
final
int
o2CorrectStart
;
final
int
o2CorrectDiffer
;
final
int
humidityCorrect
;
final
int
oxygenCorrect
;
final
int
eTempCorrect
;
final
String
xyyModel
;
final
String
unknown
;
final
int
wifible
;
final
int
levelLight
;
final
int
openO2
;
final
int
airConditioner
;
final
int
co2Correct
;
final
int
co2Measurement
;
final
int
co2Setting
;
final
int
co2FastClearSwitch
;
final
int
openO2SettingMax
;
final
int
noOpenO2SettingMax
;
final
int
windSet
;
final
String
rangeData1
;
final
String
rangeData2
;
final
String
rangeData3
;
final
int
co2Safety
;
final
int
co2SettingMin
;
final
int
blueLight
;
final
int
redLight
;
final
int
fcSwitch
;
final
int
o2SupplyTime
;
final
int
totalTime
;
final
int
ratioTemp2
;
final
int
ratioTemp1
;
final
int
sp
;
final
int
dp
;
final
int
mean
;
final
int
pulseRate
;
final
int
xyyStatus
;
final
int
xyyError1
;
final
String
sn
;
final
int
topTempCorrect
;
final
int
midTempCorrect
;
final
int
o2OpenCorrectStart
;
final
int
o2OpenCorrectStep
;
final
int
o2CorrectMax
;
final
int
o2OpenCorrectMax
;
final
int
parsedBytes
;
Map
<
String
,
dynamic
>
toMap
()
{
return
{
'head'
:
head
,
'direct'
:
direct
,
'serviceNum'
:
serviceNum
,
'length'
:
length
,
'mainTemperature'
:
mainTemperature
,
'versionNumber'
:
versionNumber
,
'ambientTemperature'
:
ambientTemperature
,
'upperTemperature'
:
upperTemperature
,
'humidity'
:
humidity
,
'oxygenConcentration'
:
oxygenConcentration
,
'temperatureSetting'
:
temperatureSetting
,
'lowerTemperature'
:
lowerTemperature
,
'humiditySetting'
:
humiditySetting
,
'oxygenSetting'
:
oxygenSetting
,
'internalExternalCycleState'
:
internalExternalCycleState
,
'negativeIonSwitch'
:
negativeIonSwitch
,
'uvLamp'
:
uvLamp
,
'inspectionLamp'
:
inspectionLamp
,
'floodLight'
:
floodLight
,
'o2ChangeSlope'
:
o2ChangeSlope
,
'rightAtomizerTreatmentTime'
:
rightAtomizerTreatmentTime
,
'rightInfraredPhysiotherapyTime'
:
rightInfraredPhysiotherapyTime
,
'tempS1'
:
tempS1
,
'tempH1'
:
tempH1
,
'atomizerTreatmentTime'
:
atomizerTreatmentTime
,
'infraredPhysiotherapyTime'
:
infraredPhysiotherapyTime
,
'mainTempCorrect'
:
mainTempCorrect
,
'auxiliaryTemperatureCorrect'
:
auxiliaryTemperatureCorrect
,
'o2CorrectStart'
:
o2CorrectStart
,
'o2CorrectDiffer'
:
o2CorrectDiffer
,
'humidityCorrect'
:
humidityCorrect
,
'oxygenCorrect'
:
oxygenCorrect
,
'eTempCorrect'
:
eTempCorrect
,
'xyyModel'
:
xyyModel
,
'unknown'
:
unknown
,
'wifible'
:
wifible
,
'levelLight'
:
levelLight
,
'openO2'
:
openO2
,
'airConditioner'
:
airConditioner
,
'co2Correct'
:
co2Correct
,
'co2Measurement'
:
co2Measurement
,
'co2Setting'
:
co2Setting
,
'co2FastClearSwitch'
:
co2FastClearSwitch
,
'openO2SettingMax'
:
openO2SettingMax
,
'noOpenO2SettingMax'
:
noOpenO2SettingMax
,
'windSet'
:
windSet
,
'rangeData1'
:
rangeData1
,
'rangeData2'
:
rangeData2
,
'rangeData3'
:
rangeData3
,
'co2Safety'
:
co2Safety
,
'co2SettingMin'
:
co2SettingMin
,
'blueLight'
:
blueLight
,
'redLight'
:
redLight
,
'fcSwitch'
:
fcSwitch
,
'o2SupplyTime'
:
o2SupplyTime
,
'totalTime'
:
totalTime
,
'ratioTemp2'
:
ratioTemp2
,
'ratioTemp1'
:
ratioTemp1
,
'sp'
:
sp
,
'dp'
:
dp
,
'mean'
:
mean
,
'pulseRate'
:
pulseRate
,
'xyyStatus'
:
xyyStatus
,
'xyyError1'
:
xyyError1
,
'sn'
:
sn
,
'topTempCorrect'
:
topTempCorrect
,
'midTempCorrect'
:
midTempCorrect
,
'o2OpenCorrectStart'
:
o2OpenCorrectStart
,
'o2OpenCorrectStep'
:
o2OpenCorrectStep
,
'o2CorrectMax'
:
o2CorrectMax
,
'o2OpenCorrectMax'
:
o2OpenCorrectMax
,
'parsedBytes'
:
parsedBytes
,
};
}
Map
<
String
,
String
>
toMonitorPayload
({
DateTime
?
date
})
{
return
{
'sn'
:
sn
,
'date'
:
'
${(date ?? DateTime.now()).millisecondsSinceEpoch}
'
,
'mainTemp'
:
mainTemperature
.
toStringAsFixed
(
1
),
'humidity'
:
'
$humidity
'
,
'oxygenConcentration'
:
'
$oxygenConcentration
'
,
'co2Measurement'
:
'
$co2Measurement
'
,
'mainTempSetting'
:
temperatureSetting
.
toStringAsFixed
(
1
),
'humiditySetting'
:
'
$humiditySetting
'
,
'oxygenConcentrationSetting'
:
'
$oxygenSetting
'
,
'co2Setting'
:
'
$co2Setting
'
,
'windSet'
:
'
$windSet
'
,
'cycleState'
:
'
$internalExternalCycleState
'
,
'levelLight'
:
'
$levelLight
'
,
'openO2'
:
'
$openO2
'
,
'floodlight'
:
'
$floodLight
'
,
'inspectionLamp'
:
'
$inspectionLamp
'
,
'blueLight'
:
'
$blueLight
'
,
'redLight'
:
'
$redLight
'
,
'airConditioner'
:
'
$airConditioner
'
,
'wifible'
:
'
$wifible
'
,
'co2FastClearSwitch'
:
'
$co2FastClearSwitch
'
,
'rightAtomizerTreatmentTime'
:
'
$rightAtomizerTreatmentTime
'
,
'rightInfraredPhysiotherapyTime'
:
'
$rightInfraredPhysiotherapyTime
'
,
'totalTime'
:
'
$totalTime
'
,
'o2SupplyTime'
:
'
$o2SupplyTime
'
,
};
}
@override
String
toString
()
=>
toMap
().
toString
();
}
class
McuReportParser
{
static
McuReport
parseHex
(
String
hexData
)
{
final
data
=
HexReader
(
hexData
);
if
(
data
.
remainingBytes
<
mcuReportBytes
)
{
throw
RangeError
(
'完整 MCU 上报需要
$mcuReportBytes
字节,当前只有
${data.remainingBytes}
字节'
,
);
}
return
McuReport
(
head:
data
.
readHex
(
2
),
direct:
data
.
readHex
(
1
),
serviceNum:
data
.
readHex
(
1
),
length:
data
.
readUInt8
(),
mainTemperature:
data
.
readTenths
(),
versionNumber:
data
.
readTenths
(),
ambientTemperature:
data
.
readTenths
(),
upperTemperature:
data
.
readTenths
(),
humidity:
data
.
readUInt16
(),
oxygenConcentration:
data
.
readUInt16
(),
temperatureSetting:
data
.
readTenths
(),
lowerTemperature:
data
.
readTenths
(),
humiditySetting:
data
.
readUInt16
(),
oxygenSetting:
data
.
readUInt16
(),
internalExternalCycleState:
data
.
readUInt16
(),
negativeIonSwitch:
data
.
readUInt16
(),
uvLamp:
data
.
readUInt16
(),
inspectionLamp:
data
.
readUInt16
(),
floodLight:
data
.
readUInt16
(),
o2ChangeSlope:
data
.
readUInt16
(),
rightAtomizerTreatmentTime:
data
.
readUInt16
(),
rightInfraredPhysiotherapyTime:
data
.
readUInt16
(),
tempS1:
data
.
readHighByte
()
/
10
,
tempH1:
data
.
readHighByte
(),
atomizerTreatmentTime:
data
.
readUInt16
(),
infraredPhysiotherapyTime:
data
.
readHighByte
(),
mainTempCorrect:
data
.
readHighByte
(),
auxiliaryTemperatureCorrect:
data
.
readHighByte
(),
o2CorrectStart:
data
.
readHighByte
(),
o2CorrectDiffer:
data
.
readHighByte
(),
humidityCorrect:
data
.
readHighByte
(),
oxygenCorrect:
data
.
readHighByte
(),
eTempCorrect:
data
.
readHighByte
(),
xyyModel:
data
.
readHex
(
2
),
unknown:
data
.
readHex
(
2
),
wifible:
data
.
readHighByte
(),
levelLight:
data
.
readHighByte
(),
openO2:
data
.
readHighByte
(),
airConditioner:
data
.
readHighByte
(),
co2Correct:
data
.
readHighByte
(),
co2Measurement:
data
.
readUInt16
(),
co2Setting:
data
.
readUInt16
(),
co2FastClearSwitch:
data
.
readHighByte
(),
openO2SettingMax:
data
.
readHighByte
(),
noOpenO2SettingMax:
data
.
readHighByte
(),
windSet:
data
.
readHighByte
(),
rangeData1:
data
.
readHex
(
2
),
rangeData2:
data
.
readHex
(
2
),
rangeData3:
data
.
readHex
(
2
),
co2Safety:
data
.
readUInt16
(),
co2SettingMin:
data
.
readUInt16
(),
blueLight:
data
.
readHighByte
(),
redLight:
data
.
readHighByte
(),
fcSwitch:
data
.
readHighByte
(),
o2SupplyTime:
data
.
readUInt16
(),
totalTime:
data
.
readUInt16
(),
ratioTemp2:
data
.
readUInt8
(),
ratioTemp1:
data
.
readUInt8
(),
sp:
data
.
readHighByte
(),
dp:
data
.
readHighByte
(),
mean:
data
.
readHighByte
(),
pulseRate:
data
.
readHighByte
(),
xyyStatus:
data
.
readHighByte
(),
xyyError1:
data
.
readHighByte
(),
sn:
data
.
readAscii
(
10
),
topTempCorrect:
data
.
readHighByte
(),
midTempCorrect:
data
.
readHighByte
(),
o2OpenCorrectStart:
data
.
readHighByte
(),
o2OpenCorrectStep:
data
.
readHighByte
(),
o2CorrectMax:
data
.
readHighByte
(),
o2OpenCorrectMax:
data
.
readHighByte
(),
parsedBytes:
data
.
byteIndex
,
);
}
static
McuReport
parseBytes
(
List
<
int
>
bytes
)
{
return
parseHex
(
_bytesToHex
(
bytes
));
}
static
String
_bytesToHex
(
List
<
int
>
bytes
)
{
return
bytes
.
map
((
item
)
=>
item
.
toRadixString
(
16
).
padLeft
(
2
,
'0'
))
.
join
()
.
toUpperCase
();
}
}
class
McuReportFrameDecoder
{
final
List
<
int
>
_buffer
=
[];
List
<
McuReport
>
addBytes
(
List
<
int
>
bytes
)
{
if
(
bytes
.
isEmpty
)
{
return
const
[];
}
_buffer
.
addAll
(
bytes
);
final
reports
=
<
McuReport
>[];
while
(
_buffer
.
length
>=
mcuReportBytes
)
{
final
headIndex
=
_buffer
.
indexOf
(
0x68
);
if
(
headIndex
<
0
)
{
_buffer
.
clear
();
break
;
}
if
(
headIndex
>
0
)
{
_buffer
.
removeRange
(
0
,
headIndex
);
}
if
(
_buffer
.
length
<
mcuReportBytes
)
{
break
;
}
final
frame
=
List
<
int
>.
from
(
_buffer
.
take
(
mcuReportBytes
));
if
(
frame
.
last
!=
0x16
)
{
_buffer
.
removeAt
(
0
);
continue
;
}
reports
.
add
(
McuReportParser
.
parseBytes
(
frame
));
_buffer
.
removeRange
(
0
,
mcuReportBytes
);
}
return
reports
;
}
void
clear
()
{
_buffer
.
clear
();
}
}
lib/utils/mqtt/index.dart
0 → 100644
View file @
531a4d4e
export
'laki_mqtt_client.dart'
;
lib/utils/mqtt/laki_mqtt_client.dart
0 → 100644
View file @
531a4d4e
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
;
}
}
}
lib/utils/storage/storage_service.dart
View file @
531a4d4e
...
@@ -9,7 +9,11 @@ class StorageService {
...
@@ -9,7 +9,11 @@ class StorageService {
static
const
String
_keyRememberSmsCode
=
'remember_sms_code'
;
static
const
String
_keyRememberSmsCode
=
'remember_sms_code'
;
static
const
String
_keyRememberEnabled
=
'remember_enabled'
;
static
const
String
_keyRememberEnabled
=
'remember_enabled'
;
static
const
String
_keyUserInfo
=
'user_info'
;
static
const
String
_keyUserInfo
=
'user_info'
;
static
const
String
_keyUserMac
=
'user_mac'
;
static
const
String
_keyClientId
=
'client_id'
;
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
(
final
FlutterSecureStorage
_storage
=
const
FlutterSecureStorage
(
aOptions:
AndroidOptions
(
aOptions:
AndroidOptions
(
...
@@ -76,6 +80,7 @@ class StorageService {
...
@@ -76,6 +80,7 @@ class StorageService {
Future
<
UserModel
?>
getUserInfo
()
async
{
Future
<
UserModel
?>
getUserInfo
()
async
{
final
value
=
await
_storage
.
read
(
key:
_keyUserInfo
);
final
value
=
await
_storage
.
read
(
key:
_keyUserInfo
);
UserModel
.
MAC
=
await
getUserMac
();
if
(
value
==
null
||
value
.
isEmpty
)
return
null
;
if
(
value
==
null
||
value
.
isEmpty
)
return
null
;
final
parts
=
value
.
split
(
'|'
);
final
parts
=
value
.
split
(
'|'
);
if
(
parts
.
length
<
9
)
return
null
;
if
(
parts
.
length
<
9
)
return
null
;
...
@@ -96,6 +101,22 @@ class StorageService {
...
@@ -96,6 +101,22 @@ class StorageService {
await
_storage
.
delete
(
key:
_keyUserInfo
);
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 相关 ====================
// ==================== ClientId 相关 ====================
Future
<
void
>
saveClientId
(
String
clientId
)
async
{
Future
<
void
>
saveClientId
(
String
clientId
)
async
{
...
@@ -110,6 +131,30 @@ class StorageService {
...
@@ -110,6 +131,30 @@ class StorageService {
await
_storage
.
delete
(
key:
_keyClientId
);
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
({
Future
<
void
>
saveRememberCredentials
({
...
...
lib/views/home/index/cubit/home_index_cubit.dart
View file @
531a4d4e
...
@@ -6,21 +6,25 @@ import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
...
@@ -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/monitoring_service.dart'
;
import
'package:laki_icu_app/services/p2p_video_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/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'
;
import
'home_index_state.dart'
;
/// 设备凭据 —— Mock 阶段使用占位值,后续接口接入后替换
/// TODO: 从舱详情接口获取真实的 cameraSn 和 wifiPwd
const
_mockDeviceNo
=
'MOCK_DEVICE_SN'
;
const
_mockDevicePassword
=
'MOCK_WIFI_PWD'
;
class
HomeIndexCubit
extends
Cubit
<
HomeIndexState
>
{
class
HomeIndexCubit
extends
Cubit
<
HomeIndexState
>
{
final
MonitoringService
_monitoringService
;
final
MonitoringService
_monitoringService
;
final
WebrtcService
_webrtcService
;
final
WebrtcService
_webrtcService
;
final
P2pVideoService
_p2pVideoService
;
final
P2pVideoService
_p2pVideoService
;
final
StorageService
_storageService
;
final
BleBluetoothManager
_bluetoothManager
;
final
McuReportFrameDecoder
_mcuReportDecoder
;
StreamSubscription
<
WebrtcConnectionState
>?
_webrtcStateSub
;
StreamSubscription
<
WebrtcConnectionState
>?
_webrtcStateSub
;
StreamSubscription
<
P2pServiceState
>?
_p2pStateSub
;
StreamSubscription
<
P2pServiceState
>?
_p2pStateSub
;
StreamSubscription
<
List
<
BluetoothScanDevice
>>?
_bluetoothScanSub
;
StreamSubscription
<
BluetoothConnectionStatus
>?
_bluetoothStateSub
;
StreamSubscription
<
String
>?
_bluetoothMessageSub
;
StreamSubscription
<
BluetoothDataPacket
>?
_bluetoothDataSub
;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
/// 防止旧连接的异步结果污染当前模式的状态。
...
@@ -30,9 +34,14 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
...
@@ -30,9 +34,14 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
:
_monitoringService
=
MonitoringService
(),
:
_monitoringService
=
MonitoringService
(),
_webrtcService
=
WebrtcService
(),
_webrtcService
=
WebrtcService
(),
_p2pVideoService
=
P2pVideoService
(),
_p2pVideoService
=
P2pVideoService
(),
_storageService
=
StorageService
(),
_bluetoothManager
=
BleBluetoothManager
(),
_mcuReportDecoder
=
McuReportFrameDecoder
(),
super
(
const
HomeIndexState
())
{
super
(
const
HomeIndexState
())
{
_listenWebrtcState
();
_listenWebrtcState
();
_listenP2pState
();
_listenP2pState
();
_listenBluetoothState
();
_loadBoundBluetoothDevice
();
loadData
();
loadData
();
}
}
...
@@ -49,11 +58,69 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
...
@@ -49,11 +58,69 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
_p2pStateSub
=
_p2pVideoService
.
stateStream
.
listen
((
state
)
{
_p2pStateSub
=
_p2pVideoService
.
stateStream
.
listen
((
state
)
{
if
(
isClosed
)
return
;
if
(
isClosed
)
return
;
emit
(
this
.
state
.
copyWith
(
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
{
Future
<
void
>
loadData
()
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
error:
null
));
emit
(
state
.
copyWith
(
isLoading:
true
,
error:
null
));
try
{
try
{
...
@@ -203,6 +270,81 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
...
@@ -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 层使用
/// 获取 WebRTC 渲染器供 UI 层使用
WebrtcService
get
webrtcService
=>
_webrtcService
;
WebrtcService
get
webrtcService
=>
_webrtcService
;
...
@@ -213,8 +355,12 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
...
@@ -213,8 +355,12 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
Future
<
void
>
close
()
{
Future
<
void
>
close
()
{
_webrtcStateSub
?.
cancel
();
_webrtcStateSub
?.
cancel
();
_p2pStateSub
?.
cancel
();
_p2pStateSub
?.
cancel
();
_bluetoothScanSub
?.
cancel
();
_bluetoothStateSub
?.
cancel
();
_bluetoothMessageSub
?.
cancel
();
_bluetoothDataSub
?.
cancel
();
_webrtcService
.
dispose
();
_webrtcService
.
dispose
();
_p2pVideoService
.
dispose
();
_p2pVideoService
.
dispose
();
return
super
.
close
(
);
return
_bluetoothManager
.
release
().
then
((
_
)
=>
super
.
close
()
);
}
}
}
}
lib/views/home/index/cubit/home_index_state.dart
View file @
531a4d4e
...
@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
...
@@ -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/enums/video_stream_mode_enum.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.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/services/webrtc_service.dart'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
enum
HomeIndexStatus
{
enum
HomeIndexStatus
{
initial
,
initial
,
...
@@ -31,6 +32,36 @@ class HomeIndexState extends Equatable {
...
@@ -31,6 +32,36 @@ class HomeIndexState extends Equatable {
/// P2P 模式是否已连接
/// P2P 模式是否已连接
final
bool
isP2pConnected
;
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
({
const
HomeIndexState
({
this
.
status
=
HomeIndexStatus
.
initial
,
this
.
status
=
HomeIndexStatus
.
initial
,
this
.
isLoading
=
false
,
this
.
isLoading
=
false
,
...
@@ -43,6 +74,16 @@ class HomeIndexState extends Equatable {
...
@@ -43,6 +74,16 @@ class HomeIndexState extends Equatable {
this
.
videoStreamMode
=
VideoStreamMode
.
webrtc
,
this
.
videoStreamMode
=
VideoStreamMode
.
webrtc
,
this
.
isSwitchingMode
=
false
,
this
.
isSwitchingMode
=
false
,
this
.
isP2pConnected
=
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
({
HomeIndexState
copyWith
({
...
@@ -57,6 +98,18 @@ class HomeIndexState extends Equatable {
...
@@ -57,6 +98,18 @@ class HomeIndexState extends Equatable {
VideoStreamMode
?
videoStreamMode
,
VideoStreamMode
?
videoStreamMode
,
bool
?
isSwitchingMode
,
bool
?
isSwitchingMode
,
bool
?
isP2pConnected
,
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
(
return
HomeIndexState
(
status:
status
??
this
.
status
,
status:
status
??
this
.
status
,
...
@@ -66,11 +119,28 @@ class HomeIndexState extends Equatable {
...
@@ -66,11 +119,28 @@ class HomeIndexState extends Equatable {
patientInfo:
patientInfo
??
this
.
patientInfo
,
patientInfo:
patientInfo
??
this
.
patientInfo
,
alerts:
alerts
??
this
.
alerts
,
alerts:
alerts
??
this
.
alerts
,
menuItems:
menuItems
??
this
.
menuItems
,
menuItems:
menuItems
??
this
.
menuItems
,
videoConnectionState:
videoConnectionState:
videoConnectionState
??
this
.
videoConnectionState
,
videoConnectionState
??
this
.
videoConnectionState
,
videoStreamMode:
videoStreamMode
??
this
.
videoStreamMode
,
videoStreamMode:
videoStreamMode
??
this
.
videoStreamMode
,
isSwitchingMode:
isSwitchingMode
??
this
.
isSwitchingMode
,
isSwitchingMode:
isSwitchingMode
??
this
.
isSwitchingMode
,
isP2pConnected:
isP2pConnected
??
this
.
isP2pConnected
,
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 {
...
@@ -87,5 +157,15 @@ class HomeIndexState extends Equatable {
videoStreamMode
,
videoStreamMode
,
isSwitchingMode
,
isSwitchingMode
,
isP2pConnected
,
isP2pConnected
,
isBluetoothScanning
,
isBluetoothBinding
,
bindingBluetoothDeviceId
,
bluetoothDevices
,
boundBluetoothDeviceId
,
boundBluetoothDeviceName
,
bluetoothConnectionStatus
,
bluetoothMessage
,
latestBluetoothRawHex
,
latestMcuReport
,
];
];
}
}
lib/views/home/index/home_index_view.dart
View file @
531a4d4e
...
@@ -8,6 +8,7 @@ import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
...
@@ -8,6 +8,7 @@ import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import
'cubit/home_index_cubit.dart'
;
import
'cubit/home_index_cubit.dart'
;
import
'cubit/home_index_state.dart'
;
import
'cubit/home_index_state.dart'
;
import
'widgets/alert_info_card.dart'
;
import
'widgets/alert_info_card.dart'
;
import
'widgets/bluetooth_bind_dialog.dart'
;
import
'widgets/metric_card.dart'
;
import
'widgets/metric_card.dart'
;
import
'widgets/patient_info_card.dart'
;
import
'widgets/patient_info_card.dart'
;
import
'widgets/sidebar_menu_item.dart'
;
import
'widgets/sidebar_menu_item.dart'
;
...
@@ -49,7 +50,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
...
@@ -49,7 +50,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
body:
SafeArea
(
body:
SafeArea
(
child:
Column
(
child:
Column
(
children:
[
children:
[
_buildAppBar
(),
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
buildWhen:
(
previous
,
current
)
=>
previous
.
boundBluetoothDeviceId
!=
current
.
boundBluetoothDeviceId
,
builder:
(
context
,
state
)
=>
_buildAppBar
(
state
),
),
Expanded
(
Expanded
(
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
builder:
(
context
,
state
)
{
builder:
(
context
,
state
)
{
...
@@ -92,7 +98,9 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
...
@@ -92,7 +98,9 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
);
}
}
Widget
_buildAppBar
()
{
Widget
_buildAppBar
(
HomeIndexState
state
)
{
final
isBluetoothBound
=
state
.
boundBluetoothDeviceId
?.
isNotEmpty
==
true
;
return
Container
(
return
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
24
.
w
,
vertical:
12
.
h
),
padding:
EdgeInsets
.
symmetric
(
horizontal:
24
.
w
,
vertical:
12
.
h
),
decoration:
const
BoxDecoration
(
decoration:
const
BoxDecoration
(
...
@@ -106,7 +114,8 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
...
@@ -106,7 +114,8 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
const
CircleAvatar
(
const
CircleAvatar
(
backgroundColor:
Colors
.
white
,
backgroundColor:
Colors
.
white
,
radius:
24
,
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
),
SizedBox
(
width:
16
.
w
),
Text
(
Text
(
...
@@ -118,7 +127,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
...
@@ -118,7 +127,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
),
),
),
),
const
Spacer
(),
const
Spacer
(),
_buildAppBarIcon
(
Icons
.
bluetooth
,
'TSAAS'
),
_buildAppBarIcon
(
Icons
.
bluetooth
,
'TSAAS'
,
isActive:
isBluetoothBound
,
onTap:
_showBluetoothDialog
,
),
SizedBox
(
width:
16
.
w
),
SizedBox
(
width:
16
.
w
),
_buildAppBarIcon
(
Icons
.
description
,
null
),
_buildAppBarIcon
(
Icons
.
description
,
null
),
SizedBox
(
width:
16
.
w
),
SizedBox
(
width:
16
.
w
),
...
@@ -128,27 +142,52 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
...
@@ -128,27 +142,52 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
);
}
}
Widget
_buildAppBarIcon
(
IconData
icon
,
String
?
label
)
{
Widget
_buildAppBarIcon
(
return
Container
(
IconData
icon
,
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
.
w
,
vertical:
8
.
h
),
String
?
label
,
{
decoration:
BoxDecoration
(
bool
isActive
=
false
,
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
1
),
VoidCallback
?
onTap
,
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
})
{
),
final
foregroundColor
=
isActive
?
const
Color
(
0xFF003A8C
)
:
Colors
.
white
;
child:
Row
(
final
backgroundColor
=
children:
[
isActive
?
const
Color
(
0xFF00F6FF
)
:
Colors
.
transparent
;
Icon
(
icon
,
color:
Colors
.
white
,
size:
20
.
w
),
final
borderColor
=
isActive
?
const
Color
(
0xFFB8FFFF
)
:
Colors
.
white24
;
if
(
label
!=
null
)
...[
SizedBox
(
width:
6
.
w
),
return
GestureDetector
(
Text
(
onTap:
onTap
,
label
,
child:
Container
(
style:
TextStyle
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
.
w
,
vertical:
8
.
h
),
fontSize:
18
.
sp
,
decoration:
BoxDecoration
(
color:
Colors
.
white
,
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
(),
),
),
);
);
}
}
...
...
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
0 → 100644
View file @
531a4d4e
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
import
'package:laki_icu_app/views/home/index/cubit/home_index_cubit.dart'
;
import
'package:laki_icu_app/views/home/index/cubit/home_index_state.dart'
;
class
BluetoothBindDialog
extends
StatefulWidget
{
const
BluetoothBindDialog
({
super
.
key
});
@override
State
<
BluetoothBindDialog
>
createState
()
=>
_BluetoothBindDialogState
();
}
class
_BluetoothBindDialogState
extends
State
<
BluetoothBindDialog
>
{
bool
_hasAutoStartedScan
=
false
;
@override
void
initState
()
{
super
.
initState
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
if
(!
mounted
||
_hasAutoStartedScan
)
return
;
_hasAutoStartedScan
=
true
;
final
cubit
=
context
.
read
<
HomeIndexCubit
>();
if
(!
cubit
.
state
.
isBluetoothScanning
)
{
cubit
.
scanBluetoothDevices
();
}
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
Dialog
(
backgroundColor:
const
Color
(
0xFFF4F4F4
),
insetPadding:
EdgeInsets
.
zero
,
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
builder:
(
context
,
state
)
{
return
SizedBox
.
expand
(
child:
_buildContent
(
context
,
state
),
);
},
),
);
}
Widget
_buildContent
(
BuildContext
context
,
HomeIndexState
state
)
{
return
Container
(
width:
double
.
infinity
,
// margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
padding:
EdgeInsets
.
fromLTRB
(
108
.
w
,
52
.
h
,
108
.
w
,
94
.
h
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFF79A9EE
),
border:
Border
.
all
(
color:
const
Color
(
0xFF0799FF
),
width:
3
.
w
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildTopBar
(
context
),
SizedBox
(
height:
38
.
h
),
Expanded
(
child:
_buildDevicePanel
(
context
,
state
)),
],
),
);
}
Widget
_buildTopBar
(
BuildContext
context
)
{
return
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'设备连接'
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
42
.
sp
,
fontWeight:
FontWeight
.
w400
,
),
),
SizedBox
(
height:
14
.
h
),
Text
(
'搜索蓝牙设备完成配对连接'
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.84
),
fontSize:
22
.
sp
,
),
),
],
),
),
IconButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
icon:
Icon
(
Icons
.
close
,
color:
Colors
.
white
,
size:
48
.
w
),
tooltip:
'关闭'
,
padding:
EdgeInsets
.
zero
,
constraints:
BoxConstraints
.
tight
(
Size
(
64
.
w
,
64
.
w
)),
),
],
);
}
Widget
_buildDevicePanel
(
BuildContext
context
,
HomeIndexState
state
)
{
return
Container
(
width:
double
.
infinity
,
padding:
EdgeInsets
.
fromLTRB
(
72
.
w
,
56
.
h
,
72
.
w
,
34
.
h
),
decoration:
BoxDecoration
(
gradient:
const
LinearGradient
(
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
colors:
[
Color
(
0xFF0028E8
),
Color
(
0xFF001646
)],
),
borderRadius:
BorderRadius
.
circular
(
14
.
r
),
border:
Border
.
all
(
color:
Colors
.
white
.
withValues
(
alpha:
0.2
)),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withValues
(
alpha:
0.26
),
blurRadius:
12
.
r
,
offset:
Offset
(
0
,
6
.
h
),
),
],
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildPanelStatus
(
context
,
state
),
SizedBox
(
height:
28
.
h
),
Expanded
(
child:
_buildDeviceList
(
context
,
state
)),
_buildLatestData
(
state
),
],
),
);
}
Widget
_buildPanelStatus
(
BuildContext
context
,
HomeIndexState
state
)
{
final
message
=
state
.
isBluetoothScanning
?
'自动发现附近可配对的监护舱设备,搜索中...'
:
state
.
bluetoothDevices
.
isEmpty
?
'未发现设备,可重新搜索附近蓝牙设备'
:
'自动发现附近可配对的监护舱设备'
;
return
Row
(
children:
[
Expanded
(
child:
Text
(
message
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.88
),
fontSize:
21
.
sp
,
fontWeight:
FontWeight
.
w400
,
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
),
SizedBox
(
width:
16
.
w
),
TextButton
.
icon
(
onPressed:
state
.
isBluetoothScanning
?
null
:
()
=>
context
.
read
<
HomeIndexCubit
>().
scanBluetoothDevices
(),
icon:
state
.
isBluetoothScanning
?
SizedBox
(
width:
18
.
w
,
height:
18
.
w
,
child:
CircularProgressIndicator
(
strokeWidth:
2
.
w
,
color:
Colors
.
white
,
),
)
:
Icon
(
Icons
.
refresh
,
size:
22
.
w
),
label:
Text
(
state
.
isBluetoothScanning
?
'搜索中'
:
'重新搜索'
),
style:
TextButton
.
styleFrom
(
foregroundColor:
Colors
.
white
,
disabledForegroundColor:
Colors
.
white70
,
textStyle:
TextStyle
(
fontSize:
17
.
sp
),
),
),
],
);
}
Widget
_buildDeviceList
(
BuildContext
context
,
HomeIndexState
state
)
{
if
(
state
.
bluetoothDevices
.
isEmpty
)
{
return
Center
(
child:
Text
(
state
.
isBluetoothScanning
?
'正在扫描附近设备...'
:
'暂无设备'
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.76
),
fontSize:
24
.
sp
,
),
),
);
}
final
devices
=
_sortDevicesForDisplay
(
state
);
return
ListView
.
separated
(
padding:
EdgeInsets
.
zero
,
itemCount:
devices
.
length
,
separatorBuilder:
(
_
,
__
)
=>
SizedBox
(
height:
18
.
h
),
itemBuilder:
(
context
,
index
)
{
final
device
=
devices
[
index
];
final
isBound
=
state
.
boundBluetoothDeviceId
==
device
.
remoteId
;
final
isBinding
=
state
.
bindingBluetoothDeviceId
==
device
.
remoteId
;
return
_BluetoothDeviceRow
(
key:
ValueKey
(
device
.
remoteId
),
device:
device
,
isBound:
isBound
,
isBinding:
isBinding
,
isDisabled:
state
.
isBluetoothBinding
,
onTap:
isBound
?
()
=>
_confirmUnbindBluetoothDevice
(
context
,
device
)
:
()
=>
context
.
read
<
HomeIndexCubit
>().
bindBluetoothDevice
(
device
,
),
);
},
);
}
List
<
BluetoothScanDevice
>
_sortDevicesForDisplay
(
HomeIndexState
state
)
{
final
boundDeviceId
=
state
.
boundBluetoothDeviceId
;
if
(
boundDeviceId
==
null
||
boundDeviceId
.
isEmpty
)
{
return
state
.
bluetoothDevices
;
}
return
List
<
BluetoothScanDevice
>.
from
(
state
.
bluetoothDevices
)
..
sort
((
left
,
right
)
{
final
leftIsBound
=
left
.
remoteId
==
boundDeviceId
;
final
rightIsBound
=
right
.
remoteId
==
boundDeviceId
;
if
(
leftIsBound
==
rightIsBound
)
{
return
0
;
}
return
leftIsBound
?
-
1
:
1
;
});
}
Future
<
void
>
_confirmUnbindBluetoothDevice
(
BuildContext
context
,
BluetoothScanDevice
device
,
)
async
{
final
shouldUnbind
=
await
showDialog
<
bool
>(
context:
context
,
builder:
(
dialogContext
)
{
return
AlertDialog
(
title:
const
Text
(
'解绑蓝牙设备'
),
content:
Text
(
'是否要解绑
${device.name}
?解绑后可重新选择设备绑定。'
),
actions:
[
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
dialogContext
).
pop
(
false
),
child:
const
Text
(
'取消'
),
),
FilledButton
(
onPressed:
()
=>
Navigator
.
of
(
dialogContext
).
pop
(
true
),
child:
const
Text
(
'确认解绑'
),
),
],
);
},
);
if
(
shouldUnbind
==
true
&&
context
.
mounted
)
{
await
context
.
read
<
HomeIndexCubit
>().
unbindBluetoothDevice
();
}
}
Widget
_buildLatestData
(
HomeIndexState
state
)
{
final
report
=
state
.
latestMcuReport
;
final
rawHex
=
state
.
latestBluetoothRawHex
;
final
message
=
state
.
bluetoothMessage
;
if
(
report
==
null
&&
(
rawHex
==
null
||
rawHex
.
isEmpty
)
&&
(
message
==
null
||
message
.
isEmpty
))
{
return
SizedBox
(
height:
10
.
h
);
}
if
(
report
!=
null
)
{
return
_BluetoothReportSummary
(
report:
report
);
}
return
Padding
(
padding:
EdgeInsets
.
only
(
top:
18
.
h
),
child:
Text
(
rawHex
!=
null
&&
rawHex
.
isNotEmpty
?
'最新数据:
$rawHex
'
:
message
!,
style:
TextStyle
(
color:
rawHex
!=
null
&&
rawHex
.
isNotEmpty
?
const
Color
(
0xFF00F6FF
)
:
Colors
.
white
.
withValues
(
alpha:
0.66
),
fontSize:
15
.
sp
,
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
);
}
}
class
_BluetoothReportSummary
extends
StatelessWidget
{
const
_BluetoothReportSummary
({
required
this
.
report
});
final
McuReport
report
;
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
width:
double
.
infinity
,
margin:
EdgeInsets
.
only
(
top:
18
.
h
),
padding:
EdgeInsets
.
symmetric
(
horizontal:
18
.
w
,
vertical:
14
.
h
),
decoration:
BoxDecoration
(
color:
Colors
.
black
.
withValues
(
alpha:
0.18
),
borderRadius:
BorderRadius
.
circular
(
8
.
r
),
border:
Border
.
all
(
color:
Colors
.
white
.
withValues
(
alpha:
0.12
)),
),
child:
Row
(
children:
[
Expanded
(
flex:
2
,
child:
Text
(
report
.
sn
.
isEmpty
?
'SN --'
:
'SN
${report.sn}
'
,
style:
TextStyle
(
color:
const
Color
(
0xFF00F6FF
),
fontSize:
15
.
sp
,
fontWeight:
FontWeight
.
w600
,
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
),
_BluetoothReportMetric
(
label:
'温度'
,
value:
report
.
mainTemperature
.
toStringAsFixed
(
1
),
unit:
'℃'
,
),
_BluetoothReportMetric
(
label:
'湿度'
,
value:
'
${report.humidity}
'
,
unit:
'%'
,
),
_BluetoothReportMetric
(
label:
'氧气'
,
value:
'
${report.oxygenConcentration}
'
,
unit:
'%'
,
),
_BluetoothReportMetric
(
label:
'CO2'
,
value:
'
${report.co2Measurement}
'
,
unit:
'ppm'
,
),
],
),
);
}
}
class
_BluetoothReportMetric
extends
StatelessWidget
{
const
_BluetoothReportMetric
({
required
this
.
label
,
required
this
.
value
,
required
this
.
unit
,
});
final
String
label
;
final
String
value
;
final
String
unit
;
@override
Widget
build
(
BuildContext
context
)
{
return
SizedBox
(
width:
128
.
w
,
child:
RichText
(
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
text:
TextSpan
(
children:
[
TextSpan
(
text:
'
$label
'
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.58
),
fontSize:
13
.
sp
,
),
),
TextSpan
(
text:
value
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
16
.
sp
,
fontWeight:
FontWeight
.
w600
,
),
),
TextSpan
(
text:
unit
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.72
),
fontSize:
12
.
sp
,
),
),
],
),
),
);
}
}
class
_BluetoothDeviceRow
extends
StatelessWidget
{
const
_BluetoothDeviceRow
({
super
.
key
,
required
this
.
device
,
required
this
.
isBound
,
required
this
.
isBinding
,
required
this
.
isDisabled
,
required
this
.
onTap
,
});
final
BluetoothScanDevice
device
;
final
bool
isBound
;
final
bool
isBinding
;
final
bool
isDisabled
;
final
VoidCallback
onTap
;
@override
Widget
build
(
BuildContext
context
)
{
const
highlightColor
=
Color
(
0xFF00F6FF
);
final
textColor
=
isBound
?
highlightColor
:
Colors
.
white
;
final
statusText
=
isBound
?
'已连接'
:
isBinding
?
'连接中'
:
'未连接'
;
return
InkWell
(
onTap:
isDisabled
?
null
:
onTap
,
borderRadius:
BorderRadius
.
circular
(
8
.
r
),
child:
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
0
,
vertical:
14
.
h
),
child:
Row
(
children:
[
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
device
.
name
,
style:
TextStyle
(
color:
textColor
,
fontSize:
26
.
sp
,
fontWeight:
isBound
?
FontWeight
.
w600
:
FontWeight
.
w400
,
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
SizedBox
(
height:
5
.
h
),
Text
(
'
${device.remoteId}
RSSI
${device.rssi}
'
,
style:
TextStyle
(
color:
Colors
.
white
.
withValues
(
alpha:
0.42
),
fontSize:
13
.
sp
,
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
),
SizedBox
(
width:
32
.
w
),
SizedBox
(
width:
138
.
w
,
child:
Text
(
statusText
,
textAlign:
TextAlign
.
left
,
style:
TextStyle
(
color:
textColor
,
fontSize:
25
.
sp
,
fontWeight:
isBound
?
FontWeight
.
w600
:
FontWeight
.
w400
,
),
),
),
],
),
),
);
}
}
macos/Flutter/GeneratedPluginRegistrant.swift
View file @
531a4d4e
...
@@ -9,6 +9,7 @@ import device_info_plus
...
@@ -9,6 +9,7 @@ import device_info_plus
import
flutter_image_compress_macos
import
flutter_image_compress_macos
import
flutter_secure_storage_macos
import
flutter_secure_storage_macos
import
flutter_webrtc
import
flutter_webrtc
import
reactive_ble_mobile
import
share_plus
import
share_plus
import
shared_preferences_foundation
import
shared_preferences_foundation
import
url_launcher_macos
import
url_launcher_macos
...
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
...
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterImageCompressMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterImageCompressMacosPlugin"
))
FlutterImageCompressMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterImageCompressMacosPlugin"
))
FlutterSecureStoragePlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterSecureStoragePlugin"
))
FlutterSecureStoragePlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterSecureStoragePlugin"
))
FlutterWebRTCPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterWebRTCPlugin"
))
FlutterWebRTCPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterWebRTCPlugin"
))
ReactiveBlePlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"ReactiveBlePlugin"
))
SharePlusMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharePlusMacosPlugin"
))
SharePlusMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharePlusMacosPlugin"
))
SharedPreferencesPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharedPreferencesPlugin"
))
SharedPreferencesPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharedPreferencesPlugin"
))
UrlLauncherPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"UrlLauncherPlugin"
))
UrlLauncherPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"UrlLauncherPlugin"
))
...
...
pubspec.yaml
View file @
531a4d4e
...
@@ -50,16 +50,15 @@ dependencies:
...
@@ -50,16 +50,15 @@ dependencies:
logger
:
^2.7.0
logger
:
^2.7.0
fluttertoast
:
^9.0.0
fluttertoast
:
^9.0.0
permission_handler
:
^11.3.1
permission_handler
:
^11.3.1
flutter_usbcamera
:
^0.0.1
flutter_usbcamera
:
path
:
./third_party/flutter_usbcamera
path_provider
:
^2.1.2
path_provider
:
^2.1.2
flutter_webrtc
:
flutter_webrtc
:
path
:
./plugins/flutter_webrtc
path
:
./plugins/flutter_webrtc
vsdk
:
vsdk
:
path
:
./plugins/vsdk
path
:
./plugins/vsdk
# flutter_usbcamera: ^0.0.1
flutter_reactive_ble
:
^5.5.0
# fl_chart: ^0.71.0
mqtt_client
:
^10.5.1
# fl_chart: ^1.1.0
dev_dependencies
:
dev_dependencies
:
flutter_test
:
flutter_test
:
sdk
:
flutter
sdk
:
flutter
...
@@ -84,6 +83,7 @@ dev_dependencies:
...
@@ -84,6 +83,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
# The following section is specific to Flutter packages.
flutter
:
flutter
:
default-flavor
:
dev
# The following line ensures that the Material Icons font is
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# included with your application, so that you can use the icons in
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment