I'm developing an app using Flutter. I keep encountering errors and I'm not sure how to fix them.
- App Functionality: Reads two barcodes and sends the results via TCP communication with another PC, delimited by ''. When the other PC sends an 'OK' response, the app should display the result on the screen in a table for the user to confirm.
- App Execution Process: Developing using VS Code. Creating an emulator with Android Studio and running the app. Using Hercules to create a virtual TCP server to act as the other PC.
- Current Issue: When sending the barcode to the TCP server, it receives the result correctly. However, when the TCP server sends an 'OK' response, an error occurs.
- Error] I/flutter ( 7118): Sending data to the server... I/flutter ( 7118): Error reading response from server: Bad state: Stream has already been listened to. I/flutter ( 7118): Response received from server: D/EGL_emulation( 7118): app_time_stats: avg=14.15ms min=4.33ms max=145.76ms count=47
Raw code:
import 'package:flutter/material.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'dart:io';
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// MaterialApp 설정: 앱의 기본적인 UI 테마와 홈 화면을 정의합니다.
return MaterialApp(
title: 'Barcode Scanner and Sender',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MyHomePage(title: 'Barcode Scanner for Hermes Rework'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
// class _MyHomePageState extends State<MyHomePage> {
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
AnimationController? _timerAnimationController;
// 바코드 값을 저장하기 위한 변수들
String _machineBarcode = '';
String _pcbBarcode = '';
String _connectionStatus = 'Disconnected';
// 스캔된 데이터를 저장하기 위한 리스트
final List<Map<String, String>> _scannedData = [];
// 소켓 통신을 관리하는 클래스의 인스턴스
final SocketManager _socketManager = SocketManager();
@override
void initState() {
super.initState();
// 앱이 시작될 때 소켓 연결을 초기화합니다.
initSocketConnection();
// Initialize the animation controller for the 10-second animation
_timerAnimationController = AnimationController(
vsync: this,
duration: Duration(seconds: 10),
);
}
@override
void dispose() {
_timerAnimationController?.dispose();
super.dispose();
}
// Socket 연결을 초기화하고 상태를 업데이트하는 메서드
void initSocketConnection() async {
// bool isConnected = await _socketManager.initConnection("10.223.141.31", 5000);
bool isConnected = await _socketManager.initConnection();
setState(() {
_connectionStatus = isConnected ? 'Connected' : 'Failed to connect';
});
}
void reconnectToServer() async {
// bool isConnected = await _socketManager.initConnection("10.223.141.31", 5000);
bool isConnected = await _socketManager.initConnection();
setState(() {
_connectionStatus = isConnected ? 'Connected' : 'Failed to connect';
});
}
// 바코드를 스캔하는 메서드
Future<void> scanBarcode() async {
String barcodeScanRes;
try {
// FlutterBarcodeScanner 플러그인을 사용하여 바코드 스캐너를 실행합니다.
barcodeScanRes = await FlutterBarcodeScanner.scanBarcode(
'#ff6666',
'Cancel',
true,
ScanMode.BARCODE,
);
if (!mounted) return;
setState(() {
// 스캔된 바코드 값을 적절한 변수에 저장합니다.
if (_machineBarcode.isEmpty) {
_machineBarcode = barcodeScanRes;
} else if (_pcbBarcode.isEmpty && _machineBarcode.isNotEmpty) {
_pcbBarcode = barcodeScanRes;
}
});
} catch (e) {
barcodeScanRes = 'Failed to get platform version.';
}
}
void sendScannedData() async {
if (_machineBarcode.isNotEmpty && _pcbBarcode.isNotEmpty) {
// 애니메이션 시작
_timerAnimationController?.forward(from: 0);
try {
final response = await _socketManager.sendData('M##$_machineBarcode##P##$_pcbBarcode').timeout(
Duration(seconds: 10),
onTimeout: () => 'Timeout error', // 타임아웃 처리
);
if (response.trim() == 'OK') {
_updateScannedData('OK');
} else {
_showError(response);
}
} catch (e) {
_showError(e.toString()); // 예외 발생 시 에러 메시지 처리
}
// 바코드 리셋
setState(() {
_machineBarcode = '';
_pcbBarcode = '';
});
}
}
void _showError(String error) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Error: $error"),
));
}
void _updateScannedData(String status) {
final currentTime = DateFormat('HH:mm:ss').format(DateTime.now());
_scannedData.insert(0, {
'time': currentTime,
'machine': _machineBarcode,
'pcb': _pcbBarcode,
'status': status,
});
setState(() {}); //상태 변경을 반영하기 위해 setState를 호출
}
// void _showError(String error) {
// // 여기에 에러 메시지를 표시하는 코드를 추가
// // 예: 다이얼로그를 띄우거나 스낵바를 표시
// }
// }
////////////////////////////////////////////////////////
// 설정 UI를 표시하는 메서드
void _showSettingsDialog() {
String newIp = _socketManager._ip;
String newPort = _socketManager._port.toString();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Settings"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(labelText: "IP Address"),
controller: TextEditingController(text: newIp),
onChanged: (value) {
newIp = value;
},
),
TextField(
decoration: InputDecoration(labelText: "Port"),
controller: TextEditingController(text: newPort),
onChanged: (value) {
newPort = value;
},
),
],
),
actions: <Widget>[
TextButton(
child: Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text("Save"),
onPressed: () {
_socketManager.saveSettings(newIp, int.parse(newPort));
Navigator.of(context).pop();
initSocketConnection();
},
),
],
);
},
);
}
////////////////////////////////////////////////////////
Future<void> _openSettingsPage() async {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SettingsPage()),
);
// IP 주소와 포트 번호를 변경한 경우 변경된 값으로 소켓 연결 초기화
initSocketConnection();
}
@override
Widget build(BuildContext context) {
// Scaffold 위젯으로 앱의 기본 구조를 만듭니다.
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
// Machine과 PCB 바코드 값을 표시하는 위젯
// 바코드 값이 없으면 사용자에게 스캔하라는 메시지를 보여줍니다.
ListTile(
title: const Text('Machine Barcode:'),
subtitle: Text(_machineBarcode.isNotEmpty ? _machineBarcode : 'Please scan the barcode'),
leading: const Icon(Icons.qr_code_scanner),
),
ListTile(
title: const Text('PCB Barcode:'),
subtitle: Text(_pcbBarcode.isNotEmpty ? _pcbBarcode : 'Please scan the barcode'),
leading: const Icon(Icons.qr_code_scanner),
),
ListTile(
title: Text('Server Connection Status:'),
subtitle: Text(_connectionStatus),
leading: Icon(
_connectionStatus == 'Connected' ? Icons.check_circle : Icons.error,
color: _connectionStatus == 'Connected' ? Colors.green : Colors.red,
),
),
// 연결 재시도 버튼 추가
if (_connectionStatus == 'Failed to connect')
ElevatedButton(
onPressed: reconnectToServer,
child: Text('Reconnect to Server'),
),
// Send 버튼, 항상 표시되지만, 두 바코드가 스캔되었을 때만 활성화됩니다.
ElevatedButton.icon(
icon: const Icon(Icons.send),
label: const Text('Send'),
onPressed: _machineBarcode.isNotEmpty && _pcbBarcode.isNotEmpty ? sendScannedData : null,
),
// 애니메이션 표시
SizeTransition(
sizeFactor: _timerAnimationController!,
axisAlignment: -1,
child: CircularProgressIndicator(),
),
// 스캔된 데이터를 표시하는 DataTable 위젯
Expanded(
child: _scannedData.isNotEmpty
? DataTable(
columns: const [
DataColumn(label: Text('Time')),
DataColumn(label: Text('Machine')),
DataColumn(label: Text('PCB')),
DataColumn(label: Text('Status')), // Status 컬럼 추가
],
rows: _scannedData.map<DataRow>((data) => DataRow(
cells: [
DataCell(Text(
data['time']!,
softWrap: true, // 텍스트를 여러 줄로 나눕니다.
overflow: TextOverflow.visible, // 텍스트가 잘리지 않도록 합니다.
)),
DataCell(Text(data['machine']!)),
DataCell(Text(data['pcb']!)),
DataCell(Text(
data['status'] ?? 'Pending',
softWrap: true,
overflow: TextOverflow.visible,
)),
],
)).toList(),
)
: const Center(
child: Text(
'No data available',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
),
),
],
),
// 스캔 버튼 및 설정 버튼 추가
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: scanBarcode,
child: Icon(Icons.camera_alt),
tooltip: 'Scan',
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: _showSettingsDialog,
child: Icon(Icons.settings),
tooltip: 'Settings',
),
],
),
);
}
}
class SocketManager {
// 기본 IP 주소와 포트 번호 설정값
static const String defaultIP = "192.168.200.102";
static const int defaultPort = 5000;
Socket? _socket;
bool _isConnected = false;
String _ip = defaultIP; // 저장된 IP 주소 또는 기본값
int _port = defaultPort; // 저장된 포트 번호 또는 기본값
StreamSubscription? _streamSubscription; // 스트림 구독을 위한 변수
// 설정값을 로드하는 메서드
Future<void> _loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_ip = prefs.getString('ip') ?? defaultIP;
_port = prefs.getInt('port') ?? defaultPort;
}
StreamController<String>? _streamController; // 스트림 컨트롤러 추가
Future<bool> initConnection() async {
await _loadSettings();
try {
_socket = await Socket.connect(_ip, _port);
_isConnected = true;
await _streamSubscription?.cancel(); // 기존 구독 취소
await _streamController?.close(); // 기존 스트림 컨트롤러 닫기
_streamController = StreamController<String>.broadcast(); // 새로운 브로드캐스트 스트림 컨트롤러 생성
_streamSubscription = _socket!.listen(
(List<int> data) {
final String response = String.fromCharCodes(data).trim();
_streamController!.add(response); // 스트림 컨트롤러를 통해 데이터 추가
},
onDone: () {
print("Socket has been closed");
_isConnected = false;
},
onError: (error) {
print("Error: $error");
_isConnected = false;
}
);
return true;
} catch (e) {
_isConnected = false;
return false;
}
}
// }
// 사용자가 설정한 IP 주소와 포트 번호를 저장하는 메서드
Future<void> saveSettings(String ip, int port) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('ip', ip);
await prefs.setInt('port', port);
_ip = ip;
_port = port;
}
Future<String> sendData(String data) async {
if (!_isConnected || _socket == null) {
// 연결 상태 확인 및 재연결 시도
print("Socket is not connected. Attempting to reconnect...");
_isConnected = await initConnection();
if (!_isConnected) {
print("Failed to reconnect to the server.");
return "Failed to connect to server";
}
}
try {
print("Sending data to the server...");
_socket!.write(data);
await _socket!.flush();
// 서버 응답 대기 - 10초 타임아웃 설정
String response = await _readResponseFromServer().timeout(
Duration(seconds: 10),
onTimeout: () {
print("Timeout waiting for response from server.");
return 'Timeout error';
}
);
print("Response received from server: $response");
return response.trim(); // 공백 제거 및 반환
} catch (e) {
_isConnected = false;
print("Error occurred while sending data or receiving response: $e");
return "Error: $e";
}
}
// 서버로부터의 응답을 비동기적으로 읽는 메서드
Future<String> _readResponseFromServer() async {
if (_socket != null) {
StringBuffer responseBuffer = StringBuffer();
try {
// asBroadcastStream을 사용하여 스트림을 BroadcastStream으로 변환
await for (List<int> event in _socket!.asBroadcastStream()) {
String response = String.fromCharCodes(event);
responseBuffer.write(response);
if (response.endsWith('\n')) {
break;
}
}
} catch (e) {
print("Error reading response from server: $e");
}
return responseBuffer.toString();
} else {
print("Socket is null.");
return "No response";
}
}
}
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final TextEditingController _ipController = TextEditingController();
final TextEditingController _portController = TextEditingController();
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String ip = prefs.getString('ip') ?? SocketManager.defaultIP;
int port = prefs.getInt('port') ?? SocketManager.defaultPort;
setState(() {
_ipController.text = ip;
_portController.text = port.toString();
});
}
Future<void> _saveSettings() async {
String ip = _ipController.text;
int port = int.tryParse(_portController.text) ?? SocketManager.defaultPort;
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('ip', ip);
await prefs.setInt('port', port);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('IP Address:'),
TextField(controller: _ipController),
SizedBox(height: 16),
Text('Port Number:'),
TextField(controller: _portController),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_saveSettings();
Navigator.pop(context);
},
child: Text('Save'),
),
],
),
),
);
}
}