From 00c54159d471d1bb6d6180024c2e039c6a2a5327 Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Sun, 28 Dec 2025 14:07:04 -0600 Subject: [PATCH] support 2-stage calibration sequence --- .../secure_transmissions.dart | 2 +- .../individualControl/peripheral_screen.dart | 234 +++++++++++++++++- 2 files changed, 222 insertions(+), 14 deletions(-) diff --git a/lib/BlindMasterResources/secure_transmissions.dart b/lib/BlindMasterResources/secure_transmissions.dart index 8c5b27d..d8596a1 100644 --- a/lib/BlindMasterResources/secure_transmissions.dart +++ b/lib/BlindMasterResources/secure_transmissions.dart @@ -8,7 +8,7 @@ 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 = fromDevice; +String host = local; int port = 3000; String socketString = "$scheme://$host:$port"; String scheme = 'http'; diff --git a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart index 0c308ca..9594b44 100644 --- a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart +++ b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart @@ -26,6 +26,8 @@ class _PeripheralScreenState extends State { bool loaded = false; bool calibrated = false; bool calibrating = false; + bool deviceConnected = true; // Track device connection status + int calibrationStage = 0; // 0=not started, 1=tilt up, 2=tilt down double _blindPosition = 5.0; DateTime? lastSet; String lastSetMessage = ""; @@ -65,6 +67,32 @@ class _PeripheralScreenState extends State { socket = await connectSocket(); if (socket == null) throw Exception("Unsuccessful socket connection"); socket?.on("success", (_) { + socket?.on("device_connected", (data) { + if (data is Map) { + if (data['deviceID'] == widget.deviceId) { + if (!mounted) return; + setState(() { + deviceConnected = true; + }); + } + } + }); + + socket?.on("device_disconnected", (data) { + if (data is Map) { + if (data['deviceID'] == widget.deviceId) { + if (!mounted) return; + setState(() { + deviceConnected = false; + // Reset calibration if it was in progress + if (calibrating) { + calibrationStage = 0; + } + }); + } + } + }); + socket?.on("posUpdates", (list) { for (var update in list) { if (update is Map) { @@ -77,6 +105,7 @@ class _PeripheralScreenState extends State { } } }); + socket?.on("calib", (periphData) { if (periphData is Map) { if (periphData['periphID'] == widget.peripheralId) { @@ -84,10 +113,52 @@ class _PeripheralScreenState extends State { setState(() { calibrating = true; calibrated = false; + calibrationStage = 0; // Waiting for device to be ready }); } } }); + + socket?.on("calib_error", (errorData) { + if (errorData is Map) { + if (errorData['periphID'] == widget.peripheralId) { + if (!mounted) return; + setState(() { + calibrating = false; + calibrationStage = 0; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(errorData['message'] ?? 'Calibration error'), + backgroundColor: Colors.red, + ) + ); + } + } + }); + + socket?.on("calib_stage1_ready", (periphData) { + if (periphData is Map) { + if (periphData['periphID'] == widget.peripheralId) { + if (!mounted) return; + setState(() { + calibrationStage = 1; // Device ready for tilt up + }); + } + } + }); + + socket?.on("calib_stage2_ready", (periphData) { + if (periphData is Map) { + if (periphData['periphID'] == widget.peripheralId) { + if (!mounted) return; + setState(() { + calibrationStage = 2; // Device ready for tilt down + }); + } + } + }); + socket?.on("calib_done", (periphData) { if (periphData is Map) { if (periphData['periphID'] == widget.peripheralId) { @@ -95,6 +166,7 @@ class _PeripheralScreenState extends State { setState(() { calibrating = false; calibrated = true; + calibrationStage = 0; }); } } @@ -106,6 +178,17 @@ class _PeripheralScreenState extends State { } Future calibrate() async { + if (!deviceConnected) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Device must be connected to calibrate'), + backgroundColor: Colors.orange, + ) + ); + return; + } + try { final payload = { 'periphId': widget.peripheralId @@ -131,9 +214,13 @@ class _PeripheralScreenState extends State { if (response == null) throw Exception("auth error"); if (response.statusCode != 202) throw Exception("Server Error"); + + // Only update state if cancel succeeded + if (!mounted) return; setState(() { calibrated = false; calibrating = false; + calibrationStage = 0; }); } catch (e) { if (!mounted) return; @@ -141,6 +228,32 @@ class _PeripheralScreenState extends State { } } + Future completeStage1() async { + // User confirms they've tilted blinds all the way up + // Tell device to proceed to stage 2 + socket?.emit("user_stage1_complete", { + "periphID": widget.peripheralId, + "periphNum": widget.peripheralNum, + "deviceID": widget.deviceId + }); + setState(() { + calibrationStage = 0; // Wait for device acknowledgment + }); + } + + Future completeStage2() async { + // User confirms they've tilted blinds all the way down + // Tell device calibration is complete + socket?.emit("user_stage2_complete", { + "periphID": widget.peripheralId, + "periphNum": widget.peripheralNum, + "deviceID": widget.deviceId + }); + setState(() { + calibrationStage = 0; // Wait for device acknowledgment + }); + } + Future getName() async { try { final payload = { @@ -157,6 +270,23 @@ class _PeripheralScreenState extends State { } } + Future checkDeviceConnection() async { + try { + final payload = { + 'deviceId': widget.deviceId + }; + final response = await secureGet('device_connection_status', queryParameters: payload); + if (response == null) throw Exception("auth error"); + if (response.statusCode != 200) throw Exception("Server Error"); + final body = json.decode(response.body); + if (!mounted) return; + setState(() => deviceConnected = body['connected']); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); + } + } + Future loop() async{ try { final payload = { @@ -204,6 +334,7 @@ class _PeripheralScreenState extends State { Future initAll() async{ getName(); + checkDeviceConnection(); loop(); } @@ -340,6 +471,34 @@ class _PeripheralScreenState extends State { ), backgroundColor: Theme.of(context).primaryColorLight, foregroundColor: Colors.white, + bottom: !deviceConnected ? PreferredSize( + preferredSize: const Size.fromHeight(48.0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + color: Colors.orange.shade700, + child: Row( + children: [ + const Icon( + Icons.wifi_off, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Device Disconnected', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ) : null, ), body: loaded @@ -357,18 +516,64 @@ class _PeripheralScreenState extends State { Container( padding: EdgeInsets.all(20), child: Text( - "Calibrating... Check again soon." + calibrationStage == 0 + ? "Preparing device for calibration..." + : calibrationStage == 1 + ? "Tilt the blinds ALL THE WAY UP" + : "Tilt the blinds ALL THE WAY DOWN", + style: GoogleFonts.aBeeZee( + fontSize: 20, + fontWeight: FontWeight.bold + ), + textAlign: TextAlign.center, ), ), - ElevatedButton( - onPressed: cancelCalib, - child: const Text( - "Cancel", - style: TextStyle( - color: Colors.red - ), + if (calibrationStage == 0) + CircularProgressIndicator( + color: Theme.of(context).primaryColorLight, + ), + SizedBox(height: 20), + if (calibrationStage == 1 || calibrationStage == 2) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: calibrationStage == 1 ? completeStage1 : completeStage2, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15), + ), + child: const Text( + "Complete", + style: TextStyle(fontSize: 16), + ) + ), + SizedBox(width: 20), + ElevatedButton( + onPressed: cancelCalib, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15), + ), + child: const Text( + "Cancel", + style: TextStyle(fontSize: 16), + ) + ), + ], + ) + else + ElevatedButton( + onPressed: cancelCalib, + child: const Text( + "Cancel", + style: TextStyle( + color: Colors.red + ), + ) ) - ) ] ) ) @@ -488,7 +693,10 @@ class _PeripheralScreenState extends State { ), ), ElevatedButton( - onPressed: calibrate, + onPressed: deviceConnected ? calibrate : null, + style: ElevatedButton.styleFrom( + backgroundColor: deviceConnected ? null : Colors.grey, + ), child: const Text("Calibrate") ) ], @@ -512,9 +720,9 @@ class _PeripheralScreenState extends State { FloatingActionButton( heroTag: "recalibrate", tooltip: "Recalibrate Peripheral", - onPressed: recalibrate, - foregroundColor: Theme.of(context).highlightColor, - backgroundColor: Theme.of(context).primaryColorDark, + onPressed: deviceConnected ? recalibrate : null, + foregroundColor: deviceConnected ? Theme.of(context).highlightColor : Colors.grey.shade400, + backgroundColor: deviceConnected ? Theme.of(context).primaryColorDark : Colors.grey.shade300, child: Icon(Icons.swap_vert), ), ],