Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 80 additions & 62 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:convert'; // jsonEncode 등을 위해 필요할 수 있음 (bleService 내부에서 처리하지만 안전하게)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:ambient_node/screens/splash_screen.dart';
import 'package:ambient_node/screens/dashboard_screen.dart';
Expand All @@ -10,8 +10,6 @@ import 'package:ambient_node/screens/settings_screen.dart';
import 'package:ambient_node/services/analytics_service.dart';
import 'package:ambient_node/services/ble_service.dart';

class AiService {}

void main() {
runApp(const MyApp());
}
Expand Down Expand Up @@ -45,9 +43,7 @@ class _SplashWrapperState extends State<SplashWrapper> {

@override
Widget build(BuildContext context) {
if (_showMain) {
return const MainShell();
}
if (_showMain) return const MainShell();

return SplashScreen(
onFinish: () {
Expand Down Expand Up @@ -81,6 +77,7 @@ class _MainShellState extends State<MainShell> {

String? selectedUserName;
String? selectedUserImagePath;
String? selectedUserId; // userId 저장 추가

@override
void initState() {
Expand Down Expand Up @@ -130,37 +127,34 @@ class _MainShellState extends State<MainShell> {
print('[Main] 연결 해제 오류: $e');
}
} else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DeviceSelectionScreen(
bleService: ble,
onConnectionChanged: (isConnected) {
// 연결 성공 시 초기 상태 전송은 하지 않음 (사용자가 조작할 때 전송)
},
onDeviceNameChanged: (name) {
if (mounted) setState(() => deviceName = name);
},
),
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DeviceSelectionScreen(
bleService: ble,
onConnectionChanged: (isConnected) {},
onDeviceNameChanged: (name) {
if (mounted) setState(() => deviceName = name);
},
),
);
));
}
}

void _showSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message), duration: const Duration(seconds: 2)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 2)),
);
}

// [수정됨] 1. 풍속 변경 전용 함수 (Action 포함)
// 대시보드에서 속도 변경 시 호출
void _sendSpeedChange(int newSpeed) {
if (!connected) return;

// 안전장치: 0~5 사이로 강제 변환
int targetSpeed = newSpeed.clamp(0, 5);

final data = {
'action': 'speed_change', // Gateway가 인식하는 필수 키
'action': 'speed_change',
'speed': targetSpeed,
'user_list': _getCurrentUserList(),
'timestamp': DateTime.now().toIso8601String(),
};

Expand All @@ -172,13 +166,14 @@ class _MainShellState extends State<MainShell> {
}
}

// [수정됨] 2. 모드(트래킹) 변경 전용 함수 (Action 포함)
// 대시보드에서 모드 변경 시 호출
void _sendModeChange(bool isAiMode) {
if (!connected) return;

final data = {
'action': 'mode_change', // Gateway가 인식하는 필수 키
'action': 'mode_change',
'mode': isAiMode ? 'ai' : 'manual',
'user_list': _getCurrentUserList(),
'timestamp': DateTime.now().toIso8601String(),
};

Expand All @@ -190,61 +185,72 @@ class _MainShellState extends State<MainShell> {
}
}

// 현재 선택된 사용자 정보를 user_list 형식으로 반환하는 헬퍼
List<Map<String, dynamic>> _getCurrentUserList() {
if (selectedUserId == null || selectedUserName == null) return [];

return [
{
'user_id': selectedUserId,
'username': selectedUserName,
'role': 1,
},
];
}

@override
Widget build(BuildContext context) {
final screens = [
DashboardScreen(
connected: connected,
onConnect: handleConnect,

speed: speed,
// [중요] setSpeed에서 _sendSpeedChange 호출
setSpeed: (v) {
setState(() => speed = v);
_sendSpeedChange(v);
try { AnalyticsService.onSpeedChanged(v); } catch (e) {}
try {
AnalyticsService.onSpeedChanged(v);
} catch (e) {}
},

trackingOn: trackingOn,
// [중요] setTrackingOn에서 _sendModeChange 호출
setTrackingOn: (v) {
setState(() => trackingOn = v);
_sendModeChange(v);
try { v ? AnalyticsService.onFaceTrackingStart() : AnalyticsService.onFaceTrackingStop(); } catch (e) {}
try {
v ? AnalyticsService.onFaceTrackingStart() : AnalyticsService.onFaceTrackingStop();
} catch (e) {}
},

openAnalytics: () => setState(() => _index = 2),
deviceName: deviceName,
selectedUserName: selectedUserName,
selectedUserImagePath: selectedUserImagePath,
),

ControlScreen(
connected: connected,
deviceName: deviceName,
onConnect: handleConnect,
dataStream: _bleDataStreamController.stream,
selectedUserName: selectedUserName,
onUserSelectionChanged: (userName, userImagePath) {
onUserSelectionChanged: (userName, userImagePath, userId) {
setState(() {
selectedUserName = userName;
selectedUserImagePath = userImagePath;
this.selectedUserId = userId;
});
try { AnalyticsService.onUserChanged(userName); } catch (e) {}
try {
AnalyticsService.onUserChanged(userName);
} catch (e) {}
},
onUserDataSend: (data) {
print('🔵 BLE 전송: $data');
ble.sendJson(data);
},
),

AnalyticsScreen(selectedUserName: selectedUserName),

SettingsScreen(
connected: connected,
sendJson: (data) => ble.sendJson(data),
),

];

return Scaffold(
Expand All @@ -270,36 +276,37 @@ class _MainShellState extends State<MainShell> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(
icon: Icons.dashboard_outlined,
label: '대시보드',
isSelected: _index == 0,
onTap: () => setState(() => _index = 0),
),
icon: Icons.dashboard_outlined,
label: '대시보드',
isSelected: _index == 0,
onTap: () => setState(() => _index = 0)),
_buildNavItem(
icon: Icons.control_camera,
label: '제어',
isSelected: _index == 1,
onTap: () => setState(() => _index = 1),
),
icon: Icons.control_camera,
label: '제어',
isSelected: _index == 1,
onTap: () => setState(() => _index = 1)),
_buildNavItem(
icon: Icons.analytics_outlined,
label: '분석',
isSelected: _index == 2,
onTap: () => setState(() => _index = 2),
),
icon: Icons.analytics_outlined,
label: '분석',
isSelected: _index == 2,
onTap: () => setState(() => _index = 2)),
_buildNavItem(
icon: Icons.settings_outlined,
label: '설정',
isSelected: _index == 3,
onTap: () => setState(() => _index = 3),
),
icon: Icons.settings_outlined,
label: '설정',
isSelected: _index == 3,
onTap: () => setState(() => _index = 3)),
],
),
),
);
}

Widget _buildNavItem({required IconData icon, required String label, required bool isSelected, required VoidCallback onTap}) {
Widget _buildNavItem({
required IconData icon,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
Expand All @@ -308,12 +315,23 @@ class _MainShellState extends State<MainShell> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24, color: isSelected ? const Color(0xFF3A90FF) : const Color(0xFF838799)),
Icon(icon,
size: 24,
color: isSelected
? const Color(0xFF3A90FF)
: const Color(0xFF838799)),
const SizedBox(height: 5),
Text(label, textAlign: TextAlign.center, style: TextStyle(color: isSelected ? const Color(0xFF3A90FF) : const Color(0xFF838799), fontSize: 13, fontFamily: 'Sen', fontWeight: FontWeight.w400)),
Text(label,
textAlign: TextAlign.center,
style: TextStyle(
color:
isSelected ? const Color(0xFF3A90FF) : const Color(0xFF838799),
fontSize: 13,
fontFamily: 'Sen',
fontWeight: FontWeight.w400)),
],
),
),
);
}
}
}
Loading