feat: integrate RTL-TCP server support
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:lbjconsole/models/merged_record.dart';
|
||||
import 'package:lbjconsole/models/train_record.dart';
|
||||
@@ -13,15 +14,25 @@ import 'package:lbjconsole/services/ble_service.dart';
|
||||
import 'package:lbjconsole/services/database_service.dart';
|
||||
import 'package:lbjconsole/services/notification_service.dart';
|
||||
import 'package:lbjconsole/services/background_service.dart';
|
||||
import 'package:lbjconsole/services/rtl_tcp_service.dart';
|
||||
import 'package:lbjconsole/themes/app_theme.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class _ConnectionStatusWidget extends StatefulWidget {
|
||||
final BLEService bleService;
|
||||
final RtlTcpService rtlTcpService;
|
||||
final DateTime? lastReceivedTime;
|
||||
final DateTime? rtlTcpLastReceivedTime;
|
||||
final bool rtlTcpEnabled;
|
||||
final bool rtlTcpConnected;
|
||||
|
||||
const _ConnectionStatusWidget({
|
||||
required this.bleService,
|
||||
required this.rtlTcpService,
|
||||
required this.lastReceivedTime,
|
||||
required this.rtlTcpLastReceivedTime,
|
||||
required this.rtlTcpEnabled,
|
||||
required this.rtlTcpConnected,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -58,19 +69,32 @@ class _ConnectionStatusWidgetState extends State<_ConnectionStatusWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isRtlTcpMode = widget.rtlTcpEnabled;
|
||||
final rtlTcpConnected = widget.rtlTcpConnected;
|
||||
|
||||
final isConnected = isRtlTcpMode ? rtlTcpConnected : _isConnected;
|
||||
final statusColor = isRtlTcpMode
|
||||
? (rtlTcpConnected ? Colors.green : Colors.red)
|
||||
: (_isConnected ? Colors.green : Colors.red);
|
||||
final statusText = isRtlTcpMode
|
||||
? (rtlTcpConnected ? '已连接' : '未连接')
|
||||
: _deviceStatus;
|
||||
|
||||
final lastReceivedTime = isRtlTcpMode ? widget.rtlTcpLastReceivedTime : widget.lastReceivedTime;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.lastReceivedTime == null || !_isConnected) ...[
|
||||
Text(_deviceStatus,
|
||||
if (lastReceivedTime == null || !isConnected) ...[
|
||||
Text(statusText,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
],
|
||||
_LastReceivedTimeWidget(
|
||||
lastReceivedTime: widget.lastReceivedTime,
|
||||
isConnected: _isConnected,
|
||||
lastReceivedTime: lastReceivedTime,
|
||||
isConnected: isConnected,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -79,7 +103,7 @@ class _ConnectionStatusWidgetState extends State<_ConnectionStatusWidget> {
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _isConnected ? Colors.green : Colors.red,
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
@@ -179,14 +203,23 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
String _mapType = 'webview';
|
||||
|
||||
late final BLEService _bleService;
|
||||
late final RtlTcpService _rtlTcpService;
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
final DatabaseService _databaseService = DatabaseService.instance;
|
||||
|
||||
StreamSubscription? _connectionSubscription;
|
||||
StreamSubscription? _rtlTcpConnectionSubscription;
|
||||
StreamSubscription? _dataSubscription;
|
||||
StreamSubscription? _rtlTcpDataSubscription;
|
||||
StreamSubscription? _lastReceivedTimeSubscription;
|
||||
StreamSubscription? _rtlTcpLastReceivedTimeSubscription;
|
||||
StreamSubscription? _settingsSubscription;
|
||||
DateTime? _lastReceivedTime;
|
||||
DateTime? _rtlTcpLastReceivedTime;
|
||||
bool _isHistoryEditMode = false;
|
||||
bool _rtlTcpEnabled = false;
|
||||
bool _rtlTcpConnected = false;
|
||||
bool _isConnected = false;
|
||||
final GlobalKey<HistoryScreenState> _historyScreenKey =
|
||||
GlobalKey<HistoryScreenState>();
|
||||
final GlobalKey<RealtimeScreenState> _realtimeScreenKey =
|
||||
@@ -197,23 +230,47 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_bleService = BLEService();
|
||||
_rtlTcpService = RtlTcpService();
|
||||
_bleService.initialize();
|
||||
_loadRtlTcpSettings();
|
||||
_initializeServices();
|
||||
_checkAndStartBackgroundService();
|
||||
_setupConnectionListener();
|
||||
_setupLastReceivedTimeListener();
|
||||
_setupSettingsListener();
|
||||
_loadMapType();
|
||||
}
|
||||
|
||||
Future<void> _loadMapType() async {
|
||||
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
||||
final settings = await DatabaseService.instance.getAllSettings();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mapType = settings['mapType']?.toString() ?? 'webview';
|
||||
_mapType = settings?['mapType']?.toString() ?? 'webview';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _loadRtlTcpSettings() async {
|
||||
developer.log('rtl_tcp: load_settings');
|
||||
final settings = await _databaseService.getAllSettings();
|
||||
developer.log('rtl_tcp: settings_loaded: enabled=${(settings?['rtlTcpEnabled'] ?? 0) == 1}, host=${settings?['rtlTcpHost']?.toString() ?? '127.0.0.1'}, port=${settings?['rtlTcpPort']?.toString() ?? '14423'}');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_rtlTcpEnabled = (settings?['rtlTcpEnabled'] ?? 0) == 1;
|
||||
_rtlTcpConnected = _rtlTcpService.isConnected;
|
||||
});
|
||||
|
||||
if (_rtlTcpEnabled && !_rtlTcpConnected) {
|
||||
final host = settings?['rtlTcpHost']?.toString() ?? '127.0.0.1';
|
||||
final port = settings?['rtlTcpPort']?.toString() ?? '14423';
|
||||
developer.log('rtl_tcp: auto_connect');
|
||||
_connectToRtlTcp(host, port);
|
||||
} else {
|
||||
developer.log('rtl_tcp: skip_connect: enabled=$_rtlTcpEnabled, connected=$_rtlTcpConnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkAndStartBackgroundService() async {
|
||||
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
||||
final backgroundServiceEnabled =
|
||||
@@ -233,22 +290,92 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_rtlTcpLastReceivedTimeSubscription =
|
||||
_rtlTcpService.lastReceivedTimeStream.listen((time) {
|
||||
if (mounted) {
|
||||
if (_rtlTcpEnabled) {
|
||||
setState(() {
|
||||
_rtlTcpLastReceivedTime = time;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setupSettingsListener() {
|
||||
developer.log('rtl_tcp: setup_listener');
|
||||
_settingsSubscription =
|
||||
DatabaseService.instance.onSettingsChanged((settings) {
|
||||
if (mounted && _currentIndex == 1) {
|
||||
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
|
||||
developer.log('rtl_tcp: settings_changed: enabled=${(settings?['rtlTcpEnabled'] ?? 0) == 1}, host=${settings?['rtlTcpHost']?.toString() ?? '127.0.0.1'}, port=${settings?['rtlTcpPort']?.toString() ?? '14423'}');
|
||||
if (mounted) {
|
||||
final rtlTcpEnabled = (settings?['rtlTcpEnabled'] ?? 0) == 1;
|
||||
if (rtlTcpEnabled != _rtlTcpEnabled) {
|
||||
setState(() {
|
||||
_rtlTcpEnabled = rtlTcpEnabled;
|
||||
});
|
||||
|
||||
if (rtlTcpEnabled) {
|
||||
final host = settings?['rtlTcpHost']?.toString() ?? '127.0.0.1';
|
||||
final port = settings?['rtlTcpPort']?.toString() ?? '14423';
|
||||
_connectToRtlTcp(host, port);
|
||||
} else {
|
||||
_rtlTcpConnectionSubscription?.cancel();
|
||||
_rtlTcpDataSubscription?.cancel();
|
||||
_rtlTcpLastReceivedTimeSubscription?.cancel();
|
||||
_rtlTcpService.disconnect();
|
||||
setState(() {
|
||||
_rtlTcpConnected = false;
|
||||
_rtlTcpLastReceivedTime = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentIndex == 1) {
|
||||
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setupConnectionListener() {
|
||||
_connectionSubscription = _bleService.connectionStream.listen((connected) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isConnected = connected;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_rtlTcpConnectionSubscription = _rtlTcpService.connectionStream.listen((connected) {
|
||||
if (mounted) {
|
||||
if (_rtlTcpEnabled) {
|
||||
setState(() {
|
||||
_rtlTcpConnected = connected;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _connectToRtlTcp(String host, String port) async {
|
||||
developer.log('rtl_tcp: connect: $host:$port');
|
||||
try {
|
||||
await _rtlTcpService.connect(host: host, port: port);
|
||||
developer.log('rtl_tcp: connect_req_sent');
|
||||
} catch (e) {
|
||||
developer.log('rtl_tcp: connect_fail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connectionSubscription?.cancel();
|
||||
_rtlTcpConnectionSubscription?.cancel();
|
||||
_dataSubscription?.cancel();
|
||||
_rtlTcpDataSubscription?.cancel();
|
||||
_lastReceivedTimeSubscription?.cancel();
|
||||
_rtlTcpLastReceivedTimeSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
@@ -274,6 +401,18 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
_realtimeScreenKey.currentState!.addNewRecord(record);
|
||||
}
|
||||
});
|
||||
|
||||
_rtlTcpDataSubscription = _rtlTcpService.dataStream.listen((record) {
|
||||
developer.log('rtl_tcp: recv_data: train=${record.train}');
|
||||
developer.log('rtl_tcp: recv_json: ${jsonEncode(record.toJson())}');
|
||||
_notificationService.showTrainNotification(record);
|
||||
if (_historyScreenKey.currentState != null) {
|
||||
_historyScreenKey.currentState!.addNewRecord(record);
|
||||
}
|
||||
if (_realtimeScreenKey.currentState != null) {
|
||||
_realtimeScreenKey.currentState!.addNewRecord(record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showConnectionDialog() {
|
||||
@@ -282,7 +421,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) =>
|
||||
_PixelPerfectBluetoothDialog(bleService: _bleService),
|
||||
_PixelPerfectBluetoothDialog(bleService: _bleService, rtlTcpEnabled: _rtlTcpEnabled),
|
||||
).then((_) {
|
||||
_bleService.setAutoConnectBlocked(false);
|
||||
if (!_bleService.isManualDisconnect) {
|
||||
@@ -325,19 +464,26 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
_ConnectionStatusWidget(
|
||||
bleService: _bleService,
|
||||
lastReceivedTime: _lastReceivedTime,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bluetooth, color: Colors.white),
|
||||
onPressed: _showConnectionDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
_ConnectionStatusWidget(
|
||||
bleService: _bleService,
|
||||
rtlTcpService: _rtlTcpService,
|
||||
lastReceivedTime: _lastReceivedTime,
|
||||
rtlTcpLastReceivedTime: _rtlTcpLastReceivedTime,
|
||||
rtlTcpEnabled: _rtlTcpEnabled,
|
||||
rtlTcpConnected: _rtlTcpConnected,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_rtlTcpEnabled ? Icons.wifi : Icons.bluetooth,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: _showConnectionDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,6 +564,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
||||
SettingsScreen(
|
||||
onSettingsChanged: () {
|
||||
_loadMapType();
|
||||
_loadRtlTcpSettings();
|
||||
},
|
||||
),
|
||||
];
|
||||
@@ -464,7 +611,8 @@ enum _ScanState { initial, scanning, finished }
|
||||
|
||||
class _PixelPerfectBluetoothDialog extends StatefulWidget {
|
||||
final BLEService bleService;
|
||||
const _PixelPerfectBluetoothDialog({required this.bleService});
|
||||
final bool rtlTcpEnabled;
|
||||
const _PixelPerfectBluetoothDialog({required this.bleService, required this.rtlTcpEnabled});
|
||||
@override
|
||||
State<_PixelPerfectBluetoothDialog> createState() =>
|
||||
_PixelPerfectBluetoothDialogState();
|
||||
@@ -477,13 +625,28 @@ class _PixelPerfectBluetoothDialogState
|
||||
StreamSubscription? _connectionSubscription;
|
||||
StreamSubscription? _lastReceivedTimeSubscription;
|
||||
DateTime? _lastReceivedTime;
|
||||
StreamSubscription? _rtlTcpConnectionSubscription;
|
||||
bool _rtlTcpConnected = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectionSubscription = widget.bleService.connectionStream.listen((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
if (!widget.bleService.isConnected) {
|
||||
|
||||
_rtlTcpConnectionSubscription = widget.bleService.rtlTcpService?.connectionStream.listen((connected) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_rtlTcpConnected = connected;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.rtlTcpEnabled && widget.bleService.rtlTcpService != null) {
|
||||
_rtlTcpConnected = widget.bleService.rtlTcpService!.isConnected;
|
||||
}
|
||||
|
||||
if (!widget.bleService.isConnected && !widget.rtlTcpEnabled) {
|
||||
_startScan();
|
||||
}
|
||||
}
|
||||
@@ -491,6 +654,7 @@ class _PixelPerfectBluetoothDialogState
|
||||
@override
|
||||
void dispose() {
|
||||
_connectionSubscription?.cancel();
|
||||
_rtlTcpConnectionSubscription?.cancel();
|
||||
_lastReceivedTimeSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -536,13 +700,15 @@ class _PixelPerfectBluetoothDialogState
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = widget.bleService.isConnected;
|
||||
return AlertDialog(
|
||||
title: const Text('蓝牙设备'),
|
||||
title: Text(widget.rtlTcpEnabled ? 'RTL-TCP 模式' : '蓝牙设备'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: SingleChildScrollView(
|
||||
child: isConnected
|
||||
? _buildConnectedView(context, widget.bleService.connectedDevice)
|
||||
: _buildDisconnectedView(context),
|
||||
child: widget.rtlTcpEnabled
|
||||
? _buildRtlTcpView(context)
|
||||
: (isConnected
|
||||
? _buildConnectedView(context, widget.bleService.connectedDevice)
|
||||
: _buildDisconnectedView(context)),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@@ -605,6 +771,31 @@ class _PixelPerfectBluetoothDialogState
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildRtlTcpView(BuildContext context) {
|
||||
final isConnected = _rtlTcpConnected;
|
||||
final currentAddress = widget.bleService.rtlTcpService?.currentAddress ?? '未配置';
|
||||
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.wifi, size: 48, color: isConnected ? Colors.green : Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Text(isConnected ? '已连接' : '未连接',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Text('$currentAddress',
|
||||
style: TextStyle(color: isConnected ? Colors.green : Colors.grey)),
|
||||
const SizedBox(height: 16),
|
||||
if (_lastReceivedTime != null && isConnected) ...[
|
||||
_LastReceivedTimeWidget(
|
||||
lastReceivedTime: _lastReceivedTime,
|
||||
isConnected: isConnected,
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildDeviceListView() {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
|
||||
@@ -28,6 +28,8 @@ class SettingsScreen extends StatefulWidget {
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late DatabaseService _databaseService;
|
||||
late TextEditingController _deviceNameController;
|
||||
late TextEditingController _rtlTcpHostController;
|
||||
late TextEditingController _rtlTcpPortController;
|
||||
|
||||
String _deviceName = '';
|
||||
bool _backgroundServiceEnabled = false;
|
||||
@@ -39,19 +41,143 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
GroupBy _groupBy = GroupBy.trainAndLoco;
|
||||
TimeWindow _timeWindow = TimeWindow.unlimited;
|
||||
String _mapType = 'map';
|
||||
bool _rtlTcpEnabled = false;
|
||||
String _rtlTcpHost = '127.0.0.1';
|
||||
String _rtlTcpPort = '14423';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_databaseService = DatabaseService.instance;
|
||||
_deviceNameController = TextEditingController();
|
||||
_rtlTcpHostController = TextEditingController();
|
||||
_rtlTcpPortController = TextEditingController();
|
||||
_loadSettings();
|
||||
_loadRecordCount();
|
||||
}
|
||||
|
||||
Widget _buildRtlTcpSettings() {
|
||||
return Card(
|
||||
color: AppTheme.tertiaryBlack,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.wifi,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Text('RTL-TCP 接收', style: AppTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('启用RTL-TCP接收', style: AppTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
value: _rtlTcpEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rtlTcpEnabled = value;
|
||||
});
|
||||
_saveSettings();
|
||||
},
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: _rtlTcpEnabled,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: '服务器地址',
|
||||
hintText: '输入RTL-TCP服务器地址',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
hintStyle: const TextStyle(color: Colors.white54),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.white54),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.white54),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
controller: _rtlTcpHostController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rtlTcpHost = value;
|
||||
});
|
||||
_saveSettings();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: '服务器端口',
|
||||
hintText: '输入RTL-TCP服务器端口',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
hintStyle: const TextStyle(color: Colors.white54),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.white54),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.white54),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
controller: _rtlTcpPortController,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rtlTcpPort = value;
|
||||
});
|
||||
_saveSettings();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deviceNameController.dispose();
|
||||
_rtlTcpHostController.dispose();
|
||||
_rtlTcpPortController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -71,6 +197,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_groupBy = settings.groupBy;
|
||||
_timeWindow = settings.timeWindow;
|
||||
_mapType = settingsMap['mapType']?.toString() ?? 'webview';
|
||||
_rtlTcpEnabled = (settingsMap['rtlTcpEnabled'] ?? 0) == 1;
|
||||
_rtlTcpHost = settingsMap['rtlTcpHost']?.toString() ?? '127.0.0.1';
|
||||
_rtlTcpPort = settingsMap['rtlTcpPort']?.toString() ?? '14423';
|
||||
_rtlTcpHostController.text = _rtlTcpHost;
|
||||
_rtlTcpPortController.text = _rtlTcpPort;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -95,6 +226,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
'groupBy': _groupBy.name,
|
||||
'timeWindow': _timeWindow.name,
|
||||
'mapType': _mapType,
|
||||
'rtlTcpEnabled': _rtlTcpEnabled ? 1 : 0,
|
||||
'rtlTcpHost': _rtlTcpHost,
|
||||
'rtlTcpPort': _rtlTcpPort,
|
||||
});
|
||||
widget.onSettingsChanged?.call();
|
||||
}
|
||||
@@ -110,6 +244,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
const SizedBox(height: 20),
|
||||
_buildAppSettings(),
|
||||
const SizedBox(height: 20),
|
||||
_buildRtlTcpSettings(),
|
||||
const SizedBox(height: 20),
|
||||
_buildMergeSettings(),
|
||||
const SizedBox(height: 20),
|
||||
_buildDataManagement(),
|
||||
@@ -204,7 +340,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('后台保活服务', style: AppTheme.bodyLarge),
|
||||
Text('保持应用在后台运行', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
@@ -232,8 +367,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('LBJ消息通知', style: AppTheme.bodyLarge),
|
||||
Text('接收LBJ消息通知', style: AppTheme.caption),
|
||||
Text('通知服务', style: AppTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
@@ -255,8 +389,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('地图显示方式', style: AppTheme.bodyLarge),
|
||||
Text('选择地图组件类型', style: AppTheme.caption),
|
||||
Text('地图组件类型', style: AppTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
DropdownButton<String>(
|
||||
@@ -293,7 +426,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('隐藏只有时间有效的记录', style: AppTheme.bodyLarge),
|
||||
Text('不显示只有时间信息的记录', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
@@ -342,7 +474,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('启用记录合并', style: AppTheme.bodyLarge),
|
||||
Text('合并相同内容的LBJ记录', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
@@ -452,7 +583,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('隐藏不可分组记录', style: AppTheme.bodyLarge),
|
||||
Text('不显示无法分组的记录', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
|
||||
@@ -12,6 +12,7 @@ const String _notificationChannelName = 'LBJ Console 后台服务';
|
||||
const String _notificationChannelDescription = '保持蓝牙连接稳定';
|
||||
const int _notificationId = 114514;
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
class BackgroundService {
|
||||
static final FlutterBackgroundService _service = FlutterBackgroundService();
|
||||
static bool _isInitialized = false;
|
||||
|
||||
@@ -5,11 +5,17 @@ import 'dart:math';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:lbjconsole/models/train_record.dart';
|
||||
import 'package:lbjconsole/services/database_service.dart';
|
||||
import 'package:lbjconsole/services/rtl_tcp_service.dart';
|
||||
|
||||
class BLEService {
|
||||
static final BLEService _instance = BLEService._internal();
|
||||
factory BLEService() => _instance;
|
||||
BLEService._internal();
|
||||
BLEService._internal() {
|
||||
_rtlTcpService = RtlTcpService();
|
||||
}
|
||||
|
||||
late final RtlTcpService _rtlTcpService;
|
||||
RtlTcpService? get rtlTcpService => _rtlTcpService;
|
||||
|
||||
static const String TAG = "LBJ_BT_FLUTTER";
|
||||
static final Guid serviceUuid = Guid("0000ffe0-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
@@ -13,7 +13,7 @@ class DatabaseService {
|
||||
DatabaseService._internal();
|
||||
|
||||
static const String _databaseName = 'train_database';
|
||||
static const _databaseVersion = 7;
|
||||
static const _databaseVersion = 8;
|
||||
|
||||
static const String trainRecordsTable = 'train_records';
|
||||
static const String appSettingsTable = 'app_settings';
|
||||
@@ -91,6 +91,14 @@ class DatabaseService {
|
||||
await db.execute(
|
||||
'ALTER TABLE $appSettingsTable ADD COLUMN mapSettingsTimestamp INTEGER');
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
await db.execute(
|
||||
'ALTER TABLE $appSettingsTable ADD COLUMN rtlTcpEnabled INTEGER NOT NULL DEFAULT 0');
|
||||
await db.execute(
|
||||
'ALTER TABLE $appSettingsTable ADD COLUMN rtlTcpHost TEXT NOT NULL DEFAULT "127.0.0.1"');
|
||||
await db.execute(
|
||||
'ALTER TABLE $appSettingsTable ADD COLUMN rtlTcpPort TEXT NOT NULL DEFAULT "14423"');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreate(Database db, int version) async {
|
||||
@@ -141,7 +149,10 @@ class DatabaseService {
|
||||
timeWindow TEXT NOT NULL DEFAULT 'unlimited',
|
||||
mapTimeFilter TEXT NOT NULL DEFAULT 'unlimited',
|
||||
hideUngroupableRecords INTEGER NOT NULL DEFAULT 0,
|
||||
mapSettingsTimestamp INTEGER
|
||||
mapSettingsTimestamp INTEGER,
|
||||
rtlTcpEnabled INTEGER NOT NULL DEFAULT 0,
|
||||
rtlTcpHost TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
rtlTcpPort TEXT NOT NULL DEFAULT '14423'
|
||||
)
|
||||
''');
|
||||
|
||||
@@ -168,8 +179,11 @@ class DatabaseService {
|
||||
'groupBy': 'trainAndLoco',
|
||||
'timeWindow': 'unlimited',
|
||||
'mapTimeFilter': 'unlimited',
|
||||
'hideUngroupableRecords': 0,
|
||||
'mapSettingsTimestamp': null,
|
||||
'hideUngroupableRecords': 0,
|
||||
'mapSettingsTimestamp': null,
|
||||
'rtlTcpEnabled': 0,
|
||||
'rtlTcpHost': '127.0.0.1',
|
||||
'rtlTcpPort': '14423',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
404
lib/services/rtl_tcp_service.dart
Normal file
404
lib/services/rtl_tcp_service.dart
Normal file
@@ -0,0 +1,404 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:math';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gbk_codec/gbk_codec.dart';
|
||||
import 'package:lbjconsole/models/train_record.dart';
|
||||
import 'package:lbjconsole/services/database_service.dart';
|
||||
|
||||
const String _lbjInfoAddr = "1234000";
|
||||
const String _lbjInfo2Addr = "1234002";
|
||||
const String _lbjSyncAddr = "1234008";
|
||||
const int _functionDown = 1;
|
||||
const int _functionUp = 3;
|
||||
|
||||
class _LbJState {
|
||||
String train = "<NUL>";
|
||||
int direction = -1;
|
||||
String speed = "NUL";
|
||||
String positionKm = " <NUL>";
|
||||
String time = "<NUL>";
|
||||
String lbjClass = "NA";
|
||||
String loco = "<NUL>";
|
||||
String route = "********";
|
||||
|
||||
String posLonDeg = "";
|
||||
String posLonMin = "";
|
||||
String posLatDeg = "";
|
||||
String posLatMin = "";
|
||||
|
||||
String _info2Hex = "";
|
||||
|
||||
void reset() {
|
||||
train = "<NUL>";
|
||||
direction = -1;
|
||||
speed = "NUL";
|
||||
positionKm = " <NUL>";
|
||||
time = "<NUL>";
|
||||
lbjClass = "NA";
|
||||
loco = "<NUL>";
|
||||
route = "********";
|
||||
posLonDeg = "";
|
||||
posLonMin = "";
|
||||
posLatDeg = "";
|
||||
posLatMin = "";
|
||||
_info2Hex = "";
|
||||
}
|
||||
|
||||
String _recodeBCD(String numericStr) {
|
||||
return numericStr
|
||||
.replaceAll('.', 'A')
|
||||
.replaceAll('U', 'B')
|
||||
.replaceAll(' ', 'C')
|
||||
.replaceAll('-', 'D')
|
||||
.replaceAll(')', 'E')
|
||||
.replaceAll('(', 'F');
|
||||
}
|
||||
|
||||
int _hexToChar(String hex1, String hex2) {
|
||||
final String hex = "$hex1$hex2";
|
||||
return int.tryParse(hex, radix: 16) ?? 0;
|
||||
}
|
||||
|
||||
String _gbkToUtf8(List<int> gbkBytes) {
|
||||
try {
|
||||
final validBytes = gbkBytes.where((b) => b != 0).toList();
|
||||
return gbk.decode(validBytes);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void updateFromRaw(String addr, int func, String numeric) {
|
||||
if (func == _functionDown || func == _functionUp) {
|
||||
direction = func;
|
||||
}
|
||||
switch (addr) {
|
||||
case _lbjInfoAddr:
|
||||
final RegExp infoRegex = RegExp(r'^\s*(\S+)\s+(\S+)\s+(\S+)');
|
||||
final match = infoRegex.firstMatch(numeric);
|
||||
if (match != null) {
|
||||
train = match.group(1) ?? "<NUL>";
|
||||
speed = match.group(2) ?? "NUL";
|
||||
|
||||
String pos = match.group(3)?.trim() ?? "";
|
||||
|
||||
if (pos.isEmpty) {
|
||||
positionKm = " <NUL>";
|
||||
} else if (pos.length > 1) {
|
||||
positionKm =
|
||||
"${pos.substring(0, pos.length - 1)}.${pos.substring(pos.length - 1)}";
|
||||
} else {
|
||||
positionKm = "0.$pos";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case _lbjInfo2Addr:
|
||||
String buffer = numeric;
|
||||
if (buffer.length < 50) return;
|
||||
_info2Hex = _recodeBCD(buffer);
|
||||
|
||||
if (_info2Hex.length >= 4) {
|
||||
try {
|
||||
List<int> classBytes = [
|
||||
_hexToChar(_info2Hex[0], _info2Hex[1]),
|
||||
_hexToChar(_info2Hex[2], _info2Hex[3]),
|
||||
];
|
||||
lbjClass = String.fromCharCodes(classBytes
|
||||
.where((b) => b > 0x1F && b < 0x7F && b != 0x22 && b != 0x2C));
|
||||
} catch (e) {}
|
||||
}
|
||||
if (buffer.length >= 12) loco = buffer.substring(4, 12);
|
||||
|
||||
List<int> routeBytes = List<int>.filled(17, 0);
|
||||
|
||||
if (_info2Hex.length >= 18) {
|
||||
try {
|
||||
routeBytes[0] = _hexToChar(_info2Hex[14], _info2Hex[15]);
|
||||
routeBytes[1] = _hexToChar(_info2Hex[16], _info2Hex[17]);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (_info2Hex.length >= 22) {
|
||||
try {
|
||||
routeBytes[2] = _hexToChar(_info2Hex[18], _info2Hex[19]);
|
||||
routeBytes[3] = _hexToChar(_info2Hex[20], _info2Hex[21]);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (_info2Hex.length >= 30) {
|
||||
try {
|
||||
routeBytes[4] = _hexToChar(_info2Hex[22], _info2Hex[23]);
|
||||
routeBytes[5] = _hexToChar(_info2Hex[24], _info2Hex[25]);
|
||||
routeBytes[6] = _hexToChar(_info2Hex[26], _info2Hex[27]);
|
||||
routeBytes[7] = _hexToChar(_info2Hex[28], _info2Hex[29]);
|
||||
} catch (e) {}
|
||||
}
|
||||
route = _gbkToUtf8(routeBytes);
|
||||
|
||||
if (buffer.length >= 39) {
|
||||
posLonDeg = buffer.substring(30, 33);
|
||||
posLonMin = "${buffer.substring(33, 35)}.${buffer.substring(35, 39)}";
|
||||
}
|
||||
if (buffer.length >= 47) {
|
||||
posLatDeg = buffer.substring(39, 41);
|
||||
posLatMin = "${buffer.substring(41, 43)}.${buffer.substring(43, 47)}";
|
||||
}
|
||||
break;
|
||||
|
||||
case _lbjSyncAddr:
|
||||
if (numeric.length >= 5)
|
||||
time = "${numeric.substring(1, 3)}:${numeric.substring(3, 5)}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double _convertMagSqToRssi(double magsqRaw) {
|
||||
if (magsqRaw <= 0) return -120.0;
|
||||
double rssi = 10 * log(magsqRaw) / log(10);
|
||||
return (rssi - 30.0).clamp(-120.0, -20.0);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toTrainRecordJson(double magsqRaw) {
|
||||
final now = DateTime.now();
|
||||
final double finalRssi = _convertMagSqToRssi(magsqRaw);
|
||||
|
||||
String gpsPosition = "";
|
||||
if (posLatDeg.isNotEmpty && posLatMin.isNotEmpty) {
|
||||
gpsPosition = "${posLatDeg}°${posLatMin}′";
|
||||
}
|
||||
if (posLonDeg.isNotEmpty && posLonMin.isNotEmpty) {
|
||||
gpsPosition +=
|
||||
(gpsPosition.isEmpty ? "" : " ") + "${posLonDeg}°${posLonMin}′";
|
||||
}
|
||||
|
||||
String kmPosition = positionKm.replaceAll(' <NUL>', '');
|
||||
|
||||
final jsonData = {
|
||||
'uniqueId': '${now.millisecondsSinceEpoch}_${Random().nextInt(9999)}',
|
||||
'receivedTimestamp': now.millisecondsSinceEpoch,
|
||||
'timestamp': now.millisecondsSinceEpoch,
|
||||
'rssi': finalRssi,
|
||||
'train': train.replaceAll('<NUL>', ''),
|
||||
'loco': loco.replaceAll('<NUL>', ''),
|
||||
'speed': speed.replaceAll('NUL', ''),
|
||||
'position': kmPosition,
|
||||
'positionInfo': gpsPosition,
|
||||
'route': route.replaceAll('********', ''),
|
||||
'lbjClass': lbjClass.replaceAll('NA', ''),
|
||||
'time': time.replaceAll('<NUL>', ''),
|
||||
'direction': (direction == 1 || direction == 3) ? direction : 0,
|
||||
'locoType': "",
|
||||
};
|
||||
return jsonData;
|
||||
}
|
||||
}
|
||||
|
||||
class RtlTcpService {
|
||||
static final RtlTcpService _instance = RtlTcpService._internal();
|
||||
factory RtlTcpService() => _instance;
|
||||
RtlTcpService._internal();
|
||||
static const _methodChannel =
|
||||
MethodChannel('org.noxylva.lbjconsole/rtl_tcp_method');
|
||||
static const _eventChannel =
|
||||
EventChannel('org.noxylva.lbjconsole/rtl_tcp_event');
|
||||
final StreamController<String> _statusController =
|
||||
StreamController<String>.broadcast();
|
||||
final StreamController<TrainRecord> _dataController =
|
||||
StreamController<TrainRecord>.broadcast();
|
||||
final StreamController<bool> _connectionController =
|
||||
StreamController<bool>.broadcast();
|
||||
final StreamController<DateTime?> _lastReceivedTimeController =
|
||||
StreamController<DateTime?>.broadcast();
|
||||
|
||||
Stream<String> get statusStream => _statusController.stream;
|
||||
Stream<TrainRecord> get dataStream => _dataController.stream;
|
||||
Stream<bool> get connectionStream => _connectionController.stream;
|
||||
Stream<DateTime?> get lastReceivedTimeStream =>
|
||||
_lastReceivedTimeController.stream;
|
||||
String _deviceStatus = "未连接";
|
||||
bool _isConnected = false;
|
||||
DateTime? _lastReceivedTime;
|
||||
StreamSubscription? _eventChannelSubscription;
|
||||
final _LbJState _state = _LbJState();
|
||||
Timer? _reconnectTimer;
|
||||
String _lastHost = '127.0.0.1';
|
||||
String _lastPort = '14423';
|
||||
static const Duration _reconnectInterval = Duration(seconds: 2);
|
||||
bool _isEnabled = false;
|
||||
|
||||
static const bool _logRaw = true;
|
||||
static const bool _logParsed = true;
|
||||
String _lastRawMessage = "";
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
String get deviceStatus => _deviceStatus;
|
||||
String get currentAddress => '$_lastHost:$_lastPort';
|
||||
bool get isEnabled => _isEnabled;
|
||||
|
||||
void _updateConnectionState(bool connected, String status) {
|
||||
if (_isConnected == connected && _deviceStatus == status) return;
|
||||
final wasConnected = _isConnected;
|
||||
_isConnected = connected;
|
||||
_deviceStatus = status;
|
||||
if (!connected) {
|
||||
_lastReceivedTime = null;
|
||||
if (wasConnected) _state.reset();
|
||||
if (_isEnabled) _startAutoReconnect();
|
||||
} else {
|
||||
_cancelAutoReconnect();
|
||||
}
|
||||
_statusController.add(_deviceStatus);
|
||||
_connectionController.add(_isConnected);
|
||||
_lastReceivedTimeController.add(_lastReceivedTime);
|
||||
}
|
||||
|
||||
void _startAutoReconnect() {
|
||||
if (!_isEnabled || _reconnectTimer != null) return;
|
||||
developer.log('RTL-TCP: 启动自动重连定时器', name: 'RtlTcpService');
|
||||
_reconnectTimer = Timer.periodic(_reconnectInterval, (timer) {
|
||||
if (_isConnected || !_isEnabled) {
|
||||
_cancelAutoReconnect();
|
||||
return;
|
||||
}
|
||||
developer.log('RTL-TCP: 自动重连: 尝试连接...', name: 'RtlTcpService');
|
||||
_internalConnect();
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelAutoReconnect() {
|
||||
if (_reconnectTimer != null) {
|
||||
developer.log('RTL-TCP: 取消自动重连', name: 'RtlTcpService');
|
||||
_reconnectTimer!.cancel();
|
||||
_reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToEventChannel() {
|
||||
if (_eventChannelSubscription != null) {
|
||||
_eventChannelSubscription?.cancel();
|
||||
}
|
||||
|
||||
_eventChannelSubscription = _eventChannel.receiveBroadcastStream().listen(
|
||||
(dynamic event) {
|
||||
try {
|
||||
final map = event as Map;
|
||||
|
||||
if (map.containsKey('connected')) {
|
||||
final connected = map['connected'] as bool? ?? false;
|
||||
if (_isConnected != connected) {
|
||||
_updateConnectionState(connected, connected ? "已连接" : "已断开");
|
||||
}
|
||||
}
|
||||
|
||||
if (map.containsKey('address')) {
|
||||
final addr = map['address'] as String;
|
||||
|
||||
if (addr != _lbjInfoAddr &&
|
||||
addr != _lbjInfo2Addr &&
|
||||
addr != _lbjSyncAddr) {
|
||||
return;
|
||||
}
|
||||
|
||||
final func = int.tryParse(map['func'] as String? ?? '-1') ?? -1;
|
||||
final numeric = map['numeric'] as String;
|
||||
final magsqRaw = (map['magsqRaw'] as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
final String currentRawMessage = "$addr|$func|$numeric";
|
||||
if (currentRawMessage == _lastRawMessage) {
|
||||
return;
|
||||
}
|
||||
_lastRawMessage = currentRawMessage;
|
||||
|
||||
if (_logRaw) {
|
||||
developer.log('RTL-TCP-RAW: $currentRawMessage',
|
||||
name: 'RTL-TCP-Data');
|
||||
}
|
||||
|
||||
if (!_isConnected) {
|
||||
_updateConnectionState(true, "已连接");
|
||||
}
|
||||
_lastReceivedTime = DateTime.now();
|
||||
_lastReceivedTimeController.add(_lastReceivedTime);
|
||||
|
||||
_state.updateFromRaw(addr, func, numeric);
|
||||
|
||||
if (addr == _lbjInfoAddr || addr == _lbjInfo2Addr) {
|
||||
final jsonData = _state.toTrainRecordJson(magsqRaw);
|
||||
final trainRecord = TrainRecord.fromJson(jsonData);
|
||||
|
||||
if (_logParsed) {
|
||||
developer.log('RTL-TCP-PARSED: ${jsonEncode(jsonData)}',
|
||||
name: 'RTL-TCP-Data');
|
||||
}
|
||||
|
||||
_dataController.add(trainRecord);
|
||||
DatabaseService.instance.insertRecord(trainRecord);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
developer.log('RTL-TCP StateMachine Error: $e',
|
||||
name: 'RTL-TCP', error: e, stackTrace: s);
|
||||
_updateConnectionState(false, "数据解析错误");
|
||||
}
|
||||
},
|
||||
onError: (dynamic error) {
|
||||
_updateConnectionState(false, "数据通道错误");
|
||||
_eventChannelSubscription?.cancel();
|
||||
_eventChannelSubscription = null;
|
||||
},
|
||||
onDone: () {
|
||||
_updateConnectionState(false, "连接已断开");
|
||||
_eventChannelSubscription = null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _internalConnect() async {
|
||||
if (_eventChannelSubscription == null) {
|
||||
_listenToEventChannel();
|
||||
}
|
||||
|
||||
try {
|
||||
await _methodChannel
|
||||
.invokeMethod('connect', {'host': _lastHost, 'port': _lastPort});
|
||||
} on PlatformException catch (e) {
|
||||
_updateConnectionState(false, "连接失败: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect({String? host, String? port}) async {
|
||||
final settings = await DatabaseService.instance.getAllSettings();
|
||||
final dbHost = host ?? settings?['rtl_tcp_host'] ?? '127.0.0.1';
|
||||
final dbPort = port ?? settings?['rtl_tcp_port'] ?? '14423';
|
||||
_isEnabled = true;
|
||||
_lastHost = dbHost;
|
||||
_lastPort = dbPort;
|
||||
if (_isConnected) return;
|
||||
_updateConnectionState(false, "正在连接...");
|
||||
_internalConnect();
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_isEnabled = false;
|
||||
_cancelAutoReconnect();
|
||||
await _eventChannelSubscription?.cancel();
|
||||
_eventChannelSubscription = null;
|
||||
try {
|
||||
await _methodChannel.invokeMethod('disconnect');
|
||||
} catch (e) {
|
||||
developer.log('RTL-TCP: 原生断开出错: $e', name: 'RTL-TCP');
|
||||
}
|
||||
_updateConnectionState(false, "已禁用");
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_statusController.close();
|
||||
_dataController.close();
|
||||
_connectionController.close();
|
||||
_lastReceivedTimeController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user