This commit is contained in:
Aditya Pulipaka
2025-07-10 18:52:04 -05:00
commit e0a41761ec
166 changed files with 8444 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class BlindmasterProgressIndicator extends StatelessWidget {
const BlindmasterProgressIndicator({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Center(
child: CircularProgressIndicator(
color: Theme.of(context).primaryColorDark,
)
)
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
SnackBar errorSnackbar(
Object e, {
Color backgroundColor = const Color.fromARGB(255, 196, 26, 14),
Duration duration = const Duration(seconds: 3),
}) {
return SnackBar(
backgroundColor: Color.fromARGB(255, 196, 26, 14),
content: Text(
e.toString().replaceFirst(RegExp(r'^[^:]+:\s*'), ''),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Colors.white
)
)
);
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
PageRouteBuilder<dynamic> fadeTransition(Widget nextScreen) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => nextScreen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: Duration(milliseconds: 500)
);
}

View File

@@ -0,0 +1,113 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:socket_io_client/socket_io_client.dart' as IO;
String local = Platform.isAndroid ? '10.0.2.2' : 'localhost';
String fromDevice = '192.168.1.190';
String host = local;
int port = 3000;
String socketString = "$scheme://$host:$port";
String scheme = 'http';
Future<http.Response?> secureGet(String path, {Map<String, dynamic>? queryParameters}) async{
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'token');
if (token == null) return null;
final uri = Uri(
scheme: scheme,
host: host,
port: port, // your host
path: path, // your path
queryParameters: queryParameters?.map((key, value) => MapEntry(key, value.toString())),
);
return await http
.get(
uri,
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
)
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
}
Future<http.Response?> securePost(Map<String, dynamic> payload, String path) async{
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'token');
if (token == null) return null;
final uri = Uri(
scheme: scheme,
host: host,
port: port, // your host
path: path, // your path
);
return await http.post(
uri,
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: json.encode(payload),
)
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
}
Future<http.Response> regularGet(String path) async {
final uri = Uri(
scheme: scheme,
host: host,
port: port, // your host
path: path, // your path
);
return await http.get(
uri,
headers: {
'Content-Type': 'application/json',
}
)
.timeout(const Duration(seconds: 10)); // 🚀 Timeout added
}
Future<http.Response> regularPost(Map<String, dynamic> payload, String path) async{
final uri = Uri(
scheme: scheme,
host: host,
port: port, // your host
path: path, // your path
);
return await http.post(
uri,
headers: {
'Content-Type': 'application/json',
},
body: json.encode(payload)
).timeout(const Duration(seconds: 10)); // 🚀 Timeout added
}
Future<IO.Socket?> connectSocket() async {
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'token');
if (token == null) return null;
final socket = IO.io(
socketString,
IO.OptionBuilder()
.setTransports(['websocket'])
.setAuth({'token': token})
.disableAutoConnect().build(),
);
socket.connect();
return socket;
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class BlindMasterMainInput extends StatelessWidget {
const BlindMasterMainInput(this.label, {super.key, this.controller, this.validator, this.color, this.password = false});
final String label;
final TextEditingController? controller;
final Color? color;
final bool password;
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10),
child:TextFormField(
validator: validator,
obscureText: password,
enableSuggestions: false,
autocorrect: false,
controller: controller,
style: TextStyle(
color: color
),
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
labelText: label,
labelStyle: TextStyle(color: color),
contentPadding: EdgeInsets.all(10),
),
)
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class TitleText extends StatelessWidget {
const TitleText(this.text, {super.key, this.txtClr});
final String text;
final Color? txtClr;
@override
Widget build(BuildContext context) {
return Text(
text,
style: GoogleFonts.aBeeZee(
color: txtClr,
fontSize: 50
),
textAlign: TextAlign.center,
);
}
}

View File

@@ -0,0 +1,152 @@
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:flutter/material.dart';
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
import 'package:blind_master/BlindMasterResources/title_text.dart';
class CreateUserScreen extends StatefulWidget {
const CreateUserScreen({super.key});
@override
State<CreateUserScreen> createState() => _CreateUserScreenState();
}
class _CreateUserScreenState extends State<CreateUserScreen> {
final emailController = TextEditingController();
final nameController = TextEditingController();
final passwordController = TextEditingController();
final _passFormKey = GlobalKey<FormState>();
final _emailFormKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
}
Future<void> attemptCreate() async{
try {
if (!_emailFormKey.currentState!.validate() || !_passFormKey.currentState!.validate()) {
throw Exception('Invalid information entered!');
}
final payload = {
'name': nameController.text.trim(),
'email': emailController.text.trim(),
'password': passwordController.text,
};
final response = await regularPost(payload, 'create_user');
if (response.statusCode == 201) {
if(mounted) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green[800],
content: Text(
"Account Successfully Created!",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15),
),
)
);
Navigator.pop(context);
}
} else {
if (response.statusCode == 409) throw Exception('Email Already In Use!');
throw Exception('Create failed: ${response.statusCode}');
}
} catch(e) {
if (!mounted) return;
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
return;
}
}
String? confirmPasswordValidator(String? input) {
if (input == passwordController.text) {
return null;
}
else {
return "Passwords do not match!";
}
}
String? emailValidator(String? input) {
if (input == null || input.isEmpty) {
return 'Email is required';
}
final emailPattern = r'^[^@]+@[^@]+\.[^@]+$';
if (!RegExp(emailPattern).hasMatch(input)) {
return 'Enter a valid email address';
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Theme.of(context).primaryColorLight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TitleText("Create Account", txtClr: Colors.white),
Container(
padding: EdgeInsets.fromLTRB(40, 10, 40, 10),
child: Column(
children: [
BlindMasterMainInput("Preferred Name (optional)", controller: nameController),
SizedBox(height: 20),
Form(
key: _emailFormKey,
autovalidateMode: AutovalidateMode.onUnfocus,
child: BlindMasterMainInput(
"Email",
controller: emailController,
validator: emailValidator,
),
),
Form(
key: _passFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
BlindMasterMainInput(
"Password",
password: true,
controller: passwordController
),
BlindMasterMainInput(
"Confirm Password",
validator: confirmPasswordValidator,
password: true,
)
],
)
)
],
),
),
ElevatedButton(
onPressed: attemptCreate,
child: Text(
"Create"
),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterScreens/Startup/create_user_screen.dart';
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/fade_transition.dart';
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
import 'package:blind_master/BlindMasterResources/title_text.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool inputWrong = false;
@override
void initState() {
super.initState();
}
@override
void dispose() {
// Clean up the controller when the widget is removed from the
// widget tree.
emailController.dispose();
passwordController.dispose();
super.dispose();
}
Future<void> attemptSignIn() async {
final email = emailController.text.trim();
final password = passwordController.text;
final payload = {
'email': email,
'password': password,
}; // query parameters
try {
final response = await regularPost(payload, 'login');
if (response.statusCode != 200) {
if (response.statusCode == 400) {throw Exception('Email and Password Necessary');}
else if (response.statusCode == 500) {throw Exception('Server Error');}
else {throw Exception('Incorrect email or password');}
}
final body = json.decode(response.body) as Map<String, dynamic>;
final token = body['token'] as String;
if (token.isEmpty) throw Exception('Token Not Received');
final storage = FlutterSecureStorage();
await storage.write(key: 'token', value: token);
} catch(e) {
if (!mounted) return;
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
return;
}
if (!mounted) return;
Navigator.pushReplacement(
context,
fadeTransition(HomeScreen()),
);
}
void switchToCreate() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CreateUserScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Theme.of(context).primaryColorLight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TitleText("BlindMaster"),
Container(
padding: EdgeInsets.fromLTRB(40, 10, 40, 10),
child: Column(
children: [
BlindMasterMainInput("Email", controller: emailController),
BlindMasterMainInput("Password", controller: passwordController, password: true,),
],
),
),
Container(
padding: EdgeInsets.only(bottom:10),
child: ElevatedButton(
onPressed: attemptSignIn,
child: Text(
"Log In"
),
)
),
ElevatedButton(
onPressed: switchToCreate,
child: Text(
"Create Account"
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:async';
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
import 'package:blind_master/BlindMasterScreens/Startup/login_screen.dart';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/fade_transition.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
Widget nextScreen = LoginScreen();
@override
void initState() {
super.initState();
_routeNext();
}
Future<void> _routeNext() async {
await verifyToken();
_animateBackgroundBasedOnTime();
}
Future<void> verifyToken() async{
final storage = FlutterSecureStorage();
try {
http.Response? response = await secureGet('verify');
if (response == null) {
nextScreen = LoginScreen();
return;
}
if (response.statusCode == 200) {
final body = json.decode(response.body) as Map<String, dynamic>;
final newToken = body['token'];
if (newToken != null) {
await storage.write(key: 'token', value: newToken); // ✅ Rotate
}
nextScreen = HomeScreen();
} else {
nextScreen = LoginScreen();
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
nextScreen = LoginScreen();
}
}
void _animateBackgroundBasedOnTime() {
// Optionally navigate to your main app after a short delay
Timer(const Duration(seconds: 3), () {
Navigator.pushReplacement(
context,
// MaterialPageRoute(builder: (context) => const MainAppScreen())
fadeTransition(nextScreen),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedContainer(
duration: const Duration(milliseconds: 500),
color: Theme.of(context).primaryColorLight,
child: Center(
child: Image.asset('assets/images/2xwhite.png', height: 250)
),
),
);
}
}

View File

@@ -0,0 +1,191 @@
import 'dart:async';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterScreens/addingDevices/device_setup.dart';
import 'package:blind_master/utils_from_FBPExample/extra.dart';
import 'package:blind_master/utils_from_FBPExample/scan_result_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:google_fonts/google_fonts.dart';
class AddDevice extends StatefulWidget {
const AddDevice({super.key});
@override
State<AddDevice> createState() => _AddDeviceState();
}
class _AddDeviceState extends State<AddDevice> {
List<ScanResult> _scanResults = [];
bool _isScanning = false;
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
late StreamSubscription<bool> _isScanningSubscription;
BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _adapterStateSubscription;
bool _isConnecting = false;
@override
void initState() {
super.initState();
initBluetoothandStartScan();
}
@override
void dispose() {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
_adapterStateSubscription.cancel();
super.dispose();
}
Future<void> _startScan() async {
try {
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: 15),
withServices: [
Guid("181C"),
],
webOptionalServices: [
Guid("181C"), // user input
],
);
} catch (e, backtrace) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
print("backtrace: $backtrace");
}
setState(() {});
}
Future<void> initBluetoothandStartScan() async {
_adapterStateSubscription = FlutterBluePlus.adapterState.listen((state) {
if (mounted) {
setState(() => _adapterState = state);
if (state == BluetoothAdapterState.on) {
_startScan();
}
}
});
_scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
if (mounted) {
setState(() => _scanResults = results);
// setState(() => _scanResults = results.where((r) => r.device.platformName == "BlindMaster").toList());
}
}, onError: (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
});
_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
if (mounted) {
setState(() => _isScanning = state);
}
});
}
Future onConnectPressed(BluetoothDevice device) async {
if (_isConnecting) return;
_isConnecting = true;
await device.connectAndUpdateStream().catchError((e) {
if(!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
});
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DeviceSetup(device: device))
).then((_) {
device.disconnectAndUpdateStream().catchError((e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
});
_isConnecting = false;
});
}
Future onRefresh() async {
if (_isScanning == false) {
await _startScan();
}
if (mounted) {
setState(() {});
}
return Future.delayed(Duration(milliseconds: 500));
}
Widget _buildScanResultTiles() {
final res = _scanResults.where((r) => r.advertisementData.advName == "BlindMaster Device" && r.rssi > -55);
return (res.isNotEmpty)
? ListView(
children: [
...res.map((r) => ScanResultTile(result: r, onTap: () => onConnectPressed(r.device)))
],
)
: _isScanning ? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Theme.of(context).primaryColorLight,
),
SizedBox(
height: 10,
),
Text(
"Nothing Yet...",
)
]
),
)
: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: const Center(
child: Text(
"No BlindMaster devices found nearby",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Add a Device",
style: GoogleFonts.aBeeZee(),
),
backgroundColor: Theme.of(context).primaryColorLight,
),
body: _adapterState != BluetoothAdapterState.on
? SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: const Center(
child: Text(
"Bluetooth is off.\nPlease turn it on to scan for devices.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
)
)
: RefreshIndicator(
onRefresh: onRefresh,
child: _buildScanResultTiles()
),
);
}
}

View File

@@ -0,0 +1,329 @@
import 'dart:async';
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterScreens/addingDevices/set_device_name.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:google_fonts/google_fonts.dart';
class DeviceSetup extends StatefulWidget {
final BluetoothDevice device;
const DeviceSetup({super.key, required this.device});
@override
State<DeviceSetup> createState() => _DeviceSetupState();
}
class _DeviceSetupState extends State<DeviceSetup> {
List<BluetoothService> _services = [];
List<String> openNetworks = [];
List<String> pskNetworks = [];
late StreamSubscription<List<int>> _ssidSub;
StreamSubscription<List<int>>? _confirmSub;
Widget? wifiList;
String? message;
String? password;
final passControl = TextEditingController();
@override void initState() {
super.initState();
initSetup();
}
@override
void dispose() {
_ssidSub.cancel();
_confirmSub?.cancel();
passControl.dispose();
super.dispose();
}
Future setWifiListListener(BluetoothCharacteristic ssidListChar) async {
setState(() {
wifiList = null;
});
await ssidListChar.setNotifyValue(true);
_ssidSub = ssidListChar.onValueReceived.listen((List<int> value) {
List<String> ssidList = [];
bool noNetworks = false;
try {
final val = utf8.decode(value);
if (val == ';') noNetworks = true;
ssidList = val
.split(';')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
openNetworks = ssidList
.where((s) => s.split(',')[1] == "OPEN")
.map((s) => s.split(',')[0])
.toList();
pskNetworks = ssidList
.where((s) => s.split(',')[1] == "SECURED")
.map((s) => s.split(',')[0])
.toList();
} catch (e) {
if(!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
setState(() {
wifiList = noNetworks
? SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: const Center(
child: Text(
"No networks found...",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15),
),
),
),
)
: ssidList.isNotEmpty
? ListView(
children: [
...buildSSIDs()
],
)
: null;
});
});
}
List<Widget> buildSSIDs() {
List<Widget> open = openNetworks.map((s) {
return Card(
child: ListTile(
leading: const Icon(Icons.wifi),
title: Text(s),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () {
openConnect(s);
},
),
);
}).toList();
List<Widget> secure = pskNetworks.map((s) {
return Card(
child: ListTile(
leading: const Icon(Icons.wifi_password),
title: Text(s),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () {
setPassword(s);
},
),
);
}).toList();
return open + secure;
}
Future openConnect(String ssid) async {
await transmitWiFiDetails(ssid, "");
}
Future discoverServices() async{
try {
_services = await widget.device.discoverServices();
} catch (e) {
if(!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
return;
}
try {
_services = _services.where((s) => s.uuid.str.toUpperCase() == "181C").toList();
if (_services.length != 1) throw Exception("Invalid Bluetooth Broadcast");
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
return;
}
}
Future initSetup() async {
await discoverServices();
final ssidListChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0000");
await setWifiListListener(ssidListChar);
refreshWifiList();
}
Future setPassword(String ssid) async {
String? password = await showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(
ssid,
style: GoogleFonts.aBeeZee(),
),
content: Form(
autovalidateMode: AutovalidateMode.onUnfocus,
child: TextFormField(
controller: passControl,
obscureText: true,
decoration: const InputDecoration(hintText: "Enter password"),
validator: (value) {
if (value == null) return "null input";
if (value.length < 8) return "not long enough!";
return null;
},
textInputAction: TextInputAction.send,
onFieldSubmitted: (value) => Navigator.pop(dialogContext, passControl.text),
),
),
actions: [
TextButton(
onPressed: () {
passControl.clear();
Navigator.pop(dialogContext);
},
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext, passControl.text);
passControl.clear();
},
child: const Text("Connect"),
),
],
);
}
);
await transmitWiFiDetails(ssid, password);
}
Future transmitWiFiDetails(String ssid, String? password) async {
if (password == null) return;
setState(() {
wifiList = null;
message = "Attempting Connection...";
});
final ssidEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0001");
try {
try {
await ssidEntryChar.write(utf8.encode(ssid), withoutResponse: ssidEntryChar.properties.writeWithoutResponse);
} catch (e) {
throw Exception("SSID Write Error");
}
} catch (e){
if(!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
refreshWifiList();
return;
}
final passEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0002");
try {
try {
await passEntryChar.write(utf8.encode(password), withoutResponse: passEntryChar.properties.writeWithoutResponse);
} catch (e) {
throw Exception("Password Write Error");
}
} catch (e){
if(!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
refreshWifiList();
return;
}
final connectConfirmChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0005");
final tokenEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0003");
await connectConfirmChar.setNotifyValue(true);
_confirmSub = connectConfirmChar.onValueReceived.listen((List<int> connectVal) {
try {
final connectResponse = utf8.decode(connectVal);
if (connectResponse == "Connected") {
if (!mounted) return;
_confirmSub?.cancel();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SetDeviceName(tokenEntryChar: tokenEntryChar, device: widget.device),
)
).then((_) {
if (widget.device.isConnected) {
refreshWifiList();
}
});
} else if (connectResponse == "Error") {
_confirmSub?.cancel();
throw Exception("SSID/Password Incorrect");
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
refreshWifiList();
return;
}
});
}
Future refreshWifiList() async{
final ssidRefreshChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0004");
setState(() {
message = null;
});
try {
try {
await ssidRefreshChar.write(utf8.encode("refresh"), withoutResponse: ssidRefreshChar.properties.writeWithoutResponse);
} catch (e) {
throw Exception ("Refresh Error");
}
} catch (e) {
if (!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Select WiFi Network",
style: GoogleFonts.aBeeZee(),
),
backgroundColor: Theme.of(context).primaryColorLight,
),
body: RefreshIndicator(
onRefresh: refreshWifiList,
child: wifiList ?? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Theme.of(context).primaryColorLight,
),
SizedBox(
height: 10,
),
Text(
message ?? "Fetching Networks...",
textAlign: TextAlign.center,
)
]
),
)
),
);
}
}

View File

@@ -0,0 +1,128 @@
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
import 'package:blind_master/BlindMasterScreens/home_screen.dart';
import 'package:blind_master/utils_from_FBPExample/extra.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:google_fonts/google_fonts.dart';
class SetDeviceName extends StatefulWidget {
const SetDeviceName({super.key, required this.tokenEntryChar, required this.device});
final BluetoothCharacteristic tokenEntryChar;
final BluetoothDevice device;
@override
State<SetDeviceName> createState() => _SetDeviceNameState();
}
class _SetDeviceNameState extends State<SetDeviceName> {
final deviceNameController = TextEditingController();
Widget? screen;
@override
void initState() {
initScreen();
super.initState();
}
@override
void dispose() {
deviceNameController.dispose();
super.dispose();
}
void initScreen() {
screen = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlindMasterMainInput(
"Device Name (Different from others)",
controller: deviceNameController,
),
ElevatedButton(
onPressed: onPressed,
child: Text(
"Add to Account"
)
),
],
);
}
Future addDevice(String name) async {
final payload = {'deviceName': name};
String? token;
try {
final tokenResponse = await securePost(payload, 'add_device');
if (tokenResponse == null) return;
if (tokenResponse.statusCode != 201) {
if (tokenResponse.statusCode == 404) {throw Exception("Somehow the id of your device wasn't found??");}
else if (tokenResponse.statusCode == 409) {throw Exception('Device Name in Use');}
else {throw Exception('Server Error');}
}
final jsonResponse = json.decode(tokenResponse.body) as Map<String, dynamic>;
final fetchedToken = jsonResponse['token'];
if (fetchedToken == null || fetchedToken is! String) {
throw Exception('Invalid token in response');
}
token = fetchedToken;
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
return;
}
try {
try {
await widget.tokenEntryChar.write(utf8.encode(token), withoutResponse: widget.tokenEntryChar.properties.writeWithoutResponse);
} catch (e) {
throw Exception("Token Write Error");
}
} catch (e){
if(!mounted)return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
return;
}
await widget.device.disconnectAndUpdateStream().catchError((e) {
if(!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
});
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
(route) => false,
);
}
Future onPressed() async {
setState(() {
screen = null;
});
await(addDevice(deviceNameController.text));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Name Your Device",
style: GoogleFonts.aBeeZee(),
),
backgroundColor: Theme.of(context).primaryColorLight,
),
body: SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: Center(
child: screen ?? CircularProgressIndicator(color: Theme.of(context).primaryColorLight),
)
)
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class GroupsMenu extends StatefulWidget {
const GroupsMenu({super.key});
@override
State<GroupsMenu> createState() => _GroupsMenuState();
}
class _GroupsMenuState extends State<GroupsMenu> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -0,0 +1,75 @@
import 'package:blind_master/BlindMasterScreens/groupControl/groups_menu.dart';
import 'package:blind_master/BlindMasterScreens/individualControl/devices_menu.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int currentPageIndex = 0;
String greeting = "";
@override
void initState() {
super.initState();
getGreeting();
}
void getGreeting() {
final hour = DateTime.now().hour;
if (hour >= 5 && hour < 12) {
greeting = "Good Morning!";
} else if (hour >= 12 && hour < 18) {
greeting = "Good Afternoon!";
} else if (hour >= 18 && hour < 22) {
greeting = "Good Evening!";
} else {greeting = "😴";}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).primaryColorLight,
centerTitle: false,
title: Text(
greeting,
style: GoogleFonts.aBeeZee(),
),
foregroundColor: Colors.white,
),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
setState(() {
currentPageIndex = index;
});
},
indicatorColor: Theme.of(context).primaryColorDark,
selectedIndex: currentPageIndex,
destinations: const <Widget>[
NavigationDestination(
selectedIcon: Icon(Icons.blinds_rounded),
icon: Icon(Icons.blinds_closed_rounded),
label: 'Devices',
),
NavigationDestination(
icon: Icon(Icons.window_outlined),
selectedIcon: Icon(Icons.window_rounded),
label: 'Groups',
),
],
),
body:
<Widget>[
DevicesMenu(),
GroupsMenu(),
][currentPageIndex],
);
}
}

View File

@@ -0,0 +1,406 @@
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
import 'package:blind_master/BlindMasterScreens/individualControl/peripheral_screen.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class DeviceScreen extends StatefulWidget {
final int deviceId;
const DeviceScreen({super.key, required this.deviceId});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen> {
bool enabled = false;
final _newPeripheralNameController = TextEditingController();
final _hubRenameController = TextEditingController();
List<Map<String, dynamic>> peripherals = [];
List occports = [];
Widget? peripheralList;
String deviceName = "...";
@override
void initState() {
super.initState();
initAll();
}
Future initAll() async {
await getDeviceName();
await populatePeripherals();
}
Future getDeviceName() async {
try {
final payload = {
"deviceId": widget.deviceId
};
final response = await secureGet('device_name', queryParameters: payload);
if (response == null) throw Exception("no response!");
if (response.statusCode == 200) {
final body = json.decode(response.body) as Map<String, dynamic>;
setState(() {
deviceName = body['device_name'];
});
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future populatePeripherals() async {
setState(() {
peripheralList = null;
});
try {
final payload = {
"deviceId": widget.deviceId
};
final response = await secureGet('peripheral_list', queryParameters: payload);
if (response == null) throw Exception("no response!");
if (response.statusCode == 200) {
final body = json.decode(response.body) as Map<String, dynamic>;
final names = body['peripheral_names'] as List;
final ids = body['peripheral_ids'] as List;
occports = body['port_nums'] as List;
peripherals = List.generate(names.length, (i) => {
'id': ids[i],
'name': names[i],
'port': occports[i]
});
peripherals.sort((a, b) => (a['port'] as int).compareTo(b['port'] as int));
enabled = peripherals.length < 4;
}
setState(() {
peripheralList = RefreshIndicator(
onRefresh: populatePeripherals,
child: peripherals.isEmpty ? SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: const Center(
child: Text(
"No peripherals found...\nAdd one using the '+' button",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
) : ListView.builder(
itemCount: peripherals.length,
itemBuilder: (context, i) {
final peripheral = peripherals[i];
return Dismissible(
key: Key(peripheral['id'].toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
// Ask for confirmation (optional)
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Peripheral'),
content: const Text('Are you sure you want to delete this peripheral?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
);
},
onDismissed: (direction) => deletePeripheral(peripheral['id'], i),
child: Card(
child: ListTile(
leading: const Icon(Icons.blinds),
title: Text(peripheral['name']),
subtitle: Text("Port #${peripheral['port']}"),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PeripheralScreen(peripheralId: peripheral['id'],
peripheralNum: peripheral['port'], deviceId: widget.deviceId,),
),
).then((_) { populatePeripherals(); });
},
),
),
);
},
),
);
});
return Future.delayed(Duration(milliseconds: 500));
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future deletePeripheral(int id, int i) async {
setState(() {
peripherals.removeAt(i);
peripheralList = null;
});
final payload = {
'periphId': id,
};
try {
final response = await securePost(payload, 'delete_peripheral');
if (response == null) return;
if (response.statusCode != 204) {
if (response.statusCode == 404) {throw Exception('Device Not Found');}
else if (response.statusCode == 500) {throw Exception('Server Error');}
}
if (mounted){
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Deleted',
textAlign: TextAlign.center,
)
),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
populatePeripherals();
}
void addPeripheral() {
var freePorts = <int>{};
for (int i = 1; i < 5; i++) {
freePorts.add(i);
}
freePorts = freePorts.difference(occports.toSet());
int? port = freePorts.firstOrNull;
showDialog(
context: context,
builder: (BuildContext dialogContext) { // Use dialogContext for navigation within the dialog
return AlertDialog(
title: Text(
'New Peripheral',
style: GoogleFonts.aBeeZee(),
),
content: Column(
mainAxisSize: MainAxisSize.min, // Keep column compact
children: <Widget>[
TextFormField(
controller: _newPeripheralNameController,
decoration: const InputDecoration(
labelText: 'Peripheral Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
DropdownButtonFormField<int>(
value: port,
decoration: const InputDecoration(
labelText: 'Hub Port',
border: OutlineInputBorder(),
),
items: freePorts.map((int number) {
return DropdownMenuItem<int>(
value: number,
child: Text('$number'),
);
}).toList(),
onChanged: (int? newValue) {
setState(() {
port = newValue;
});
},
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: const Text(
"Cancel",
style: TextStyle(
color: Colors.red
),
)
),
ElevatedButton(
onPressed: () {
uploadPeriphData(_newPeripheralNameController.text, port);
Navigator.of(dialogContext).pop();
},
child: const Text("Add"),
),
]
)
],
);
}
);
}
Future uploadPeriphData(String name, int? port) async {
try {
if (name.isEmpty || port == null) {
throw Exception("Name and Port Required");
}
final payload = {
'device_id': widget.deviceId,
'port_num': port,
'peripheral_name': name
};
final response = await securePost(payload, 'add_peripheral');
if (response == null) throw Exception("Auth Error");
if (response.statusCode != 201) {
if (response.statusCode == 409) throw Exception("Choose a unique name!");
throw Exception("Server Error");
}
populatePeripherals();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
void rename() {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
"Rename Hub",
style: GoogleFonts.aBeeZee(),
),
content: BlindMasterMainInput("New Hub Name", controller: _hubRenameController,),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: const Text(
"Cancel",
style: TextStyle(
color: Colors.red
),
)
),
ElevatedButton(
onPressed: () {
updateHubName(_hubRenameController.text, widget.deviceId);
Navigator.of(dialogContext).pop();
},
child: const Text("Confirm")
)
],
)
],
);
}
);
}
Future updateHubName(String name, int id) async {
try {
if (name.isEmpty) throw Exception("New name cannot be empty!");
final payload = {
'deviceId': id,
'newName': name,
};
final response = await securePost(payload, 'rename_device');
if (response == null) throw Exception("Auth Error");
if (response.statusCode != 204) {
if (response.statusCode == 409) throw Exception("Choose a unique name!");
throw Exception("Server Error");
}
getDeviceName();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
deviceName,
style: GoogleFonts.aBeeZee(),
),
backgroundColor: Theme.of(context).primaryColorLight,
foregroundColor: Colors.white,
),
body: peripheralList ?? SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Center(
child: CircularProgressIndicator(
color: Theme.of(context).primaryColorLight,
),
)
),
floatingActionButton: Container(
padding: EdgeInsets.all(25),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
foregroundColor: Theme.of(context).highlightColor,
heroTag: "rename",
onPressed: rename,
tooltip: "Rename Hub",
child: Icon(Icons.drive_file_rename_outline_sharp),
),
FloatingActionButton(
backgroundColor: enabled
? Theme.of(context).primaryColorDark
: Theme.of(context).disabledColor,
foregroundColor: Theme.of(context).highlightColor,
heroTag: "add",
onPressed: enabled ? addPeripheral : null,
tooltip: "Add Peripheral",
child: Icon(Icons.add),
)
],
)
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}

View File

@@ -0,0 +1,175 @@
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterScreens/addingDevices/add_device.dart';
import 'package:blind_master/BlindMasterScreens/individualControl/device_screen.dart';
import 'package:flutter/material.dart';
class DevicesMenu extends StatefulWidget {
const DevicesMenu({super.key});
@override
State<DevicesMenu> createState() => _DevicesMenuState();
}
class _DevicesMenuState extends State<DevicesMenu> {
List<Map<String, dynamic>> devices = [];
Widget? deviceList;
@override
void initState() {
super.initState();
getDevices();
}
Future getDevices() async {
try{
final response = await secureGet('device_list');
if (response == null) throw Exception("no response!");
if (response.statusCode == 200) {
final body = json.decode(response.body) as Map<String, dynamic>;
final names = body['devices'] as List;
final ids = body['device_ids'] as List;
devices = List.generate(names.length, (i) => {
'id': ids[i],
'name': names[i],
});
}
} catch(e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
errorSnackbar(e)
);
}
setState(() {
deviceList = RefreshIndicator(
onRefresh: getDevices,
child: devices.isEmpty
? SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: const Center(
child: Text(
"No hubs found...\nAdd one using the '+' button",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
)
: ListView.builder(
itemCount: devices.length,
itemBuilder: (context, i) {
final device = devices[i];
return Dismissible(
key: Key(device['id'].toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
// Ask for confirmation (optional)
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Hub'),
content: const Text('Are you sure you want to delete this hub?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
);
},
onDismissed: (direction) {
// Actually delete the device
deleteDevice(device['id'], i);
},
child: Card(
child: ListTile(
leading: const Icon(Icons.blinds),
title: Text(device['name']),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DeviceScreen(deviceId: device['id']),
),
).then((_) { getDevices(); });
},
),
),
);
},
),
);
});
return Future.delayed(Duration(milliseconds: 500));
}
Future deleteDevice(int id, int i) async {
setState(() {
devices.removeAt(i);
deviceList = null;
});
print("deleting");
final payload = {
'deviceId': id,
};
try {
final response = await securePost(payload, 'delete_device');
if (response == null) return;
if (response.statusCode != 204) {
if (response.statusCode == 404) {throw Exception('Device Not Found');}
else if (response.statusCode == 500) {throw Exception('Server Error');}
}
if (mounted){
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Deleted',
textAlign: TextAlign.center,
)
),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
getDevices();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: deviceList ?? const Center(child: CircularProgressIndicator()),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddDevice()),
);
},
foregroundColor: Theme.of(context).highlightColor,
backgroundColor: Theme.of(context).primaryColorDark,
child: Icon(Icons.add),
),
);
}
}

View File

@@ -0,0 +1,524 @@
import 'dart:convert';
import 'package:blind_master/BlindMasterResources/blindmaster_progress_indicator.dart';
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
import 'package:blind_master/BlindMasterResources/text_inputs.dart';
import 'package:blind_master/BlindMasterScreens/schedules_screen.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
class PeripheralScreen extends StatefulWidget {
const PeripheralScreen({super.key, required this.peripheralId, required this.deviceId, required this.peripheralNum});
final int peripheralId;
final int peripheralNum;
final int deviceId;
@override
State<PeripheralScreen> createState() => _PeripheralScreenState();
}
class _PeripheralScreenState extends State<PeripheralScreen> {
IO.Socket? socket;
String imagePath = "";
String peripheralName = "...";
bool loaded = false;
bool calibrated = false;
bool calibrating = false;
double _blindPosition = 5.0;
DateTime? lastSet;
String lastSetMessage = "";
final _peripheralRenameController = TextEditingController();
void getImage() {
final hour = DateTime.now().hour;
if (hour >= 5 && hour < 10) {
imagePath = 'assets/images/MorningSill.png';
} else if (hour >= 10 && hour < 18) {
imagePath = 'assets/images/NoonSill.png';
} else if (hour >= 18 && hour < 22) {
imagePath = 'assets/images/EveningSill.png';
} else {
imagePath = 'assets/images/NightSill.png';
}
}
@override
void initState() {
super.initState();
initAll();
initSocket();
}
@override
void dispose() {
socket?.disconnect();
socket?.dispose();
super.dispose();
}
Future<void> initSocket() async {
try {
socket = await connectSocket();
if (socket == null) throw Exception("Unsuccessful socket connection");
socket?.on("success", (_) {
socket?.on("posUpdates", (list) {
for (var update in list) {
if (update is Map<String, dynamic>) {
if (update['periphID'] == widget.peripheralId) {
if (!mounted) return;
setState(() {
_blindPosition = (update['pos'] as int).toDouble();
});
}
}
}
});
socket?.on("calib", (periphData) {
if (periphData is Map<String, dynamic>) {
if (periphData['periphID'] == widget.peripheralId) {
if (!mounted) return;
setState(() {
calibrating = true;
calibrated = false;
});
}
}
});
socket?.on("calib_done", (periphData) {
if (periphData is Map<String, dynamic>) {
if (periphData['periphID'] == widget.peripheralId) {
if (!mounted) return;
setState(() {
calibrating = false;
calibrated = true;
});
}
}
});
});
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future<void> calibrate() async {
try {
final payload = {
'periphId': widget.peripheralId
};
final response = await securePost(payload, 'calib');
if (response == null) throw Exception("auth error");
if (response.statusCode != 202) throw Exception("Server Error");
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future<void> cancelCalib() async {
try {
final payload = {
'periphId': widget.peripheralId
};
final response = await securePost(payload, 'cancel_calib');
if (response == null) throw Exception("auth error");
if (response.statusCode != 202) throw Exception("Server Error");
setState(() {
calibrated = false;
calibrating = false;
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future<void> getName() async {
try {
final payload = {
'periphId': widget.peripheralId
};
final response = await secureGet('peripheral_name', queryParameters: payload);
if (response == null) throw Exception("auth error");
if (response.statusCode != 200) throw Exception("Server Error");
final body = json.decode(response.body);
setState(() => peripheralName = body['name']);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future loop() async{
try {
final payload = {
'periphId': widget.peripheralId
};
final response = await secureGet('peripheral_status', queryParameters: payload);
if (response == null) throw Exception("auth error");
if (response.statusCode != 200) {
if (response.statusCode == 404) throw Exception("Device Not Found");
throw Exception("Server Error");
}
final body = json.decode(response.body) as Map<String, dynamic>;
if (!body['await_calib']){
if (!body['calibrated']) {
calibrated = false;
calibrating = false;
}
else {
getImage();
final nowUtc = DateTime.now().toUtc();
final lastSetUtc = DateTime.parse(body['last_set']);
final Duration difference = nowUtc.difference(lastSetUtc);
if (!lastSetUtc.isUtc) throw Exception("Why isn't the server giving UTC?");
final diffDays = difference.inDays > 0;
final diffHours = difference.inHours > 0;
final diffMins = difference.inMinutes > 0;
lastSetMessage = "Last set ${diffDays ? '${difference.inDays.toString()} days' : diffHours ? '${difference.inHours.toString()} hours' : diffMins ? '${difference.inMinutes.toString()} minutes' : '${difference.inSeconds.toString()} seconds'} ago";
_blindPosition = (body['last_pos'] as int).toDouble();
calibrated = true;
calibrating = false;
}
}
else {
calibrating = true;
}
setState(() {loaded = true;});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
Future initAll() async{
getName();
loop();
}
void rename() {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
"Rename Peripheral",
style: GoogleFonts.aBeeZee(),
),
content: BlindMasterMainInput("New Peripheral Name", controller: _peripheralRenameController,),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: const Text(
"Cancel",
style: TextStyle(
color: Colors.red
),
)
),
ElevatedButton(
onPressed: () {
updatePeriphName(_peripheralRenameController.text, widget.peripheralId);
Navigator.of(dialogContext).pop();
},
child: const Text("Confirm")
)
],
)
],
);
}
);
}
Future updatePeriphName(String name, int id) async {
try {
if (name.isEmpty) throw Exception("New name cannot be empty!");
final payload = {
'periphId': id,
'newName': name,
};
final response = await securePost(payload, 'rename_peripheral');
if (response == null) throw Exception("Auth Error");
if (response.statusCode != 204) {
if (response.statusCode == 409) throw Exception("Choose a unique name!");
throw Exception("Server Error");
}
getName();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
void recalibrate() {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
"Recalibrate Peripheral",
style: GoogleFonts.aBeeZee(),
),
content: const Text(
"This will take under a minute",
textAlign: TextAlign.center,
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: const Text(
"Cancel",
style: TextStyle(
color: Colors.red
),
)
),
ElevatedButton(
onPressed: () {
calibrate();
Navigator.of(dialogContext).pop();
},
child: const Text("Confirm")
)
],
)
],
);
}
);
}
Future updateBlindPosition() async {
try {
final payload = {
'periphId': widget.peripheralId,
'periphNum': widget.peripheralNum,
'deviceId': widget.deviceId,
'newPos': _blindPosition.toInt(),
};
final response = await securePost(payload, 'manual_position_update');
if (response == null) throw Exception("Auth Error");
if (response.statusCode != 202) {
throw Exception("Server Error");
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
peripheralName,
style: GoogleFonts.aBeeZee(),
),
backgroundColor: Theme.of(context).primaryColorLight,
foregroundColor: Colors.white,
),
body: loaded
? (calibrating
? RefreshIndicator(
onRefresh: initAll,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(20),
child: Text(
"Calibrating... Check again soon."
),
),
ElevatedButton(
onPressed: cancelCalib,
child: const Text(
"Cancel",
style: TextStyle(
color: Colors.red
),
)
)
]
)
)
)
)
)
: (calibrated
? Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: Container(
padding: EdgeInsets.fromLTRB(0, 20, 0, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
),
Stack(
children: [
// Background image
Align(
alignment: Alignment.center,
child: Image.asset(
imagePath,
// fit: BoxFit.cover,
width: MediaQuery.of(context).size.width * 0.7,
),
),
Align(
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
height: MediaQuery.of(context).size.width * 0.68,
width: MediaQuery.of(context).size.width * 0.7,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(10, (index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: _blindPosition < 5 ?
5.4 * (5 - _blindPosition)
: 5.4 * (_blindPosition - 5),
width: MediaQuery.of(context).size.width * 0.65, // example
color: const Color.fromARGB(255, 121, 85, 72),
);
}),
),
)
)
],
),
// Slider on the side
Expanded(
child: Center(
child: RotatedBox(
quarterTurns: -1,
child: Slider(
value: _blindPosition,
activeColor: Theme.of(context).primaryColorDark,
thumbColor: Theme.of(context).primaryColorLight,
inactiveColor: Theme.of(context).primaryColorDark,
min: 0,
max: 10,
divisions: 10,
onChanged: (value) {
setState(() {
_blindPosition = value;
updateBlindPosition();
});
},
),
),
)
)
],
),
)
),
Container(
padding: EdgeInsets.all(25),
child: Text(
lastSetMessage
),
),
Container(
padding: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SchedulesScreen()
)
);
},
child: Text(
"Set Schedules"
)
),
)
]
)
: SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(20),
child: Text(
"Peripheral Not Calibrated"
),
),
ElevatedButton(
onPressed: calibrate,
child: const Text("Calibrate")
)
],
)
)
)))
: BlindmasterProgressIndicator(),
floatingActionButton: Container(
padding: EdgeInsets.all(25),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton(
heroTag: "rename",
tooltip: "Rename Peripheral",
onPressed: rename,
foregroundColor: Theme.of(context).highlightColor,
backgroundColor: Theme.of(context).primaryColorDark,
child: Icon(Icons.drive_file_rename_outline_sharp),
),
FloatingActionButton(
heroTag: "recalibrate",
tooltip: "Recalibrate Peripheral",
onPressed: recalibrate,
foregroundColor: Theme.of(context).highlightColor,
backgroundColor: Theme.of(context).primaryColorDark,
child: Icon(Icons.swap_vert),
),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class SchedulesScreen extends StatefulWidget {
const SchedulesScreen({super.key});
@override
State<SchedulesScreen> createState() => _SchedulesScreenState();
}
class _SchedulesScreenState extends State<SchedulesScreen> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

65
lib/main.dart Normal file
View File

@@ -0,0 +1,65 @@
import 'package:blind_master/BlindMasterScreens/Startup/splash_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
Map<String, Color> getBackgroundBasedOnTime() {
final hour = DateTime.now().hour;
Color secondaryLight;
Color primary;
Color secondaryDark;
if (hour >= 5 && hour < 10) {
// Morning
primary = Colors.orange;
secondaryLight = const Color.fromARGB(255, 255, 204, 128);
secondaryDark = const Color.fromARGB(255, 174, 104, 0);
} else if (hour >= 10 && hour < 18) {
// Afternoon
primary = Colors.blue;
secondaryLight = const Color.fromARGB(255, 144, 202, 249);
secondaryDark = const Color.fromARGB(255, 0, 92, 168);
} else {
// Evening/Night
primary = const Color.fromARGB(255, 71, 17, 137);
secondaryLight = const Color.fromARGB(255, 186, 130, 255);
secondaryDark = const Color.fromARGB(255, 40, 0, 89);
}
return {
'primary': primary,
'secondaryLight': secondaryLight,
'secondaryDark': secondaryDark,
};
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final colors = getBackgroundBasedOnTime();
return MaterialApp(
home: SplashScreen(),
theme: ThemeData(
useMaterial3: true,
primaryColorLight: colors['primary'],
highlightColor: Colors.black,
disabledColor: Colors.grey,
primaryColorDark: colors['secondaryLight'],
brightness: Brightness.light
),
darkTheme: ThemeData(
useMaterial3: true,
highlightColor: Colors.white,
primaryColorLight: colors['primary'],
disabledColor: Colors.grey[800],
primaryColorDark: colors['secondaryDark'],
brightness: Brightness.dark
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'utils.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
final Map<DeviceIdentifier, StreamControllerReemit<bool>> _cglobal = {};
final Map<DeviceIdentifier, StreamControllerReemit<bool>> _dglobal = {};
/// connect & disconnect + update stream
extension Extra on BluetoothDevice {
// convenience
StreamControllerReemit<bool> get _cstream {
_cglobal[remoteId] ??= StreamControllerReemit(initialValue: false);
return _cglobal[remoteId]!;
}
// convenience
StreamControllerReemit<bool> get _dstream {
_dglobal[remoteId] ??= StreamControllerReemit(initialValue: false);
return _dglobal[remoteId]!;
}
// get stream
Stream<bool> get isConnecting {
return _cstream.stream;
}
// get stream
Stream<bool> get isDisconnecting {
return _dstream.stream;
}
// connect & update stream
Future<void> connectAndUpdateStream() async {
_cstream.add(true);
try {
await connect(mtu: null);
} finally {
_cstream.add(false);
}
}
// disconnect & update stream
Future<void> disconnectAndUpdateStream({bool queue = true}) async {
_dstream.add(true);
try {
await disconnect(queue: queue);
} finally {
_dstream.add(false);
}
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class ScanResultTile extends StatefulWidget {
const ScanResultTile({super.key, required this.result, this.onTap});
final ScanResult result;
final VoidCallback? onTap;
@override
State<ScanResultTile> createState() => _ScanResultTileState();
}
class _ScanResultTileState extends State<ScanResultTile> {
BluetoothConnectionState _connectionState = BluetoothConnectionState.disconnected;
late StreamSubscription<BluetoothConnectionState> _connectionStateSubscription;
@override
void initState() {
super.initState();
_connectionStateSubscription = widget.result.device.connectionState.listen((state) {
_connectionState = state;
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
_connectionStateSubscription.cancel();
super.dispose();
}
bool get isConnected {
return _connectionState == BluetoothConnectionState.connected;
}
Widget _buildTitle(BuildContext context) {
return Text(
widget.result.advertisementData.advName,
overflow: TextOverflow.ellipsis,
);
}
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: _buildTitle(context),
subtitle: Text(
"Signal Strength (Less = farther): ${widget.result.rssi}",
overflow: TextOverflow.ellipsis,
),
trailing: Icon(Icons.arrow_forward_ios_rounded),
onTap: widget.result.advertisementData.connectable ? widget.onTap : null,
)
);
}
}

View File

@@ -0,0 +1,153 @@
import 'dart:async';
// It is essentially a stream but:
// 1. we cache the latestValue of the stream
// 2. the "latestValue" is re-emitted whenever the stream is listened to
class StreamControllerReemit<T> {
T? _latestValue;
final StreamController<T> _controller = StreamController<T>.broadcast();
StreamControllerReemit({T? initialValue}) : _latestValue = initialValue;
Stream<T> get stream {
return _latestValue != null ? _controller.stream.newStreamWithInitialValue(_latestValue as T) : _controller.stream;
}
T? get value => _latestValue;
void add(T newValue) {
_latestValue = newValue;
_controller.add(newValue);
}
Future<void> close() {
return _controller.close();
}
}
// return a new stream that immediately emits an initial value
extension _StreamNewStreamWithInitialValue<T> on Stream<T> {
Stream<T> newStreamWithInitialValue(T initialValue) {
return transform(_NewStreamWithInitialValueTransformer(initialValue));
}
}
// Helper for 'newStreamWithInitialValue' method for streams.
class _NewStreamWithInitialValueTransformer<T> extends StreamTransformerBase<T, T> {
/// the initial value to push to the new stream
final T initialValue;
/// controller for the new stream
late StreamController<T> controller;
/// subscription to the original stream
late StreamSubscription<T> subscription;
/// new stream listener count
var listenerCount = 0;
_NewStreamWithInitialValueTransformer(this.initialValue);
@override
Stream<T> bind(Stream<T> stream) {
if (stream.isBroadcast) {
return _bind(stream, broadcast: true);
} else {
return _bind(stream);
}
}
Stream<T> _bind(Stream<T> stream, {bool broadcast = false}) {
/////////////////////////////////////////
/// Original Stream Subscription Callbacks
///
/// When the original stream emits data, forward it to our new stream
void onData(T data) {
controller.add(data);
}
/// When the original stream is done, close our new stream
void onDone() {
controller.close();
}
/// When the original stream has an error, forward it to our new stream
void onError(Object error) {
controller.addError(error);
}
/// When a client listens to our new stream, emit the
/// initial value and subscribe to original stream if needed
void onListen() {
// Emit the initial value to our new stream
controller.add(initialValue);
// listen to the original stream, if needed
if (listenerCount == 0) {
subscription = stream.listen(
onData,
onError: onError,
onDone: onDone,
);
}
// count listeners of the new stream
listenerCount++;
}
//////////////////////////////////////
/// New Stream Controller Callbacks
///
/// (Single Subscription Only) When a client pauses
/// the new stream, pause the original stream
void onPause() {
subscription.pause();
}
/// (Single Subscription Only) When a client resumes
/// the new stream, resume the original stream
void onResume() {
subscription.resume();
}
/// Called when a client cancels their
/// subscription to the new stream,
void onCancel() {
// count listeners of the new stream
listenerCount--;
// when there are no more listeners of the new stream,
// cancel the subscription to the original stream,
// and close the new stream controller
if (listenerCount == 0) {
subscription.cancel();
controller.close();
}
}
//////////////////////////////////////
/// Return New Stream
///
// create a new stream controller
if (broadcast) {
controller = StreamController<T>.broadcast(
onListen: onListen,
onCancel: onCancel,
);
} else {
controller = StreamController<T>(
onListen: onListen,
onPause: onPause,
onResume: onResume,
onCancel: onCancel,
);
}
return controller.stream;
}
}