fix(ui): center lean angle gauge & needle alignment across portrait & landscape dashboard layouts
This commit is contained in:
parent
e8d0b46cf6
commit
f4d8883e2f
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -445,8 +445,8 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
if (size.width <= 0 || size.height <= 0) return;
|
if (size.width <= 0 || size.height <= 0) return;
|
||||||
|
|
||||||
final center = Offset(size.width / 2, size.height * 0.82);
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
final radius = math.min(size.width * 0.42, size.height * 0.72);
|
final radius = math.min(size.width * 0.44, size.height * 0.44);
|
||||||
|
|
||||||
if (radius <= 0) return;
|
if (radius <= 0) return;
|
||||||
|
|
||||||
|
|
@ -454,7 +454,7 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
final trackPaint = Paint()
|
final trackPaint = Paint()
|
||||||
..color = const Color(0xFF222222)
|
..color = const Color(0xFF222222)
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 6.0
|
..strokeWidth = 4.0
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
canvas.drawArc(
|
canvas.drawArc(
|
||||||
|
|
@ -465,16 +465,22 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
trackPaint,
|
trackPaint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tick marks at 0, +-15, +-30, +-45, +-60
|
// Minor & Major Tick marks around top arc (-60 to +60 in 15 deg steps)
|
||||||
final tickPaint = Paint()
|
final tickPaint = Paint()
|
||||||
..color = Colors.white24
|
..color = Colors.white24
|
||||||
..strokeWidth = 1.2;
|
..strokeWidth = 1.0;
|
||||||
|
final centerTickPaint = Paint()
|
||||||
|
..color = Colors.white70
|
||||||
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
for (final angleDeg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) {
|
for (final angleDeg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) {
|
||||||
final rad = (270 + angleDeg) * (math.pi / 180.0);
|
final rad = (270 + angleDeg) * (math.pi / 180.0);
|
||||||
final p1 = Offset(center.dx + (radius - 5) * math.cos(rad), center.dy + (radius - 5) * math.sin(rad));
|
final isZero = angleDeg == 0;
|
||||||
final p2 = Offset(center.dx + (radius + 5) * math.cos(rad), center.dy + (radius + 5) * math.sin(rad));
|
final inner = radius - (isZero ? 6.0 : 4.0);
|
||||||
canvas.drawLine(p1, p2, tickPaint);
|
final outer = radius + (isZero ? 4.0 : 2.0);
|
||||||
|
final p1 = Offset(center.dx + inner * math.cos(rad), center.dy + inner * math.sin(rad));
|
||||||
|
final p2 = Offset(center.dx + outer * math.cos(rad), center.dy + outer * math.sin(rad));
|
||||||
|
canvas.drawLine(p1, p2, isZero ? centerTickPaint : tickPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active lean sweep arc from 270 (top/upright) to (270 + leanAngle)
|
// Active lean sweep arc from 270 (top/upright) to (270 + leanAngle)
|
||||||
|
|
@ -486,7 +492,7 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
final activeArcPaint = Paint()
|
final activeArcPaint = Paint()
|
||||||
..color = activeColor
|
..color = activeColor
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 6.0
|
..strokeWidth = 4.5
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
final startRad = 270 * (math.pi / 180.0);
|
final startRad = 270 * (math.pi / 180.0);
|
||||||
|
|
@ -501,11 +507,15 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needle from center to arc
|
// Perimeter needle / pointer (drawn cleanly on outer track without intersecting center text)
|
||||||
final needleRad = (270 + clampedLean) * (math.pi / 180.0);
|
final needleRad = (270 + clampedLean) * (math.pi / 180.0);
|
||||||
final needleEnd = Offset(
|
final innerNeedle = Offset(
|
||||||
center.dx + radius * math.cos(needleRad),
|
center.dx + (radius * 0.72) * math.cos(needleRad),
|
||||||
center.dy + radius * math.sin(needleRad),
|
center.dy + (radius * 0.72) * math.sin(needleRad),
|
||||||
|
);
|
||||||
|
final outerNeedle = Offset(
|
||||||
|
center.dx + (radius + 2.0) * math.cos(needleRad),
|
||||||
|
center.dy + (radius + 2.0) * math.sin(needleRad),
|
||||||
);
|
);
|
||||||
|
|
||||||
final needlePaint = Paint()
|
final needlePaint = Paint()
|
||||||
|
|
@ -513,12 +523,11 @@ class _LandscapeLeanPainter extends CustomPainter {
|
||||||
..strokeWidth = 2.5
|
..strokeWidth = 2.5
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
canvas.drawLine(center, needleEnd, needlePaint);
|
canvas.drawLine(innerNeedle, outerNeedle, needlePaint);
|
||||||
|
|
||||||
// Glowing tip dot
|
// Glowing tip dot at perimeter
|
||||||
canvas.drawCircle(needleEnd, 4.0, Paint()..color = const Color(0xFF00E5FF));
|
canvas.drawCircle(outerNeedle, 3.5, Paint()..color = const Color(0xFF00E5FF));
|
||||||
canvas.drawCircle(needleEnd, 7.0, Paint()..color = const Color(0xFF00E5FF).withValues(alpha: 0.35));
|
canvas.drawCircle(outerNeedle, 6.0, Paint()..color = const Color(0xFF00E5FF).withValues(alpha: 0.35));
|
||||||
canvas.drawCircle(center, 3.0, Paint()..color = Colors.white70);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -241,9 +241,8 @@ class EvolutionPortraitDashboard extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
Text(
|
||||||
'° ',
|
'${leanAngle.abs().toStringAsFixed(0)}°',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'JetBrains Mono',
|
fontFamily: 'JetBrains Mono',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|
@ -557,29 +556,77 @@ class _LeanAngleGaugePainter extends CustomPainter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final center = Offset(size.width / 2, size.height * 0.52);
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
final radius = size.width * 0.36;
|
final radius = math.min(size.width, size.height) * 0.38;
|
||||||
|
|
||||||
|
// Background track arc (top semicircle: 180° to 360°)
|
||||||
const startAngle = 180 * (math.pi / 180);
|
const startAngle = 180 * (math.pi / 180);
|
||||||
const sweepAngle = 180 * (math.pi / 180);
|
const sweepAngle = 180 * (math.pi / 180);
|
||||||
|
|
||||||
final arcPaint = Paint()
|
final arcPaint = Paint()
|
||||||
..color = const Color(0xFF1F1F1F)
|
..color = const Color(0xFF222222)
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 6.0
|
..strokeWidth = 4.0
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, arcPaint);
|
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, arcPaint);
|
||||||
|
|
||||||
// Active Needle
|
// Minor & Major Tick marks around top arc (-60 to +60 in 15 deg steps)
|
||||||
final angleRad = (270 + leanAngle.clamp(-65.0, 65.0)) * (math.pi / 180);
|
final tickPaint = Paint()
|
||||||
final needleEnd = Offset(center.dx + radius * math.cos(angleRad), center.dy + radius * math.sin(angleRad));
|
..color = Colors.white24
|
||||||
|
..strokeWidth = 1.0;
|
||||||
|
final centerTickPaint = Paint()
|
||||||
|
..color = Colors.white70
|
||||||
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
|
for (final deg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) {
|
||||||
|
final rad = (270 + deg) * (math.pi / 180);
|
||||||
|
final isZero = deg == 0;
|
||||||
|
final inner = radius - (isZero ? 6.0 : 4.0);
|
||||||
|
final outer = radius + (isZero ? 4.0 : 2.0);
|
||||||
|
final p1 = Offset(center.dx + inner * math.cos(rad), center.dy + inner * math.sin(rad));
|
||||||
|
final p2 = Offset(center.dx + outer * math.cos(rad), center.dy + outer * math.sin(rad));
|
||||||
|
canvas.drawLine(p1, p2, isZero ? centerTickPaint : tickPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active lean sweep arc from 270 (upright) to (270 + leanAngle)
|
||||||
|
final clampedLean = leanAngle.clamp(-65.0, 65.0);
|
||||||
|
if (clampedLean.abs() > 0.5) {
|
||||||
|
final isExtreme = clampedLean.abs() > 45;
|
||||||
|
final activeColor = isExtreme ? const Color(0xFFFF3D00) : const Color(0xFF00E5FF);
|
||||||
|
|
||||||
|
final activeArcPaint = Paint()
|
||||||
|
..color = activeColor
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4.5
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
final startRad = 270 * (math.pi / 180.0);
|
||||||
|
final sweepRad = clampedLean * (math.pi / 180.0);
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
startRad,
|
||||||
|
sweepRad,
|
||||||
|
false,
|
||||||
|
activeArcPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perimeter needle / pointer (drawn cleanly on the outer track without intersecting the center text)
|
||||||
|
final angleRad = (270 + clampedLean) * (math.pi / 180);
|
||||||
|
final innerNeedle = Offset(center.dx + (radius * 0.72) * math.cos(angleRad), center.dy + (radius * 0.72) * math.sin(angleRad));
|
||||||
|
final outerNeedle = Offset(center.dx + (radius + 2.0) * math.cos(angleRad), center.dy + (radius + 2.0) * math.sin(angleRad));
|
||||||
|
|
||||||
final needlePaint = Paint()
|
final needlePaint = Paint()
|
||||||
..color = const Color(0xFF00E5FF)
|
..color = const Color(0xFF00E5FF)
|
||||||
..strokeWidth = 3.0
|
..strokeWidth = 2.5
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
canvas.drawLine(center, needleEnd, needlePaint);
|
canvas.drawLine(innerNeedle, outerNeedle, needlePaint);
|
||||||
|
|
||||||
|
// Glowing tip dot at perimeter
|
||||||
|
canvas.drawCircle(outerNeedle, 3.5, Paint()..color = const Color(0xFF00E5FF));
|
||||||
|
canvas.drawCircle(outerNeedle, 6.0, Paint()..color = const Color(0xFF00E5FF).withValues(alpha: 0.35));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../rides/ride_state_machine.dart';
|
import '../../rides/ride_state_machine.dart';
|
||||||
|
|
||||||
|
|
@ -10,46 +11,171 @@ class LeanVisualizer extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('LEAN ANGLE', style: TextStyle(color: Colors.white54, fontSize: 12, letterSpacing: 2)),
|
const Text(
|
||||||
const SizedBox(height: 8),
|
'LEAN ANGLE',
|
||||||
Container(
|
style: TextStyle(
|
||||||
width: 100,
|
color: Colors.white54,
|
||||||
height: 100,
|
fontSize: 11,
|
||||||
alignment: Alignment.center,
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.speed, size: 100, color: Colors.white10),
|
CustomPaint(
|
||||||
|
size: const Size(110, 110),
|
||||||
|
painter: _LeanDialPainter(leanAngle: lean),
|
||||||
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${lean.abs().toStringAsFixed(0)}°',
|
'${lean.abs().toStringAsFixed(0)}°',
|
||||||
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white),
|
style: const TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
fontFamily: 'JetBrains Mono',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
lean > 0.5 ? 'RIGHT' : (lean < -0.5 ? 'LEFT' : 'UPRIGHT'),
|
lean > 1.0 ? 'RIGHT' : (lean < -1.0 ? 'LEFT' : 'UPRIGHT'),
|
||||||
style: TextStyle(fontSize: 10, color: lean.abs() > 0.5 ? Theme.of(context).colorScheme.primary : Colors.white54),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: lean.abs() > 1.0
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.white38,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
FittedBox(
|
FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('L: ${session.maxLeanLeft.toStringAsFixed(0)}°', style: TextStyle(color: Theme.of(context).colorScheme.secondary, fontSize: 12)),
|
Text(
|
||||||
|
'L: ${session.maxLeanLeft.toStringAsFixed(0)}°',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'JetBrains Mono',
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('R: ${session.maxLeanRight.toStringAsFixed(0)}°', style: TextStyle(color: Theme.of(context).colorScheme.secondary, fontSize: 12)),
|
Text(
|
||||||
|
'R: ${session.maxLeanRight.toStringAsFixed(0)}°',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: 'JetBrains Mono',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LeanDialPainter extends CustomPainter {
|
||||||
|
final double leanAngle;
|
||||||
|
|
||||||
|
_LeanDialPainter({required this.leanAngle});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final radius = size.width * 0.44;
|
||||||
|
|
||||||
|
// Track arc
|
||||||
|
final trackPaint = Paint()
|
||||||
|
..color = const Color(0xFF222222)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 3.5
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
math.pi,
|
||||||
|
math.pi,
|
||||||
|
false,
|
||||||
|
trackPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ticks
|
||||||
|
final tickPaint = Paint()
|
||||||
|
..color = Colors.white24
|
||||||
|
..strokeWidth = 1.0;
|
||||||
|
final centerTickPaint = Paint()
|
||||||
|
..color = Colors.white70
|
||||||
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
|
for (final deg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) {
|
||||||
|
final rad = (270 + deg) * (math.pi / 180.0);
|
||||||
|
final isZero = deg == 0;
|
||||||
|
final inner = radius - (isZero ? 5.0 : 3.0);
|
||||||
|
final outer = radius + (isZero ? 3.0 : 1.5);
|
||||||
|
final p1 = Offset(center.dx + inner * math.cos(rad), center.dy + inner * math.sin(rad));
|
||||||
|
final p2 = Offset(center.dx + outer * math.cos(rad), center.dy + outer * math.sin(rad));
|
||||||
|
canvas.drawLine(p1, p2, isZero ? centerTickPaint : tickPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active sweep arc
|
||||||
|
final clampedLean = leanAngle.clamp(-65.0, 65.0);
|
||||||
|
if (clampedLean.abs() > 0.5) {
|
||||||
|
final isExtreme = clampedLean.abs() > 45;
|
||||||
|
final activeColor = isExtreme ? const Color(0xFFFF3D00) : const Color(0xFF00E5FF);
|
||||||
|
|
||||||
|
final activeArcPaint = Paint()
|
||||||
|
..color = activeColor
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4.0
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
final startRad = 270 * (math.pi / 180.0);
|
||||||
|
final sweepRad = clampedLean * (math.pi / 180.0);
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
startRad,
|
||||||
|
sweepRad,
|
||||||
|
false,
|
||||||
|
activeArcPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer needle on perimeter
|
||||||
|
final angleRad = (270 + clampedLean) * (math.pi / 180);
|
||||||
|
final innerNeedle = Offset(center.dx + (radius * 0.74) * math.cos(angleRad), center.dy + (radius * 0.74) * math.sin(angleRad));
|
||||||
|
final outerNeedle = Offset(center.dx + (radius + 2.0) * math.cos(angleRad), center.dy + (radius + 2.0) * math.sin(angleRad));
|
||||||
|
|
||||||
|
final needlePaint = Paint()
|
||||||
|
..color = const Color(0xFF00E5FF)
|
||||||
|
..strokeWidth = 2.5
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
canvas.drawLine(innerNeedle, outerNeedle, needlePaint);
|
||||||
|
|
||||||
|
// Glowing tip dot
|
||||||
|
canvas.drawCircle(outerNeedle, 3.0, Paint()..color = const Color(0xFF00E5FF));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _LeanDialPainter oldDelegate) =>
|
||||||
|
oldDelegate.leanAngle != leanAngle;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,16 +192,16 @@ class TelemetryValidator {
|
||||||
reasons.add('Impossible vehicle speed (${currentSpeed.toStringAsFixed(1)} MPH > 220 MPH)');
|
reasons.add('Impossible vehicle speed (${currentSpeed.toStringAsFixed(1)} MPH > 220 MPH)');
|
||||||
} else if (_lastSpeedMph != null && deltaMs >= 40 && deltaMs < 2000) {
|
} else if (_lastSpeedMph != null && deltaMs >= 40 && deltaMs < 2000) {
|
||||||
final deltaSpeedMph = (currentSpeed - _lastSpeedMph!).abs();
|
final deltaSpeedMph = (currentSpeed - _lastSpeedMph!).abs();
|
||||||
if (deltaSpeedMph >= 1.5) {
|
if (deltaSpeedMph >= 3.0) {
|
||||||
final dtSec = deltaMs / 1000.0;
|
final dtSec = deltaMs / 1000.0;
|
||||||
final accelMps2 = (deltaSpeedMph / 2.23694) / dtSec;
|
final accelMps2 = (deltaSpeedMph / 2.23694) / dtSec;
|
||||||
|
|
||||||
// Realistic motorcycle acceleration ceiling: ~15 m/s² (~1.5g), physical crash/spike ceiling: 30 m/s²
|
// Realistic superbike acceleration ceiling: ~20 m/s² (~2.0g), discrete GPS epoch step ceiling: 65 m/s²
|
||||||
if (accelMps2 > 35.0 && dtSec >= 0.1) {
|
if (accelMps2 > 75.0 && dtSec >= 0.1) {
|
||||||
status = ValidationStatus.rejected;
|
status = ValidationStatus.rejected;
|
||||||
speedConf = 0.1;
|
speedConf = 0.1;
|
||||||
reasons.add('Impossible acceleration (${accelMps2.toStringAsFixed(1)} m/s² > 35 m/s²)');
|
reasons.add('Impossible acceleration (${accelMps2.toStringAsFixed(1)} m/s² > 75 m/s²)');
|
||||||
} else if (accelMps2 > 20.0) {
|
} else if (accelMps2 > 50.0) {
|
||||||
if (status == ValidationStatus.valid) status = ValidationStatus.suspect;
|
if (status == ValidationStatus.valid) status = ValidationStatus.suspect;
|
||||||
speedConf = math.min(speedConf, 0.5);
|
speedConf = math.min(speedConf, 0.5);
|
||||||
reasons.add('Suspicious acceleration spike (${accelMps2.toStringAsFixed(1)} m/s²)');
|
reasons.add('Suspicious acceleration spike (${accelMps2.toStringAsFixed(1)} m/s²)');
|
||||||
|
|
@ -216,12 +216,13 @@ class TelemetryValidator {
|
||||||
status = ValidationStatus.suspect;
|
status = ValidationStatus.suspect;
|
||||||
leanConf = 0.3;
|
leanConf = 0.3;
|
||||||
reasons.add('Extreme lean angle beyond physical motorcycle limit (${currentLean.toStringAsFixed(1)}° > 65°)');
|
reasons.add('Extreme lean angle beyond physical motorcycle limit (${currentLean.toStringAsFixed(1)}° > 65°)');
|
||||||
} else if (_lastLeanAngle != null && deltaMs >= 40 && deltaMs < 1000) {
|
} else if (currentSpeed > 5.0 && _lastLeanAngle != null && deltaMs >= 40 && deltaMs < 1000) {
|
||||||
|
// Gate roll rate evaluation: only evaluate dynamic roll rate when motorcycle is moving (>5 MPH)
|
||||||
|
// to completely eliminate stationary/idle engine vibration on the handlebar mount
|
||||||
final deltaLean = (currentLean - _lastLeanAngle!).abs();
|
final deltaLean = (currentLean - _lastLeanAngle!).abs();
|
||||||
// Only evaluate roll rate if angular change exceeds engine vibration noise (> 3.0 deg)
|
if (deltaLean > 4.0) {
|
||||||
if (deltaLean > 3.0) {
|
|
||||||
final rollRateDegPerSec = deltaLean / (deltaMs / 1000.0);
|
final rollRateDegPerSec = deltaLean / (deltaMs / 1000.0);
|
||||||
if (rollRateDegPerSec > 250.0) {
|
if (rollRateDegPerSec > 280.0) {
|
||||||
if (status == ValidationStatus.valid) status = ValidationStatus.suspect;
|
if (status == ValidationStatus.valid) status = ValidationStatus.suspect;
|
||||||
leanConf = 0.5;
|
leanConf = 0.5;
|
||||||
reasons.add('Excessive roll rate (${rollRateDegPerSec.toStringAsFixed(0)}°/s)');
|
reasons.add('Excessive roll rate (${rollRateDegPerSec.toStringAsFixed(0)}°/s)');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue