using System.Threading.Tasks; using UnityEngine; namespace MyInfoMate.Vr.Net { /// /// Appairage du casque — item E2 du lot XR-4. /// /// Le flux est celui de la tablette, à un paramètre près /// (tablet-app/lib/Screens/Configuration/config_view.dart:274 et son /// _fetchAppKey) : /// /// code PIN → GET /api/instance/app-key → clé d'API + id d'instance /// la clé → GET /api/configuration → les configurations de l'instance /// POST /api/device avec appType = VR → le casque existe dans la flotte /// /// /// ⚠️ Le paramètre qui change tout, c'est appType. Sans lui, le serveur /// crée une tablette (DeviceController.Create, défaut Tablet) : le casque /// atterrirait dans l'onglet Kiosk du manager. Avec VR, il est rattaché à /// l'ApplicationInstance VR — et un 404 signifie que le canal VR n'est pas /// activé sur cette instance. /// public class PairingService { /// Valeur 3 de l'enum AppType côté serveur — persistée en int. public const int AppTypeVr = 3; /// Nom de l'enum ApiKeyAppType, envoyé tel quel en query. const string ApiKeyAppTypeVr = "VrApp"; const string PrefsBaseUrl = "myinfomate.baseUrl"; const string PrefsApiKey = "myinfomate.apiKey"; const string PrefsInstanceId = "myinfomate.instanceId"; const string PrefsDeviceId = "myinfomate.deviceId"; const string PrefsConfigurationId = "myinfomate.configurationId"; public class Pairing { public string BaseUrl; public string ApiKey; public string InstanceId; public string DeviceId; public string ConfigurationId; } class AppKeyResponse { public string Key; public string InstanceId; } class ConfigurationSummary { public string Id; public string Label; } class DeviceResponse { public string Id; public string Identifier; } /// /// L'identifiant matériel du casque. SystemInfo.deviceUniqueIdentifier est /// stable pour une installation donnée — c'est lui que DeviceController.Create /// utilise pour reconnaître un appareil déjà enregistré au lieu d'en créer un second. /// public static string HeadsetIdentifier => SystemInfo.deviceUniqueIdentifier; /// L'appairage précédent, ou null si ce casque n'a jamais été appairé. public static Pairing Restore() { var key = PlayerPrefs.GetString(PrefsApiKey, null); if (string.IsNullOrEmpty(key)) return null; return new Pairing { BaseUrl = PlayerPrefs.GetString(PrefsBaseUrl, null), ApiKey = key, InstanceId = PlayerPrefs.GetString(PrefsInstanceId, null), DeviceId = PlayerPrefs.GetString(PrefsDeviceId, null), ConfigurationId = PlayerPrefs.GetString(PrefsConfigurationId, null) }; } public static void Forget() { foreach (var k in new[] { PrefsBaseUrl, PrefsApiKey, PrefsInstanceId, PrefsDeviceId, PrefsConfigurationId }) PlayerPrefs.DeleteKey(k); PlayerPrefs.Save(); } /// /// Appaire ce casque. vide = la première /// configuration de l'instance, ce qui suffit à une borne qui n'en a qu'une. /// public async Task> PairAsync( string baseUrl, string pinCode, string headsetName, string configurationId = null) { var client = new ApiClient(baseUrl); var keyResult = await client.GetAsync( $"/api/instance/app-key?pinCode={UnityWebRequestEscape(pinCode)}&appType={ApiKeyAppTypeVr}"); if (!keyResult.Ok) return Fail(keyResult.Error); if (keyResult.Value == null || string.IsNullOrEmpty(keyResult.Value.Key)) return Fail("Ce code PIN ne correspond à aucun lieu."); client.ApiKey = keyResult.Value.Key; var instanceId = keyResult.Value.InstanceId; if (string.IsNullOrEmpty(configurationId)) { var configs = await client.GetAsync( $"/api/configuration?instanceId={instanceId}"); if (!configs.Ok) return Fail(configs.Error); if (configs.Value == null || configs.Value.Length == 0) return Fail("Ce lieu n'a encore aucun contenu publié."); configurationId = configs.Value[0].Id; } // Le serveur crée lui-même l'AppConfigurationLink porteur de ce DeviceId : // c'est ce lien que l'onglet XR du manager affiche comme carte de casque. var device = await client.PostAsync("/api/device", new { identifier = HeadsetIdentifier, name = headsetName, instanceId, configurationId, appType = AppTypeVr, connected = true }); if (!device.Ok) { // 404 sur cette route ne veut pas dire « introuvable » au sens courant : // il n'y a pas d'ApplicationInstance VR sur cette instance. return Fail(device.Error == "Ce contenu n'existe plus sur le serveur." ? "Le canal VR n'est pas activé pour ce lieu." : device.Error); } var pairing = new Pairing { BaseUrl = client.BaseUrl, ApiKey = client.ApiKey, InstanceId = instanceId, DeviceId = device.Value?.Id, ConfigurationId = configurationId }; Save(pairing); return new ApiClient.Result { Value = pairing }; } static void Save(Pairing pairing) { PlayerPrefs.SetString(PrefsBaseUrl, pairing.BaseUrl); PlayerPrefs.SetString(PrefsApiKey, pairing.ApiKey); PlayerPrefs.SetString(PrefsInstanceId, pairing.InstanceId); PlayerPrefs.SetString(PrefsDeviceId, pairing.DeviceId); PlayerPrefs.SetString(PrefsConfigurationId, pairing.ConfigurationId); PlayerPrefs.Save(); } static ApiClient.Result Fail(string error) => new ApiClient.Result { Error = error }; static string UnityWebRequestEscape(string value) => UnityEngine.Networking.UnityWebRequest.EscapeURL(value); } }