flutter_sdk/example/lib/services/cat_eye_monitor_service.dart
2026-08-20 09:25:05 +08:00

152 lines
4.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart' as crypto;
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import 'package:starcloud/blue/blue_features.dart';
import 'package:starcloud/blue/blue_read_write.dart';
import 'package:starcloud/blue/entity/blue_message.dart';
import 'package:starcloud/device/ble/base/base_sender.dart';
import 'package:starcloud/device/ble/ble_reply.dart';
import 'package:starcloud/device/ble/business/ble_type.dart';
import 'package:starcloud/device/ble/tool/ble_tool.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart';
import 'package:starcloud/tool/sm4_encipher/sm4.dart';
/// 主动监控前将猫眼从省电模式切换到实时监控模式。
///
/// 实时模式由 BLE feature bit 64 写入锁端。
/// 省电模式(catEyeMode=0)下锁固件会忽略主动 TalkRequest。
///
/// 公开 Flutter SDK 文档未定义 cat-eye 云端设置 API;因此这里不能调用
/// 非公开的 `updateCatEyeConfig` 并把其服务端权限错误当作 BLE 操作失败。
class CatEyeMonitorService {
static const int _catEyeFeatureBit = 64;
static const int _realtimeMode = 2;
Future<void> enableRealtimeMode({
required StarCloudLock lock,
}) async {
final context = await StarCloudLockContext.fromLock(lock);
final featureData = <int>[
_realtimeMode,
0, 0, 0, 0, 0, 0, // 非自定义模式的录像参数
0, 0, // detectionDistance / realTimePicture
1, 10, 1, 1, // 自动亮屏、亮屏时长、逗留/异常告警
];
final blue = BlueReadWrite.shareManager();
if (blue == null) {
throw const StarCloudException('Bluetooth service is unavailable');
}
blue.deviceName = lock.bluetooth.bluetoothDeviceName;
if (!Platform.isAndroid) blue.mac = lock.mac;
blue.type = BlueFeaturesType.lock;
blue.clearCallback();
final reply = await blue.writeAndReturn(
_SetCatEyeModeCommand(
keyId: context.keyId,
userId: context.userId,
featureData: featureData,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
),
getPrivateKey: () => context.privateKey,
timeout: const Duration(seconds: 20),
);
if (reply.status != 0) {
throw StarCloudException(
'Failed to enable cat-eye realtime mode over BLE',
raw: 'status=0x${reply.status.toRadixString(16)}',
);
}
}
}
class _SetCatEyeModeCommand extends SenderProtocol {
_SetCatEyeModeCommand({
required this.keyId,
required this.userId,
required this.featureData,
required List<int> token,
required this.needAuthor,
required this.publicKey,
required this.privateKey,
}) : token = List<int>.from(token),
super(CommandType.generalExtendedCommond);
final String keyId;
final String userId;
final List<int> featureData;
final int needAuthor;
final List<int> publicKey;
final List<int> privateKey;
@override
List<int>? token;
@override
List<int> messageDetail() {
final data = <int>[0x30, 0x30, 72];
var subData = <int>[];
final keyBytes = utf8.encode(keyId);
subData.addAll(keyBytes);
subData = BLETool.getFixedLengthList(subData, 40 - keyBytes.length);
final userBytes = utf8.encode(userId);
subData.addAll(userBytes);
subData = BLETool.getFixedLengthList(subData, 20 - userBytes.length);
subData
..add(CatEyeMonitorService._catEyeFeatureBit)
..add(featureData.length)
..addAll(featureData);
if (needAuthor == 0) {
subData.add(0);
} else {
final authData = <int>[
...userBytes,
...keyBytes,
...?token,
...publicKey,
];
final authCode = crypto.md5.convert(authData).bytes;
subData
..add(authCode.length)
..addAll(authCode);
}
data
..add(subData.length)
..addAll(subData);
while (data.length % 16 != 0) {
data.add(0);
}
return SM4.encrypt(data, key: privateKey, mode: SM4CryptoMode.ECB);
}
@override
BLEReply blueMessageToReply(BlueMessage message) {
return _SetCatEyeModeReply.parseData(commandType, message.data);
}
}
class _SetCatEyeModeReply extends BLEReply {
_SetCatEyeModeReply.parseData(
CommandType commandType,
List<int> data,
) : super.parseData(commandType, data) {
// 通用扩展应答:[0x30, 0x30, mainStatus, subCommand, subStatus, ...]
// 原 App 以 mainStatus 为本次设置结果。
status = data.length > 2 ? data[2] : 0xff;
errorWithStatus(status);
if (status == 0x06 && data.length >= 9) {
token = data.sublist(5, 9);
}
}
}