652 lines
26 KiB
Dart
652 lines
26 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class AdminPage extends StatefulWidget {
|
|
@override
|
|
_AdminPageState createState() => _AdminPageState();
|
|
}
|
|
|
|
//Inisialisasi dan State Management
|
|
class _AdminPageState extends State<AdminPage> {
|
|
BluetoothState _bluetoothState = BluetoothState.UNKNOWN;
|
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
|
final FlutterBluetoothSerial _bluetooth = FlutterBluetoothSerial.instance;
|
|
BluetoothConnection? connection;
|
|
late int _deviceState;
|
|
bool isDisconnecting = false;
|
|
|
|
bool? get isConnected => connection != null && connection!.isConnected;
|
|
|
|
List<BluetoothDevice> _devicesList = [];
|
|
List<Map<String, String>> _addedDevices = [];
|
|
BluetoothDevice? _device;
|
|
bool _connected = false;
|
|
bool _isButtonUnavailable = false;
|
|
|
|
TextEditingController bluetoothNameController = TextEditingController();
|
|
TextEditingController bluetoothAddressController = TextEditingController();
|
|
TextEditingController passwordController = TextEditingController();
|
|
|
|
int _passwordPrefixCounter = 1;
|
|
|
|
String? _displayedIP;
|
|
//Inisialisasi dan Lifecycle Methods
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
FlutterBluetoothSerial.instance.state.then((state) {
|
|
setState(() {
|
|
_bluetoothState = state;
|
|
});
|
|
});
|
|
_deviceState = 0;
|
|
enableBluetooth();
|
|
FlutterBluetoothSerial.instance
|
|
.onStateChanged()
|
|
.listen((BluetoothState state) {
|
|
setState(() {
|
|
_bluetoothState = state;
|
|
if (_bluetoothState == BluetoothState.STATE_OFF) {
|
|
_isButtonUnavailable = true;
|
|
}
|
|
getPairedDevices();
|
|
});
|
|
});
|
|
loadAddedDevices();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (isConnected!) {
|
|
isDisconnecting = true;
|
|
connection?.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
// Fungsi untuk Mengaktifkan Bluetooth jika tidak aktif.
|
|
Future<void> enableBluetooth() async {
|
|
_bluetoothState = await FlutterBluetoothSerial.instance.state;
|
|
if (_bluetoothState == BluetoothState.STATE_OFF) {
|
|
await FlutterBluetoothSerial.instance.requestEnable();
|
|
await getPairedDevices();
|
|
} else {
|
|
await getPairedDevices();
|
|
}
|
|
}
|
|
|
|
//Mendapatkan daftar perangkat yang sudah terhubung (paired).
|
|
Future<void> getPairedDevices() async {
|
|
List<BluetoothDevice> devices = [];
|
|
try {
|
|
devices = await _bluetooth.getBondedDevices();
|
|
} on PlatformException {
|
|
debugPrint("Error");
|
|
}
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_devicesList = devices;
|
|
});
|
|
}
|
|
|
|
//Memuat perangkat yang telah ditambahkan dari SharedPreferences
|
|
Future<void> loadAddedDevices() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
String? devicesJson = prefs.getString('added_devices');
|
|
if (devicesJson != null) {
|
|
List<Map<String, dynamic>> devices =
|
|
List<Map<String, dynamic>>.from(jsonDecode(devicesJson));
|
|
setState(() {
|
|
_addedDevices =
|
|
devices.map((device) => Map<String, String>.from(device)).toList();
|
|
_passwordPrefixCounter = _addedDevices.length + 1;
|
|
});
|
|
}
|
|
}
|
|
|
|
//Menyimpan perangkat yang telah ditambahkan ke SharedPreferences.
|
|
Future<void> saveAddedDevices() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
String devicesJson = jsonEncode(_addedDevices);
|
|
await prefs.setString('added_devices', devicesJson);
|
|
}
|
|
|
|
//Fungsi untuk Menampilkan Pesan
|
|
void showMessage(String message,
|
|
{Duration duration = const Duration(seconds: 3)}) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
duration: duration,
|
|
),
|
|
);
|
|
}
|
|
|
|
//Fungsi untuk Mendapatkan Prefix Berikutnya
|
|
int _getNextAvailablePrefix() {
|
|
List<int> usedPrefixes = _addedDevices
|
|
.map((device) => int.parse(device['prefix'] ?? '0'))
|
|
.toList();
|
|
usedPrefixes.sort();
|
|
|
|
for (int i = 1; i <= usedPrefixes.length; i++) {
|
|
if (!usedPrefixes.contains(i)) {
|
|
return i;
|
|
}
|
|
}
|
|
return usedPrefixes.length + 1;
|
|
}
|
|
|
|
//Ui
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
return false;
|
|
},
|
|
child: Scaffold(
|
|
key: _scaffoldKey,
|
|
appBar: AppBar(
|
|
title: const Text("Smart Door"),
|
|
backgroundColor: Color.fromARGB(255, 243, 146, 34),
|
|
actions: <Widget>[
|
|
ElevatedButton.icon(
|
|
icon: const Icon(
|
|
Icons.refresh,
|
|
color: Color.fromARGB(255, 0, 0, 0),
|
|
),
|
|
label: const Text(
|
|
"",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
onPressed: () async {
|
|
await getPairedDevices().then((_) {
|
|
showMessage('Daftar perangkat diperbarui');
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: <Widget>[
|
|
Visibility(
|
|
visible: _isButtonUnavailable &&
|
|
_bluetoothState == BluetoothState.STATE_ON,
|
|
child: const LinearProgressIndicator(
|
|
backgroundColor: Colors.yellow,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: <Widget>[
|
|
const Expanded(
|
|
child: Text(
|
|
'Aktifkan Bluetooth',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
Switch(
|
|
value: _bluetoothState.isEnabled,
|
|
onChanged: (bool value) {
|
|
future() async {
|
|
if (value) {
|
|
await FlutterBluetoothSerial.instance
|
|
.requestEnable();
|
|
} else {
|
|
await FlutterBluetoothSerial.instance
|
|
.requestDisable();
|
|
}
|
|
await getPairedDevices();
|
|
_isButtonUnavailable = false;
|
|
if (_connected) {
|
|
_disconnect();
|
|
}
|
|
}
|
|
|
|
future().then((_) {
|
|
setState(() {});
|
|
});
|
|
},
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Stack(
|
|
children: <Widget>[
|
|
Column(
|
|
children: <Widget>[
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: 10),
|
|
child: Text(
|
|
"PERANGKAT TERPASANG",
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
color: Color.fromARGB(255, 0, 0, 0),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
const Text(
|
|
'Perangkat:',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(width: 10),
|
|
DropdownButton(
|
|
items: _getDeviceItems(),
|
|
onChanged: (value) async {
|
|
if (isConnected!) {
|
|
await _disconnect(); // Putuskan koneksi jika ada
|
|
}
|
|
setState(() => _device = value);
|
|
await _connectToSelectedDevice();
|
|
},
|
|
value: _devicesList.isNotEmpty ? _device : null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'Perangkat Ditambahkan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Spacer(),
|
|
IconButton(
|
|
icon: Icon(Icons.add, color: Colors.blue),
|
|
onPressed: () async {
|
|
if (!isConnected!) {
|
|
showMessage(
|
|
'Harap hubungkan Bluetooth terlebih dahulu');
|
|
return;
|
|
}
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text('Tambah Perangkat'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller:
|
|
bluetoothNameController,
|
|
decoration: const InputDecoration(
|
|
labelText:
|
|
'Nama Perangkat Bluetooth',
|
|
),
|
|
),
|
|
TextField(
|
|
controller:
|
|
bluetoothAddressController,
|
|
decoration: const InputDecoration(
|
|
labelText:
|
|
'Alamat Perangkat Bluetooth',
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 50,
|
|
child: TextField(
|
|
readOnly: true,
|
|
controller:
|
|
TextEditingController(
|
|
text:
|
|
_getNextAvailablePrefix()
|
|
.toString()
|
|
.padLeft(2, '0'),
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: 'Prefix',
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 10),
|
|
Expanded(
|
|
child: TextField(
|
|
controller:
|
|
passwordController,
|
|
decoration:
|
|
const InputDecoration(
|
|
labelText: 'Password',
|
|
),
|
|
obscureText: true,
|
|
keyboardType: TextInputType
|
|
.number, // Hanya angka yang diperbolehkan
|
|
inputFormatters: <TextInputFormatter>[
|
|
FilteringTextInputFormatter
|
|
.digitsOnly // Filter hanya angka
|
|
]),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text('Batal'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
String name =
|
|
bluetoothNameController.text;
|
|
String address =
|
|
bluetoothAddressController
|
|
.text;
|
|
String password =
|
|
passwordController.text;
|
|
|
|
bool addressExists =
|
|
_addedDevices.any((device) =>
|
|
device['address'] ==
|
|
address);
|
|
if (addressExists) {
|
|
showMessage(
|
|
'Alamat perangkat sudah ada dalam daftar');
|
|
return;
|
|
}
|
|
if (name.isNotEmpty &&
|
|
address.isNotEmpty &&
|
|
password.isNotEmpty &&
|
|
password.length >= 6) {
|
|
Navigator.of(context).pop();
|
|
int nextPrefix =
|
|
_getNextAvailablePrefix();
|
|
await addDeviceToESP32(
|
|
name,
|
|
address,
|
|
nextPrefix
|
|
.toString()
|
|
.padLeft(2, '0') +
|
|
password);
|
|
setState(() {
|
|
_addedDevices.add({
|
|
'name': name,
|
|
'address': address,
|
|
'prefix':
|
|
nextPrefix.toString()
|
|
});
|
|
saveAddedDevices();
|
|
bluetoothNameController
|
|
.clear();
|
|
bluetoothAddressController
|
|
.clear();
|
|
passwordController.clear();
|
|
});
|
|
await _disconnect();
|
|
} else {
|
|
showMessage(
|
|
'Nama, alamat, dan password tidak boleh kosong atau password kurang dari 6 karakter');
|
|
}
|
|
},
|
|
child: Text('Tambah'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
Table(
|
|
border: TableBorder.all(color: Colors.black),
|
|
columnWidths: const <int, TableColumnWidth>{
|
|
0: FlexColumnWidth(2),
|
|
1: FlexColumnWidth(2),
|
|
2: FlexColumnWidth(1),
|
|
},
|
|
defaultVerticalAlignment:
|
|
TableCellVerticalAlignment.middle,
|
|
children: [
|
|
TableRow(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text('Nama',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold)),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text('Alamat',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold)),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text('Hapus',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
..._addedDevices.map((device) {
|
|
return TableRow(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(device['name'] ?? ''),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(device['address'] ?? ''),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.delete,
|
|
color: Colors.red),
|
|
onPressed: () {
|
|
showDeleteConfirmationDialog(device);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
if (_displayedIP != null)
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'IP Address: $_displayedIP',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
color: Colors.blue,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<DropdownMenuItem<BluetoothDevice>> _getDeviceItems() {
|
|
List<DropdownMenuItem<BluetoothDevice>> items = [];
|
|
if (_devicesList.isEmpty) {
|
|
items.add(const DropdownMenuItem(
|
|
child: Text('NONE'),
|
|
));
|
|
} else {
|
|
_devicesList.forEach((device) {
|
|
items.add(DropdownMenuItem(
|
|
child: Text(device.name ?? ''),
|
|
value: device,
|
|
));
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
|
|
//Menghubungkan ke perangkat Bluetooth yang dipilih.
|
|
Future<void> _connectToSelectedDevice() async {
|
|
setState(() {
|
|
_isButtonUnavailable = true;
|
|
});
|
|
if (_device == null) {
|
|
showMessage('Tidak ada perangkat yang dipilih');
|
|
} else {
|
|
if (isConnected!) {
|
|
await _disconnect(); // Putuskan koneksi jika ada
|
|
}
|
|
await BluetoothConnection.toAddress(_device!.address).then((_connection) {
|
|
showMessage('Terhubung ke perangkat');
|
|
connection = _connection;
|
|
setState(() {
|
|
_connected = true;
|
|
});
|
|
connection!.input!.listen((Uint8List data) {
|
|
String response = utf8.decode(data);
|
|
print("Data diterima: $response");
|
|
if (response.contains('{"ip":"')) {
|
|
setState(() {
|
|
_displayedIP = jsonDecode(response)['ip'];
|
|
print("IP Address: $_displayedIP");
|
|
});
|
|
}
|
|
}).onDone(() {
|
|
if (isDisconnecting) {
|
|
showMessage('Terputus secara lokal');
|
|
} else {
|
|
showMessage('Terputus secara remote');
|
|
}
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
|
|
// Kirim perintah setelah berhasil terhubung
|
|
String message = jsonEncode({
|
|
"action": "get_ip",
|
|
});
|
|
connection!.output.add(utf8.encode(message + "\r\n"));
|
|
connection!.output.allSent;
|
|
}).catchError((error) {
|
|
showMessage('Tidak bisa terhubung, terjadi kesalahan');
|
|
debugPrint(error.toString());
|
|
});
|
|
setState(() => _isButtonUnavailable = false);
|
|
}
|
|
}
|
|
|
|
//Memutuskan koneksi Bluetooth.
|
|
Future<void> _disconnect() async {
|
|
await connection?.close();
|
|
showMessage('Perangkat terputus');
|
|
if (!connection!.isConnected) {
|
|
setState(() {
|
|
_connected = false;
|
|
_isButtonUnavailable = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
//Menambahkan perangkat ke ESP32 dengan mengirimkan data melalui Bluetooth.
|
|
Future<void> addDeviceToESP32(
|
|
String name, String address, String password) async {
|
|
if (isConnected!) {
|
|
String message = jsonEncode({
|
|
"action": "add",
|
|
"name": name,
|
|
"address": address,
|
|
"password": password
|
|
});
|
|
connection!.output.add(utf8.encode(message + "\r\n"));
|
|
await connection!.output.allSent;
|
|
}
|
|
}
|
|
|
|
//Menghapus perangkat dari ESP32 dengan mengirimkan data melalui Bluetooth.
|
|
Future<void> deleteDeviceFromESP32(String address) async {
|
|
if (isConnected!) {
|
|
String message = jsonEncode({
|
|
"action": "delete",
|
|
"address": address,
|
|
});
|
|
connection!.output.add(utf8.encode(message + "\r\n"));
|
|
await connection!.output.allSent;
|
|
}
|
|
}
|
|
|
|
void showDeleteConfirmationDialog(Map<String, String> device) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text('Konfirmasi Hapus'),
|
|
content: Text('Apakah Anda yakin ingin menghapus perangkat ini?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text('Tidak'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
if (!isConnected!) {
|
|
showMessage('Harap hubungkan Bluetooth terlebih dahulu');
|
|
return;
|
|
}
|
|
Navigator.of(context).pop();
|
|
await deleteDeviceFromESP32(device['address']!);
|
|
setState(() {
|
|
_addedDevices.remove(device);
|
|
saveAddedDevices();
|
|
});
|
|
await _disconnect();
|
|
},
|
|
child: Text('Iya'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|