Compare commits
2 Commits
1628bd4c55
...
1736ce9159
Author | SHA1 | Date | |
---|---|---|---|
1736ce9159 | |||
cac2180e6e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,6 +31,7 @@ migrate_working_dir/
|
|||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
|
.vscode/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
|
@ -34,8 +34,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
carIpController.text = '192.168.137.210';
|
carIpController.text = '192.168.137.122';
|
||||||
cameraIpController.text = '192.168.137.87';
|
cameraIpController.text = '192.168.137.138';
|
||||||
GlobalSettingState globalSettingState = context.watch<GlobalSettingState>();
|
GlobalSettingState globalSettingState = context.watch<GlobalSettingState>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: globalSettingState.cameraIP != '' && globalSettingState.carIP != '' ?
|
body: globalSettingState.cameraIP != '' && globalSettingState.carIP != '' ?
|
||||||
|
@ -9,6 +9,7 @@ import 'package:marscar_controller/main.dart';
|
|||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
|
||||||
class PlayerPage extends StatefulWidget {
|
class PlayerPage extends StatefulWidget {
|
||||||
const PlayerPage({super.key});
|
const PlayerPage({super.key});
|
||||||
@ -23,6 +24,8 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
|
|
||||||
bool finishConnection = false;
|
bool finishConnection = false;
|
||||||
|
|
||||||
|
Timer? _reconnectionTimer;
|
||||||
|
|
||||||
final int callBackPeriod = 20;
|
final int callBackPeriod = 20;
|
||||||
|
|
||||||
RawDatagramSocket? _udpSocket;
|
RawDatagramSocket? _udpSocket;
|
||||||
@ -31,11 +34,31 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Socket? _timestampSocket;
|
Socket? _timestampSocket;
|
||||||
StreamSubscription? _socketSubscription;
|
StreamSubscription? _socketSubscription;
|
||||||
|
|
||||||
|
final List<int> _latencyReadings = [];
|
||||||
|
final int _latencyWindow = 30;
|
||||||
|
|
||||||
Timer? _latencyTestTimer;
|
Timer? _latencyTestTimer;
|
||||||
String _controlLatencyResult = '正在连接控制...';
|
String _controlLatencyResult = '正在连接控制器...';
|
||||||
|
|
||||||
|
final List<int> _controlLatencyReadings = [];
|
||||||
|
final int _controlLatencyWindow = 10;
|
||||||
|
|
||||||
Timer? _udpSendTimer;
|
Timer? _udpSendTimer;
|
||||||
|
|
||||||
|
final List<FlSpot> _videoLatencyHistory = [];
|
||||||
|
final List<FlSpot> _controlLatencyHistory = [];
|
||||||
|
final int _maxHistoryCount = 100;
|
||||||
|
|
||||||
|
Timer? _connectionHealthTimer;
|
||||||
|
DateTime? _lastMessageReceivedTime;
|
||||||
|
|
||||||
|
String _fpsInfo = 'FPS: N/A';
|
||||||
|
String _bitrateInfo = '速率: N/A';
|
||||||
|
Timer? _infoTimer;
|
||||||
|
|
||||||
|
double maxPawRateScale = 1.0;
|
||||||
|
Set<double> _pawScaleSelection = {1.0};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -48,7 +71,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
|
|
||||||
_initializeUdpSocket();
|
_initializeUdpSocket();
|
||||||
|
|
||||||
_latencyTestTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_latencyTestTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
||||||
if (_udpSocket == null || globalSettingState.carIP.isEmpty) return;
|
if (_udpSocket == null || globalSettingState.carIP.isEmpty) return;
|
||||||
|
|
||||||
final sendTime = DateTime.now().millisecondsSinceEpoch;
|
final sendTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
@ -65,6 +88,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_infoTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
_updateStreamInfo();
|
||||||
|
});
|
||||||
|
|
||||||
_udpSendTimer = Timer.periodic(Duration(milliseconds: callBackPeriod), (timer) {
|
_udpSendTimer = Timer.periodic(Duration(milliseconds: callBackPeriod), (timer) {
|
||||||
if (_udpSocket == null || globalSettingState.carIP.isEmpty) return;
|
if (_udpSocket == null || globalSettingState.carIP.isEmpty) return;
|
||||||
|
|
||||||
@ -80,6 +107,13 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
reset = 0;
|
reset = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (getball == 1) {
|
||||||
|
List<int> data = utf8.encode('GETBALL=1');
|
||||||
|
_udpSocket!.send(data, InternetAddress(globalSettingState.carIP), 80);
|
||||||
|
setState(() {
|
||||||
|
getball = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showErrorSnackBar('控制失败: $e');
|
_showErrorSnackBar('控制失败: $e');
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
@ -89,6 +123,15 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (player.platform is NativePlayer) {
|
if (player.platform is NativePlayer) {
|
||||||
final props = {
|
final props = {
|
||||||
'profile': 'low-latency',
|
'profile': 'low-latency',
|
||||||
|
'untimed': 'yes',
|
||||||
|
'demuxer-max-bytes': '102400',
|
||||||
|
'demuxer-max-back-bytes': '102400',
|
||||||
|
'demuxer-readahead-secs': '0.1',
|
||||||
|
'cache': 'no',
|
||||||
|
'cache-pause': 'no',
|
||||||
|
'vd-lavc-threads': '1',
|
||||||
|
'framedrop': 'vo',
|
||||||
|
'hwdec': 'auto-safe',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (final entry in props.entries) {
|
for (final entry in props.entries) {
|
||||||
@ -100,6 +143,76 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
_connectToTimestampServer(globalSettingState.cameraIP);
|
_connectToTimestampServer(globalSettingState.cameraIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleDisconnection() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_reconnectionTimer?.isActive ?? false) return;
|
||||||
|
|
||||||
|
_socketSubscription?.cancel();
|
||||||
|
_timestampSocket?.destroy();
|
||||||
|
_latencyReadings.clear();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_pingResult = '图传已断开, 10秒后重连...';
|
||||||
|
finishConnection = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalSettingState globalSettingState = context.read<GlobalSettingState>();
|
||||||
|
final ip = globalSettingState.cameraIP;
|
||||||
|
|
||||||
|
if (ip.isNotEmpty) {
|
||||||
|
_reconnectionTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||||
|
if (!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.open(Media('rtsp://${globalSettingState.cameraIP}:8554/live'));
|
||||||
|
_connectToTimestampServer(ip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startHealthCheckTimer() {
|
||||||
|
_connectionHealthTimer?.cancel();
|
||||||
|
_connectionHealthTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||||
|
if (!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastMessageReceivedTime != null &&
|
||||||
|
DateTime.now().difference(_lastMessageReceivedTime!) > const Duration(seconds: 10)) {
|
||||||
|
_connectionHealthTimer?.cancel();
|
||||||
|
_showErrorSnackBar('图传信号丢失,正在重连...');
|
||||||
|
_handleDisconnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateStreamInfo() async {
|
||||||
|
if (player.platform is! NativePlayer) return;
|
||||||
|
|
||||||
|
final nativePlayer = player.platform as NativePlayer;
|
||||||
|
try {
|
||||||
|
final bitrate = await nativePlayer.getProperty('video-bitrate');
|
||||||
|
final fps = await nativePlayer.getProperty('video-fps');
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
final bitrateInKbps = (double.tryParse(bitrate) ?? 0) / 1024;
|
||||||
|
_bitrateInfo = '速率: ${bitrateInKbps.toStringAsFixed(0)} Kbps';
|
||||||
|
final double? fpsValue = double.tryParse(fps);
|
||||||
|
if (fpsValue != null) {
|
||||||
|
_fpsInfo = 'FPS: ${fpsValue.toStringAsFixed(1)}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Handle potential errors if properties are not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _initializeUdpSocket() async {
|
void _initializeUdpSocket() async {
|
||||||
try {
|
try {
|
||||||
_udpSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
_udpSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||||
@ -114,9 +227,22 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
final sentTimestamp = int.tryParse(sentTimestampStr);
|
final sentTimestamp = int.tryParse(sentTimestampStr);
|
||||||
if (sentTimestamp != null) {
|
if (sentTimestamp != null) {
|
||||||
final latency = DateTime.now().millisecondsSinceEpoch - sentTimestamp;
|
final latency = DateTime.now().millisecondsSinceEpoch - sentTimestamp;
|
||||||
|
|
||||||
|
_controlLatencyReadings.add(latency);
|
||||||
|
if (_controlLatencyReadings.length > _controlLatencyWindow) {
|
||||||
|
_controlLatencyReadings.removeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final avgLatency = _controlLatencyReadings.reduce((a, b) => a + b) / _controlLatencyReadings.length;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_controlLatencyResult = '控制延迟: $latency ms';
|
_controlLatencyResult = '控制RTT: ${avgLatency.toStringAsFixed(0)} ms';
|
||||||
|
|
||||||
|
_controlLatencyHistory.add(FlSpot(DateTime.now().millisecondsSinceEpoch.toDouble(), avgLatency));
|
||||||
|
if (_controlLatencyHistory.length > _maxHistoryCount) {
|
||||||
|
_controlLatencyHistory.removeAt(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,8 +263,8 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double lx = 0.0, ly = 0.0, rx = 0.0, ry = 0.0, paw = 0.0;
|
double lx = 0.0, ly = 0.0, rx = 0.0, ry = 0.0, paw = 0.0, forward = 0.0, backward = 0.0, left = 0.0, right = 0.0, up = 0.0, down = 0.0;
|
||||||
int forward = 0, backward = 0, left = 0, right = 0, up = 0, down = 0, reset = 0;
|
int getball = 0, reset = 0;
|
||||||
bool hasSendZeroPack = false;
|
bool hasSendZeroPack = false;
|
||||||
|
|
||||||
void _connectToTimestampServer(String ip) async {
|
void _connectToTimestampServer(String ip) async {
|
||||||
@ -158,11 +284,19 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
|
|
||||||
_timestampSocket = await Socket.connect(ip, 8555, timeout: const Duration(seconds: 5));
|
_timestampSocket = await Socket.connect(ip, 8555, timeout: const Duration(seconds: 5));
|
||||||
|
|
||||||
|
_reconnectionTimer?.cancel();
|
||||||
|
|
||||||
|
_lastMessageReceivedTime = DateTime.now();
|
||||||
|
_startHealthCheckTimer();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
finishConnection = true;
|
||||||
|
});
|
||||||
|
|
||||||
_socketSubscription = _timestampSocket?.listen(
|
_socketSubscription = _timestampSocket?.listen(
|
||||||
(data) {
|
(data) {
|
||||||
setState(() {
|
_lastMessageReceivedTime = DateTime.now();
|
||||||
finishConnection = true;
|
|
||||||
});
|
|
||||||
final message = utf8.decode(data).trim();
|
final message = utf8.decode(data).trim();
|
||||||
final serverTimestampStr = message.split('\n').last;
|
final serverTimestampStr = message.split('\n').last;
|
||||||
|
|
||||||
@ -171,10 +305,26 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (serverTimestamp != null) {
|
if (serverTimestamp != null) {
|
||||||
final clientTimestamp = DateTime.now().millisecondsSinceEpoch;
|
final clientTimestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final latency = clientTimestamp - serverTimestamp;
|
final latency = clientTimestamp - serverTimestamp;
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
if (latency > 0) {
|
||||||
_pingResult = '图传延迟: ${latency.abs()} ms';
|
_latencyReadings.add(latency);
|
||||||
});
|
if (_latencyReadings.length > _latencyWindow) {
|
||||||
|
_latencyReadings.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_latencyReadings.isNotEmpty) {
|
||||||
|
final avgLatency = _latencyReadings.reduce((a, b) => a + b) / _latencyReadings.length;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_pingResult = '图传延迟: ${avgLatency.toStringAsFixed(0)} ms';
|
||||||
|
|
||||||
|
_videoLatencyHistory.add(FlSpot(DateTime.now().millisecondsSinceEpoch.toDouble(), avgLatency));
|
||||||
|
if (_videoLatencyHistory.length > _maxHistoryCount) {
|
||||||
|
_videoLatencyHistory.removeAt(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,28 +333,27 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_pingResult = '图传出错';
|
_pingResult = '图传出错';
|
||||||
finishConnection = true;
|
_handleDisconnection();
|
||||||
});
|
});
|
||||||
_showErrorSnackBar('图传出错');
|
_showErrorSnackBar('图传出错');
|
||||||
_timestampSocket?.destroy();
|
_handleDisconnection();
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_pingResult = '图传已断开';
|
_pingResult = '图传已断开';
|
||||||
finishConnection = true;
|
|
||||||
});
|
});
|
||||||
_showErrorSnackBar('图传已断开');
|
_showErrorSnackBar('图传已断开');
|
||||||
_timestampSocket?.destroy();
|
_handleDisconnection();
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pingResult = '连接图传失败';
|
_pingResult = '连接图传失败';
|
||||||
finishConnection = true;
|
|
||||||
});
|
});
|
||||||
_showErrorSnackBar('连接图传失败');
|
_showErrorSnackBar('连接图传失败');
|
||||||
|
_handleDisconnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +368,11 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
_udpSendTimer?.cancel();
|
_udpSendTimer?.cancel();
|
||||||
_udpSocket?.close();
|
_udpSocket?.close();
|
||||||
_latencyTestTimer?.cancel();
|
_latencyTestTimer?.cancel();
|
||||||
|
_latencyReadings.clear();
|
||||||
|
_controlLatencyReadings.clear();
|
||||||
|
_infoTimer?.cancel();
|
||||||
|
_connectionHealthTimer?.cancel();
|
||||||
|
_reconnectionTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,15 +408,161 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 64,
|
right: 0,
|
||||||
child: Container(
|
left: 0,
|
||||||
padding: EdgeInsets.all(8.0),
|
child: Center(
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
width: 300,
|
||||||
),
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
'$_pingResult\n$_controlLatencyResult',
|
borderRadius: BorderRadius.circular(8),
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
color: const Color.fromARGB(100, 0, 0, 0),
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
collapsedIconColor: Colors.white,
|
||||||
|
iconColor: Colors.white,
|
||||||
|
title: Text(
|
||||||
|
'$_pingResult $_bitrateInfo $_fpsInfo\n$_controlLatencyResult',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 16, 4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'图传延迟 (ms)',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
spots: _videoLatencyHistory,
|
||||||
|
isCurved: true,
|
||||||
|
color: Colors.white,
|
||||||
|
barWidth: 2,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
dotData: const FlDotData(show: false),
|
||||||
|
belowBarData: BarAreaData(show: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 40,
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
return Text(
|
||||||
|
'${value.toInt()}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
gridData: FlGridData(
|
||||||
|
show: true,
|
||||||
|
drawVerticalLine: true,
|
||||||
|
getDrawingHorizontalLine: (value) => const FlLine(
|
||||||
|
color: Color.fromARGB(50, 255, 255, 255),
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
getDrawingVerticalLine: (value) => const FlLine(
|
||||||
|
color: Color.fromARGB(50, 255, 255, 255),
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
borderData: FlBorderData(
|
||||||
|
show: true,
|
||||||
|
border: Border.all(color: const Color.fromARGB(80, 255, 255, 255)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 16, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'控制RTT (ms)',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
spots: _controlLatencyHistory,
|
||||||
|
isCurved: true,
|
||||||
|
color: Colors.white,
|
||||||
|
barWidth: 2,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
dotData: const FlDotData(show: false),
|
||||||
|
belowBarData: BarAreaData(show: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 40,
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
return Text(
|
||||||
|
'${value.toInt()}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
gridData: FlGridData(
|
||||||
|
show: true,
|
||||||
|
drawVerticalLine: true,
|
||||||
|
getDrawingHorizontalLine: (value) => const FlLine(
|
||||||
|
color: Color.fromARGB(50, 255, 255, 255),
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
getDrawingVerticalLine: (value) => const FlLine(
|
||||||
|
color: Color.fromARGB(50, 255, 255, 255),
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
borderData: FlBorderData(
|
||||||
|
show: true,
|
||||||
|
border: Border.all(color: const Color.fromARGB(80, 255, 255, 255)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -270,6 +570,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
left: 64,
|
left: 64,
|
||||||
bottom: 32,
|
bottom: 32,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
@ -294,6 +595,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
right: 128,
|
right: 128,
|
||||||
bottom: 180,
|
bottom: 180,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
@ -317,10 +619,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
} else {
|
} else {
|
||||||
var angle = -atan2(details.y, details.x), pi_18 = pi / 18.0;
|
var angle = -atan2(details.y, details.x), pi_18 = pi / 18.0;
|
||||||
setState(() {
|
setState(() {
|
||||||
left = angle < - pi_18 * 12 || angle > pi_18 * 12 ? 1 : 0;
|
left = angle < - pi_18 * 12 || angle > pi_18 * 12 ? details.x * maxPawRateScale : 0;
|
||||||
right = angle > - pi_18 * 6 && angle < pi_18 * 6 ? 1 : 0;
|
right = angle > - pi_18 * 6 && angle < pi_18 * 6 ? details.x * maxPawRateScale : 0;
|
||||||
forward = angle > pi_18 * 3 && angle < pi_18 * 15 ? 1 : 0;
|
forward = angle > pi_18 * 3 && angle < pi_18 * 15 ? -details.y * maxPawRateScale : 0;
|
||||||
backward = angle > - pi_18 * 15 && angle < - pi_18 * 3 ? 1 : 0;
|
backward = angle > - pi_18 * 15 && angle < - pi_18 * 3 ? -details.y * maxPawRateScale : 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -330,6 +632,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
bottom: 180,
|
bottom: 180,
|
||||||
right: 64,
|
right: 64,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
mode: JoystickMode.vertical,
|
mode: JoystickMode.vertical,
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
@ -352,8 +655,8 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
listener: (details) {
|
listener: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
up = details.y < 0 ? 1: 0;
|
up = details.y < 0 ? -details.y * maxPawRateScale * 0.6 : 0;
|
||||||
down = details.y > 0 ? 1: 0;
|
down = details.y > 0 ? -details.y * maxPawRateScale * 0.6 : 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -362,6 +665,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
bottom: 100,
|
bottom: 100,
|
||||||
right: 64,
|
right: 64,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
mode: JoystickMode.horizontal,
|
mode: JoystickMode.horizontal,
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
@ -393,6 +697,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
bottom: 40,
|
bottom: 40,
|
||||||
right: 64,
|
right: 64,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
mode: JoystickMode.horizontal,
|
mode: JoystickMode.horizontal,
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
@ -415,7 +720,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
listener: (details) {
|
listener: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
rx = details.x * 127 + 127.5;
|
rx = details.x * 127 / 2 + 127.5;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -424,6 +729,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
right: 256,
|
right: 256,
|
||||||
child: Joystick(
|
child: Joystick(
|
||||||
|
includeInitialAnimation: false,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
period: Duration(milliseconds: callBackPeriod),
|
||||||
mode: JoystickMode.vertical,
|
mode: JoystickMode.vertical,
|
||||||
stick: CircleAvatar(
|
stick: CircleAvatar(
|
||||||
@ -453,7 +759,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
if(finishConnection) Positioned(
|
if(finishConnection) Positioned(
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 140,
|
right: 64,
|
||||||
child: IconButton.filled(
|
child: IconButton.filled(
|
||||||
iconSize: 24.0,
|
iconSize: 24.0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -463,11 +769,59 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
},
|
},
|
||||||
icon: Icon(Icons.refresh),
|
icon: Icon(Icons.refresh),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: WidgetStateProperty.all(Color.fromARGB(60, 255, 255, 255)),
|
backgroundColor: WidgetStateProperty.all(Color.fromARGB(60, 0, 0, 0)),
|
||||||
iconColor: WidgetStateProperty.all(Color.fromARGB(200, 255, 255, 255))
|
iconColor: WidgetStateProperty.all(Color.fromARGB(200, 255, 255, 255))
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
if(finishConnection) Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 120,
|
||||||
|
child: IconButton.filled(
|
||||||
|
iconSize: 24.0,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
getball = 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.shopping_basket),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(Color.fromARGB(60, 0, 0, 0)),
|
||||||
|
iconColor: WidgetStateProperty.all(Color.fromARGB(200, 255, 255, 255))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: SegmentedButton<double>(
|
||||||
|
segments: const <ButtonSegment<double>>[
|
||||||
|
ButtonSegment<double>(value: 0.05, label: Text('精确')),
|
||||||
|
ButtonSegment<double>(value: 0.2, label: Text('缓慢')),
|
||||||
|
ButtonSegment<double>(value: 0.5, label: Text('默认')),
|
||||||
|
ButtonSegment<double>(value: 1.0, label: Text('狂暴')),
|
||||||
|
],
|
||||||
|
selected: _pawScaleSelection,
|
||||||
|
onSelectionChanged: (Set<double> newSelection) {
|
||||||
|
setState(() {
|
||||||
|
if (newSelection.isNotEmpty) {
|
||||||
|
_pawScaleSelection = {newSelection.first};
|
||||||
|
maxPawRateScale = newSelection.first;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: SegmentedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
selectedForegroundColor: Theme.of(context).primaryColor,
|
||||||
|
selectedBackgroundColor: Colors.white,
|
||||||
|
side: BorderSide(color: Colors.grey.shade800),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -89,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -113,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
fl_chart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fl_chart
|
||||||
|
sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -551,4 +567,4 @@ packages:
|
|||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.27.4"
|
||||||
|
@ -42,6 +42,7 @@ dependencies:
|
|||||||
dart_ping: ^9.0.1
|
dart_ping: ^9.0.1
|
||||||
udp: 5.0.3
|
udp: 5.0.3
|
||||||
http: ^1.4.0
|
http: ^1.4.0
|
||||||
|
fl_chart: ^1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user