Files
LBJ_Console/lib/services/rtl_tcp_service.dart

413 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
print('gbkBytes: ${validBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',')}');
final result = gbk_bytes.decode(validBytes);
print('gbk decoded: $result');
return result;
} catch (e) {
print('gbk decode error: $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);
print('info2 raw: $buffer');
print('info2 hex: $_info2Hex');
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();
}
}