301 lines
11 KiB
Dart
Executable File
301 lines
11 KiB
Dart
Executable File
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
|
||
import '../l10n.dart';
|
||
import '../global.dart';
|
||
import '../services/video_talk_initializer.dart';
|
||
import '../widgets/common_widgets.dart';
|
||
import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl;
|
||
|
||
class WifiConfigTab extends StatefulWidget {
|
||
const WifiConfigTab({super.key});
|
||
|
||
@override
|
||
State<WifiConfigTab> createState() => _WifiConfigTabState();
|
||
}
|
||
|
||
class _WifiConfigTabState extends State<WifiConfigTab> {
|
||
final List<String> _log = [];
|
||
final _ssidCtrl = TextEditingController();
|
||
final _pwdCtrl = TextEditingController();
|
||
bool _loading = false;
|
||
|
||
@override
|
||
void dispose() {
|
||
_ssidCtrl.dispose();
|
||
_pwdCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _log_(String msg) {
|
||
final ts = DateTime.now().toIso8601String().substring(11, 19);
|
||
if (mounted) setState(() => _log.add('[$ts] $msg'));
|
||
debugPrint('WiFi_DEBUG: $msg');
|
||
}
|
||
|
||
Future<void> _initVideoTalk() async {
|
||
try {
|
||
_log_(S.isEn ? 'Initializing video talk...' : '正在初始化视频对讲...');
|
||
await initializeVideoTalk();
|
||
_log_(S.isEn
|
||
? 'Video talk is ready. App PeerID: ${appPeerId.value}'
|
||
: '视频对讲已初始化。App PeerID: ${appPeerId.value}');
|
||
} catch (e) {
|
||
_log_('${S.logFailed}: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _config() async {
|
||
final lock = pairedLock.value;
|
||
if (lock == null) {
|
||
_log_('ERROR: No device paired.');
|
||
return;
|
||
}
|
||
|
||
setState(() => _loading = true);
|
||
|
||
final String lockServiceToken = lockServiceAccessToken.value?.trim() ?? '';
|
||
if (signedInAccount.value == null || lockServiceToken.isEmpty) {
|
||
_log_('请先在“登录”页使用锁主账号/管理员账号登录。');
|
||
setState(() => _loading = false);
|
||
return;
|
||
}
|
||
_log_('>>> START CONFIG | HTTP + BLE 0x30F4/0x30F5');
|
||
|
||
try {
|
||
final ctx = await StarCloudLockContext.fromLock(lock);
|
||
final String myId = appPeerId.value ?? '';
|
||
if (myId.isEmpty) {
|
||
_log_(S.logConfigCancelledNeedTalkInitialization);
|
||
return;
|
||
}
|
||
|
||
_log_(S.logUsingLoginSessionForGatewayConfig);
|
||
|
||
final reply = await StarCloudClient.instance.wifi.configureWifiByContext(
|
||
ctx,
|
||
ssid: _ssidCtrl.text.trim(),
|
||
password: _pwdCtrl.text.trim(),
|
||
lockServiceAccessToken: lockServiceToken,
|
||
deviceName: lock.bluetooth.bluetoothDeviceName,
|
||
mac: lock.mac,
|
||
);
|
||
wifiConfiguredLockId.value = lock.lockId;
|
||
_log_(S.logWifiConfigCompleted);
|
||
|
||
try {
|
||
final remoteUnlockReply = await StarCloudClient.instance.lockSetting
|
||
.setRemoteUnlockByContext(ctx, enabled: true);
|
||
if (remoteUnlockReply.status == 0) {
|
||
lock.lockFeature['remoteUnlock'] = 1;
|
||
lock.lockSetting.remoteUnlock = 1;
|
||
pairedLock.value = null;
|
||
pairedLock.value = lock;
|
||
_log_(S.isEn
|
||
? 'Remote unlock enabled by default.'
|
||
: '远程开锁已默认开启。');
|
||
} else {
|
||
final status = remoteUnlockReply.status;
|
||
_log_('Remote unlock default enable failed: '
|
||
'status=$status.');
|
||
}
|
||
} catch (e) {
|
||
_log_('Remote unlock default enable failed: $e');
|
||
}
|
||
|
||
try {
|
||
final gatewayStatus =
|
||
await StarCloudClient.instance.wifi.getGatewayStatus(
|
||
lockId: lock.bluetooth.bluetoothDeviceName,
|
||
userId: ctx.userId,
|
||
deviceName: lock.bluetooth.bluetoothDeviceName,
|
||
mac: lock.mac,
|
||
);
|
||
_log_(
|
||
'GATEWAY STATUS: 0x${gatewayStatus.status.toRadixString(16).toUpperCase()}');
|
||
} catch (e) {
|
||
_log_('GATEWAY STATUS QUERY FAILED: $e');
|
||
}
|
||
|
||
final blePeerId = reply.peerId?.trim() ?? '';
|
||
if (blePeerId.isNotEmpty) {
|
||
_log_('Lock PeerID from BLE: $blePeerId');
|
||
lock.peerId = blePeerId;
|
||
pairedLock.value = null;
|
||
pairedLock.value = lock;
|
||
unawaited(_waitForLockOnline(lock.lockId));
|
||
} else {
|
||
await _refreshLockPeerId(lock);
|
||
}
|
||
} catch (e) {
|
||
_log_('${S.logFailed}: $e');
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
/// 配网成功后自动回查云端锁详情,回填锁版 peerId,并等待锁上线
|
||
Future<void> _refreshLockPeerId(scl.StarCloudLock lock) async {
|
||
try {
|
||
final fresh =
|
||
await StarCloudClient.instance.device.getDevice(lockId: lock.lockId);
|
||
pairedLock.value = fresh;
|
||
final p = (fresh.peerId ?? '').trim();
|
||
_log_(p.isNotEmpty
|
||
? 'Lock PeerID after config: $p'
|
||
: 'Lock not reported PeerID yet (may take a while).');
|
||
// 等待锁上线
|
||
unawaited(_waitForLockOnline(lock.lockId));
|
||
} catch (e) {
|
||
_log_('Refresh lock detail failed: $e (retry in Talk tab)');
|
||
}
|
||
}
|
||
|
||
/// 配网后轮询等待锁上线(最长等待 120 秒)
|
||
Future<void> _waitForLockOnline(int lockId) async {
|
||
const maxSeconds = 120;
|
||
const interval = Duration(seconds: 5);
|
||
int waited = 0;
|
||
_log_(S.isEn
|
||
? 'Waiting for lock to come online (this may take 30-60s)...'
|
||
: '等待锁上线(可能需要 30-60 秒)...');
|
||
while (waited < maxSeconds) {
|
||
await Future.delayed(interval);
|
||
waited += 5;
|
||
if (!mounted) return;
|
||
try {
|
||
final fresh =
|
||
await StarCloudClient.instance.device.getDevice(lockId: lockId);
|
||
final bool online = fresh.isOnline ?? false;
|
||
final String peerId = (fresh.peerId ?? '').trim();
|
||
if (online && peerId.isNotEmpty) {
|
||
pairedLock.value = fresh;
|
||
_log_(S.isEn
|
||
? 'SUCCESS: Lock is ONLINE! peerId=$peerId. You can start monitoring now.'
|
||
: '锁已上线!peerId=$peerId。现在可以开始监控了。');
|
||
return;
|
||
}
|
||
_log_(
|
||
'[${waited}s] ${S.isEn ? "still offline" : "仍未上线"} (peerId=${peerId.isNotEmpty ? peerId : "none"})');
|
||
} catch (e) {
|
||
_log_('Poll online status failed: $e');
|
||
}
|
||
}
|
||
_log_(S.isEn
|
||
? 'WARNING: Lock did not come online within 120s. Check WiFi signal and try restarting the lock.'
|
||
: '警告:120 秒内锁未上线。请检查 WiFi 信号,或尝试重启锁。');
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ValueListenableBuilder<bool>(
|
||
valueListenable: S.langNotifier,
|
||
builder: (context, isEn, _) {
|
||
return ValueListenableBuilder<scl.StarCloudLock?>(
|
||
valueListenable: pairedLock,
|
||
builder: (context, lock, _) {
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
ValueListenableBuilder<bool>(
|
||
valueListenable: videoTalkReady,
|
||
builder: (context, ready, _) {
|
||
return ValueListenableBuilder<bool>(
|
||
valueListenable: videoTalkInitializing,
|
||
builder: (context, initializing, _) {
|
||
return ElevatedButton.icon(
|
||
onPressed:
|
||
ready || initializing ? null : _initVideoTalk,
|
||
icon: initializing
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
),
|
||
)
|
||
: const Icon(Icons.videocam),
|
||
label: Text(ready
|
||
? '${S.btnInitTalk} (${S.initialized})'
|
||
: S.btnInitTalk),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
lock != null
|
||
? '${S.pairedStatus}: ${lock.lockName.isNotEmpty ? lock.lockName : "ID:${lock.lockId}"}'
|
||
: S.notPaired,
|
||
style: TextStyle(
|
||
color: lock != null ? Colors.green : Colors.red,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
ValueListenableBuilder<String?>(
|
||
valueListenable: signedInAccount,
|
||
builder: (context, account, _) => Card(
|
||
elevation: 0,
|
||
color: account == null
|
||
? Colors.red.shade50
|
||
: Colors.green.shade50,
|
||
child: ListTile(
|
||
leading: Icon(account == null
|
||
? Icons.error_outline
|
||
: Icons.verified_user_outlined),
|
||
title: Text(account == null
|
||
? '未登录'
|
||
: '${S.currentLoggedInAccountPrefix}$account'),
|
||
subtitle: Text(account == null
|
||
? '请先在“登录”页登录锁主账号/管理员账号。'
|
||
: S.wifiUsesAuthorizedSession),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: _ssidCtrl,
|
||
decoration: InputDecoration(
|
||
labelText: isEn ? 'WiFi SSID' : 'WIFI名称',
|
||
border: const OutlineInputBorder(),
|
||
prefixIcon: const Icon(Icons.wifi))),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: _pwdCtrl,
|
||
obscureText: true,
|
||
decoration: InputDecoration(
|
||
labelText: isEn ? 'WiFi Password' : 'WIFI密码',
|
||
border: const OutlineInputBorder(),
|
||
prefixIcon: const Icon(Icons.lock_outline))),
|
||
const SizedBox(height: 20),
|
||
ElevatedButton.icon(
|
||
onPressed: _loading || lock == null ? null : _config,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.blue,
|
||
foregroundColor: Colors.white,
|
||
minimumSize: const Size.fromHeight(50)),
|
||
icon: _loading
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
color: Colors.white, strokeWidth: 2))
|
||
: const Icon(Icons.send),
|
||
label: Text(_loading ? S.btnProcessing : S.btnStartConfig),
|
||
),
|
||
const SizedBox(height: 20),
|
||
LogArea(log: _log),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|