diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..fc248cd 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,15 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + + + diff --git a/lib/home_screen_content.dart b/lib/home_screen_content.dart index 97bac80..a89739d 100644 --- a/lib/home_screen_content.dart +++ b/lib/home_screen_content.dart @@ -4,6 +4,9 @@ import 'package:latlong2/latlong.dart'; import 'dart:ui' as ui; import 'package:smarthelmet_app/widgets/custom_header.dart'; +//25.12.03 지은 추가 +import 'package:smarthelmet_app/services/locker_api.dart'; + class HomeScreenContent extends StatefulWidget { const HomeScreenContent({super.key}); @@ -23,6 +26,25 @@ class _HomeScreenContentState extends State { final Color _toggleOffTrackColor = const Color(0xFFE0E0E0); final Color _toggleOffKnobBorderColor = const Color(0xFFE0E0E0); +// 25.12.03 지은 추가 시작 + final LockerApi _api = LockerApi(); + bool _isLoading = false; + +Future _runLockerAction(String name, Future Function() action) async { + if (_isLoading) return; + + setState(() => _isLoading = true); + + // 실제 명령 전송 + final success = await action(); + + setState(() => _isLoading = false); + + if (!mounted) return; + } + + // 25.12.03 지은 추가 끝 + int _selectedImageIndex = 0; static const BoxShadow _cleanShadow = BoxShadow( @@ -329,7 +351,21 @@ class _HomeScreenContentState extends State { child: _buildStyledToggleSwitch( 'UV LED', _controlToggles['UV LED']!, - (val) => setState(() => _controlToggles['UV LED'] = val))), + // 25.12.03 지은 수정 시작 + (val) { + _runLockerAction("UV 제어", () async { + bool success = await _api.setUV(val); + + if (success) { + setState(() => _controlToggles['UV LED'] = val); + } + return success; + } + ); + }, + ), + ), + // 25.12.03 지은 수정 끝 VerticalDivider(color: _mainBlueColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( @@ -346,7 +382,22 @@ class _HomeScreenContentState extends State { Expanded( child: _buildStyledToggleSwitch('FAN', _controlToggles['FAN']!, - (val) => setState(() => _controlToggles['FAN'] = val))), + // 25.12.03 지은 수정 시작 + (val) { + print("👉 [디버깅] fan 눌림! 값: $val"); + _runLockerAction("FAN 제어", () async { + print("👉 [디버깅] API 요청 시작..."); + bool success = await _api.setFan(val); + + if (success) { + setState(() => _controlToggles['FAN'] = val); + } + return success; + }); + }, + // 25.12.03 지은 수정 끝 + ), + ), ], ), ) diff --git a/lib/rental_process_screen.dart b/lib/rental_process_screen.dart index 18ac8ee..54fa613 100644 --- a/lib/rental_process_screen.dart +++ b/lib/rental_process_screen.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +//25.12.03 지은 추가 +import 'package:smarthelmet_app/services/locker_api.dart'; + class RentalProcessScreen extends StatefulWidget { final String stationName; @@ -26,6 +29,25 @@ class _RentalProcessScreenState extends State { static const Color _primaryGreen = Color(0xFF4CAF50); static const Color _primaryOrange = Color(0xFFFF3D00); + // 25.12.03 지은 추가 시작 + final LockerApi _api = LockerApi(); + bool _isLoading = false; + +Future _runLockerAction(String name, Future Function() action) async { + if (_isLoading) return; + + setState(() => _isLoading = true); + + // 실제 명령 전송 + final success = await action(); + + setState(() => _isLoading = false); + + if (!mounted) return; + } + + // 25.12.03 지은 추가 끝 + final PageController _pageController = PageController(); int _currentPage = 0; Timer? _timer; @@ -241,6 +263,9 @@ class _RentalProcessScreenState extends State { height: 68, child: ElevatedButton( onPressed: () { + // 25.12.03 지은 추가 + _runLockerAction("잠금 해제", () => _api.unlock()); + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('문이 열렸습니다! (OPEN 버튼 눌림)'), diff --git a/lib/services/locker_api.dart b/lib/services/locker_api.dart new file mode 100644 index 0000000..f8adae0 --- /dev/null +++ b/lib/services/locker_api.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class LockerApi { + // 안드로이드 에뮬레이터: "http://10.0.2.2:8182" + // iOS 시뮬레이터: "http://127.0.0.1:8182" + // "http://192.168.0.82:8182" + static const String baseUrl = "http://192.168.0.81:8182"; + + // 명령 전송 공통 함수 + Future sendCommand({ + required int addr, + required int value, + String target = "*" + }) async { + final url = Uri.parse('$baseUrl/modbus/write'); + + try { + final response = await http.post( + url, + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "target": target, + "unit": 1, + "addr": addr, + "value": value + }), + ); + + if (response.statusCode == 200) { + print("명령 성공: Addr($addr) -> Val($value)"); + return true; + } else { + print("명령 실패: ${response.body}"); + return false; + } + } catch (e) { + print("서버 연결 오류: $e"); + return false; + } + } + + // --- [기능별 동작 함수] --- + + // 1. 잠금 해제 (주소: 0x0016 / 값: 1) + Future unlock() async { + return await sendCommand(addr: 0x0016, value: 1); + } + + // 2. 살균 (UV) 켜기/끄기 (주소: 0x0014 / 값: 1 or 0) + Future setUV(bool isOn) async { + return await sendCommand(addr: 0x0014, value: isOn ? 1 : 0); + } + + // 3. 건조 (FAN) 켜기/끄기 (주소: 0x0015 / 값: 1 or 0) + Future setFan(bool isOn) async { + return await sendCommand(addr: 0x0015, value: isOn ? 1 : 0); + } +} \ No newline at end of file