v0.23.0 — SteamVR / OpenVR : check VrDevice avec batterie + tracking
Nouveau Kind « VrDevice » dans le bandeau de santé. Pour chaque appareil SteamVR (HMD, manettes, trackers, base stations) on lit via OpenVR : batterie %, état de charge, niveau d'activité (actif / veille / inactif) et serial number. Severity du pill calculée sur le batterie : < 15% rouge, < 30% orange, sinon vert ; charging force le vert. Implémentation : P/Invoke direct dans openvr_api.dll (BSD-3) — pas de NuGet, pas de bundling de la DLL. La runtime est résolue dynamiquement via DllImportResolver depuis : 1. %LocalAppData%\openvr\openvrpaths.vrpath (canon écrit par SteamVR), 2. fallback C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\. Si SteamVR n'est pas installé → pill « SteamVR non installé » (Unknown, gris). Si SteamVR pas lancé → pill « SteamVR non lancé / aucun HMD » (Error, rouge). Init en mode VRApplication_Background, donc on lit l'état sans devenir scene application ni monopoliser le compositor. Éditeur : nouveau radio « Appareil SteamVR » à côté de Process / Ping ; quand il est sélectionné, le champ Target devient une ComboBox d'alias (HMD, Manette gauche/droite, Tracker 1-3, Base station 1-2) au lieu du TextBox libre. Le mapping alias → TrackedDeviceIndex se fait à chaque query (controllers via GetTrackedDeviceIndexForControllerRole, trackers et base stations par énumération avec compteur de classe). Robustesse : init lazy une fois pour toute la vie du process (init/shutdown répétés coûtent 100-500 ms). Vtable IVRSystem_022 résolue par index slot dans une struct contiguë de IntPtr (pas de struct marshaling, donc immune aux changements de slots qu'on n'utilise pas). Si SteamVR crash en cours de session, le service détecte au prochain tick et marque la session broken pour retenter au tick suivant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ using PSLauncher.Core.Localization;
|
||||
using PSLauncher.Core.Manifests;
|
||||
using PSLauncher.Core.DocTool;
|
||||
using PSLauncher.Core.Health;
|
||||
using PSLauncher.Core.Health.OpenVRInterop;
|
||||
using PSLauncher.Core.Migrations;
|
||||
using PSLauncher.Core.Process;
|
||||
using PSLauncher.Core.ReportTool;
|
||||
@@ -191,6 +192,7 @@ public partial class App : Application
|
||||
configProvider: () => sp.GetRequiredService<LocalConfig>().DocTool,
|
||||
sp.GetRequiredService<ILogger<DocToolDeployer>>()));
|
||||
|
||||
services.AddSingleton<IOpenVrService, OpenVrService>();
|
||||
services.AddSingleton<ISystemHealthService, SystemHealthService>();
|
||||
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.22.0</Version>
|
||||
<AssemblyVersion>0.22.0.0</AssemblyVersion>
|
||||
<FileVersion>0.22.0.0</FileVersion>
|
||||
<Version>0.23.0</Version>
|
||||
<AssemblyVersion>0.23.0.0</AssemblyVersion>
|
||||
<FileVersion>0.23.0.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -59,10 +59,16 @@
|
||||
GroupName="Kind"
|
||||
Content="{x:Static loc:Strings.HealthEditorKindPing}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
Margin="0,0,20,0"
|
||||
Checked="OnKindChanged" />
|
||||
<RadioButton x:Name="KindVrDevice"
|
||||
GroupName="Kind"
|
||||
Content="{x:Static loc:Strings.HealthEditorKindVrDevice}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
Checked="OnKindChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Cible (process name ou IP/host) -->
|
||||
<!-- Cible (process name, IP/host, ou alias VR) -->
|
||||
<TextBlock x:Name="TargetLabel"
|
||||
Text="{x:Static loc:Strings.HealthEditorTargetProcess}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
@@ -74,6 +80,17 @@
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
FontFamily="Consolas" />
|
||||
|
||||
<!-- ComboBox d'alias VR (visible uniquement si Kind=VrDevice) -->
|
||||
<ComboBox x:Name="VrDeviceCombo"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
DisplayMemberPath="Label"
|
||||
SelectedValuePath="Alias"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<TextBlock x:Name="TargetHint"
|
||||
Text="{x:Static loc:Strings.HealthEditorTargetProcessHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
|
||||
@@ -45,6 +45,19 @@ public partial class HealthCheckEditorDialog : Window
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Aliases acceptés par OpenVrService pour le picker VrDevice.</summary>
|
||||
private static readonly VrAliasOption[] VrAliases =
|
||||
{
|
||||
new("hmd", "HMD"),
|
||||
new("left_controller", "Manette gauche"),
|
||||
new("right_controller", "Manette droite"),
|
||||
new("tracker_1", "Tracker #1"),
|
||||
new("tracker_2", "Tracker #2"),
|
||||
new("tracker_3", "Tracker #3"),
|
||||
new("base_station_1", "Base station #1"),
|
||||
new("base_station_2", "Base station #2"),
|
||||
};
|
||||
|
||||
public HealthCheckEditorDialog(HealthCheckEntry entry, bool isNew = false)
|
||||
{
|
||||
Entry = entry;
|
||||
@@ -57,10 +70,29 @@ public partial class HealthCheckEditorDialog : Window
|
||||
TargetBox.Text = entry.Target ?? "";
|
||||
RefreshMsBox.Text = entry.RefreshIntervalMs.ToString();
|
||||
|
||||
var isPing = entry.Kind?.Equals("ping", System.StringComparison.OrdinalIgnoreCase) == true;
|
||||
KindProcess.IsChecked = !isPing;
|
||||
KindPing.IsChecked = isPing;
|
||||
UpdateKindUi(isPing);
|
||||
VrDeviceCombo.ItemsSource = VrAliases;
|
||||
|
||||
var kind = (entry.Kind ?? "Process").Trim().ToLowerInvariant();
|
||||
var isPing = kind == "ping";
|
||||
var isVrDevice = kind == "vrdevice";
|
||||
var isProcess = !isPing && !isVrDevice;
|
||||
KindProcess.IsChecked = isProcess;
|
||||
KindPing.IsChecked = isPing;
|
||||
KindVrDevice.IsChecked = isVrDevice;
|
||||
|
||||
if (isVrDevice)
|
||||
{
|
||||
// Sélectionne l'alias correspondant ; fallback "hmd" si Target inconnu
|
||||
var alias = string.IsNullOrEmpty(entry.Target) ? "hmd" : entry.Target.Trim().ToLowerInvariant();
|
||||
var match = VrAliases.FirstOrDefault(a => string.Equals(a.Alias, alias, StringComparison.OrdinalIgnoreCase));
|
||||
VrDeviceCombo.SelectedValue = match?.Alias ?? "hmd";
|
||||
}
|
||||
else
|
||||
{
|
||||
VrDeviceCombo.SelectedValue = "hmd";
|
||||
}
|
||||
|
||||
UpdateKindUi(kind);
|
||||
|
||||
PingWarnBox.Text = (entry.PingWarnMs ?? 50).ToString();
|
||||
PingErrorBox.Text = (entry.PingErrorMs ?? 200).ToString();
|
||||
@@ -75,26 +107,41 @@ public partial class HealthCheckEditorDialog : Window
|
||||
IconPicker.ItemsSource = _icons;
|
||||
}
|
||||
|
||||
private void UpdateKindUi(bool isPing)
|
||||
private void UpdateKindUi(string kind)
|
||||
{
|
||||
if (isPing)
|
||||
switch (kind)
|
||||
{
|
||||
TargetLabel.Text = Strings.HealthEditorTargetPing;
|
||||
TargetHint.Text = Strings.HealthEditorTargetPingHint;
|
||||
PingAdvanced.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetLabel.Text = Strings.HealthEditorTargetProcess;
|
||||
TargetHint.Text = Strings.HealthEditorTargetProcessHint;
|
||||
PingAdvanced.Visibility = Visibility.Collapsed;
|
||||
case "ping":
|
||||
TargetLabel.Text = Strings.HealthEditorTargetPing;
|
||||
TargetHint.Text = Strings.HealthEditorTargetPingHint;
|
||||
TargetBox.Visibility = Visibility.Visible;
|
||||
VrDeviceCombo.Visibility = Visibility.Collapsed;
|
||||
PingAdvanced.Visibility = Visibility.Visible;
|
||||
break;
|
||||
case "vrdevice":
|
||||
TargetLabel.Text = Strings.HealthEditorTargetVrDevice;
|
||||
TargetHint.Text = Strings.HealthEditorTargetVrDeviceHint;
|
||||
TargetBox.Visibility = Visibility.Collapsed;
|
||||
VrDeviceCombo.Visibility = Visibility.Visible;
|
||||
PingAdvanced.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
default: // process
|
||||
TargetLabel.Text = Strings.HealthEditorTargetProcess;
|
||||
TargetHint.Text = Strings.HealthEditorTargetProcessHint;
|
||||
TargetBox.Visibility = Visibility.Visible;
|
||||
VrDeviceCombo.Visibility = Visibility.Collapsed;
|
||||
PingAdvanced.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKindChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (TargetLabel is null) return; // pendant l'init avant que tout soit chargé
|
||||
UpdateKindUi(KindPing.IsChecked == true);
|
||||
var kind = KindPing.IsChecked == true ? "ping"
|
||||
: KindVrDevice.IsChecked == true ? "vrdevice"
|
||||
: "process";
|
||||
UpdateKindUi(kind);
|
||||
}
|
||||
|
||||
private void OnIconClicked(object sender, RoutedEventArgs e)
|
||||
@@ -122,8 +169,16 @@ public partial class HealthCheckEditorDialog : Window
|
||||
|
||||
Entry.Name = name;
|
||||
Entry.Icon = string.IsNullOrEmpty(selectedIcon) ? "🔌" : selectedIcon;
|
||||
Entry.Kind = KindPing.IsChecked == true ? "Ping" : "Process";
|
||||
Entry.Target = TargetBox.Text?.Trim() ?? "";
|
||||
Entry.Kind = KindPing.IsChecked == true ? "Ping"
|
||||
: KindVrDevice.IsChecked == true ? "VrDevice"
|
||||
: "Process";
|
||||
|
||||
// Target dépend du Kind : pour VrDevice, on prend la valeur de la combo
|
||||
// (alias ASCII stable comme "hmd" / "left_controller" — ne pas localiser).
|
||||
Entry.Target = Entry.Kind == "VrDevice"
|
||||
? (VrDeviceCombo.SelectedValue as string ?? "hmd")
|
||||
: (TargetBox.Text?.Trim() ?? "");
|
||||
|
||||
Entry.RefreshIntervalMs = refreshMs;
|
||||
|
||||
if (Entry.Kind == "Ping")
|
||||
@@ -166,4 +221,7 @@ public partial class HealthCheckEditorDialog : Window
|
||||
|
||||
public IconOption(string emoji) { Emoji = emoji; }
|
||||
}
|
||||
|
||||
/// <summary>Item du ComboBox VrDevice — couple alias technique + libellé affiché.</summary>
|
||||
public sealed record VrAliasOption(string Alias, string Label);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user