Files

502 lines
17 KiB
Dart
Raw Permalink Normal View History

/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:stack_chan/util/XiaoZhi_util.dart';
import 'package:stack_chan/util/rsa_util.dart';
import 'package:stack_chan/view/popup/login_page.dart';
import '../../app_state.dart';
import '../../model/blue_device_info.dart';
import '../../model/blue_model.dart';
import '../../util/blue_util.dart';
import '../../util/mac_address_validator.dart';
import '../../util/value_constant.dart';
import 'device_name_page.dart';
enum DeviceWifiConfigStep { selectDevice, wifiConfig }
///scanBluetoothbinddevicepopup
class SelectBlueDevice extends StatefulWidget {
const SelectBlueDevice({super.key});
@override
State<StatefulWidget> createState() => _SelectBlueDeviceState();
}
class _SelectBlueDeviceState extends State<SelectBlueDevice> {
//connecttimeouttimer(preventNoWait)
Timer? _connectTimer;
//verifytimeouttimer(preventdeviceNoresponsewhen1to)
Timer? _verifyTimer;
//timeouttime:30Second(s)
static const int _connectTimeout = 30;
//verifytimeouttime:20Second(s)
static const int _verifyTimeout = 20;
String? _getDeviceId(BlueDeviceInfo blueDeviceInfo) {
final Map<int, List<int>> manufacturerDataMap =
blueDeviceInfo.advertisementData[ValueConstant.manufacturerData];
if (manufacturerDataMap.isNotEmpty) {
final MapEntry<int, List<int>> firstEntry =
manufacturerDataMap.entries.first;
final List<int> customData = firstEntry.value;
final address = customData.map((byte) {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
}).join();
return address;
}
return null;
}
RxString connectDeviceId = RxString("");
@override
void dispose() {
_connectTimer?.cancel();
_verifyTimer?.cancel();
BlueUtil.shared.characteristicCallback = null;
BlueUtil.shared.wifiSetCharacteristicCall = null;
BlueUtil.shared.onReconnectSuccess = null;
connectDeviceId.value = "";
isSuccess = false;
super.dispose();
}
bool isSuccess = false;
@override
void initState() {
super.initState();
//enterbindStreamProcess / Thread,setbindflag
_registerBlueCallbacks();
BlueUtil.shared.onReconnectSuccess = (device) {
if (mounted) {
AppState.shared.showToast("Reconnected to device successfully.");
}
_registerBlueCallbacks();
//reconnectafterretriggerdeviceverifykeyoriginallogiconlyhasfirstconnectwhentrigger
if (BlueUtil.shared.writeWifiSetCharacteristic != null) {
startVerificationEquipment(BlueUtil.shared.writeWifiSetCharacteristic!);
}
};
}
//resetconnectstate(System1wrap)
void _resetConnectState() {
connectDeviceId.value = "";
_connectTimer?.cancel();
_verifyTimer?.cancel();
if (mounted) {
setState(() {});
}
}
void _registerBlueCallbacks() {
BlueUtil.shared.characteristicCallback = (device, characteristic) async {
final properties = characteristic.properties;
final canWrite = properties.write || properties.writeWithoutResponse;
if (canWrite) {
final uuid = characteristic.uuid.toString();
if (uuid == BlueUtil.wifiSetCharacteristicUUID) {
BlueUtil.shared.writeWifiSetCharacteristic = characteristic;
if (Platform.isAndroid) {
await device
.requestMtu(512)
.then((mtu) {
})
.catchError((e) {
if (mounted) {
AppState.shared.showToast(
"Failed to set MTU. Device may not work properly.",
);
}
});
}
//enablefeatureValuenotify
try {
await characteristic.setNotifyValue(true);
startVerificationEquipment(characteristic);
} catch (e) {
_resetConnectState();
if (mounted) {
AppState.shared.showToast(
"Failed to enable device notification.",
);
}
}
return;
}
}
if (mounted) {
AppState.shared.showToast("Device configuration feature not found.");
}
};
BlueUtil.shared.wifiSetCharacteristicCall = (data) async {
try {
String json = utf8.decode(data);
final model = BlueNotifyStateModel.fromJson(json);
if (model == null) {
if (mounted) {
AppState.shared.showToast("Failed to parse device data.");
}
return;
}
if (model.cmd != null &&
model.cmd == "notifyState" &&
model.data?.type == 4) {
//toEncryptinfo
String? data = model.data?.state;
if (data == null) {
if (mounted) {
AppState.shared.showToast(
"Device did not return encryption data.",
);
}
return;
}
//RSADecrypt
final result = RsaUtil.decryptStackChanBlue(data);
//newIncrease / Add:Decryptfail/lengthNot
if (result.isEmpty || result.length < 12) {
if (mounted) {
AppState.shared.showToast(
"Device verification decryption failed.",
);
}
return;
}
if (isSuccess) return;
isSuccess = true;
_connectTimer?.cancel(); //cancelconnecttimeouttimer
_verifyTimer?.cancel(); //cancelverifytimeouttimer
final macAddress = result.substring(0, 12);
AppState.shared.showToast(
"The initially activated MAC address: $macAddress",
);
AppState.shared.deviceMac = macAddress;
AppState.shared.connectWebSocket();
activateDevice(macAddress);
}
} catch (e) {
if (mounted) {
AppState.shared.showToast("Failed to process device data.");
}
}
};
}
String formatMacAddress(String mac) {
if (mac.length != 12) return mac;
return mac
.toUpperCase()
.replaceAllMapped(RegExp(r'(.{2})'), (match) => '${match.group(1)}:')
.substring(0, 17); //removeafter
}
bool isValidMac(String mac) {
final RegExp macRegex = RegExp(r'^[0-9A-Fa-f]{12}$');
return macRegex.hasMatch(mac);
}
//activatedevice
Future<void> activateDevice(String macAddress) async {
try {
///startqueryagentconfiginfo
bool isConfiguration = await queryConfiguration(macAddress);
if (!isConfiguration) {
_resetConnectState();
//activatefail(Alreadyhashint,NoRepeat)
return;
}
if (mounted) {
AppState.shared.showToast("The AI Agent has been configured.");
}
//binddevice
bool result = await AppState.shared.bindDevice(macAddress);
if (result) {
_resetConnectState();
if (mounted) {
AppState.shared.showToast("Device bound successfully!");
//configjump
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => const DeviceNamePage()),
);
}
} else {
_resetConnectState();
//newIncrease / Add:binddevicefail
if (mounted) {
AppState.shared.showToast("Device binding failed. Please try again.");
}
}
} catch (e) {
_resetConnectState();
if (mounted) {
AppState.shared.showToast("Device activation exception.");
}
}
}
//querydeviceconfig(StreamProcess / Threaderrorhint)
Future<bool> queryConfiguration(String macAddress) async {
try {
//1. querydevicewhetheractivated (laterNotAgainquery directactivate)
// final devices = await XiaoZhiUtil.shared.getDevice(macAddress);
// if (devices.isNotEmpty) {
// if (devices.first.agent_id != null) {
//debugPrint("✅ deviceactivated");
// return true;
// }
// }
//2. generatelicense
final generateLicense = await XiaoZhiUtil.shared.generateLicense(
macAddress,
);
if (generateLicense == null || generateLicense.serialNumber == null) {
if (mounted) {
AppState.shared.showToast("Failed to generate device license.");
}
return false;
}
//3. activatedevice
final serialNumber = generateLicense.serialNumber!;
final mac = MacAddressValidator.formatMac(AppState.shared.deviceMac);
if (mac == null) {
AppState.shared.showToast("Failed to format device MAC address.");
return false;
}
bool activateResult = await XiaoZhiUtil.shared.agentsDevicesActivate(
serialNumber,
mac,
);
if (!activateResult) {
if (mounted) {
AppState.shared.showToast("Device cloud activation failed.");
}
return false;
}
///deviceMayexist Notactivate,Needverify
final checkDevice = await XiaoZhiUtil.shared.serialNumberGetDevice(
serialNumber,
);
if (checkDevice == null || checkDevice.agent_id == null) {
//activatefail
return false;
} else {
AppState.shared.showToast("Device activation successful");
return true;
}
} catch (e) {
if (mounted) {
AppState.shared.showToast("Failed to query device configuration.");
}
return false;
}
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground.resolveFrom(
context,
),
child: CustomScrollView(
slivers: [
CupertinoSliverNavigationBar(
largeTitle: Text("Select Device"),
leading: SizedBox.shrink(),
trailing: CupertinoButton(
padding: .zero,
child: Icon(
CupertinoIcons.xmark_circle_fill,
size: 25,
color: CupertinoColors.separator.resolveFrom(context),
),
onPressed: () {
AppState.shared.manualShutdownTime = DateTime.now();
BlueUtil.shared.characteristicCallback = null;
BlueUtil.shared.wifiSetCharacteristicCall = null;
BlueUtil.shared.onReconnectSuccess = null;
CupertinoSheetRoute.popSheet(context);
},
),
),
SliverList.list(
children: [
Obx(
() => CupertinoListSection.insetGrouped(
children: AppState.shared.blueDeviceList.isNotEmpty
? AppState.shared.blueDeviceList.map((deviceInfo) {
String? deviceId = _getDeviceId(deviceInfo);
return CupertinoListTile(
onTap: () async {
if (!AppState.shared.isLogin.value) {
if (mounted) {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) =>
const LoginPage(isWelCome: true),
),
);
}
return;
}
//Repeatclick:Alreadyinconnectin
final deviceRemoteId = deviceInfo.device.remoteId
.toString();
if (deviceRemoteId == connectDeviceId.value) {
if (mounted) {
AppState.shared.showToast(
"Already connecting... Please wait patiently.",
);
}
return;
}
//startconnect
isSuccess = false;
connectDeviceId.value = deviceRemoteId;
AppState.shared.blueDeviceList.refresh();
//newIncrease / Add:connecttimeouttimer
_connectTimer?.cancel();
_connectTimer = Timer(
Duration(seconds: _connectTimeout),
() {
_resetConnectState();
if (mounted) {
AppState.shared.showToast(
"Device connection timed out. Please try again.",
);
}
},
);
//newIncrease / Add:BluetoothconnectThrowsCatch
try {
if (mounted) {
AppState.shared.showToast(
"Connecting to device...",
);
}
await BlueUtil.shared.connect(
deviceInfo.device,
);
} catch (e) {
_resetConnectState();
if (mounted) {
AppState.shared.showToast(
"Bluetooth connection failed: ${e.toString()}",
);
}
}
},
leading: Image.asset(
"assets/image1.png",
width: 28,
height: 28,
),
title: Text(deviceInfo.device.advName),
subtitle: deviceId != null
? Text("ID: $deviceId")
: null,
trailing:
deviceInfo.device.remoteId.toString() ==
connectDeviceId.value
? const CupertinoActivityIndicator()
: SvgPicture.asset(
"assets/chevron.right.svg",
width: 15,
height: 15,
colorFilter: ColorFilter.mode(
CupertinoColors.secondaryLabel
.resolveFrom(context),
BlendMode.srcIn,
),
),
);
}).toList()
: [CupertinoListTile(title: Text("No devices found"))],
),
),
],
),
],
),
);
}
//startverifydevice(senddata)
void startVerificationEquipment(
BluetoothCharacteristic characteristic,
) async {
try {
if (mounted) {
AppState.shared.showToast("Verifying device...");
}
//startverifytimeouttimer
_verifyTimer?.cancel();
_verifyTimer = Timer(Duration(seconds: _verifyTimeout), () {
_resetConnectState();
if (mounted) {
AppState.shared.showToast(
"Device verification timed out. Please try again.",
);
}
});
//data
final dateTimeString = DateTime.now().millisecondsSinceEpoch.toString();
final BlueEncryptionDecryption data = BlueEncryptionDecryption(
cmd: "handshake",
data: dateTimeString,
);
final jsonString = jsonEncode(data.toJson());
//senddata
bool result = await BlueUtil.shared.sendWifiSetData(jsonString);
if (result) {
} else {
_verifyTimer?.cancel();
AppState.shared.showToast(
"The equipment may have been disconnected. Please reconfigure it on the StackChan end.",
);
_resetConnectState();
}
} catch (e) {
_verifyTimer?.cancel();
_resetConnectState();
if (mounted) {
AppState.shared.showToast(
"Failed to send device verification command.",
);
}
}
}
}