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>
180 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|