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>
118 lines
4.6 KiB
C#
118 lines
4.6 KiB
C#
using System;
|
|
using System.Collections;
|
|
using MyInfoMate.Vr.Net;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Kiosk
|
|
{
|
|
/// <summary>
|
|
/// Ce que le casque dit de lui-même au manager — lot <b>XR-5</b>.
|
|
///
|
|
/// L'onglet XR affiche batterie, version et dernier vu depuis le 12/09, mais rien ne
|
|
/// les alimentait : c'est ce trou-là que ce composant comble. <b>Un exploitant qui
|
|
/// voit « vu il y a 3 minutes, 12 % » sait quoi faire ; « — » ne lui apprend rien.</b>
|
|
///
|
|
/// <b>Pourquoi pas MQTT.</b> Le plan prévoit MQTTnet à terme, et le serveur publie
|
|
/// déjà sur <c>player/{deviceId}</c>. Mais à un à trois casques, un battement HTTP
|
|
/// périodique fait le même travail pour dix fois moins de code, sans connexion
|
|
/// permanente à maintenir ni bibliothèque à embarquer. MQTT redeviendra utile le jour
|
|
/// où il faudra <b>pousser</b> vers le casque — recharger une config à distance —
|
|
/// pas pour remonter trois chiffres.
|
|
///
|
|
/// L'échec est silencieux, comme tout le reste de la borne : une statistique perdue
|
|
/// ne vaut pas un message au visiteur.
|
|
/// </summary>
|
|
public class FleetReporter : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// 3 min : assez fréquent pour qu'un exploitant voie un casque tomber avant la
|
|
/// fin d'une visite, assez rare pour ne rien coûter en batterie ni en réseau.
|
|
/// </summary>
|
|
const float IntervalSeconds = 180f;
|
|
|
|
ApiClient _client;
|
|
string _deviceId;
|
|
string _configurationId;
|
|
|
|
/// <summary>
|
|
/// Le manager a assigné une <b>autre configuration</b> à ce casque. C'est le
|
|
/// « pousser vers le casque » du lot XR-5, obtenu sans MQTT : la réponse au
|
|
/// battement porte déjà la configuration courante de l'appareil, il suffit de la
|
|
/// comparer.
|
|
///
|
|
/// <b>Ce que ça coûte</b> : la latence du battement, trois minutes au pire.
|
|
/// <b>Ce que ça évite</b> : une connexion permanente à maintenir sur une borne,
|
|
/// une bibliothèque MQTT embarquée dans le build, et un client de plus à
|
|
/// reconnecter quand le wifi du musée tombe. Le serveur publie déjà sur
|
|
/// <c>player/{deviceId}</c> (<c>DeviceController.cs:413</c>) : le jour où trois
|
|
/// minutes seront trop, tout est prêt côté serveur.
|
|
/// </summary>
|
|
public event Action<string> ConfigurationChanged;
|
|
|
|
public static FleetReporter Attach(GameObject host, ApiClient client,
|
|
string deviceId, string configurationId)
|
|
{
|
|
if (string.IsNullOrEmpty(deviceId)) return null;
|
|
|
|
var reporter = host.AddComponent<FleetReporter>();
|
|
reporter._client = client;
|
|
reporter._deviceId = deviceId;
|
|
reporter._configurationId = configurationId;
|
|
reporter.StartCoroutine(reporter.Loop());
|
|
return reporter;
|
|
}
|
|
|
|
class DeviceResponse
|
|
{
|
|
public string ConfigurationId;
|
|
}
|
|
|
|
IEnumerator Loop()
|
|
{
|
|
// Un premier battement tout de suite : le casque doit apparaître en ligne
|
|
// dès qu'il démarre, pas trois minutes plus tard.
|
|
Send();
|
|
|
|
while (true)
|
|
{
|
|
yield return new WaitForSeconds(IntervalSeconds);
|
|
Send();
|
|
}
|
|
}
|
|
|
|
async void Send()
|
|
{
|
|
var result = await _client.PutAsync<DeviceResponse>(
|
|
$"/api/device/{_deviceId}/heartbeat", new
|
|
{
|
|
batteryLevel = BatteryPercent(),
|
|
appVersion = Application.version
|
|
});
|
|
|
|
if (!result.Ok)
|
|
{
|
|
Debug.Log($"[Fleet] Battement non envoyé : {result.Error}");
|
|
return;
|
|
}
|
|
|
|
var assigned = result.Value?.ConfigurationId;
|
|
if (string.IsNullOrEmpty(assigned) || assigned == _configurationId) return;
|
|
|
|
Debug.Log($"[Fleet] Nouvelle configuration assignée : {assigned}");
|
|
_configurationId = assigned;
|
|
ConfigurationChanged?.Invoke(assigned);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Le niveau en pourcentage, ou null si le casque ne le donne pas —
|
|
/// <c>SystemInfo.batteryLevel</c> renvoie -1 là où l'information n'existe pas,
|
|
/// et envoyer « -100 % » serait pire que ne rien envoyer.
|
|
/// </summary>
|
|
static string BatteryPercent()
|
|
{
|
|
var level = SystemInfo.batteryLevel;
|
|
return level < 0f ? null : Mathf.RoundToInt(level * 100f).ToString();
|
|
}
|
|
}
|
|
}
|