vr-app/unity-overlay/Assets/Scripts/Net/PairingService.cs
Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
Premier commit du cinquième front. Trois parties :

- `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205),
  un APK unique pour tous les clients. Menu flottant à sélection au regard,
  appairage, chargement de scène GLB, POI, cache de contenu, télémétrie.
- `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec
  les pièges rencontrés consignés dans son README.
- `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript,
  three.js), partagé avec les autres fronts.
- `docs/` : état des lieux, setup Unity, décisions d'architecture et plan
  d'exécution en 9 étapes.

La scène est décrite par un `scene.json` poussé par `adb push` : l'app le
préfère à celui embarqué dans l'APK.

Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par
Git LFS dès ce premier commit — les y faire entrer après coup demanderait de
réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake
(`Library/`, `.utmp/`, Burst debug) sont ignorés.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:26:07 +02:00

180 lines
7.0 KiB
C#

using System.Threading.Tasks;
using UnityEngine;
namespace MyInfoMate.Vr.Net
{
/// <summary>
/// Appairage du casque — item <b>E2</b> du lot XR-4.
///
/// Le flux est celui de la tablette, à un paramètre près
/// (<c>tablet-app/lib/Screens/Configuration/config_view.dart:274</c> et son
/// <c>_fetchAppKey</c>) :
/// <list type="number">
/// <item>code PIN → <c>GET /api/instance/app-key</c> → clé d'API + id d'instance</item>
/// <item>la clé → <c>GET /api/configuration</c> → les configurations de l'instance</item>
/// <item><c>POST /api/device</c> avec <c>appType = VR</c> → le casque existe dans la flotte</item>
/// </list>
///
/// ⚠️ <b>Le paramètre qui change tout, c'est <c>appType</c>.</b> Sans lui, le serveur
/// crée une tablette (<c>DeviceController.Create</c>, défaut <c>Tablet</c>) : le casque
/// atterrirait dans l'onglet Kiosk du manager. Avec <c>VR</c>, il est rattaché à
/// l'<c>ApplicationInstance</c> VR — et un 404 signifie que le canal VR n'est pas
/// activé sur cette instance.
/// </summary>
public class PairingService
{
/// <summary>Valeur 3 de l'enum <c>AppType</c> côté serveur — persistée en int.</summary>
public const int AppTypeVr = 3;
/// <summary>Nom de l'enum <c>ApiKeyAppType</c>, envoyé tel quel en query.</summary>
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;
}
/// <summary>
/// L'identifiant matériel du casque. <c>SystemInfo.deviceUniqueIdentifier</c> est
/// stable pour une installation donnée — c'est lui que <c>DeviceController.Create</c>
/// utilise pour reconnaître un appareil déjà enregistré au lieu d'en créer un second.
/// </summary>
public static string HeadsetIdentifier => SystemInfo.deviceUniqueIdentifier;
/// <summary>L'appairage précédent, ou null si ce casque n'a jamais été appairé.</summary>
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();
}
/// <summary>
/// Appaire ce casque. <paramref name="configurationId"/> vide = la première
/// configuration de l'instance, ce qui suffit à une borne qui n'en a qu'une.
/// </summary>
public async Task<ApiClient.Result<Pairing>> PairAsync(
string baseUrl, string pinCode, string headsetName, string configurationId = null)
{
var client = new ApiClient(baseUrl);
var keyResult = await client.GetAsync<AppKeyResponse>(
$"/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<ConfigurationSummary[]>(
$"/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<DeviceResponse>("/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<Pairing> { 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<Pairing> Fail(string error) =>
new ApiClient.Result<Pairing> { Error = error };
static string UnityWebRequestEscape(string value) =>
UnityEngine.Networking.UnityWebRequest.EscapeURL(value);
}
}