Cet article présentera l'expérience de l'utilisation de l'approche acteur dans un projet intéressant de système de contrôle automatisé pour un théâtre. C'est exactement l'impression d'utilisation, rien de plus.
Récemment, j'ai pu participer à une tâche très intéressante - la modernisation, mais en fait - le développement d'un nouveau système de contrôle automatisé pour le levage de crémaillères pour l'un des théâtres.
Un théâtre moderne (s'il est grand) est une organisation assez complexe. Beaucoup de personnes, d'équipements et de systèmes divers y participent. L'un de ces systèmes est le système de commande pour «monter et descendre» le décor sur la scène. Les spectacles modernes, et plus d'opéras et de ballets, sont de plus en plus saturés de moyens techniques chaque année. Il utilise beaucoup de décors complexes et leurs mouvements pendant l'action. Le paysage est activement utilisé dans les plans de mise en scène, élargissant le sens de ce qui se passe et même «jouant votre propre rôle de soutien»). En général, il était très intéressant de se familiariser avec la vie en coulisses du théâtre et de découvrir ce qui s'y passe pendant les représentations. Après tout, les spectateurs ordinaires ne voient que ce qui se passe sur scène.
Mais cet article est encore technique et j'ai voulu y partager l'expérience de l'utilisation de l'approche acteur pour implémenter la gestion. Et partagez également l'expérience de l'utilisation de l'un des rares frameworks d'acteurs C ++ - sobjectizer .
Pourquoi exactement lui? Nous le regardons depuis longtemps. Il y a des articles sur un habr, il a une excellente documentation détaillée avec des exemples. Le projet est assez mature. Un rapide coup d'œil aux exemples a montré que les développeurs opèrent avec des concepts «familiers» (états, temporisations, événements), c'est-à-dire on ne s'attendait pas à de gros problèmes de compréhension et de maîtrise, à utiliser dans notre projet. Et oui, surtout, les développeurs sont adéquats et sympathiques, prêts à vous aider avec des conseils (en russe) . Nous avons donc décidé d'essayer ...
On fait quoi?
Alors, à quoi ressemble notre «objet de contrôle»? Le système d'ascenseurs shtanketovy - c'est 62 shankets (tuyaux métalliques) sur toute la largeur de la scène accrochée au-dessus de cette même scène, environ tous les 30 à 40 cm du bord de la scène en profondeur. Les shankets eux-mêmes sont suspendus à des cordes et peuvent monter ou descendre sur la scène (mouvement vertical). Dans chaque représentation (ou opéra ou ballet), une partie des strophes est utilisée pour la décoration. Le décor y est accroché et déplacé (si le script l'exige) pendant l'action. Le mouvement lui-même est effectué sur commande des opérateurs (ils ont des panneaux de commande spéciaux) en utilisant le système «moteur - câble - contrepoids» (à peu près le même que les ascenseurs dans les maisons). Les moteurs sont situés sur les bords de la scène (sur plusieurs niveaux), de sorte qu'ils ne sont pas visibles pour le spectateur. Tous les moteurs sont divisés en 8 groupes et chaque groupe dispose de trois convertisseurs de fréquence (IF). Dans chaque groupe, trois moteurs peuvent être activés simultanément, chacun étant connecté à son propre onduleur. Au total, nous avons un système de 62 moteurs et 24 onduleurs que nous devons contrôler.
Notre tâche était de développer une interface opérateur pour gérer cette économie, ainsi que de mettre en œuvre des algorithmes de gestion. Le système comprend trois postes de contrôle. Deux postes de commande sont situés directement au-dessus de la scène et un poste est situé dans la salle des machines (où se trouvent les armoires de commande) et est conçu pour surveiller les travaux d'un électricien en service. Dans les armoires de commande, il y a des contrôleurs qui exécutent des commandes, contrôlent le PWM, alimentent les moteurs, suivent la position des tiges. Sur les deux télécommandes supérieures se trouvent des moniteurs, une unité centrale où les algorithmes de contrôle et la boule de commande tournent comme une «souris». Un réseau Ethernet est utilisé entre les panneaux de contrôle. Chaque armoire de commande possède un canal RS485 (soit 8 canaux) de chacun des deux panneaux de commande. La gestion peut être effectuée simultanément à partir des deux télécommandes (qui sont au-dessus de la scène), mais en même temps, une seule des télécommandes (désignée par l'opérateur comme opérateur principal) échange avec les armoires, la deuxième console à ce moment est considérée comme une sauvegarde et l'échange est désactivé sur elle.
Et ici les acteurs
Du point de vue des algorithmes, l'ensemble du système est construit sur des événements. Soit ce sont des changements dans les capteurs, soit les actions de l'opérateur, soit le début d'un certain temps (temporisateurs). Et de tels algorithmes sont très bien placés par le système d'acteurs qui traitent les événements entrants, forment une sorte de réponse, et tout cela en fonction de leur état. Dans le sobjectizer, tous ces mécanismes sortent de la boîte. Les principaux principes sur lesquels un tel système est basé peuvent être attribués: l'interaction entre les acteurs se produit à travers des messages, les acteurs peuvent avoir des états et se déplacer entre eux, dans chaque état, l'acteur ne traite que les messages qui l'intéressent en ce moment. Fait intéressant, dans un sobjectiseur, travailler avec des acteurs est conceptuellement distinct de travailler avec des flux de travail. C'est-à-dire Vous pouvez décrire les acteurs dont vous avez besoin, réaliser leur logique, réaliser leur interaction à travers des messages. Mais ensuite, résolvez séparément le problème de l'allocation de threads (ressources) pour leur travail. Ceci est assuré par les soi-disant "répartiteurs" qui sont responsables d'une politique particulière de travail avec les threads. Par exemple, il y a un répartiteur qui alloue un thread séparé pour chaque acteur avec lequel travailler, il y a un répartiteur qui fournit un pool de threads (c'est-à-dire qu'il peut y avoir plus d'acteurs que de threads) avec la possibilité de définir le nombre maximum de threads, il y a un répartiteur qui alloue un thread pour tous. La présence de répartiteurs fournit un mécanisme très flexible pour mettre en place un système d'acteurs adapté à vos besoins. Vous pouvez combiner des groupes d'acteurs pour travailler avec l'un des répartiteurs, tout en changeant un type de répartiteur en un autre, cela change essentiellement une ligne de code. Selon les auteurs du framework, il n'est pas difficile non plus d'écrire votre propre répartiteur unique. Cela n'était pas nécessaire dans notre projet, car tout ce dont nous avions besoin était déjà dans le sobjectizer.
Une autre caractéristique intéressante est la présence du concept de «coopération» des acteurs. La coopération est un groupe d'acteurs qui peuvent tous exister ou tous être détruits (ou non lancés) si au moins un acteur de la coopération n'a pas pu commencer à travailler ou terminer. Je n'ai même pas peur de donner une telle analogie ( même s'il s'agit d'un autre "opéra" ) que le concept de «coopération» est comme le concept de «foyers» dans le Kubernetes désormais à la mode, il ne semble que dans le sobjectizer, il est apparu plus tôt ...
Au moment de la création, chaque acteur est inclus dans la coopération (la coopération peut consister en un acteur), s'attache à l'un ou l'autre répartiteur et commence à travailler. Dans le même temps, les acteurs (et la coopération) peuvent (facilement) être créés dynamiquement en grand nombre, et comme les développeurs le promettent, ce n'est pas cher. Tous les acteurs échangent entre eux via des " boîtes aux lettres " (mbox). C'est également un concept assez intéressant et fort dans le sobjectizer. Il fournit un mécanisme très flexible pour le traitement des messages entrants. Premièrement, plusieurs destinataires peuvent se cacher derrière une boîte. C'est vraiment très pratique. Par exemple, une boîte est créée dans laquelle les événements provenant de capteurs externes sont reçus et chaque acteur souscrit aux événements qui l'intéressent. Cela fournit un style de fonctionnement «publier / s'abonner». Deuxièmement, les développeurs ont fourni la possibilité de créer relativement facilement leur propre implémentation de boîtes aux lettres qui peuvent prétraiter les messages entrants (par exemple, les filtrer ou les distribuer d'une manière spéciale entre les consommateurs). De plus, chaque acteur a sa propre boîte aux lettres et peut même lui envoyer un «lien» dans des messages à d'autres acteurs, par exemple, afin qu'ils puissent envoyer une sorte de notification comme réponse de retour.
Dans notre projet, afin d'assurer l'indépendance des groupes de moteurs entre eux, ainsi que d'assurer le fonctionnement «asynchrone» des moteurs au sein du groupe, tous les objets de contrôle ont été divisés en 8 groupes (selon le nombre d'armoires de commande), chacun comptant trois travailleurs (car pas plus de trois moteurs peuvent fonctionner en groupe à la fois).
Il faut également dire que le sobjectizer (dans la version actuelle 5.5) ne contient pas de mécanismes d'interprocessus et d'interaction réseau et laisse cette partie aux développeurs. Les auteurs l'ont fait très délibérément , afin que le cadre soit plus «facile». De plus, les mécanismes d'interaction réseau «une fois» existaient dans les versions précédentes, mais étaient exclus. Cependant, cela ne cause aucun inconvénient, car en effet l'interaction réseau est très dépendante des tâches à résoudre, des protocoles d'échange utilisés, etc. Ici, une implémentation universelle ne peut pas être optimale dans tous les cas.
Dans notre cas, pour la communication réseau et interprocessus, nous avons utilisé l'un de nos développements de longue date - la bibliothèque libuniset2 . Par conséquent, l'architecture de notre système ressemble à ceci:
- libuniset fournit une communication réseau et interprocessus (basée sur des capteurs)
- sobjectizer fournit la création d'un système d'acteurs interagissant les uns avec les autres (dans le même espace d'adressage) mettant en œuvre des algorithmes de contrôle.
Alors, je vous rappelle que nous avons 62 moteurs. Chaque moteur peut être connecté à l'onduleur, le support correspondant peut recevoir les coordonnées auxquelles vous devez arriver et la vitesse à laquelle vous devez vous déplacer. De plus, le moteur présente les conditions suivantes:
- prêt à partir
- connecté
- courir (tourner)
- accident
- connexion (état transitoire)
- arrêt (état transitoire)
En conséquence, chaque «moteur» est représenté dans le système par un acteur qui implémente la logique des transitions entre les états, traite les événements des capteurs et émet des commandes de contrôle. Dans sobjectizer, les acteurs sont faciles à créer, il suffit d'hériter votre classe de la classe de base so_5 :: agent_t. En même temps, le constructeur doit prendre le soi-disant contexte :: so_5 :: context_t comme premier argument, les autres arguments étant déterminés par les besoins du développeur.
class Drive_A: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); ... }
Parce que cet article n'est pas pédagogique, je ne fournirai donc pas ici les textes détaillés des descriptions de cours ou de méthodes. L'article voulait juste montrer à quel point il est facile (en quelques lignes) avec sobjectizer de faire tout cela. Permettez-moi de vous rappeler que le projet a une excellente documentation détaillée, avec un tas d'exemples différents.
Et quels sont les «états» de ces acteurs? De quoi tu parles?
L'utilisation des états et des transitions entre eux pour ACS est généralement un sujet natif. Ce «concept» s'intègre très bien dans la gestion d'événements. Dans sobjectizer, ce concept est pris en charge au niveau de l'API. Dans une classe d'acteurs, les états sont assez facilement déclarés
class Drive_A final: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); virtual ~Drive_A();
et en outre, pour chaque état, le développeur détermine les gestionnaires nécessaires. Souvent, certaines actions sont requises lors de l'entrée dans un état et lors de sa sortie. Ceci est également prévu dans le sobjectizer, vous définissez tout aussi facilement vos gestionnaires pour ces événements («state entry», «state exit»). On estime que les développeurs du passé ont une vaste expérience ACS-shny ...
Gestionnaires d'événements
Les gestionnaires d'événements, c'est là que la logique de votre application est implémentée. Comme mentionné ci-dessus, un abonnement est fait à une boîte aux lettres spécifique et pour un certain état de l'acteur. Si un acteur n'a pas d'états explicitement déclarés dans le code, alors il est implicitement dans l'état spécial "default_state". Dans différents états, vous pouvez définir différents gestionnaires pour les mêmes événements. Si vous n'avez pas spécifié de gestionnaire d'événements dans cette boîte aux lettres, il sera simplement ignoré (c'est-à-dire qu'il n'existera tout simplement pas pour l'acteur).
La syntaxe de définition des gestionnaires est très simple. Il suffit d'indiquer votre fonction. Aucun type ou argument de modèle n'est requis. Tout est déduit automatiquement de la définition de la fonction. 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 );
Voici un exemple d'abonnement à des événements dans une boîte spécifique pour l'état st_base. Fait intéressant, dans cet exemple, st_base est l'état de base pour les autres états et, par conséquent, cet abonnement sera valide pour tous les états qui sont «hérités» de st_base. Cette approche vous permet de vous débarrasser du "copier-coller" pour déterminer les mêmes gestionnaires pour différents états. Dans le même temps, dans un état spécifique, vous pouvez soit remplacer le gestionnaire spécifié, soit le "désactiver" (supprimer).
Il existe une autre façon de définir les gestionnaires. Il s'agit d'une définition directe des fonctions lambda. C'est un moyen très pratique, car les gestionnaires sont souvent des fonctions courtes dans quelques actions, envoyer quelque chose à quelqu'un ou changer 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(); });
Au début, cette syntaxe semble compliquée. Mais en seulement quelques jours de développement actif, vous vous y habituez et commence même à l'aimer. Parce que toute la logique du travail de l'acteur dans un état ou un autre peut tenir dans un code assez court et tout sera devant vos yeux. Par exemple, dans l'exemple illustré, dans l'état déconnecté (st_disconnecting), soit la transition vers l'état déconnecté (st_off.) Ou l'état de protection (st_protection) se produit si un message sur une sorte d'échec se produit. Un tel code est assez facile à lire.
Soit dit en passant, pour les cas simples lorsqu'un événement doit simplement entrer dans un état, il existe une syntaxe encore plus courte:
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);
La gestion
Comment fonctionne la gestion de toute cette économie? Comme mentionné ci-dessus, deux télécommandes sont fournies pour le contrôle direct du mouvement des shtankets. Sur chaque télécommande, il y a un moniteur, un manipulateur (trackball) et un cadran de vitesse (en plus de "l'ordinateur" caché dans la télécommande sur lequel tout tourne et des tas de convertisseurs de toutes sortes). Le système a plusieurs modes de contrôle du mouvement des shtankets. Manuel et "mode script". A propos du "mode scénario" sera discuté plus loin, et maintenant un peu sur le "mode manuel". Dans ce mode, l'opérateur sélectionne le segment souhaité, le prépare pour le mouvement (connecte le moteur à l'onduleur), définit la marque (position cible) du segment, et dès qu'il définit la vitesse supérieure à zéro, les segments commencent à se déplacer. Pour régler la vitesse, un ajusteur physique spécial est utilisé, sous la forme d'un «potentiomètre avec bouton», mais il existe également un «écran de réglage» de la vitesse. Le plus "tourné", le plus fort va plus vite. La vitesse maximale est limitée à 1,5 m / s. Bouton de vitesse - un pour tous. C'est-à-dire En mode manuel, tous les porte-outils connectés à l'opérateur se déplacent à la même vitesse définie. Bien qu'ils puissent se déplacer dans différentes directions (cela dépend de l'endroit où l'opérateur les a dirigés). Bien sûr, il est difficile pour une personne de garder une trace de plus de deux ou trois shtankets en même temps, donc généralement ils ne bougent pas beaucoup en mode manuel. Depuis deux stations, les opérateurs peuvent gérer simultanément chacun de leurs shtankets. De plus, chaque console (opérateur) possède son propre régulateur de vitesse.
Du point de vue de l'implémentation, le mode manuel ne contient pas de logique particulière. La commande de connexion du moteur provient de l'interface graphique, est convertie en message à l'acteur correspondant, qui y travaille. En passant par les états «off» -> «connecting» -> «connected». La même chose avec le réglage de la position pour le mouvement du stunket et le réglage de la vitesse. Tous ces événements arrivent à l'acteur sous forme de messages auxquels il réagit. Sauf s'il peut être noté que l'interface graphique et le processus de contrôle lui-même sont des processus différents et qu'entre eux il y a une interaction "interprocessus" à travers les "capteurs" utilisant libuniset2 .
Mode d'exécution de script (encore une fois, ces acteurs?)
En fait, le mode de contrôle manuel est principalement utilisé uniquement pour sortir pendant les répétitions ou dans des cas simples. Le mode principal dans lequel le contrôle est en cours est le «mode d'exécution de script» ou, brièvement, le «mode de script». Dans ce mode, chaque shtank se déplace vers son point avec les paramètres spécifiés dans le script (vitesse et marque cible). Pour l'opérateur, le contrôle dans ce mode consiste en deux commandes simples:
- préparez-vous (le bon groupe de moteurs est connecté)
- allons-y (le groupe commence à se déplacer vers les positions cibles définies pour chacun).
L'ensemble du scénario est divisé en soi-disant «agendas». Un agenda est un mouvement d'un groupe shtanket. C'est-à-dire chaque programme comprend un groupe de shtankets, avec la vitesse cible et la marque où vous devez venir. En fait, le script est divisé en actes, les actes sont divisés en peintures, les peintures sont divisées en citations à comparaître et les citations à comparaître sont déjà constituées de «buts» pour des shtankets spécifiques. Mais du point de vue de la gestion, cette division n'est pas importante, car c'est à l'ordre du jour que des paramètres spécifiques de mouvement sont finalement indiqués.
Pour mettre en œuvre ce régime, le système d'acteurs est remonté au mieux. Un «lecteur de script» a été développé qui crée un groupe d'acteurs spéciaux et les lance. Nous avons développé deux types d'acteurs: les acteurs-acteurs, conçus pour effectuer des tâches pour un shtanket spécifique, et un acteur-coordinateur, qui répartit les tâches entre les interprètes. De plus, des acteurs performants sont créés selon les besoins, si au moment de la prochaine équipe n'est pas libre. L'acteur coordinateur est responsable de la création et du maintien du pool d'acteurs performants. Par conséquent, la direction ressemble à ceci:
- instruction charge le script
- "Retourne" à l'ordre du jour souhaité (va généralement juste dans une rangée).
- au bon moment, appuie sur le bouton «préparer», par lequel une commande (message) est envoyée à l'acteur coordinateur pour chaque formulaire inclus dans l'agenda actuel avec les paramètres de mouvement.
- L'acteur-coordinateur examine son bassin d'acteurs libres, en prend un gratuit (s'il n'en crée pas un nouveau) et lui donne une tâche (nombre de shankets et paramètres de mouvement).
- Chaque acteur-acteur ayant reçu la tâche commence à exécuter la commande «se préparer». C'est-à-dire il connecte le moteur et entre en mode veille de la commande «go».
- le moment venu, l'opérateur donne la commande "allons-y"
- l'équipe "go" vient au coordinateur. Il l'envoie à tous ses interprètes actuellement actifs et ils commencent «l'exécution».
Il convient de noter que l'agenda contient des paramètres supplémentaires. Par exemple, commencez le mouvement avec un retard de N secondes ou commencez le mouvement uniquement après une commande d'opérateur spéciale distincte. Par conséquent, la liste des états pour chaque acteur performant est assez longue: "prêt à exécuter la commande suivante", "prêt à bouger", "mouvement retardé", "en attente de la commande de l'opérateur", "mouvement", "exécution terminée", "dysfonctionnement" .
Une fois que le shanket a atteint (ou non) la marque spécifiée, l'acteur-interprète informe le coordinateur de la tâche terminée. Le coordinateur donne l'ordre de désactiver ce moteur (s'il ne participe plus à l'agenda actuel) ou émet de nouveaux paramètres de mouvement. À son tour, l'acteur-interprète a reçu une commande pour éteindre le moteur, l'éteint et passe dans un état d'attente pour de nouvelles commandes, ou commence à exécuter une nouvelle commande.
En raison du fait que le sobjectizer dispose d'une API bien pensée et pratique pour travailler avec les états, le code d'implémentation est assez concis. Par exemple, le retard sur le mouvement est décrit sur une seule ligne:
st_delay.time_limit( std::chrono::milliseconds{target->delay()}, st_moving ); st_delay.activate(); ...
La fonction time_limit définit une limite de temps sur combien peut être dépensé dans un état donné et quel état doit être passé après un temps spécifié (st_moving).
Acteurs de la protection
Bien sûr, pendant le fonctionnement, des dysfonctionnements peuvent survenir. Le système est requis pour gérer ces situations. Il y avait aussi une place pour l'utilisation des acteurs. Considérez plusieurs de ces protections:
- protection contre les surintensités
- protection contre les défaillances de mesure
- protection contre les mouvements dans la direction opposée (et cela peut être le cas si quelque chose ne va pas avec le capteur ou le compteur)
- protection contre les mouvements sans commande
- contrôle de l'exécution de l'équipe (contrôle que le shtanket a commencé à bouger)
Vous pouvez voir que toutes ces protections sont indépendantes (autosuffisantes) du point de vue de la mise en œuvre, et devraient fonctionner "en parallèle". C'est-à-dire n'importe quelle condition peut fonctionner. Dans le même temps, la logique de vérification des conditions de déclenchement pour chacune des protections est propre, parfois un retard (temporisateur) est nécessaire pour le déclenchement, parfois un traitement préalable de plusieurs mesures précédentes est nécessaire, etc. Par conséquent, la mise en œuvre de chaque type de protection en tant que petit acteur distinct s'est avérée très pratique. Tous ces acteurs sont lancés en plus (en coopération) de l'acteur principal qui met en œuvre la logique de contrôle. Cette approche facilite l'ajout de types de défenses supplémentaires simplement en ajoutant un autre acteur au groupe. Dans le même temps, la mise en place d'un tel acteur reste assez simple et compréhensible, car Il implémente une seule fonction.
Les acteurs de la protection ont également plusieurs États. Fondamentalement, ils ne s'allument (passent à l'état «marche») que lorsque le moteur est connecté ou que la tige est en mouvement. Lorsque les conditions de protection sont déclenchées, ils publient une notification de la protection (avec un code de sécurité et quelques détails pour la journalisation), l'acteur principal répond déjà à cette notification qui, si nécessaire, éteint le moteur et passe en mode protection.
En conclusion ..
... bien sûr, cet article n'est pas une sorte de "découverte". L'approche acteur a longtemps été utilisée avec succès dans de nombreux systèmes. Mais pour moi, c'était la première expérience de l'utilisation consciente de l'approche acteur pour construire des algorithmes de systèmes de contrôle dans un projet relativement petit. Et l'expérience a été assez réussie. J'espère avoir pu montrer que les acteurs sont très bien superposés aux algorithmes de contrôle, ils ont trouvé une place littéralement partout.
D'après l'expérience des projets précédents, il était clair que d'une manière ou d'une autre, nous mettions en œuvre «quelque chose comme ça» (états, messagerie, contrôle de flux, etc.), mais ce n'était pas une approche unifiée. En utilisant le sobjectizer, nous avons obtenu un outil de développement concis et léger qui prend une tonne de problèmes. Il n'est plus nécessaire (explicite) d'utiliser des outils de synchronisation (mutex, etc.), il n'y a pas de travail explicite avec les streams, pas de réalisations de la machine à états. Tout cela dans le cadre, logiquement interconnecté et présenté comme une API pratique, en plus, sans perdre le contrôle des détails. L'expérience a donc été intéressante. Pour ceux qui doutent encore, je recommande de prêter attention à l'approche acteur et au framework sobjectizer en particulier. Il laisse des émotions positives.
Et l'approche acteur fonctionne vraiment! Surtout au théâtre.