Se o projeto for "Teatro", use atores ...

Este artigo irá contar sobre a experiência de usar a abordagem de ator em um projeto interessante de um sistema de controle automatizado para o teatro. Esta é exatamente a impressão de uso, nada mais.


Recentemente, pude participar de uma tarefa muito interessante - modernização, mas de fato - o desenvolvimento de um novo sistema de controle automatizado para elevar racks de um dos cinemas.


Um teatro moderno (se for grande) é uma organização bastante complexa. Muitas pessoas, equipamentos e vários sistemas estão envolvidos nele. Um desses sistemas é o sistema de controle para "elevar e abaixar" o cenário no palco. Apresentações modernas, e mais óperas e balés, estão ficando cada vez mais saturadas de meios técnicos a cada ano. Ele usa muitos cenários complexos e seus movimentos durante a ação. O cenário é usado ativamente nos planos de direção, expandindo o significado do que está acontecendo e até “desempenhando seu próprio papel de apoio”). Em geral, foi muito interessante familiarizar-se com a vida nos bastidores do teatro e descobrir o que acontece durante as apresentações. Afinal, os espectadores comuns veem apenas o que está acontecendo no palco.


Mas este artigo ainda é técnico e eu queria compartilhar a experiência de usar a abordagem de ator para implementar o gerenciamento. E também compartilhe a experiência de usar uma das poucas estruturas de ator C ++ - sobjectizer .


Por que exatamente ele? Estamos de olho nele há muito tempo. Existem artigos em um habr, com excelente documentação detalhada com exemplos. O projeto é bastante maduro. Uma rápida olhada nos exemplos mostrou que os desenvolvedores operam com conceitos "familiares" (estados, temporizadores, eventos), ou seja, grandes problemas não eram esperados com compreensão e domínio, para uso em nosso projeto. E sim, importante, os desenvolvedores são adequados e amigáveis, prontos para ajudar com conselhos (em russo) . Então decidimos tentar ...


O que estamos fazendo?


Então, como é o nosso "objeto de controle"? O sistema de elevadores shtanketovy - são 62 hastes (tubos de metal) em toda a largura do palco, suspensas acima dessa mesma cena, aproximadamente a cada 30 a 40 cm da borda do palco em profundidade. As hastes são suspensas em cordas e podem subir ou descer no palco (movimento vertical). Em cada performance (ou ópera ou balé), parte das estrofes é usada para decoração. O cenário é pendurado neles e movido (se o script exigir) durante a ação. O movimento em si é realizado sob o comando dos operadores (eles têm painéis de controle especiais) usando o sistema "motor - cabo - contrapeso" (quase o mesmo que elevadores em residências). Os motores estão localizados nas bordas do palco (em várias camadas), para que não fiquem visíveis para o espectador. Todos os motores são divididos em 8 grupos e cada grupo possui três conversores de frequência (IF). Em cada grupo, três motores podem ser ativados simultaneamente, cada um conectado ao seu próprio inversor. No total, temos um sistema de 62 motores e 24 inversores, que devemos controlar.


Nossa tarefa era desenvolver uma interface de operação para gerenciar essa economia, bem como implementar algoritmos de gerenciamento. O sistema inclui três postos de controle. Dois postos de controle estão localizados diretamente acima do palco e um posto está localizado na sala de máquinas (onde estão os armários de controle) e foi projetado para monitorar o trabalho de um eletricista de plantão. Nos armários de controle existem controladores que executam comandos, controlam PWM, fornecem energia aos motores, rastreiam a posição das hastes. Nos dois controles remotos superiores estão os monitores, uma unidade de sistema onde os algoritmos de controle e o trackball estão girando como um "mouse". Uma rede Ethernet é usada entre os painéis de controle. Cada gabinete de controle possui um canal RS485 (ou seja, 8 canais) de cada um dos dois painéis de controle. O gerenciamento pode ser realizado simultaneamente em ambos os controles remotos (que estão acima do palco), mas ao mesmo tempo apenas um dos controles remotos (designado pelo operador como operador principal) está trocando com os gabinetes, o segundo console neste momento é considerado um backup e a troca é desativada.


E aqui os atores


Do ponto de vista dos algoritmos, todo o sistema é construído sobre eventos. Essas são algumas mudanças nos sensores, ou as ações do operador, ou o início de algum tempo (temporizadores). E tais algoritmos são muito bem colocados pelo sistema de atores que processam eventos recebidos, formam algum tipo de resposta e tudo isso, dependendo do seu estado. No sobjectador, todos esses mecanismos saem da caixa. Os principais princípios nos quais esse sistema se baseia podem ser atribuídos: a interação entre os atores ocorre por meio de mensagens, os atores podem ter estados e se mover entre eles, em cada estado o ator processa apenas as mensagens que lhe interessam no momento. O interessante é que, em um sobjetitador, trabalhar com atores é conceitualmente separado de trabalhar com fluxos de trabalho. I.e. Você pode descrever os atores de que precisa, perceber sua lógica, interagir com mensagens. Mas, em seguida, resolva separadamente o problema de alocar threads (recursos) para seu trabalho. Isso é garantido pelos chamados "despachantes", responsáveis ​​por uma política específica de trabalho com threads. Por exemplo, existe um despachante que aloca um encadeamento separado para cada ator trabalhar, há um despachante que fornece um conjunto de encadeamentos (ou seja, pode haver mais atores que encadeamentos) com a capacidade de definir o número máximo de encadeamentos, há um despachante que aloca um encadeamento para todos. A presença de despachantes fornece um mecanismo muito flexível para configurar um sistema de ator para atender às suas necessidades. Você pode combinar grupos de atores para trabalhar com um dos despachantes, enquanto altera um tipo de despachante para outro, isso basicamente altera uma linha de código. Segundo os autores da estrutura, escrever seu próprio despachante exclusivo também não é difícil. Isso não era necessário em nosso projeto, porque tudo o que precisávamos já estava no sobjetador.


Outra característica interessante é a presença do conceito de “cooperação” dos atores. Cooperação é um grupo de atores que podem existir ou serem destruídos (ou não lançados) se pelo menos um ator da cooperação não puder iniciar o trabalho ou concluir. Não tenho medo de fazer essa analogia ( mesmo que seja de outra "ópera" ) de que o conceito de "cooperação" é como o conceito de "lareiras" nos Kubernetes, agora na moda, parece apenas no sobjetador, surgiu mais cedo ...


No momento da criação, cada ator é incluído na cooperação (a cooperação pode consistir em um ator), fica ligado a um ou outro despachante e começa a trabalhar. Ao mesmo tempo, atores (e cooperação) podem (facilmente) ser criados dinamicamente em grandes números e, como prometem os desenvolvedores, não é caro. Todos os atores trocam entre si através de " caixas de correio " (mbox). Este também é um conceito bastante interessante e forte no sobjetador. Ele fornece um mecanismo muito flexível para processar mensagens recebidas. Em primeiro lugar, mais de um destinatário pode estar escondido atrás de uma caixa. É realmente muito conveniente. Por exemplo, é criada uma caixa na qual os eventos de sensores externos são recebidos e cada ator assina eventos de seu interesse. Isso fornece um estilo de operação "publicar / assinar". Em segundo lugar, os desenvolvedores ofereceram a oportunidade de criar com relativa facilidade sua própria implementação de caixas de correio que podem pré-processar mensagens recebidas (por exemplo, de alguma forma, filtrá-las ou distribuí-las de uma maneira especial entre os consumidores). Além disso, cada ator tem sua própria caixa de correio e pode até enviar um "link" a ela em mensagens para outros atores, por exemplo, para que eles possam enviar algum tipo de notificação como resposta de retorno.


Em nosso projeto, para garantir a independência dos grupos de motores entre si, além de garantir a operação "assíncrona" dos motores dentro do grupo, todos os objetos de controle foram divididos em 8 grupos (de acordo com o número de gabinetes de controle), cada um com três trabalhadores. fluxo (uma vez que não mais que três motores podem operar em um grupo por vez).
Também deve-se dizer que o sobjetador (na versão atual 5.5) não contém mecanismos de interação entre processos e redes e deixa essa parte para os desenvolvedores. Os autores fizeram isso deliberadamente , para que o quadro seja mais "fácil". Além disso, os mecanismos de interação de rede “uma vez” existiam nas versões anteriores, mas foram excluídos. No entanto, isso não causa nenhum inconveniente, porque na verdade a interação da rede depende muito das tarefas que estão sendo resolvidas, dos protocolos de troca usados ​​etc. Aqui, uma implementação universal não pode ser ideal para todos os casos.


No nosso caso, para comunicação em rede e interprocessos, usamos um de nossos desenvolvimentos de longa data - a biblioteca libuniset2 . Como resultado, a arquitetura do nosso sistema é mais ou menos assim:


  • libuniset fornece comunicação de rede e interprocessos (com base em sensores)
  • o sobjectizer fornece a criação de um sistema de atores interagindo entre si (no mesmo espaço de endereço) implementando algoritmos de controle.

Então, deixe-me lembrá-lo, temos 62 motores. Cada motor pode ser conectado ao inversor, o suporte correspondente pode receber a coordenada à qual você deve chegar e a velocidade com a qual deve se mover. Além disso, o mecanismo possui as seguintes condições:


  • pronto para ir
  • conectado
  • correndo (girando)
  • acidente
  • conexão (estado transitório)
  • desligamento (estado transitório)

Como resultado, cada “mecanismo” é representado no sistema por um ator que implementa a lógica de transições entre estados, processando eventos de sensores e emitindo comandos de controle. No sobjectizer, os atores são criados facilmente, apenas herdam sua classe da classe base so_5 :: agent_t. Nesse caso, o construtor deve aceitar o chamado contexto: so_5 :: context_t como o primeiro argumento, os argumentos restantes são determinados pela necessidade do desenvolvedor.


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

Porque Como este artigo não é educacional, não fornecerei aqui os textos detalhados das descrições de classes ou métodos. O artigo só queria mostrar como é fácil (em poucas linhas) com a ajuda do sobjectizer tudo isso é implementado. Deixe-me lembrá-lo de que o projeto possui uma excelente documentação detalhada, com vários exemplos diferentes.


E quais são os "estados" desses atores? Do que você está falando?


O uso de estados e transições entre eles para o ACS geralmente é um tópico nativo. Esse "conceito" se encaixa muito bem na manipulação de eventos. No sobjectizer, esse conceito é suportado no nível da API. Em uma classe de ator, os estados são facilmente declarados


 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" }; ... } 

e ainda, para cada estado, o desenvolvedor determina os manipuladores necessários. Freqüentemente, algumas ações são necessárias ao entrar e sair de um estado. Isso também é fornecido no sobjectizer, você define com facilidade os manipuladores para esses eventos ("entrada de estado", "saída de estado"). Considera-se que os desenvolvedores no passado têm uma vasta experiência com ACS-shny ...


Manipuladores de eventos


Manipuladores de eventos, é aqui que a lógica do seu aplicativo é implementada. Como mencionado acima, é feita uma assinatura para uma caixa de correio específica e para um determinado estado do ator. Se um ator não tiver estados declarados explicitamente no código, ele estará implicitamente no estado especial "default_state". Em estados diferentes, você pode definir manipuladores diferentes para os mesmos eventos. Se você não especificou o manipulador de nenhum evento nesta caixa de correio, ele será simplesmente ignorado (ou seja, simplesmente não existirá para o ator).


A sintaxe para definir manipuladores é muito simples. É o suficiente para indicar sua função. Nenhum tipo ou argumento de modelo é necessário. Tudo é deduzido automaticamente da definição da função. Por exemplo:


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

Aqui está um exemplo de inscrição em eventos em uma caixa específica para o estado st_base. Curiosamente, neste exemplo, st_base é o estado base para outros estados e, portanto, essa assinatura será válida para todos os estados que são "herdados" do st_base. Essa abordagem permite que você se livre de "copiar e colar" para determinar os mesmos manipuladores para diferentes estados. Ao mesmo tempo, em um estado específico, você pode substituir o manipulador especificado ou "desativá-lo" (suprimir).


Há outra maneira de definir manipuladores. Esta é uma definição direta de funções lambda. Essa é uma maneira muito conveniente, porque geralmente os manipuladores são funções curtas em algumas ações, enviam algo a alguém ou alternam o estado.


 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(); }); 

A princípio, essa sintaxe parece complicada. Mas em apenas alguns dias de desenvolvimento ativo, você se acostuma e até começa a gostar. Como toda a lógica do trabalho do ator em um estado ou outro pode se encaixar em um código bastante curto e tudo estará diante de seus olhos. Por exemplo, no exemplo mostrado, no estado desconectado (st_disconnecting), a transição para o estado desconectado (st_off.) Ou o estado de proteção (st_protection) ocorre se uma mensagem sobre algum tipo de falha ocorrer. Esse código é muito fácil de ler.


A propósito, para casos simples em que um evento só precisa entrar em algum estado, há uma sintaxe ainda mais curta:


 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); 

Gerência


Como funciona a gestão de toda essa economia? Como mencionado acima, dois controles remotos são fornecidos para o controle direto do movimento dos shtankets. Cada controle remoto possui um monitor, um manipulador (trackball) e uma discagem rápida (além do “computador” oculto no controle remoto no qual tudo gira e empilha todos os tipos de conversores). O sistema possui vários modos de controlar o movimento dos shtankets. Manual e "modo de script". Sobre o "modo de cenário" será discutido mais adiante, e agora um pouco sobre o "modo manual". Nesse modo, o operador seleciona a haste desejada, prepara-a para o movimento (conecta o motor ao inversor), define a marca (posição de destino) da haste e, assim que define a velocidade maior que zero, as hastes começam a se mover. Para definir a velocidade, é utilizado um ajustador físico especial, na forma de um “potenciômetro com um botão”, mas também existe um “ajustador de tela” de velocidade. Quanto mais "virado", mais mais alto vai mais rápido. A velocidade máxima é limitada a 1,5 m / s. Botão de velocidade - um para todos. I.e. No modo manual, todas as hastes conectadas pelo operador se movem na mesma velocidade definida. Embora eles possam se mover em direções diferentes (depende de onde o operador os direcionou). Obviamente, é difícil para uma pessoa acompanhar mais de dois ou três shtankets ao mesmo tempo; portanto, geralmente eles não estão se movendo muito no modo manual. Em duas estações, os operadores podem gerenciar simultaneamente cada um de seus shtankets. Além disso, cada console (operador) possui seu próprio controlador de velocidade.


Do ponto de vista da implementação, o modo manual não contém nenhuma lógica especial. O comando para conectar o mecanismo vem da interface gráfica, é convertido em uma mensagem para o ator correspondente, que trabalha nele. Passando pelos estados “desligado” -> “conectando” -> “conectado”. O mesmo acontece com a definição da posição para o movimento do stunket e a velocidade. Todos esses eventos chegam ao ator na forma de mensagens às quais ele reage. A menos que se note que a interface gráfica e o processo de controle em si são processos diferentes e entre eles existe uma interação entre processos através dos "sensores" usando libuniset2 .


Modo de execução de script (novamente, esses atores?)


De fato, o modo de controle manual é usado principalmente apenas para conviver durante os ensaios ou em casos simples. O principal modo em que o controle está em andamento é o "modo de execução de script" ou, brevemente, o "modo de script". Nesse modo, cada shtank se move para o seu ponto com os parâmetros especificados no script (velocidade e marca de destino). Para o operador, o controle neste modo consiste em dois comandos simples:


  • prepare-se (o grupo certo de motores está conectado)
  • vamos lá (o grupo começa a se mover para as posições de destino definidas para cada).

Todo o cenário é dividido nas chamadas "agendas". Uma agenda é um movimento de um grupo shtanket. I.e. cada agenda inclui um grupo de shtankets, com a velocidade e a marca desejadas para onde você precisa. De fato, o roteiro é dividido em atos, atos são divididos em pinturas, pinturas são divididas em intimações, e intimações já consistem em "objetivos" para shtankets específicos. Mas, do ponto de vista da gerência, essa divisão não é importante, porque está na agenda que parâmetros específicos do movimento são indicados no final.


Para implementar esse regime, o sistema de atores surgiu da melhor maneira possível. Um "script player" foi desenvolvido que cria um grupo de atores especiais e os lança. Desenvolvemos dois tipos de atores: atores-atores, projetados para executar tarefas para um shtanket específico, e um ator-coordenador, que distribui tarefas entre os artistas. Além disso, os atores são criados conforme necessário, se no momento da próxima equipe não estiver livre. O ator coordenador é responsável por criar e manter o pool de atores performáticos. Como resultado, o gerenciamento se parece com isso:


  • A instrução carrega o script
  • "Vira" para a agenda desejada (geralmente apenas segue uma fila).
  • no momento certo, pressiona o botão "preparar", de acordo com o qual um comando (mensagem) é enviado ao ator coordenador para cada formulário incluído na agenda atual com parâmetros de movimento.
  • O coordenador de atores examina seu pool de atores com desempenho livre, pega um grátis (se ele não cria um novo) e dá a ele uma tarefa (número de hastes e parâmetros de movimento).
  • Cada ator-ator que recebeu a tarefa começa a cumprir o comando "prepare-se". I.e. ele conecta o mecanismo e entra no modo de espera do comando "go".
  • quando chegar a hora, o operador dará o comando "vamos lá"
  • a equipe "vai" vem ao coordenador. Ele envia para todos os artistas atualmente ativos e eles começam a "execução".

Vale ressaltar que na agenda existem parâmetros adicionais. Por exemplo, inicie o movimento com um atraso de N segundos ou inicie o movimento somente após um comando de operador especial separado. Portanto, a lista de estados para cada ator é bastante grande: "pronto para executar o próximo comando", "pronto para mover", "movimento atrasado", "aguardando o comando do operador", "movimento", "execução concluída", "mau funcionamento" .


Depois que o shanket alcançou com sucesso (ou não) a marca especificada, o ator-executor notifica o coordenador da tarefa concluída. O coordenador fornece o comando para desligar esse mecanismo (se não estiver mais participando da agenda atual) ou emite novos parâmetros de movimento. Por sua vez, o ator-intérprete recebeu um comando para desligar o mecanismo, o desliga e entra em um estado de espera por novos comandos ou começa a executar um novo comando.


Devido ao fato de o sobjectizer ter uma API bem pensada e conveniente para trabalhar com estados, o código de implementação é bastante conciso. Por exemplo, o atraso no movimento é descrito em uma linha:


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

A função time_limit define um limite de tempo para quanto pode ser gasto em um determinado estado e qual estado deve ser passado após um tempo especificado (st_moving).


Atores de proteção


Obviamente, durante a operação, podem ocorrer problemas de funcionamento. O sistema é necessário para lidar com essas situações. Aqui também havia um lugar para o uso de atores. Considere várias destas proteções:


  • sobre a proteção atual
  • proteção contra falha de medição
  • proteção contra movimento na direção oposta (e isso pode acontecer, se algo estiver errado com o sensor ou o medidor)
  • proteção contra movimento sem comando
  • controle da execução da equipe (controle de que o shtanket começou a se mover)

Você pode ver que todas essas proteções são independentes (auto-suficientes) do ponto de vista da implementação e devem funcionar "em paralelo". I.e. qualquer condição pode funcionar. Ao mesmo tempo, a lógica de verificar as condições de acionamento de cada uma das proteções tem sua própria, às vezes é necessário um atraso (temporizador) para acionar, às vezes o processamento preliminar de várias medições anteriores, etc. Portanto, a implementação de cada tipo de proteção como um pequeno ator separado acabou sendo muito conveniente. Todos esses atores são lançados além (em cooperação) do ator principal que implementa a lógica de controle. Essa abordagem facilita a adição de tipos adicionais de defesas simplesmente adicionando outro ator ao grupo. Ao mesmo tempo, a implementação de um ator assim permanece bastante fácil e compreensível, porque Ele implementa apenas uma função.


Os atores de proteção também têm vários estados. Basicamente, eles ligam (entram no estado "ligado") somente quando o motor está conectado ou a haste está em movimento. Quando as condições de proteção são acionadas, eles publicam uma notificação sobre a proteção que está sendo acionada (com um código de segurança e alguns detalhes para o log), o ator principal já está respondendo a essa notificação, que, se necessário, desliga o mecanismo e passa para o modo de proteção.


Como conclusão ..


... é claro que este artigo não é algum tipo de "descoberta". A abordagem do ator há muito tempo é usada com sucesso em muitos sistemas. Mas, para mim, foi a primeira experiência de usar conscientemente a abordagem de ator para criar algoritmos de sistema de controle em um projeto relativamente pequeno. E a experiência foi bastante bem-sucedida. Espero ter conseguido mostrar que os atores estão muito bem sobrepostos aos algoritmos de controle, pois encontraram um lugar literalmente em todo lugar.


Com a experiência de projetos anteriores, ficou claro que, de uma forma ou de outra, estávamos implementando “algo assim” (estados, mensagens, controle de fluxo etc.), mas essa não era uma abordagem unificada. Usando o sobjectizer, obtivemos uma ferramenta de desenvolvimento leve e concisa que enfrenta muitos problemas. Não é mais necessário (explícito) usar ferramentas de sincronização (mutexes, etc.), não há trabalho explícito com fluxos, nem realizações da máquina de estado. Tudo isso está na estrutura, logicamente interconectado e apresentado como uma API conveniente, além disso, sem perder o controle sobre os detalhes. Então a experiência foi interessante. Para quem ainda duvida, recomendo prestar atenção à abordagem do ator e à estrutura do sobjectador em particular. Ele deixa emoções positivas.


E a abordagem do ator realmente funciona! Especialmente no teatro.

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


All Articles