Système de localisation en temps réel pour le suivi des membres sur une carte

Un système de localisation en temps réel en arrière-plan est requis pour surveiller la position des membres au sein d'une application et les afficher sur une carte. Le serveur backend utilise PHP pour l'efficacité, tandis que l'interface frontend est une Mini Program WeChat qui exploite les API wx.startLocationUpdateBackground et wx.onLocationChange pour obtenir les positions.

Architecture proposée

  1. Lorsqu'un membre accède à la page de la Mini Program, l'activation de la localisation en arrière-plan est initiée via wx.startLocationUpdateBackground.
  2. Les changemants de position sont capturés par wx.onLocationChange, et les données sont transmises à un service API pour traitement.
  3. Le service API publie les informations de position sur un canal Redis spécifique à l'aide de redis.publish, permettatn une diffusion en temps réel.
  4. Un service Python, déployé dans un conteneur Docker, s'abonne au canal Redis. Il vérifie si des opérateurs sont en écoute pour un membre donné et leur transmet les données via WebSocket.
  5. L'interface opérateur reçoit les positions et les affiche dynamiquement sur la carte.

Étapes de mise en œuvre

1. Configuration préalable

Obtenir l'appid et le secret pour la Mini Program WeChat, et activer les fonctionnalités de localisation en arrière-plan dans les paramètres de développement. Les API sont généralement accessibles en mode bêta, mais nécessitent une approbation pour la production.

2. Côté Mini Program WeChat

Initialiser la localisation en arrière-plan et gérer les mises à jour de position.


fonctions: {
    ecouterPosition() {
        uni.demarrerMiseAJourLocalisationArrierePlan({
            reussite: () => {
                uni.surChangementPosition((resultat) => {
                    this.$api.envoyerRequete({
                        url: '/api/position/deplacement',
                        donnees: resultat
                    })
                });
            },
            echec: erreur => {
                console.log('Échec de l\'écoute de position -> ', erreur);
            }
        })
    },
    demarrerLocalisation() {
        uni.obtenirParametres({
            reussite: (parametres) => {
                if (parametres.parametresAuth['scope.userLocationBackground']) {
                    this.ecouterPosition();
                } else {
                    uni.autoriser({
                        scope: 'scope.userLocationBackground',
                        reussite: () => {
                            this.ecouterPosition();
                        },
                        echec: () => {
                            uni.afficherModal({
                                titre: 'Avis',
                                contenu: 'Autorisation échouée. Veuillez activer la localisation en arrière-plan via les paramètres.',
                                reussite: r => {
                                    if (r.confirmer) {
                                        uni.ouvrirParametres({
                                            complet: t => {
                                                if (t.parametresAuth['scope.userLocationBackground']) {
                                                    this.ecouterPosition();
                                                } else {
                                                    console.log('Échec d\'autorisation ->', t);
                                                }
                                            }
                                        })
                                    }
                                }
                            })
                        }
                    })
                }
            }
        })
    }
}

3. Configuration des permissions dans manifest.json


"mp-weixin" : {
    "permissions": {
        "scope.userLocationBackground" : {
            "desc" : "Pour fournir un service précis et pratique, nous devons accéder à votre position en continu en arrière-plan. Nous nous engageons à protéger votre vie privée."
        },
        "scope.userLocation" : {
            "desc" : "Pour améliorer la qualité du service, nous nécessitons votre position actuelle."
        }
    },
    "requiredPrivateInfos" : [
        "onLocationChange",
        "startLocationUpdateBackground"
    ],
    "requiredBackgroundModes" : [ "location" ],
}

4. Service backend PHP

Publier les données de position sur un canal Redis.


Cache::store('redis')->publish("canal_position", json_encode([
    'identifiant_utilisateur' => $id_utilisateur,
    'longitude' => input('longitude', ''),
    'latitude' => input('latitude', ''),
]));

5. Service Python pour WebSocket

Ce service écoute le canal Redis et gère les conneixons WebSocket des opérateurs. Note : Assurez-vous que l'hôte Redis correspond à l'adresse IP de la machine hôte si exécuté dans un conteneur Docker.


#!/usr/bin/env python3
import asyncio
import json
import logging
from redis.asyncio import Redis
from redis.exceptions import RedisError
from websockets.server import serve, WebSocketServerProtocol
from typing import Set, Dict

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class GestionnairePositions:
    def __init__(self):
        self.connexions: Dict[str, Set[WebSocketServerProtocol]] = {}
        self.redis_client = None
        self.abonnement_redis = None
        self.config_redis = {
            'hote': '127.0.0.1',
            'port': 6379,
            'mot_de_passe': None,
            'base': 0,
            'decoder_reponses': True,
            'retenter_sur_timeout': True,
            'socket_keepalive': True
        }

    async def initialiser_redis(self):
        tentatives_max = 5
        compteur_tentatives = 0
        while compteur_tentatives < tentatives_max:
            try:
                if self.redis_client:
                    await self.redis_client.close()
                self.redis_client = Redis(**self.config_redis)
                self.abonnement_redis = self.redis_client.pubsub()
                await self.redis_client.ping()
                logger.info("Connexion Redis établie")
                return True
            except Exception as e:
                compteur_tentatives += 1
                logger.error(f"Tentative de connexion Redis {compteur_tentatives} échouée: {e}")
                if compteur_tentatives < tentatives_max:
                    await asyncio.sleep(5)
                else:
                    logger.error("Max de tentatives atteint, impossible de se connecter à Redis")
                    return False

    async def souscripteur_redis(self):
        while True:
            try:
                if self.redis_client is None or not await self.redis_client.ping():
                    logger.info("Tentative de connexion à Redis...")
                    if not await self.initialiser_redis():
                        await asyncio.sleep(5)
                        continue
                await self.abonnement_redis.subscribe('canal_position')
                logger.info("Abonnement réussi au canal_position")
                while True:
                    message = await self.abonnement_redis.get_message(ignore_subscribe_messages=True)
                    if message and message['type'] == 'message':
                        try:
                            donnees = json.loads(message['data'])
                            uid = str(donnees.get('identifiant_utilisateur'))
                            logger.info(f"Mise à jour de position reçue pour le membre {uid}")
                            if uid in self.connexions and self.connexions[uid]:
                                clients_a_supprimer = set()
                                nb_envois_reussis = 0
                                for websocket in self.connexions[uid]:
                                    try:
                                        await websocket.send(json.dumps(donnees))
                                        nb_envois_reussis += 1
                                    except Exception as e:
                                        logger.error(f"Échec d'envoi via WebSocket: {e}")
                                        clients_a_supprimer.add(websocket)
                                for ws in clients_a_supprimer:
                                    self.connexions[uid].discard(ws)
                                if not self.connexions[uid]:
                                    del self.connexions[uid]
                            else:
                                logger.info(f"Aucun client trouvé pour le membre {uid}")
                        except json.JSONDecodeError as e:
                            logger.error(f"Message JSON invalide: {e}")
                    await asyncio.sleep(0.1)
            except RedisError as e:
                logger.error(f"Erreur Redis: {e}")
                await asyncio.sleep(5)
            except Exception as e:
                logger.error(f"Erreur inattendue: {e}")
                await asyncio.sleep(5)

    async def enregistrer_connexion(self, websocket: WebSocketServerProtocol, uid: str):
        if uid not in self.connexions:
            self.connexions[uid] = set()
        self.connexions[uid].add(websocket)
        logger.info(f"Nouvelle connexion enregistrée pour uid: {uid}")

    async def supprimer_connexion(self, websocket: WebSocketServerProtocol, uid: str):
        if uid in self.connexions:
            self.connexions[uid].discard(websocket)
            if not self.connexions[uid]:
                del self.connexions[uid]
            logger.info(f"Connexion supprimée pour uid: {uid}")

    async def traiter_connexion(self, websocket: WebSocketServerProtocol):
        adresse_client = websocket.remote_address
        logger.info(f"Nouvelle connexion WebSocket de {adresse_client}")
        uid = None
        try:
            message_initial = await websocket.recv()
            donnees = json.loads(message_initial)
            uid = str(donnees.get('identifiant_utilisateur'))
            if not uid:
                await websocket.close(1002, "uid requis")
                return
            await self.enregistrer_connexion(websocket, uid)
            try:
                async for message in websocket:
                    logger.debug(f"Message reçu de {adresse_client}: {message}")
            finally:
                if uid:
                    await self.supprimer_connexion(websocket, uid)
        except Exception as e:
            logger.error(f"Erreur dans le gestionnaire WebSocket pour {adresse_client}: {e}")
            if uid:
                await self.supprimer_connexion(websocket, uid)

async def demarrer_serveur():
    gestionnaire = GestionnairePositions()
    logger.info("Démarrage du serveur WebSocket de localisation...")
    tache_souscripteur = asyncio.create_task(gestionnaire.souscripteur_redis())
    async with serve(gestionnaire.traiter_connexion, "0.0.0.0", 30582):
        logger.info("Serveur WebSocket actif sur ws://0.0.0.0:30582")
        try:
            await asyncio.Future()
        finally:
            tache_souscripteur.cancel()

if __name__ == "__main__":
    asyncio.run(demarrer_serveur())

6. Client opérateur


const socket = new WebSocket('ws://exemple.com:30582');

socket.onopen = () => {
  console.log('Connexion WebSocket établie');
};

socket.onmessage = (evenement) => {
  const donnees = JSON.parse(evenement.donnees);
  // Mettre à jour l'affichage de la carte avec les nouvelles coordonnées
  console.log("Position mise à jour -> " + donnees);
  // Intégrer avec l'API cartographique pour le traçage ou le marquage
};

socket.onclose = () => {
  console.log('Connexion WebSocket fermée');
};

socket.onerror = (erreur) => {
  console.log('Erreur WebSocket');
};

En suivant ces étapes, le système permet une surveillance en temps réel des positions des membres, avec une architecture backend en PHP et un service Python pour la communication WebSocket via Redis. Les dépendances Python nécessaires sont installées via pip install websockets redis.

Étiquettes: wechat-miniprogram PHP Redis Python WebSocket

Publié le 16 août à 14h29