Si votre projet est "Théâtre", utilisez des acteurs

Il y a une histoire sur une expérience d'utilisation du modèle d'acteur dans un projet intéressant de développement d'un système de contrôle automatique pour un théâtre. Ci-dessous, je vais dire mes impressions, pas plus que cela.


Il n'y a pas si longtemps, j'ai participé à une tâche passionnante: la modernisation du système de contrôle automatique (ACS) pour les palans à latte, mais en fait, c'était le développement d'un nouvel ACS.


Un théâtre moderne (surtout s'il est grand) est une organisation très complexe. Il existe de nombreuses personnes, divers mécanismes et systèmes. Un de ces systèmes est ACS pour la manipulation de levage et de dépose du paysage. Les performances modernes, comme les opéras et les ballets, utilisent d'année en année des moyens de plus en plus techniques. Le décor est activement utilisé par les réalisateurs du spectacle et joue même son propre rôle important. C'était fascinant de découvrir ce qui se passe derrière les rideaux car les spectateurs ordinaires ne peuvent voir que des actions sur la scène.


Mais ceci est un article technique, et je veux partager mon expérience de l'utilisation du modèle d'acteur pour écrire un système de contrôle. Et partagez mes impressions sur l'utilisation de l'un des frameworks d'acteurs pour C ++: SObjectizer .


Pourquoi avons-nous choisi ce cadre? Nous le regardons depuis longtemps. Il y a beaucoup d'articles en russe, et il a une documentation merveilleuse et beaucoup d'exemples. Le projet ressemble à un projet mature. Un bref aperçu des exemples a montré que les développeurs de SObjectizer utilisent les mêmes termes (états, temporisateurs, événements, etc.) et nous ne nous attendions pas à de gros problèmes pour l'étudier et l'utiliser. Et encore un autre facteur important: l'équipe de SObjectizer est utile et toujours prête à nous aider. Nous avons donc décidé d'essayer.


On fait quoi?


Parlons de la cible de notre projet. Le système de palans à lattes comprend 62 lattes (tubes métalliques). Chaque latte est aussi longue que toute la scène. Ils sont suspendus sur des cordes en parallèle avec des espaces de 30 à 40 cm, à partir du bord avant de la scène. Chaque latte peut être relevée ou abaissée. Certains d'entre eux sont utilisés dans un spectacle de décors. Le décor est fixé sur latte et est déplacé vers le haut / bas pendant la représentation. Les commandes des opérateurs déclenchent le mouvement. Un système de "contrepoids corde-moteur" est similaire à celui utilisé dans les ascenseurs des bâtiments résidentiels. Les moteurs sont placés à l'extérieur de la scène, donc les spectateurs ne les voient pas. Tous les moteurs sont divisés en 8 groupes, et chaque groupe dispose de 3 convertisseurs de fréquence (FC). Au plus trois moteurs peuvent être utilisés en même temps dans un groupe, chacun d'eux est connecté à un FC séparé. Nous avons donc un système de 62 moteurs et 24 FC, et nous devons contrôler ce système.


Notre tâche était de développer une interface homme-machine (IHM) pour contrôler ce système et mettre en œuvre des algorithmes de contrôle. Le système comprend trois stations de contrôle. Deux d'entre eux sont placés juste au-dessus de la scène, et un dans la salle des machines (ce poste est utilisé par un électricien de service). Il y a aussi des blocs de contrôle avec des contrôleurs dans la salle des machines. Ces contrôleurs exécutent des commandes de contrôle, effectuent une modulation de largeur d'impulsion (PWM), allument ou éteignent les moteurs, contrôlent la position des lattes. Deux postes de commande au-dessus de la scène ont des écrans, des unités système et des boules de commande comme dispositifs de pointage. Les stations de contrôle sont connectées via Ethernet. Chaque poste de commande est connecté à des blocs de commande par canal RS485. Les deux stations situées au-dessus de la scène peuvent être utilisées pour contrôler le système en même temps, mais une seule station peut être active. La station active est sélectionnée par un opérateur; la deuxième station sera passive; la station passive a son canal RS485 désactivé.


Pourquoi les acteurs?


Du point de vue des algorithmes, le système est construit au-dessus des événements. Données des capteurs, actions de l'opérateur, expiration des temporisations ... Ce sont tous des exemples d'événements. Le modèle d'acteur fonctionne bien pour de tels algorithmes: les acteurs gèrent les événements entrants et forment des actions sortantes en fonction de leur état actuel. Ces mécanismes sont disponibles dans SObjectizer juste à la sortie de la boîte.


Les principes de base de ces systèmes sont les suivants: les acteurs interagissent via des messages asynchrones, les acteurs ont des états et passent d'un état à un autre, seuls les messages significatifs pour l'état actuel sont traités.


Il est intéressant de noter que les acteurs sont découplés des threads de travail dans SObjectizer. Cela signifie que vous pouvez d'abord implémenter et déboguer vos acteurs et ensuite seulement décider quel thread de travail sera utilisé pour chaque acteur. Il existe des "répartiteurs" qui implémentent diverses stratégies liées aux threads. Par exemple, il existe un répartiteur qui fournit un thread de travail distinct pour chaque acteur; il existe un répartiteur de pool de threads qui fournit un pool de threads de travail de taille fixe; il y a un répartiteur qui exécute tous les acteurs sur le même thread.


La présence de répartiteurs offre un moyen très flexible de régler un système d'acteurs pour nos besoins. On peut regrouper certains acteurs pour travailler sur le même contexte. Nous pouvons changer le type de répartiteur par une seule ligne de code. Les développeurs de SObjectizer disent que l'écriture d'un répartiteur personnalisé n'est pas une tâche complexe. Mais il n'était pas nécessaire d'écrire notre propre répartiteur dans ce projet; tout ce dont nous avions besoin a été trouvé dans SObjectizer.


Encore une autre caractéristique intéressante est la coopération des acteurs. Une coopération est un groupe d'acteurs qui peut exister si et seulement si tous les acteurs ont démarré avec succès. La coopération ne peut pas démarrer si au moins l'un de ses acteurs n'a pas démarré. Il semble qu'il y ait une analogie entre les coopérations de SObjectizer et les pods de Kubernetes, mais il semble également que les coopérations de SObjectizer soient apparues plus tôt ...


Lorsqu'un acteur est créé, il est ajouté à la coopération (la coopération peut contenir un seul acteur) et est lié à un répartiteur. Il est facile de créer dynamiquement des coopérations et des acteurs et les développeurs de SObjectizer disent que c'est une opération plutôt bon marché.


Tous les acteurs interagissent entre eux via des "boîtes de message" (mbox). C'est encore un autre concept intéressant et puissant de SObjectizer. Il offre un moyen flexible de traitement des messages.


Au début, il peut y avoir plusieurs destinataires derrière une mbox. C'est assez utile. Par exemple, il peut y avoir une mbox utilisée par les capteurs pour publier de nouvelles données. Les acteurs peuvent créer des abonnements pour cette mbox, et les acteurs abonnés recevront les données qu'ils souhaitent. Cela permet de travailler de manière "Publier / S'abonner".


Dans un second temps, les développeurs de SObjectizer ont envisagé la possibilité de créer une mbox personnalisée. Il est relativement facile de créer une mbox personnalisée avec un traitement spécial des messages entrants (comme le filtrage ou la répartition entre plusieurs abonnés en fonction du contenu du message).


Il existe également une mbox personnelle pour chaque acteur et les acteurs peuvent transmettre une référence à cette mbox dans les messages aux autres acteurs (qui permet de répondre directement à un acteur spécifique).


Dans notre projet, nous avons divisé tous les objets contrôlés en huit groupes (un groupe pour chaque boîtier de commande). Trois threads de travail ont été créés pour chaque groupe (c'est parce que seuls trois moteurs peuvent fonctionner en même temps). Cela nous a permis d'avoir une indépendance entre les groupes de moteurs. Il a également permis de travailler de manière asynchrone avec les moteurs de chaque groupe.


Il est nécessaire de mentionner que SObjectizer-5 ne dispose d'aucun mécanisme d'interaction interprocessus et / et réseau. Il s'agit d'une décision consciente des développeurs de SObjectizer; ils voulaient rendre SObjectizer aussi léger que possible. De plus, la prise en charge transparente de la mise en réseau existait dans certaines versions précédentes de SObjectizer mais a été supprimée. Cela ne nous a pas dérangés car un mécanisme de mise en réseau dépend fortement d'une tâche, des protocoles utilisés et d'autres conditions. Il n'y a pas de solution universelle unique pour tous les cas.


Dans notre cas, nous avons utilisé notre ancienne bibliothèque libuniset2 pour les communications réseau et interprocessus. En conséquence, libuniset2 prend en charge les communications avec les capteurs et les blocs de contrôle, et SObjectizer prend en charge les acteurs et les interactions entre les acteurs au sein d'un même processus.


Comme je l'ai dit plus tôt, il y a 62 moteurs. Chaque moteur peut être connecté à un FC (convertisseur de fréquence); une coordonnée de destination peut être spécifiée pour la latte correspondante; la vitesse de déplacement de la latte peut également être spécifiée. Et en plus de cela, chaque moteur a les états suivants:


  • prĂŞt Ă  travailler;
  • connectĂ©;
  • travailler;
  • dysfonctionnement;
  • connexion (un Ă©tat de transition);
  • dĂ©connexion (un Ă©tat de transition);

Chaque moteur est représenté dans le système par un acteur qui implémente la transition entre les états, gère les données des capteurs et émet des commandes. Il n'est pas difficile de créer un acteur dans SObjectizer: héritez simplement votre classe du type so_5::agent_t . Le premier argument du constructeur de l'acteur doit être de type context_t , tous les autres arguments peuvent être définis comme le souhaite un développeur.


 class Drive_A: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); ... } 

Je ne montrerai pas la description détaillée des classes et des méthodes car ce n'est pas un tutoriel. Je veux juste montrer à quel point tout cela peut être fait facilement dans SObjectizer (en quelques lignes littéralement). Permettez-moi de vous rappeler que SObjectizer possède une excellente documentation et de nombreux exemples.


Quel est "l'état" d'un acteur? De quoi parle-t-on?


L'utilisation des états et la transition entre eux est un «sujet natif» pour les systèmes de contrôle. Ce concept est très bon pour la gestion d'événements. Ce concept est pris en charge dans SObjectizer au niveau de l'API. Les états sont déclarés dans la classe de l'acteur:


 class Drive_A final: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); virtual ~Drive_A(); //  state_t st_base {this}; state_t st_disabled{ initial_substate_of{st_base}, "disabled" }; state_t st_preinit{ substate_of{st_base}, "preinit" }; state_t st_off{ substate_of{st_base}, "off" }; state_t st_connecting{ substate_of{st_base}, "connecting" }; state_t st_disconnecting{ substate_of{st_base}, "disconnecting" }; state_t st_connected{ substate_of{st_base}, "connected" }; ... } 

puis les gestionnaires d'événements sont définis pour chaque état. Parfois, il est nécessaire de faire quelque chose à l'entrée ou à la sortie d'un État. Ceci est également pris en charge dans SObjectizer via les gestionnaires on_enter / on_exit. Il semble que les développeurs de SObjectizer aient une expérience dans le développement de systèmes de contrôle.


Gestionnaires d'événements


Un gestionnaire d'événements est un endroit où votre logique d'application est implémentée. Comme je l'ai dit plus tôt, un abonnement est créé pour une mbox particulière et un état spécifique. Si un acteur n'a pas d'états explicitement spécifiés, il se trouve dans un "état_par défaut" spécial.


Différents gestionnaires peuvent être définis pour le même événement dans différents états. Si vous ne définissez pas de gestionnaire pour un événement, cet événement sera ignoré (un acteur ne le saura pas).


Il existe une syntaxe simple pour définir les gestionnaires d'événements. Vous spécifiez une méthode et il n'est pas nécessaire de spécifier des types ou des paramètres de modèle supplémentaires. Par exemple:


 so_subscribe(drv->so_mbox()) .in(st_base) .event( &Drive_A::on_get_info ) .event( &Drive_A::on_control ) .event( &Drive_A::off_control ); 

C'est un exemple d'abonnement aux événements d'une mbox spécifique dans l'état st_base. Il convient de mentionner que st_base est un état de base pour certains autres états et que l'abonnement sera hérité par les états dérivés. Cette approche permet de se débarrasser du copier-coller pour des gestionnaires d'événements similaires dans différents états. Mais le gestionnaire d'événements hérité peut être redéfini pour un état particulier ou un événement peut être complètement désactivé ("supprimé").


Une autre façon de définir les gestionnaires d'événements consiste à utiliser les fonctions lambda. C'est un moyen très pratique car les gestionnaires d'événements contiennent souvent juste une ou deux lignes de code: un envoi de quelque chose quelque part ou un changement d'état:


 so_subscribe(drv->so_mbox()) .in(st_disconnecting) .event([this](const msg_disconnected_t& m) { ... st_off.activate(); }) .event([this]( const msg_failure_t& m ) { ... st_protection.activate(); }); 

Cette syntaxe semble complexe au début, mais elle devient familière juste après quelques jours de codage actif et vous commencez même à l'aimer. C'est parce que toute la logique d'un acteur peut être concise et placée sur un seul écran. Dans l'exemple illustré ci-dessus, il existe des transitions de st_disconnected à st_off ou st_protection. Ce code est facile à lire.


BTW, pour les cas simples, où une simple transition d'état est nécessaire, il existe une syntaxe spéciale:


 auto mbox = drv->so_mbox(); st_off .just_switch_to<msg_connected_t>(mbox, st_connected) .just_switch_to<msg_failure_t>(mbox, st_protection) .just_switch_to<msg_on_limit_t>(mbox, st_protection) .just_switch_to<msg_on_t>(mbox, st_on); 

Le contrĂ´le


Comment le contrôle est-il organisé? Comme mentionné ci-dessus, il existe deux postes de contrôle pour contrôler le mouvement des lattes. Chaque station de contrôle a un écran, un dispositif de pointage (trackball) et un régleur de vitesse (et nous ne comptons pas un ordinateur à l'intérieur de la station et quelques accessoires supplémentaires).


Il existe deux modes de contrôle: manuel et "mode scénario". Le "mode scénario" sera discuté plus tard, et maintenant parlons du mode manuel. Dans ce mode, un opérateur sélectionne une latte, la prépare pour le mouvement (connecte le moteur à un FC), définit la marque cible pour la latte, et lorsque la vitesse est réglée au-dessus de zéro, la latte commence à se déplacer.


Le régulateur de vitesse est un accessoire physique sous la forme d'un "potentiomètre avec poignée", mais il en existe également un virtuel sur l'écran de la station. Plus il est tourné, plus la vitesse de déplacement est élevée. La vitesse maximale est limitée à 1,5 mètre par seconde. Le régleur de vitesse est un pour tous les lattes. Cela signifie que tous les liteaux sélectionnés se déplacent à la même vitesse. Les lattes peuvent se déplacer dans des directions opposées (cela dépend de la sélection de l'opérateur). Il est évident qu'il est difficile pour un humain de contrôler plus de quelques lattes. De ce fait, seuls de petits groupes de lattes sont traités en mode manuel. Les opérateurs peuvent contrôler les lattes à partir de deux postes de contrôle en même temps. Il y a donc un régleur de vitesse séparé pour chaque station.


Du point de vue de l'implémentation, il n'y a pas de logique spécifique en mode manuel. Une commande "connecter le moteur" va de l'interface graphique, est transformée en un message correspondant à un acteur, puis est gérée par cet acteur. L'acteur passe de l'état "désactivé" à "se connecter", puis à l'état "connecté". Des choses similaires se produisent avec les commandes de positionnement d'une latte et de réglage de la vitesse de déplacement. Toutes ces commandes sont transmises à un acteur sous forme de messages. Mais il convient de mentionner que "interface graphique" et "processus de contrôle" sont des processus distincts et libuniset2 est utilisé pour IPC.


Le mode scénario (y a-t-il encore des acteurs?)


En pratique, le mode manuel n'est utilisé que pour les cas très simples ou lors des répétitions. Le mode de contrôle principal est le "mode scénario". Dans ce mode, chaque latte est déplacée vers une position spécifique avec une vitesse particulière en fonction des paramètres du scénario. Un opérateur dispose de deux commandes simples dans ce mode:


  • prĂ©parer (un groupe de moteurs est connectĂ© au FC);
  • allez (le mouvement du groupe commence).

L'ensemble du scénario est divisé en "agendas". Un «agenda» décrit un seul mouvement d'un groupe de lattes. Cela signifie qu'un "agenda" comprend des lattes et contient des destinations cibles et des vitesses pour elles. En réalité, un scénario se compose d'actes, les actes se composent d'images, l'image se compose d'agendas et l'agenda se compose de cibles pour les lattes. Mais du point de vue du contrôle, cela n'a pas d'importance, car seuls les agendas contiennent les paramètres précis du mouvement de la latte.


Le modèle Actor s'adapte parfaitement à ce cas. Nous avons développé un "joueur de scénario" qui crée un groupe d'acteurs spéciaux et les démarre. Nous avons développé deux types d'acteurs: les acteurs exécuteurs (ils contrôlent le mouvement des lattes) et les acteurs coordinateurs (ils répartissent les tâches entre exécuteurs). Les exécuteurs sont créés à la demande: lorsqu'il n'y a pas d'exécuteurs libres, un nouvel exécuteur sera créé. Le coordinateur gère le pool d'exécuteurs disponibles. Par conséquent, le contrôle ressemble approximativement à ceci:


  • un opĂ©rateur charge un scĂ©nario;
  • le "fait dĂ©filer" jusqu'Ă  l'agenda requis;
  • appuie sur le bouton "prĂ©parer" au moment appropriĂ©. Ă€ ce moment, un message est envoyĂ© Ă  un coordinateur. Ce message contient des donnĂ©es pour chaque latte de l'agenda;
  • le coordinateur examine son pool d'exĂ©cuteurs et rĂ©partit les tâches entre les exĂ©cuteurs libres (de nouveaux exĂ©cuteurs sont créés, si nĂ©cessaire);
  • chaque exĂ©cuteur reçoit une tâche et effectue des actions de prĂ©paration (connecte un moteur Ă  un FC, puis attend la commande "go");
  • l'opĂ©rateur appuie sur le bouton "go" au moment appropriĂ©;
  • la commande "go" va au coordinateur, et elle distribue la commande entre tous les exĂ©cuteurs en cours d'utilisation.

Il y a quelques paramètres supplémentaires dans les agendas. Comme "démarrer le mouvement uniquement après un délai de N secondes" ou "démarrer le mouvement uniquement après une commande supplémentaire d'un opérateur". Pour cette raison, la liste des états d'un exécuteur est assez longue: "prêt pour la prochaine commande", "prêt pour le mouvement", "retard du mouvement", "en attente de la commande de l'opérateur", "en mouvement", "terminé", "échec".


Lorsqu'une latte a réussi à atteindre la marque cible (ou en cas d'échec), l'exécuteur signale au coordinateur la fin de la tâche. Le coordinateur répond par une commande d'arrêt du moteur (si la latte ne participe plus à l'agenda) ou envoie une nouvelle tâche à l'exécuteur testamentaire. L'exécuteur éteint le moteur et passe à l'état "en attente" ou commence à traiter la nouvelle commande.


Parce que SObjectizer a une API assez réfléchie et pratique pour travailler avec les états, le code d'implémentation s'est avéré assez concis. Par exemple, un délai avant le mouvement est décrit par une simple ligne de code:


 st_delay.time_limit( std::chrono::milliseconds{target->delay()}, st_moving ); st_delay.activate(); ... 

La méthode time_limit spécifie la durée de séjour dans l'état et quel état doit être activé ensuite ( st_moving dans cet exemple).


Acteurs de la protection


Certes, des échecs peuvent survenir. Il existe des exigences pour gérer correctement ces échecs. Les acteurs sont également utilisés pour de telles tâches. Regardons quelques exemples:


  • protection contre les surintensitĂ©s;
  • protection contre le dysfonctionnement du capteur;
  • protection contre les mouvements dans la direction opposĂ©e (cela peut arriver s'il y a un problème avec les capteurs ou les actionneurs);
  • protection contre les mouvements spontanĂ©s (sans ordre);
  • contrĂ´le de l'exĂ©cution de la commande (le mouvement d'une latte doit ĂŞtre vĂ©rifiĂ©).

Nous pouvons voir que tous ces cas sont autosuffisants, mais ils devraient être contrôlés ensemble, en même temps. Cela signifie que tout échec peut se produire. Mais chaque vérification a sa logique: parfois il faut vérifier un timeout, parfois il faut analyser certaines valeurs précédentes d'un capteur. Pour cette raison, la protection est mise en œuvre sous la forme de petits acteurs. Ces acteurs s'ajoutent à la coopération de l'acteur principal qui met en œuvre la logique de contrôle. Cette approche permet d'ajouter facilement de nouveaux cas de protection: il suffit d'ajouter un autre acteur protecteur à la coopération. Le code d'un tel acteur est généralement concis et facile à comprendre, car il ne met en œuvre qu'une seule fonction.


Les acteurs protecteurs ont également plusieurs États. Habituellement, ils sont allumés quand un moteur est allumé ou quand une latte commence son mouvement. Lorsqu'un protecteur détecte une panne / un dysfonctionnement, il publie une notification (avec le code de protection et quelques détails supplémentaires à l'intérieur). L'acteur principal réagit à cette notification et effectue les actions nécessaires (comme éteindre le moteur et passer à l'état protégé).


Comme conclusion ...


... cet article n'est pas une percée bien sûr. Le modèle d'acteur est utilisé dans plusieurs systèmes différents depuis assez longtemps. Mais c'était ma première expérience d'utilisation du modèle d'acteur pour construire un système de contrôle automatique dans un projet assez petit. Et cette expérience s'est avérée très réussie. J'espère avoir montré que les acteurs sont bien adaptés aux algorithmes de contrôle: il y a des places pour les acteurs littéralement partout.


Nous avions implémenté quelque chose de similaire dans les projets précédents (je veux dire les états, l'échange de messages, la gestion des threads de travail, etc.), mais ce n'était pas une approche unifiée. En utilisant SObjectizer, nous avons obtenu un petit outil léger qui résout beaucoup de problèmes. Nous n'avons plus besoin (explicitement) d'utiliser des mécanismes de synchronisation de bas niveau (comme les mutex), il n'y a pas de gestion manuelle des threads, plus de statecharts manuscrits. Tout cela est fourni par le cadre, connecté de manière logique et exprimé sous la forme d'une API pratique, mais vous ne perdez pas le contrôle des détails. Ce fut donc une expérience passionnante. Si vous avez encore des doutes, je vous recommande de jeter un œil au modèle d'acteur et au SObjectizer en particulier. Cela laisse des émotions positives.


Le modèle d'acteur fonctionne vraiment! Surtout au théâtre.


Article original en russe

Source: https://habr.com/ru/post/fr452464/


All Articles