Funcionamento do Sistema multi-agente

Autor: felipe 115 visualizações

Sistema multi-agente

São quatro classes que compõem o Core do sistema multi-agente: Input, Data-Manager, Analysis e Utils. A classe Core é a principal responsável pelo orquestramento das demais classes agentes.

Uma das vantagens dessa utilização é conseguir fazer com que dispositivos que funcionem em sistemas operacionais diferentes se comuniquem utilizando socket. Assim, é possível que um dispositivo como o BSN que funciona no Android, envie as informações coletadas pelo sensor para um computador rodando Windows. Permitindo uma interoperabilidade que não seria possível sem o sistema multi-agente. Viabiliza também que de posse das informações coletadas pelo Core, o mesmo pode delegar instruções para as demais classes orquestradas.

Como base para comunicação foi usada a API (Application Programming Interface) Ruffles que é uma biblioteca UDP (User Datagram Protocol) totalmente gerenciada e feita para que tenha alto desempenho com latência baixa. A primeira etapa é definir o UUID (Universally Unique Identifier) da aplicação, deve ser igual no cliente que são os agentes e no servidor que é o Core, serve para a identificação na rede, a porta 5557 foi a definida para comunicação.

				
					readonly string uuid =
" b6af0cc1 -4 e8d -43 cd - b771 -321125895433 " ;
readonly int port = 5557;				
			

Depois na classe orquestrada devemos iniciar o socket, configurar os canais para comunicação e iniciar as co-rotinas.

				
					void Start()
    {
        // Initializes the client
        socket = new RuffleSocket(new Ruffles.Configuration.SocketConfig()
        {
            ChallengeDifficulty = 20, // Difficulty 20 is fairly hard
            ChannelTypes = new[]
            {
                ChannelType.Reliable,
                ChannelType.ReliableSequenced,
                ChannelType.Unreliable,
                ChannelType.UnreliableOrdered,
            },
            AllowBroadcasts = true,
            AllowUnconnectedMessages = true,
        });
        socket.Start();

        StartCoroutine(Discover());
        StartCoroutine(SendBroadcast());
    }				
			

O próximo passo é mandar na rede um broadcast com a porta e UUID determinados.

				
					IEnumerator SendBroadcast()
    {
        while (IsDiscovering)
        {
            ArraySegment<byte> data = new ArraySegment<byte>(Encoding.UTF8.
            GetBytes(uuid), 0, Encoding.UTF8.GetBytes(uuid).Length);
            socket.SendBroadcast(data, port);
            yield return new WaitForSeconds(2);
        }
    } 				
			

Posteriormente ao broadcast é o discover, ou seja, a conexão só é feita com sucesso se for recebido o mesmo UUID enviado. Enquanto não fechar a conexão nas duas pontas o método Recycle é chamado, para não ter estouro de memória, ou seja, não vai abrindo conexões de forma indeterminada. Quando concluir a conexão, ou seja, quando o UUID é encontrado, então define isDiscovering como false e isConnected como true.

				
					IEnumerator Discover()
    {
        while (IsDiscovering)
        {
            yield return new WaitForSeconds(1);
            @event = socket.Poll();
            if (@event.Type == NetworkEventType.Nothing)
            {
                @event.Recycle();
                continue;
            }

            if (@event.Type == NetworkEventType.UnconnectedData)
            {
                string message = Encoding.UTF8.GetString(@event.
                Data.Array, @event.Data.Offset, @event.Data.Count);
                if (message.Equals(uuid))
                {
                    isDiscovering = false;
                    serverConnection = socket.Connect(@event.EndPoint);
                    isConnected = true;
                    StartCoroutine(SendData());

                    yield return new WaitForSeconds(5);
                    //Connection ok
                    break;
                }
            }
        }
    }				
			

Para o envio de mensagem ao Core a co-rotina SendData só inicia o envio após a variável isConnected for true, note que o envio é configurado com o tempo de espera em 1/30f, ou seja, 30 quadros por segundo.
Existem vários tipos de canais na API Ruffles: 0 – Reliable que garante a entrega mas não a ordem; 1 – ReliableSequenced consegue garantir a entrega e a ordem; 2 – Unreliable não garante a entrega nem a ordem, pacotes duplicados são descartados; 3 – UnreliableOrdered a entrega não é garantida, mas a ordem sim, pacotes duplicados e antigos são descartados. A mensagem é enviada em forma de ArraySegment pelo método SendMessageToCore, passando como parâmetro a mensagem, o canal escolhido foi o ReliableSequenced no qual o envio será feito com garantia de entrega e ordem.

				
					  public void SendMessageToCore(string message, byte channel = 1)
    {
        ArraySegment<byte> data =
            new ArraySegment<byte>(Encoding.UTF8.
            GetBytes(message), 0, Encoding.UTF8.GetBytes(message).Length);
        serverConnection?.Send(data, channel, false, (ulong)messagesSent);
        messagesSent++;
    }

    IEnumerator SendData()
    {
        while (isConnected)
        {
            SendQuaternionList();
            yield return new WaitForSeconds(1 / 30f);
        }
    }				
			

O módulo Core pode enviar mensagem para o módulo orquestrado, essa mensagem é recebida pelo método OnMessageReceived.

				
					 void OnMessageReceived(string messageCore)
    {
       // do something
    }				
			

Com o UUID e portas já definidas, devemos iniciar também o socket no módulo do Core.

				
					 private void StartServer()
    {
        IsConnected = false;
        socket = new RuffleSocket(new Ruffles.Configuration.SocketConfig()
        {
            ChallengeDifficulty = 20, // Difficulty 20 is fairly hard
            ChannelTypes = new ChannelType[]
             {
                 ChannelType.ReliableSequenced,
             },
            AllowBroadcasts = true,
            AllowUnconnectedMessages = true,
            DualListenPort = port
        });
        socket.Start();
    }
    }				
			

No método Update o Core faz diversas tratativas, se não recebeu nenhuma mensagem chama o método Recycle para não ter estouro de pilha, se recebeu o broadcast com o UUID correspondente ele responde a classe orquestrada, se fechou a conexão nas duas pontas coloca a variável IsConnected como true, e se recebeu dados da classe orquestrada encaminha para o método OnMessageReceived.

				
					 void Update()
    {
        @event = socket.Poll();
        if (@event.Type == NetworkEventType.Nothing) //nothing received
        {
            @event.Recycle();
            return;
        }
        else if
            (@event.Type == NetworkEventType.BroadcastData) //received broadcast, if the uuid matches, respond to client
        {
            string message = Encoding.UTF8.GetString(@event.Data.Array, @event.Data.Offset, @event.Data.Count);
            if (message.Equals(uuid))
            {
                socket.SendUnconnected(@event.Data, @event.EndPoint);
            }

            Debug.Log("Broadcast: " + @event.EndPoint.AddressFamily.ToString() + " send " +
                      System.Text.Encoding.
                      UTF8.GetString(@event.Data.Array));
            // We got a broadcast. Reply to them with the same token they used
        }

        else if (@event.Type == NetworkEventType.Connect) //received connection request
        {
            clientConnection = @event.Connection;
            Debug.Log(@event.Connection.State + "to " + @event.EndPoint.AddressFamily.ToString());
            IsConnected = true;
        }
        else if (@event.Type == NetworkEventType.Data) //the client's commands go here
        {
            string message = Encoding.UTF8.GetString(@event.Data.Array, @event.Data.Offset, @event.Data.Count);
            OnMessageReceived(message);
        }

        // Recycle the event
        @event.Recycle();
    }				
			

O módulo Core, pode enviar mensagens para os agentes utilizando o método SendMessageToAgent.

				
					\textbf{\emph{Core}}},captionpos=b,label={lst:sa9_label}]
 public void SendMessageToAgent(string message, byte channel = 1)
    {
        ArraySegment<byte> data =
            new ArraySegment<byte>(Encoding.UTF8.
            GetBytes(message), 0, Encoding.UTF8.GetBytes(message).Length);

        clientConnection?.Send(data, channel, false, (ulong)messagesSent);
        messagesSent++;
    }				
			

Existe uma particularidade nos agentes, além de trocar mensagens com o Core utilizando o método SendMessageToCore, foi criado um DTO (Data Transfer Object) para que se possa enviar dados dos sensores.

O DTO envia o quaternion com o respectivo nome da rotação.

				
					[System.Serializable]
public class RotationDTO
{
    public string name;
    public QuaternionDTO rotation;

    public RotationDTO(string name, QuaternionDTO rotation)
    {
        this.name = name;
        this.rotation = rotation;
    }
}

[System.Serializable]
public class QuaternionDTO
{
    public float x, y, z, w;

    public QuaternionDTO(float x, float y, float z, float w)
    {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
    }
}				
			

Dessa forma, podemos receber os dados da BSN mediante uso do sistema multi-agente, basta utilizar o método SendQuaternionList para enviar e QuaternionListFromAgent para receber os dados, o quaternion já é atribuído ao GameObject da rotação, pois o nome é enviado junto ao quaternion.

				
					 public void SendQuaternionList()
    {
        string a = "Quaternions/{\"rotations\":[";
        List<RotationDTO> rt = new List<RotationDTO>();
        var rotatableObjects = FindObjectsOfType<RotatableObject>().Where(ro => ro.bsnDevice != null);

        foreach (RotatableObject ro in rotatableObjects)
        {
            var transform1 = ro.transform;
            var rotation = transform1.rotation;
            var quaternion = new QuaternionDTO(rotation.x, rotation.y,
                rotation.z, rotation.w);
            rt.Add(new RotationDTO(ro.SimplifiedName, quaternion));
        }

        rt.ForEach(rotation => { a += JsonUtility.ToJson(rotation) + ","; });

        a = a.TrimEnd(',') + "]}";
        SendMessageToCore(a, 1);
    }

    public void QuaternionListFromAgent(string message)
    {
        if (message.StartsWith("Quaternions/"))
        {
            string json = message.Substring(12);
            JsonUtility.FromJsonOverwrite(json, this);

            foreach (RotationDTO rdto in rotations)
            {
                Quaternion q = new Quaternion(rdto.rotation.x, rdto.rotation.y, rdto.rotation.z, rdto.rotation.w);
                GameObject.Find(rdto.name).transform.rotation = q;
            }
        }
    }   				
			
Nesta página