Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Wisp.App/BoostGaugeWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Display="{Binding BoostDisplay}"
PressureUnit="{Binding SelectedBoostPressureUnit}"
ColorNumber="{Binding BoostGaugeColorNumber}"
IsElectricMaterial="{Binding NativeGaugeFrame.IsElectric}"
IsElectricMaterial="{Binding IsElectricPowertrain}"
LowBrush="{DynamicResource BoostLowBrush}"
MidBrush="{DynamicResource BoostMidBrush}"
HighBrush="{DynamicResource BoostHighBrush}" />
Expand Down
5 changes: 5 additions & 0 deletions src/Wisp.App/DiagnosticsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,18 @@ public string DashboardGear
public double GForceOffsetX { get => _gForceOffsetX; private set => Set(ref _gForceOffsetX, value); }
public double GForceOffsetY { get => _gForceOffsetY; private set => Set(ref _gForceOffsetY, value); }
public Point GForceTrailPosition { get => _gForceTrailPosition; private set => Set(ref _gForceTrailPosition, value); }
public bool IsElectricPowertrain => _nativeGaugeFrame.IsElectric;

public NativeGaugeFrame NativeGaugeFrame
{
get => _nativeGaugeFrame;
private set
{
var powertrainChanged = value.IsElectric != _nativeGaugeFrame.IsElectric;
if (Set(ref _nativeGaugeFrame, value))
{
if (powertrainChanged)
OnPropertyChanged(nameof(IsElectricPowertrain));
OnPropertyChanged(nameof(NativePreviewFrame));
NotifyDashboardFrame();
}
Expand Down
24 changes: 23 additions & 1 deletion src/Wisp.App/NativeHudMemoryResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ public sealed partial class NativeHudMemoryResolver
private ulong _cachedSource;
private readonly NativeHudCompatibilityPack _pack;
private readonly NativeGaugeDirectResolver _nativeGaugeResolver;
private readonly TimeProvider _gaugeClock;
private static readonly TimeSpan GaugeRetryInterval = TimeSpan.FromMilliseconds(250);
private (ulong Module, ulong Source, int Car, bool Electric)? _missingGauge;
private long _lastGaugeMiss;

public NativeHudMemoryResolver(NativeHudCompatibilityPack? pack = null)
: this(TimeProvider.System, pack)
{
}

internal NativeHudMemoryResolver(TimeProvider gaugeClock, NativeHudCompatibilityPack? pack)
{
_pack = pack ?? NativeHudBuildContract.BuiltIn;
_nativeGaugeResolver = new NativeGaugeDirectResolver(_pack);
_gaugeClock = gaugeClock;
}

internal NativeHudMemoryResolver(NativeHudCompatibilityPack pack, NativeShiftPerformanceReader shiftReader)
Expand Down Expand Up @@ -191,10 +201,21 @@ private NativeGaugeDirectResult ReadNativeGaugeWithDiagnostics(
IReadOnlyProcessMemory memory, ulong moduleBase, ulong source, bool isElectric,
bool forceStructuralValidation, ulong generation, int carOrdinal, string route)
{
var identity = (moduleBase, source, carOrdinal, isElectric);
if (_missingGauge == identity && _gaugeClock.GetElapsedTime(_lastGaugeMiss) < GaugeRetryInterval)
return default;

_missingGauge = null;
_nativeGaugeResolver.DiagnosticsEnabled = TachDiagnostics.IsEnabled;
try
{
return _nativeGaugeResolver.Read(memory, moduleBase, source, isElectric, forceStructuralValidation);
var gauge = _nativeGaugeResolver.Read(memory, moduleBase, source, isElectric, forceStructuralValidation);
if (!gauge.IsAvailable)
{
_missingGauge = identity;
_lastGaugeMiss = _gaugeClock.GetTimestamp();
}
return gauge;
}
finally
{
Expand All @@ -206,6 +227,7 @@ private NativeGaugeDirectResult ReadNativeGaugeWithDiagnostics(
public void Reset()
{
_cachedSource = 0;
_missingGauge = null;
_nativeGaugeResolver.Reset();
ResetShiftPerformance();
}
Expand Down
6 changes: 3 additions & 3 deletions src/Wisp.App/OverlayWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
TemperatureUnit="{Binding SelectedTireTemperatureUnit}"
ReactiveColors="{Binding TireTemperatureReactiveColors}"
IsAttached="True"
IsElectricMaterial="{Binding NativeGaugeFrame.IsElectric}"
IsElectricMaterial="{Binding IsElectricPowertrain}"
LowBrush="{DynamicResource BoostLowBrush}"
MidBrush="{DynamicResource BoostMidBrush}"
HighBrush="{DynamicResource BoostHighBrush}" />
Expand All @@ -146,7 +146,7 @@
HorizontalAlignment="Left" VerticalAlignment="Top"
Visibility="Collapsed" IsHitTestVisible="False"
Display="{Binding PowerTorqueDisplay}"
IsElectricMaterial="{Binding NativeGaugeFrame.IsElectric}"
IsElectricMaterial="{Binding IsElectricPowertrain}"
Maximum="{Binding PowerGaugeMaximum}"
LowBrush="{Binding PowerGaugeLowBrush}"
MidBrush="{Binding PowerGaugeMidBrush}"
Expand All @@ -158,7 +158,7 @@
HorizontalAlignment="Left" VerticalAlignment="Top"
Visibility="Collapsed" IsHitTestVisible="False" IsTorque="True"
Display="{Binding PowerTorqueDisplay}"
IsElectricMaterial="{Binding NativeGaugeFrame.IsElectric}"
IsElectricMaterial="{Binding IsElectricPowertrain}"
TorqueUnit="{Binding SelectedTorqueUnit}"
Maximum="{Binding TorqueGaugeMaximum}"
LowBrush="{Binding TorqueGaugeLowBrush}"
Expand Down
2 changes: 1 addition & 1 deletion src/Wisp.App/PowerTorqueGaugeWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public PowerTorqueGaugeWindow(AppController controller, bool isTorque)
IsTorque = isTorque
};
gauge.SetBinding(PowerTorqueGaugeView.DisplayProperty, new Binding(nameof(DiagnosticsViewModel.PowerTorqueDisplay)));
gauge.SetBinding(PowerTorqueGaugeView.IsElectricMaterialProperty, new Binding("NativeGaugeFrame.IsElectric"));
gauge.SetBinding(PowerTorqueGaugeView.IsElectricMaterialProperty, new Binding(nameof(DiagnosticsViewModel.IsElectricPowertrain)));
gauge.SetBinding(PowerTorqueGaugeView.TorqueUnitProperty, new Binding(nameof(DiagnosticsViewModel.SelectedTorqueUnit)));
gauge.SetBinding(PowerTorqueGaugeView.MaximumProperty,
new Binding(isTorque ? nameof(DiagnosticsViewModel.TorqueGaugeMaximum) : nameof(DiagnosticsViewModel.PowerGaugeMaximum)));
Expand Down
2 changes: 1 addition & 1 deletion src/Wisp.App/TireTemperatureGaugeWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
Display="{Binding TireTemperatureDisplay}"
TemperatureUnit="{Binding SelectedTireTemperatureUnit}"
ReactiveColors="{Binding TireTemperatureReactiveColors}"
IsElectricMaterial="{Binding NativeGaugeFrame.IsElectric}"
IsElectricMaterial="{Binding IsElectricPowertrain}"
LowBrush="{DynamicResource BoostLowBrush}"
MidBrush="{DynamicResource BoostMidBrush}"
HighBrush="{DynamicResource BoostHighBrush}" />
Expand Down
31 changes: 31 additions & 0 deletions tests/Wisp.App.Tests/DiagnosticsViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,37 @@ public void NativeFrameCarriesExtendedPowertrainTelemetry()
Assert.Equal(64, viewModel.NativeGaugeFrame.Brake);
}

[Fact]
public void ElectricMaterialNotificationFollowsPowertrainTransitions()
{
var viewModel = new DiagnosticsViewModel(new AppSettings());
var changes = new List<bool>();
var frames = 0;
viewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(DiagnosticsViewModel.IsElectricPowertrain))
changes.Add(viewModel.IsElectricPowertrain);
if (e.PropertyName == nameof(DiagnosticsViewModel.NativeGaugeFrame))
frames++;
};
var state = DrivingState();
void Update(int cylinders, float rpm) => viewModel.Update(
state with { NumCylinders = cylinders, EngineRpm = rpm },
new IndicatedSpeed(0, 0, false, false, "Rear (RL + RR)"),
new CalibrationResult(null, 0.3, 0.2, 0, true, string.Empty, false),
default(ReceiverStatistics), TimeSpan.Zero, SpeedUnit.MilesPerHour, 60,
refreshDiagnostics: false, updateGForce: false);

Update(6, 1200);
Update(0, 1300);
Update(0, 1400);
viewModel.UpdateWaiting(default, TelemetryConnectionState.Waiting, null, 0);
viewModel.UpdateWaiting(default, TelemetryConnectionState.Waiting, null, 0);

Assert.Equal(new[] { true, false }, changes);
Assert.Equal(4, frames);
}

[Fact]
public void WaitingStateClearsRpmEvidence()
{
Expand Down
70 changes: 70 additions & 0 deletions tests/Wisp.App.Tests/NativeGaugeDirectResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,76 @@ public sealed class NativeGaugeDirectResolverTests

private static NativeGaugeLayout Layout => NativeHudBuildContract.BuiltIn.NativeGauge!;

[Fact]
public void MissingGaugeSharesRetryAcrossFullAndFastReadsAndResumesNormalSampling()
{
var memory = ValidMemory(1, false);
ConfigureHudReader(memory, Source, 314, 0);
memory.SetUInt64(Outer + Layout.OuterChildOffset, 0);
var clock = new GaugeClock();
var resolver = new NativeHudMemoryResolver(clock, NativeHudBuildContract.BuiltIn);
var discoveries = 0;
memory.BeforeRead = (address, _) =>
{
if (address == Module + Layout.RegistryGlobalRva) discoveries++;
};
var snapshot = resolver.Resolve(memory, Module, 314, 4_000, 8_000, 1);
Assert.Equal(NativeAssistProviderStatus.Ready, snapshot.Status);
Assert.False(snapshot.HasNativeNeedleState);
Assert.Equal(1, discoveries);

memory.SetUInt64(Outer + Layout.OuterChildOffset, Child);
for (var ms = 1; ms < 250; ms++)
{
clock.Milliseconds = ms;
snapshot = ms % 16 == 0
? resolver.Resolve(memory, Module, 314, 4_000, 8_000, (ulong)ms, forceSourceAudit: true)
: resolver.RefreshNativeGauge(memory, Module, snapshot, (ulong)ms, false);
Assert.Equal(0, snapshot.NativeGaugeObservedTimestamp);
}
Assert.Equal(1, discoveries);

clock.Milliseconds = 250;
snapshot = resolver.RefreshNativeGauge(memory, Module, snapshot, 250, false);
Assert.True(snapshot.HasNativeNeedleState);
memory.ResetReadCounts();
Assert.True(resolver.RefreshNativeGauge(memory, Module, snapshot, 251, false).HasNativeNeedleState);
Assert.Equal(3, memory.ReadCount);

memory.SetUInt64(Outer + Layout.OuterChildOffset, 0);
snapshot = resolver.Resolve(memory, Module, 314, 4_000, 8_000, 252, forceSourceAudit: true);
Assert.Equal(0, snapshot.NativeGaugeObservedTimestamp);
}

[Theory]
[InlineData("car")]
[InlineData("source")]
[InlineData("session")]
public void NewGaugeContextCanRecoverImmediately(string changed)
{
var clock = new GaugeClock();
var memory = ValidMemory(1, false);
ConfigureHudReader(memory, Source, 314, 0);
memory.SetUInt64(Outer + Layout.OuterChildOffset, 0);
var resolver = new NativeHudMemoryResolver(clock, NativeHudBuildContract.BuiltIn);
Assert.False(resolver.Resolve(memory, Module, 314, 4_000, 8_000, 1).HasNativeNeedleState);

var car = changed == "car" ? 315 : 314;
var source = changed == "source" ? Source + 0x10000 : Source;
if (changed == "session") resolver.Reset();
ConfigureHudReader(memory, source, (uint)car, 0);
memory.SetUInt64(Outer + Layout.OuterSourceOffset, source);
memory.SetUInt64(Outer + Layout.OuterChildOffset, Child);
Assert.True(resolver.Resolve(memory, Module, car, 4_000, 8_000, 2, forceSourceAudit: true).HasNativeNeedleState);
}

private sealed class GaugeClock : TimeProvider
{
public long Milliseconds { get; set; }
public override long TimestampFrequency => 1000;
public override long GetTimestamp() => Milliseconds;
}

[Fact]
public void RuntimeOwnedGaugeTypeRequiresUniqueFullOwnershipAndKeepsGuardedCache()
{
Expand Down
Loading