更新睡眠报告提示样式,更新ios蓝牙开关判定
This commit is contained in:
@@ -192,7 +192,7 @@
|
|||||||
"人员资料": "User information",
|
"人员资料": "User information",
|
||||||
"实时体征": "Real-time vitals",
|
"实时体征": "Real-time vitals",
|
||||||
"消息回看": "Message review",
|
"消息回看": "Message review",
|
||||||
"睡眠报告": "Health report",
|
"睡眠报告": "Sleep report",
|
||||||
"首页展示": "Home display",
|
"首页展示": "Home display",
|
||||||
"设备详情": "Device details",
|
"设备详情": "Device details",
|
||||||
"重命名": "Rename",
|
"重命名": "Rename",
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
"呼吸": "Respiration",
|
"呼吸": "Respiration",
|
||||||
"呼吸暂停": "Apnea",
|
"呼吸暂停": "Apnea",
|
||||||
"请保持静止": "Please remain still",
|
"请保持静止": "Please remain still",
|
||||||
"睡眠报告": "Health report",
|
"睡眠报告": "Sleep report",
|
||||||
"修改人员名称": "Modify user name",
|
"修改人员名称": "Modify user name",
|
||||||
"在线": "Online",
|
"在线": "Online",
|
||||||
"离线": "Offline",
|
"离线": "Offline",
|
||||||
|
|||||||
@@ -298,7 +298,6 @@
|
|||||||
"请先": "Please",
|
"请先": "Please",
|
||||||
"登录": "Login",
|
"登录": "Login",
|
||||||
"后,再查看睡眠报告": "to view sleep reports",
|
"后,再查看睡眠报告": "to view sleep reports",
|
||||||
"睡眠报告": "Health Report",
|
|
||||||
"暂无数据": "No Data",
|
"暂无数据": "No Data",
|
||||||
"发现新版本": "New Version Available",
|
"发现新版本": "New Version Available",
|
||||||
"知道了": "Back",
|
"知道了": "Back",
|
||||||
|
|||||||
441
lib/common/util/BluetoothFirmwareUpdater.dart
Normal file
441
lib/common/util/BluetoothFirmwareUpdater.dart
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:easydevice/easydevice.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
|
||||||
|
import 'package:vbvs_app/pages/mh_page/device/controller/mht_bluetooth_controller.dart';
|
||||||
|
import 'package:ef/ef.dart'; // THapp 所在的包
|
||||||
|
|
||||||
|
// 固件升级配置(区分平台)
|
||||||
|
class UpgradeConfig {
|
||||||
|
static const int androidInitialFps = 50;
|
||||||
|
static const int androidSubsequentFps = 150;
|
||||||
|
static const int androidMaxFrameSize = 120;
|
||||||
|
|
||||||
|
static const int iosInitialFps = 30;
|
||||||
|
static const int iosSubsequentFps = 100;
|
||||||
|
static const int iosMaxFrameSize = 100;
|
||||||
|
|
||||||
|
static const Duration initialPhaseDuration = Duration(seconds: 15);
|
||||||
|
|
||||||
|
static int get maxFrameSize =>
|
||||||
|
Platform.isAndroid ? androidMaxFrameSize : iosMaxFrameSize;
|
||||||
|
|
||||||
|
static int getFps(Duration elapsedTime) {
|
||||||
|
if (elapsedTime <= initialPhaseDuration) {
|
||||||
|
return Platform.isAndroid ? androidInitialFps : iosInitialFps;
|
||||||
|
} else {
|
||||||
|
return Platform.isAndroid ? androidSubsequentFps : iosSubsequentFps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Duration getFrameInterval(Duration elapsedTime) {
|
||||||
|
final fps = getFps(elapsedTime);
|
||||||
|
return Duration(milliseconds: (1000 / fps).round());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 升级任务状态
|
||||||
|
class UpgradeTask {
|
||||||
|
final String mac;
|
||||||
|
final THapp thapp;
|
||||||
|
int progress;
|
||||||
|
String status;
|
||||||
|
Completer<void> completer;
|
||||||
|
DateTime startTime;
|
||||||
|
DateTime? endTime;
|
||||||
|
|
||||||
|
UpgradeTask({
|
||||||
|
required this.mac,
|
||||||
|
required this.thapp,
|
||||||
|
this.progress = 0,
|
||||||
|
this.status = 'waiting',
|
||||||
|
required this.completer,
|
||||||
|
required this.startTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 多设备蓝牙固件升级管理器
|
||||||
|
class MultiDeviceFirmwareUpdater {
|
||||||
|
static final MultiDeviceFirmwareUpdater _instance =
|
||||||
|
MultiDeviceFirmwareUpdater._internal();
|
||||||
|
factory MultiDeviceFirmwareUpdater() => _instance;
|
||||||
|
MultiDeviceFirmwareUpdater._internal();
|
||||||
|
|
||||||
|
final MHTBlueToothController _btController = Get.find();
|
||||||
|
|
||||||
|
final Map<String, UpgradeTask> _upgradeTasks = {};
|
||||||
|
final Map<String, Uint8List> _firmwareDataCache = {};
|
||||||
|
final int _maxConcurrentUpgrades = 2;
|
||||||
|
int _currentConcurrentUpgrades = 0;
|
||||||
|
|
||||||
|
Future<void> startUpgrade({
|
||||||
|
required THapp thapp,
|
||||||
|
required String mac,
|
||||||
|
required String firmwareUrl,
|
||||||
|
}) async {
|
||||||
|
if (_upgradeTasks.containsKey(mac)) {
|
||||||
|
final existingTask = _upgradeTasks[mac]!;
|
||||||
|
if (existingTask.status == 'upgrading' ||
|
||||||
|
existingTask.status == 'downloading') {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 设备 $mac 正在升级中".tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final completer = Completer<void>();
|
||||||
|
final task = UpgradeTask(
|
||||||
|
mac: mac,
|
||||||
|
thapp: thapp,
|
||||||
|
completer: completer,
|
||||||
|
startTime: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_upgradeTasks[mac] = task;
|
||||||
|
_updateUiProgress(mac, 0, 'waiting');
|
||||||
|
|
||||||
|
_processUpgradeQueue();
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processUpgradeQueue() {
|
||||||
|
final waitingTasks =
|
||||||
|
_upgradeTasks.values.where((task) => task.status == 'waiting').toList();
|
||||||
|
|
||||||
|
for (final task in waitingTasks) {
|
||||||
|
if (_currentConcurrentUpgrades < _maxConcurrentUpgrades) {
|
||||||
|
_currentConcurrentUpgrades++;
|
||||||
|
_startSingleUpgrade(task);
|
||||||
|
}
|
||||||
|
//screen shots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startSingleUpgrade(UpgradeTask task) async {
|
||||||
|
try {
|
||||||
|
task.status = 'downloading';
|
||||||
|
|
||||||
|
if (!task.thapp.isConnected) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 设备未连接".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_firmwareDataCache.containsKey(task.mac)) {
|
||||||
|
await _downloadFirmware(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// await _sendCommand(task.thapp, "blog disable");
|
||||||
|
_sendCommand(task.thapp, "blog disable");
|
||||||
|
await Future.delayed(Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
task.status = 'upgrading';
|
||||||
|
_updateUiProgress(task.mac, 0, 'upgrading');
|
||||||
|
|
||||||
|
await _sendFirmwareFrames(task);
|
||||||
|
|
||||||
|
await _sendCommand(task.thapp, "blog enable");
|
||||||
|
await _verifyFirmware(task);
|
||||||
|
|
||||||
|
task.status = 'completed';
|
||||||
|
task.endTime = DateTime.now();
|
||||||
|
_updateUiProgress(task.mac, 100, 'completed');
|
||||||
|
|
||||||
|
TopSlideNotification.show(Get.context!,
|
||||||
|
text: "设备 ${task.mac} 固件升级成功".tr, textColor: Colors.green);
|
||||||
|
|
||||||
|
task.completer.complete();
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 设备 ${task.mac} 升级失败: $e\n$stack");
|
||||||
|
|
||||||
|
task.status = 'failed';
|
||||||
|
task.endTime = DateTime.now();
|
||||||
|
_updateUiProgress(task.mac, -1, 'failed');
|
||||||
|
|
||||||
|
// TopSlideNotification.show(Get.context!,
|
||||||
|
// text: "设备 ${task.mac} 升级失败".tr, textColor: Colors.red);
|
||||||
|
|
||||||
|
task.completer.completeError(e);
|
||||||
|
} finally {
|
||||||
|
_currentConcurrentUpgrades--;
|
||||||
|
_cleanupTask(task.mac);
|
||||||
|
_processUpgradeQueue();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _sendCommand(task.thapp, "blog enable");
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 重新开启日志失败: $e\n$stack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadFirmware(UpgradeTask task) async {
|
||||||
|
try {
|
||||||
|
final firmwareUrl = _btController.currentUpgradeUrl;
|
||||||
|
if (firmwareUrl == null) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件URL为空".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.get(Uri.parse(firmwareUrl));
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件下载失败,状态码:${response.statusCode}".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
_firmwareDataCache[task.mac] = response.bodyBytes;
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 下载固件失败: $e\n$stack");
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件下载失败".tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendFirmwareFrames(UpgradeTask task) async {
|
||||||
|
final firmwareData = _firmwareDataCache[task.mac];
|
||||||
|
if (firmwareData == null) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件数据为空".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalFrames =
|
||||||
|
(firmwareData.length / UpgradeConfig.maxFrameSize).ceil();
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
DateTime lastFrameTime = DateTime.now();
|
||||||
|
|
||||||
|
for (int sentFrames = 0; sentFrames < totalFrames; sentFrames++) {
|
||||||
|
if (task.status == 'cancelled') {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 升级已取消".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
final elapsed = stopwatch.elapsed;
|
||||||
|
final frameInterval = UpgradeConfig.getFrameInterval(elapsed);
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (now.difference(lastFrameTime) < frameInterval) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final offset = sentFrames * UpgradeConfig.maxFrameSize;
|
||||||
|
final end = offset + UpgradeConfig.maxFrameSize;
|
||||||
|
final frameData = firmwareData.sublist(
|
||||||
|
offset,
|
||||||
|
end > firmwareData.length ? firmwareData.length : end,
|
||||||
|
);
|
||||||
|
|
||||||
|
final base64Data = base64Encode(frameData);
|
||||||
|
final mmxCommand =
|
||||||
|
'''mmx path="/root/mtd/update" write base64 len=${frameData.length} offset=$offset data="$base64Data"''';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _sendCommand(task.thapp, mmxCommand);
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 发送固件帧失败: $e\n$stack");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
final waitTime = frameInterval - now.difference(lastFrameTime);
|
||||||
|
if (waitTime > Duration.zero) await Future.delayed(waitTime);
|
||||||
|
|
||||||
|
final progress = ((sentFrames + 1) / totalFrames * 100).round();
|
||||||
|
_updateUiProgress(task.mac, progress, 'upgrading');
|
||||||
|
|
||||||
|
lastFrameTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _verifyFirmware(UpgradeTask task) async {
|
||||||
|
try {
|
||||||
|
final success = await _sendVotaCommand(task.thapp);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件验证失败".tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(Duration(seconds: 10));
|
||||||
|
|
||||||
|
bool deviceRestarted = false;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (!task.thapp.isConnected) {
|
||||||
|
deviceRestarted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceRestarted) {
|
||||||
|
throw Exception("[蓝牙指令执行日志] 设备未重启,可能升级失败".tr);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 固件校验失败: $e\n$stack");
|
||||||
|
throw Exception("[蓝牙指令执行日志] 固件校验失败".tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _sendCommand(THapp thapp, String command,
|
||||||
|
{bool needResponse = false}) async {
|
||||||
|
try {
|
||||||
|
if (needResponse) {
|
||||||
|
// 需要响应的命令(如 vota 命令)
|
||||||
|
return await thapp.send(command, true, // withResponse = true
|
||||||
|
(log) {
|
||||||
|
ef.log("[蓝牙指令执行日志] ${log.log},指令为:$command");
|
||||||
|
|
||||||
|
// 这里根据具体命令判断是否成功完成
|
||||||
|
if (log.log.contains("SUCCESS") ||
|
||||||
|
log.log.contains("OK") ||
|
||||||
|
log.log.contains("COMPLETE") ||
|
||||||
|
log.log.contains("successful")) {
|
||||||
|
log.result = true; // 设置成功结果
|
||||||
|
return true; // 停止监听
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果包含错误信息,也返回完成
|
||||||
|
if (log.log.contains("ERROR") ||
|
||||||
|
log.log.contains("FAIL") ||
|
||||||
|
log.log.contains("failed")) {
|
||||||
|
log.result = false; // 设置失败结果
|
||||||
|
return true; // 停止监听
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // 继续监听
|
||||||
|
},
|
||||||
|
0, // retry = 0
|
||||||
|
15000 // timeoutms = 15000(15秒超时)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 不需要响应的命令(如 mmx write、blog disable/enable)
|
||||||
|
final success = await thapp.send(
|
||||||
|
command,
|
||||||
|
false, // withResponse = false
|
||||||
|
null, // 不需要回调
|
||||||
|
0, // retry = 0
|
||||||
|
5000 // timeoutms = 5000(5秒超时)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 命令发送可能失败: $command");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[蓝牙指令执行日志] 发送命令失败: $e\n$stack");
|
||||||
|
throw Exception("[蓝牙指令执行日志] 发送命令失败".tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateUiProgress(String mac, int progress, String status) {
|
||||||
|
if (Get.isRegistered<MHTBlueToothController>()) {
|
||||||
|
final controller = Get.find<MHTBlueToothController>();
|
||||||
|
if (controller.localUpgradeMac.containsKey(mac)) {
|
||||||
|
controller.localUpgradeMac[mac] = {
|
||||||
|
...controller.localUpgradeMac[mac]!,
|
||||||
|
"progress": progress,
|
||||||
|
"status": status,
|
||||||
|
"updateTime": DateTime.now().millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
controller.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cleanupTask(String mac) {
|
||||||
|
_firmwareDataCache.remove(mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelUpgrade(String mac) {
|
||||||
|
final task = _upgradeTasks[mac];
|
||||||
|
if (task != null) {
|
||||||
|
task.status = 'cancelled';
|
||||||
|
_updateUiProgress(mac, -1, 'cancelled');
|
||||||
|
task.completer.completeError(Exception("[蓝牙指令执行日志] 升级已取消".tr));
|
||||||
|
|
||||||
|
TopSlideNotification.show(Get.context!, text: "设备 $mac 升级已取消".tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelAllUpgrades() {
|
||||||
|
for (final mac in _upgradeTasks.keys) {
|
||||||
|
cancelUpgrade(mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? getUpgradeStatus(String mac) {
|
||||||
|
final task = _upgradeTasks[mac];
|
||||||
|
if (task == null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'mac': task.mac,
|
||||||
|
'progress': task.progress,
|
||||||
|
'status': task.status,
|
||||||
|
'startTime': task.startTime,
|
||||||
|
'endTime': task.endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Map<String, dynamic>> getAllUpgradeStatus() {
|
||||||
|
final Map<String, Map<String, dynamic>> status = {};
|
||||||
|
for (final task in _upgradeTasks.values) {
|
||||||
|
status[task.mac] = {
|
||||||
|
'progress': task.progress,
|
||||||
|
'status': task.status,
|
||||||
|
'startTime': task.startTime,
|
||||||
|
'endTime': task.endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDeviceUpgrading(String mac) {
|
||||||
|
final task = _upgradeTasks[mac];
|
||||||
|
return task != null &&
|
||||||
|
(task.status == 'downloading' || task.status == 'upgrading');
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanupCompletedTasks() {
|
||||||
|
final completedMacs = _upgradeTasks.entries
|
||||||
|
.where((entry) =>
|
||||||
|
['completed', 'failed', 'cancelled'].contains(entry.value.status))
|
||||||
|
.map((entry) => entry.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (final mac in completedMacs) {
|
||||||
|
_upgradeTasks.remove(mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _sendVotaCommand(THapp thapp) async {
|
||||||
|
try {
|
||||||
|
final result =
|
||||||
|
await thapp.send('vota path="/root/mtd/update"', true, (log) {
|
||||||
|
ef.log("[vota命令响应] ${log.log}");
|
||||||
|
|
||||||
|
// vota 命令特定的成功判断
|
||||||
|
if (log.log.contains("verification complete") ||
|
||||||
|
log.log.contains("update successful") ||
|
||||||
|
log.log.contains("rebooting")) {
|
||||||
|
log.result = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vota 命令特定的失败判断
|
||||||
|
if (log.log.contains("verification failed") ||
|
||||||
|
log.log.contains("update error")) {
|
||||||
|
log.result = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, 0, 20000 // vota 命令可能需要更长时间
|
||||||
|
);
|
||||||
|
|
||||||
|
return result == true;
|
||||||
|
} catch (e, stack) {
|
||||||
|
ef.log("[vota命令异常] $e\n$stack");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:img_picker/img_picker.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -93,4 +94,51 @@ class DailyLogUtils {
|
|||||||
|
|
||||||
return logFiles;
|
return logFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> readLogsByDateRange(
|
||||||
|
DateTime fromDate, DateTime toDate) async {
|
||||||
|
try {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
String combinedLogs = '';
|
||||||
|
|
||||||
|
// 遍历从 fromDate 到 toDate 的日期
|
||||||
|
for (DateTime date = fromDate;
|
||||||
|
!date.isAfter(toDate);
|
||||||
|
date = date.add(Duration(days: 1))) {
|
||||||
|
final dateStr = dateFormat.format(date); // 格式化日期
|
||||||
|
final file = File('${dir.path}/$dateStr.log'); // 日志文件路径
|
||||||
|
|
||||||
|
if (await file.exists()) {
|
||||||
|
final logContent = await file.readAsString();
|
||||||
|
combinedLogs += '日志日期: $dateStr\n$logContent\n\n'; // 合并日志内容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combinedLogs.isNotEmpty) {
|
||||||
|
return combinedLogs; // 返回合并后的日志
|
||||||
|
} else {
|
||||||
|
return '该时间段内没有日志记录'; // 如果没有日志
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return '读取日志失败: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分享功能
|
||||||
|
// static Future<void> exportLog(DateTime date) async {
|
||||||
|
// try {
|
||||||
|
// final dir = await getApplicationDocumentsDirectory();
|
||||||
|
// final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||||
|
// final file = File('${dir.path}/$dateStr.log');
|
||||||
|
// if (await file.exists()) {
|
||||||
|
// await Share.shareXFiles(
|
||||||
|
// [XFile(file.path)],
|
||||||
|
// text: '应用日志 $dateStr',
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// print('导出日志失败: $e');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
140
lib/common/util/FirmwareVersionService.dart
Normal file
140
lib/common/util/FirmwareVersionService.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class FirmwareVersionInfo {
|
||||||
|
final String version;
|
||||||
|
final String fileName;
|
||||||
|
final String downloadUrl;
|
||||||
|
|
||||||
|
FirmwareVersionInfo({
|
||||||
|
required this.version,
|
||||||
|
required this.fileName,
|
||||||
|
required this.downloadUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FirmwareVersionInfo{version: $version, fileName: $fileName, downloadUrl: $downloadUrl}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirmwareVersionService {
|
||||||
|
/// 从远程URL获取最新的固件版本信息
|
||||||
|
/// [baseUrl] 基础URL,如 "https://share.file.he-info.cn"
|
||||||
|
/// [firmwarePath] 固件路径,如 "/ota/aithv2/"
|
||||||
|
static Future<FirmwareVersionInfo?> getLatestFirmwareVersion({
|
||||||
|
required String baseUrl,
|
||||||
|
required String firmwarePath,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 构建完整的API URL
|
||||||
|
final apiUrl = '$baseUrl/~/api/get_file_list?uri=$firmwarePath';
|
||||||
|
|
||||||
|
// 发送HTTP请求 - 使用基础的http客户端,不依赖项目特定实现
|
||||||
|
final response = await _makeHttpRequest(apiUrl);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应数据
|
||||||
|
return _parseFirmwareData(response, baseUrl, firmwarePath);
|
||||||
|
} catch (e) {
|
||||||
|
print('获取固件版本信息失败: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送HTTP请求
|
||||||
|
static Future<dynamic> _makeHttpRequest(String url) async {
|
||||||
|
try {
|
||||||
|
// 使用基础的http客户端
|
||||||
|
final httpClient = HttpClient();
|
||||||
|
|
||||||
|
final request = await httpClient.getUrl(Uri.parse(url));
|
||||||
|
final response = await request.close();
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final responseBody = await response.transform(utf8.decoder).join();
|
||||||
|
return jsonDecode(responseBody);
|
||||||
|
} catch (e) {
|
||||||
|
print('HTTP请求失败: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析固件数据
|
||||||
|
static FirmwareVersionInfo? _parseFirmwareData(
|
||||||
|
dynamic data,
|
||||||
|
String baseUrl,
|
||||||
|
String firmwarePath
|
||||||
|
) {
|
||||||
|
if (data == null || data['list'] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final versionList = data['list'] as List;
|
||||||
|
int? maxVersionNum;
|
||||||
|
String? latestFileName;
|
||||||
|
|
||||||
|
for (var item in versionList) {
|
||||||
|
if (item is Map && item.containsKey('n')) {
|
||||||
|
final fileName = item['n'] as String;
|
||||||
|
try {
|
||||||
|
// 解析版本号 - 假设文件名格式为 "something-version.extension"
|
||||||
|
final versionMatch = _extractVersionFromFileName(fileName);
|
||||||
|
|
||||||
|
if (versionMatch != null) {
|
||||||
|
final versionNum = int.parse(versionMatch);
|
||||||
|
|
||||||
|
if (maxVersionNum == null || versionNum > maxVersionNum) {
|
||||||
|
maxVersionNum = versionNum;
|
||||||
|
latestFileName = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败跳过
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestFileName != null && maxVersionNum != null) {
|
||||||
|
// 构建下载URL
|
||||||
|
final downloadUrl = '$baseUrl/${firmwarePath.replaceFirst('/', '')}$latestFileName';
|
||||||
|
|
||||||
|
return FirmwareVersionInfo(
|
||||||
|
version: maxVersionNum.toString(),
|
||||||
|
fileName: latestFileName,
|
||||||
|
downloadUrl: downloadUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从文件名中提取版本号
|
||||||
|
static String? _extractVersionFromFileName(String fileName) {
|
||||||
|
// 多种可能的文件名格式处理
|
||||||
|
final patterns = [
|
||||||
|
// 格式: something-123.bin
|
||||||
|
RegExp(r'-(\d+)\.'),
|
||||||
|
// 格式: v123.bin
|
||||||
|
RegExp(r'v(\d+)\.'),
|
||||||
|
// 格式: firmware_123.bin
|
||||||
|
RegExp(r'_(\d+)\.'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final pattern in patterns) {
|
||||||
|
final match = pattern.firstMatch(fileName);
|
||||||
|
if (match != null && match.groupCount >= 1) {
|
||||||
|
return match.group(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
//蓝牙指令
|
//蓝牙指令
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:EasyDartModule/EasyDartModule.dart' as edm;
|
import 'package:EasyDartModule/EasyDartModule.dart' as edm;
|
||||||
import 'package:easydevice/src/app/thapp.dart';
|
import 'package:easydevice/src/app/thapp.dart';
|
||||||
import 'package:vbvs_app/common/util/DailyLogUtils.dart';
|
import 'package:vbvs_app/common/util/DailyLogUtils.dart';
|
||||||
import 'package:vbvs_app/pages/mh_page/device/model/BlueToothDataModel.dart';
|
|
||||||
|
|
||||||
// wifi列表指令
|
// wifi列表指令
|
||||||
getWifiList(THapp tHapp) async {
|
getWifiList(THapp tHapp) async {
|
||||||
@@ -111,63 +108,6 @@ Future<bool> sendWifiSetting(wifiItem, String password, THapp tHapp) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDeviceWifiStatus(THapp tHapp, int times) async {
|
|
||||||
// edm.EasyDartModule.logger.info("发送请求设备已配置网络状态指令");
|
|
||||||
// DailyLogUtils.writeLog("发送请求设备已配置网络状态指令");
|
|
||||||
// try {
|
|
||||||
// var result = await tHapp.send("at+system info", true, (ss) {
|
|
||||||
// var log = ss.log;
|
|
||||||
|
|
||||||
// // 匹配设备状态
|
|
||||||
// final statusMatch = RegExp(r'Status=([^\s]+)').firstMatch(log);
|
|
||||||
// final status = statusMatch?.group(1);
|
|
||||||
// if (status != null) {
|
|
||||||
// print('提取到的 status: $status');
|
|
||||||
|
|
||||||
// // 如果设备连接状态是 "connect",继续检测
|
|
||||||
// if (status.contains('connect')) {
|
|
||||||
// ss.result = true;
|
|
||||||
// ss.over = true;
|
|
||||||
|
|
||||||
// // 匹配 Wi-Fi 连接信息
|
|
||||||
// final wifiInfoMatch = RegExp(
|
|
||||||
// r'WIFI CONNECTED INFO:SSID=([^\s]+),RSSI=([-0-9]+),AUTH=([0-9]+),CH=([0-9]+),BSSID=([A-F0-9]+)')
|
|
||||||
// .firstMatch(log);
|
|
||||||
// if (wifiInfoMatch != null) {
|
|
||||||
// final ssid = wifiInfoMatch.group(1);
|
|
||||||
// final rssi = wifiInfoMatch.group(2);
|
|
||||||
// final auth = wifiInfoMatch.group(3);
|
|
||||||
// final ch = wifiInfoMatch.group(4);
|
|
||||||
// final bssid = wifiInfoMatch.group(5);
|
|
||||||
|
|
||||||
// // 打印并返回 Wi-Fi 信息
|
|
||||||
// print(
|
|
||||||
// 'Wi-Fi 信息: SSID=$ssid, RSSI=$rssi, AUTH=$auth, CH=$ch, BSSID=$bssid');
|
|
||||||
|
|
||||||
// // 停止监听并返回信息
|
|
||||||
// ss.result = {
|
|
||||||
// 'ssid': ssid,
|
|
||||||
// 'rssi': rssi,
|
|
||||||
// 'auth': auth,
|
|
||||||
// 'ch': ch,
|
|
||||||
// 'bssid': bssid,
|
|
||||||
// };
|
|
||||||
// ss.over = true;
|
|
||||||
// return ss.result;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 未找到状态或Wi-Fi信息时,返回 false
|
|
||||||
// return false;
|
|
||||||
// }, times);
|
|
||||||
|
|
||||||
// return result;
|
|
||||||
// } catch (e) {
|
|
||||||
// print(e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
getDeviceWifiStatus(THapp tHapp, int times) async {
|
getDeviceWifiStatus(THapp tHapp, int times) async {
|
||||||
edm.EasyDartModule.logger.info("发送请求设备已配置网络状态指令");
|
edm.EasyDartModule.logger.info("发送请求设备已配置网络状态指令");
|
||||||
DailyLogUtils.writeLog("发送请求设备已配置网络状态指令");
|
DailyLogUtils.writeLog("发送请求设备已配置网络状态指令");
|
||||||
@@ -212,7 +152,6 @@ getDeviceWifiStatus(THapp tHapp, int times) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续监听
|
// 继续监听
|
||||||
return false;
|
return false;
|
||||||
}, times);
|
}, times);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class BlueteethBindModel {
|
|||||||
int? read = 1;
|
int? read = 1;
|
||||||
double? singal = -90;
|
double? singal = -90;
|
||||||
|
|
||||||
|
bool? bluetooth = false; //蓝牙开关
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
List<BleDeviceData>? devicelist = []; //蓝牙扫描
|
List<BleDeviceData>? devicelist = []; //蓝牙扫描
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart';
|
|||||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||||
import 'package:vbvs_app/common/util/CommonVariables.dart';
|
import 'package:vbvs_app/common/util/CommonVariables.dart';
|
||||||
|
import 'package:vbvs_app/common/util/DailyLogUtils.dart';
|
||||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||||
@@ -70,6 +71,9 @@ class _InstantBodyPageState extends State<InstantBodyPage>
|
|||||||
|
|
||||||
void _initWebSocket() {
|
void _initWebSocket() {
|
||||||
// 发送WebSocket请求
|
// 发送WebSocket请求
|
||||||
|
edm.EasyDartModule.logger
|
||||||
|
.info("[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}");
|
||||||
|
DailyLogUtils.writeLog("[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}");
|
||||||
edm.EasyDartModule.websocket.sendData(jsonEncode(WebSocketMessage(
|
edm.EasyDartModule.websocket.sendData(jsonEncode(WebSocketMessage(
|
||||||
path: "/vsbs/web/rt/marttress",
|
path: "/vsbs/web/rt/marttress",
|
||||||
type: 1,
|
type: 1,
|
||||||
@@ -606,22 +610,20 @@ class _InstantBodyPageState extends State<InstantBodyPage>
|
|||||||
"MAC号".tr +
|
"MAC号".tr +
|
||||||
": ${widget.personInfo['mac'] ?? '未知数据'.tr}",
|
": ${widget.personInfo['mac'] ?? '未知数据'.tr}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc4
|
color: themeController.currentColor.sc4,
|
||||||
.withOpacity(0.2),
|
|
||||||
fontSize:
|
fontSize:
|
||||||
AppConstants().smaller_text_fontSize,
|
AppConstants().middler_text_fontSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 10.rpx,
|
height: 4.rpx,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"睡眠报告提示".tr,
|
"睡眠报告提示".tr,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc4
|
color: themeController.currentColor.sc4,
|
||||||
.withOpacity(0.2),
|
|
||||||
fontSize:
|
fontSize:
|
||||||
AppConstants().smaller_text_fontSize,
|
AppConstants().middler_text_fontSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart';
|
|||||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart'; // 引入permission_handler
|
import 'package:permission_handler/permission_handler.dart'; // 引入permission_handler
|
||||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||||
|
import 'package:vbvs_app/common/util/BluetoothHelper.dart';
|
||||||
import 'package:vbvs_app/common/util/CommonVariables.dart';
|
import 'package:vbvs_app/common/util/CommonVariables.dart';
|
||||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||||
@@ -60,6 +61,23 @@ class _BlueteethDevicePageState extends State<BlueteethDevicePage> {
|
|||||||
Get.find<BlueteethBindController>().startStatusPolling();
|
Get.find<BlueteethBindController>().startStatusPolling();
|
||||||
blueteethBindController.search.value = "";
|
blueteethBindController.search.value = "";
|
||||||
blueteethBindController.currentDeviceMac?.value = "";
|
blueteethBindController.currentDeviceMac?.value = "";
|
||||||
|
BluetoothHelper.listenBluetoothState((isOn) {
|
||||||
|
blueteethBindController.model.bluetooth = isOn;
|
||||||
|
if (isOn) {
|
||||||
|
isScanning = false;
|
||||||
|
_startScanning();
|
||||||
|
}
|
||||||
|
blueteethBindController.updateAll();
|
||||||
|
if (!isOn && !_isDialogShowing) {
|
||||||
|
_isDialogShowing = true;
|
||||||
|
blueteethBindController.model.devicelist = [];
|
||||||
|
blueteethBindController.model.betDevicelist = [];
|
||||||
|
blueteethBindController.updateAll();
|
||||||
|
_showBluetoothNotEnabledDialog().then((_) {
|
||||||
|
_isDialogShowing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查蓝牙权限
|
// 检查蓝牙权限
|
||||||
@@ -77,27 +95,6 @@ class _BlueteethDevicePageState extends State<BlueteethDevicePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> _requestBluetoothPermission() async {
|
|
||||||
// Map<Permission, PermissionStatus> statuses = await [
|
|
||||||
// Permission.bluetoothScan,
|
|
||||||
// Permission.bluetoothConnect,
|
|
||||||
// Permission.location, // Android 12 及以下仍需要位置权限来进行扫描
|
|
||||||
// ].request();
|
|
||||||
|
|
||||||
// bool allGranted = statuses[Permission.bluetoothScan]?.isGranted == true &&
|
|
||||||
// statuses[Permission.bluetoothConnect]?.isGranted == true &&
|
|
||||||
// statuses[Permission.location]?.isGranted == true;
|
|
||||||
|
|
||||||
// if (allGranted) {
|
|
||||||
// // 用户授予了权限,开始扫描
|
|
||||||
// _startScanning();
|
|
||||||
// _startPeriodicScan();
|
|
||||||
// } else {
|
|
||||||
// // 权限被拒绝,提示用户
|
|
||||||
// _showPermissionDeniedDialog();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> _requestBluetoothPermission(BuildContext context) async {
|
Future<void> _requestBluetoothPermission(BuildContext context) async {
|
||||||
Map<Permission, PermissionStatus> statuses = {};
|
Map<Permission, PermissionStatus> statuses = {};
|
||||||
bool dialogShown = false; // 标记是否弹过权限提示弹窗
|
bool dialogShown = false; // 标记是否弹过权限提示弹窗
|
||||||
@@ -200,15 +197,15 @@ class _BlueteethDevicePageState extends State<BlueteethDevicePage> {
|
|||||||
if (!mounted || isScanning) return;
|
if (!mounted || isScanning) return;
|
||||||
|
|
||||||
_scanSubscription?.cancel();
|
_scanSubscription?.cancel();
|
||||||
var bluetoothState = await FlutterBluePlus.isOn;
|
// var bluetoothState = await FlutterBluePlus.isOn;
|
||||||
if (!bluetoothState && !_isDialogShowing) {
|
// if (!bluetoothState && !_isDialogShowing) {
|
||||||
_isDialogShowing = true;
|
// _isDialogShowing = true;
|
||||||
blueteethBindController.model.blelist = [];
|
// blueteethBindController.model.blelist = [];
|
||||||
blueteethBindController.updateAll();
|
// blueteethBindController.updateAll();
|
||||||
await _showBluetoothNotEnabledDialog();
|
// await _showBluetoothNotEnabledDialog();
|
||||||
_isDialogShowing = false;
|
// _isDialogShowing = false;
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!isScanning) {
|
if (!isScanning) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,10 @@ class MHTBlueToothController extends GetControllerEx<MHTBlueToothModel> {
|
|||||||
RxString? cid = "".obs;
|
RxString? cid = "".obs;
|
||||||
|
|
||||||
RxBool isScanning = false.obs;
|
RxBool isScanning = false.obs;
|
||||||
Map<String, Map> localUpgradeMac = {}; //mac 进度
|
RxMap<String, Map> localUpgradeMac = <String, Map>{}.obs; //mac 进度
|
||||||
|
String? currentUpgradeVersion;//最新版本号
|
||||||
|
String? currentUpgradeName;//最新固件名
|
||||||
|
String? currentUpgradeUrl;//最新固件名
|
||||||
|
|
||||||
void startStatusPolling() {
|
void startStatusPolling() {
|
||||||
updateDeviceStatus().then((res) {
|
updateDeviceStatus().then((res) {
|
||||||
|
|||||||
@@ -875,22 +875,20 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
|
|||||||
"MAC号".tr +
|
"MAC号".tr +
|
||||||
": ${widget.data['mac'] ?? '未知数据'.tr}",
|
": ${widget.data['mac'] ?? '未知数据'.tr}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc4
|
color: themeController.currentColor.sc4,
|
||||||
.withOpacity(0.2),
|
|
||||||
fontSize:
|
fontSize:
|
||||||
AppConstants().smaller_text_fontSize,
|
AppConstants().middler_text_fontSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 10.rpx,
|
height: 4.rpx,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"睡眠报告提示".tr,
|
"睡眠报告提示".tr,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc4
|
color: themeController.currentColor.sc4,
|
||||||
.withOpacity(0.2),
|
|
||||||
fontSize:
|
fontSize:
|
||||||
AppConstants().smaller_text_fontSize,
|
AppConstants().middler_text_fontSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user