using System;
using System.Collections;
using MyInfoMate.Vr.Net;
using UnityEngine;
namespace MyInfoMate.Vr.Kiosk
{
///
/// Ce que le casque dit de lui-même au manager — lot XR-5.
///
/// 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. Un exploitant qui
/// voit « vu il y a 3 minutes, 12 % » sait quoi faire ; « — » ne lui apprend rien.
///
/// Pourquoi pas MQTT. Le plan prévoit MQTTnet à terme, et le serveur publie
/// déjà sur player/{deviceId}. 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 pousser 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.
///
public class FleetReporter : MonoBehaviour
{
///
/// 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.
///
const float IntervalSeconds = 180f;
ApiClient _client;
string _deviceId;
string _configurationId;
///
/// Le manager a assigné une autre configuration à 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.
///
/// Ce que ça coûte : la latence du battement, trois minutes au pire.
/// Ce que ça évite : 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
/// player/{deviceId} (DeviceController.cs:413) : le jour où trois
/// minutes seront trop, tout est prêt côté serveur.
///
public event Action ConfigurationChanged;
public static FleetReporter Attach(GameObject host, ApiClient client,
string deviceId, string configurationId)
{
if (string.IsNullOrEmpty(deviceId)) return null;
var reporter = host.AddComponent();
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(
$"/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);
}
///
/// Le niveau en pourcentage, ou null si le casque ne le donne pas —
/// SystemInfo.batteryLevel renvoie -1 là où l'information n'existe pas,
/// et envoyer « -100 % » serait pire que ne rien envoyer.
///
static string BatteryPercent()
{
var level = SystemInfo.batteryLevel;
return level < 0f ? null : Mathf.RoundToInt(level * 100f).ToString();
}
}
}