Iniciando com CC3000: Seu microcontrolador com Wi-Fi – Parte 2

Iniciando com CC3000
Este post faz parte da série Iniciando com CC3000

Na primeira parte dessa série, eu dei uma breve explicação sobre o CC3000 e destaquei algumas de suas principais características. Nesta segunda parte, vou dar um pequeno tutorial introdutório de como fazer um projeto bem simples utilizando o CC3000.

Ferramentas Necessárias

Apesar de existirem diversas Launch Pads da Texas Instruments, eu optei por utilizar a tradicional LaunchPad da Value Line do MSP430. Além de ser a LaunchPad menos custosa, serve para provar o conceito que é possível implementar uma comunicação Wi-Fi com placas com pouquíssimos recursos e microcontroladores de baixo custo. 

Como hardware, será necessário para implementar este projeto:

  • Uma LaunchPad com um MSP430G2553;
  • Um Boosterpack do CC3000;
  • Um roteador Wi-Fi;
  • Um computador com conexão Wi-FI.

Como software será necessário:

ATENÇÃO: Abaixo você pode fazer download de um .zip contendo o projeto do Code Compoder 5, o Hercules SETUP utility, o Java Smart Config desktop client e o patch 1.11.1 do CC3000.

Pack_Tutorial_CC3000

Descrição do Projeto

Conceitualmente, o projeto que vamos implementar é basicamente um termômetro Wi-Fi, com apenas um botão de interface, e nada mais. Basicamente, o termômetro vai se conectar à uma rede Wi-Fi e começar a fazer broadcast do valor da temperatura medido através de um Socket UDP em uma porta específica (no caso foi escolhida a porta 4444). O computador que estiver interessado na informação do sensor deve monitorar pacotes UDP na porta em questão. De forma a poder passar as informações para o sensor de que rede ele deve se conectar, deve-se segurar o botão por pelo menos 5 segundos para o mesmo começar o procedimento do SmartConfig. 

thermometer_project

Tendo definido o projeto conceitualmente, podemos, então, organizar como é lógica interna que rege o termômetro. Ela pode ser representada por uma máquina de estados simplificada, como abaixo.

FSM_WifiThermometer

Após alimentado, o termômetro realiza todas as rotinas de inicialização internas e tenta se conectar à alguma rede pré-configurada. Caso nenhuma rede ainda tenha sido configurada, o mesmo vai ficar tentando se conectar, porém não vai conseguir. Depois de conseguir se conectar à rede, ele abre o Socket UDP com o IP xxx.xxx.xxx.255, que normalmente é utilizado para broadcast. Com o Socket aberto, ele começa a enviar o valor da temperatura amostrado a cada 5 segundos. Depois da inicialização, o procedimento do SmartConfig pode ser utilizado em qualquer um dos estados. Depois de realizado o SmartConfig o termômetro deve tentar se conectar na rede configurada.

Descrição do Firmware

Para não lotar o projeto de arquivos de código fonte eu já estou distribuindo o projeto com as bibliotecas CC3000HostDriver e CC3000 Spi pré-compiladas. Os códigos das bibliotecas são distribuídos pela Texas. Se você tiver interesse em portar o código para outra plataforma, vai ter modificar apenas o CC3000 Spi, porém obviamente recompilar todos os códigos.

O código está razoavelmente comentado, porém irei dar uma breve descrição sobre a estrutura do mesmo. Eu tenho o costume de separar o acesso específico de hardware em arquivos separados, no caso identificados por HAL. Isto simplifica o processo de portabilidade para outras plataformas. Não vou entrar em detalhes sobre a configuração do hardware do MSP430G2553. Os três periféricos que merecerem algum destaque no caso são a SPI, o ADC e o Timer A1. O SPI foi inicializado com as rotinas da biblioteca CC3000. O Timer A1 foi configurado para gerar uma interrupção a cada ~500ms e o ADC foi configurado para fazer a leitura do sensor de temperatura interno do MSP430.

Na função main (corpo do programa) incialmente é realizada a inicialização do Watchdog (muito importante no caso do MSP430), é assinalado qual será o callback da interrupção do Timer A, configura todo o hardware do MSP430, roda a função de inicialização do servidor e finalmente habilita as interrupções. No loop principal do programa é processada a função UServer_asyncProcess, que basicamente cuida dos processos assíncronos (ao timer) da comunicação do servidor.

void main (void)
{
	/*
	 * Inicializa o WatchDog Timer
	 */
	HAL_wdInit();

	/*
	 * Assinala qual será a função de callback do ISR to Timer
	 */
	timer0.cb = Timer0_cb;

	/*
	 * Configura o Hardware
	 */
	HAL_hardwareConfig();

	/*
	 * Inicializar o Servidor
	 */
	UServer_init();

	/*
	 * Habilita Interrupções Globais
	 */
	HAL_enableInterrupts();
    for(;;)
    {
    	/*
    	 * Processa a rotina assícrona do servidor
    	 */
    	UServer_asyncProcess();
    }
}

Dentro do callback do ISR do timer, temos basicamente um monitor do botão da placa, que gera um request para o processo do SmartConfig se o mesmo for pressionado por mais de 5 segundos. Fora isso temos a rotina UServer_syncProcess, que cuida dos processos síncronos (ao timer) do CC3000. 

static uint8_t Timer0_cb(void)
{
	/*
	 * Aqui é verificado se o botão está apertado.
	 * Se estiver, é inicializada uma contagem decrescente.
	 * Se o botão for segurado por pelo menos 5 segundos,
	 * é requisitado ao servidor realizar o SmartConfig.
	 * A rotina só começa o SmartConfig quando o botão é solto.
	 */
	static uint8_t swPressedCntr = 10;
	if(sw.read())
	{
		if(!swPressedCntr)
		{
			UServer_requestSmartConfig();
		}
		swPressedCntr = 10;
	}
	else
	{
		if(swPressedCntr)
			swPressedCntr--;
	}
	/*
	 * Processa a rotinas sícrona do servidor a cada 500ms
	 */
	UServer_syncProcess();

	return(0);
}

Com a função main e o callback to timer, temos basicamente o esqueleto de funcionamento do programa. Agora entramos nas rotinas específicas ao servidor, que lidam com o CC3000. Dentro da rotina de inicialização do servidor acontece a configuração incial do módulo CC3000 e do CC3000HostDriver. Primeiro sempre temos que chamar a função wlan_init, que passa para o CC3000HostDriver qual será a função de callback de eventos assíncronos do CC3000, e algumas rotinas de acesso ao hardware do seu microcontrolador. O nome das rotinas é bem autoexplicativo, porém as funcionalidades que precisam ser passadas são para ler o pino de interrupção do CC3000, habilitar e desabilitar a interrupção referente a este mesmo pino e uma função para controlar o pino que habilita e desabilita o CC3000.É importante ressaltar que, apesar de essas rotinas estarem implementadas no HAL, eu não nomeei as mesmas corretamente de forma a manter compatibilidade com os exemplos fornecidos pela Texas Instruments (que utilizam os mesmos nomes para tais funções).

Depois de configurado o CC3000HostDriver, temos que chamar a rotina wlan_start, que propriamente implementa toda a  pré-inicialização do CC3000. Posteriormente temos que chamar a função wlan_set_event_mask, que configura quais serão os eventos do CC3000 que serão “mascarados’ ou seja, ou seja, não irão gerar eventos para a nossa aplicação. Para finalizar é feita a configuração da rede com a função netapp_dhcp. Como vamos utilizar IP dinâmico, temos que passar o IP, Default Gateway, endereço DNS como zero. O único que é especificado no caso é a máscara de rede configurada como 255.255.255.0 (filtrar pelos bytes 3 bytes mais significativos).

void UServer_init(void)
{
	/*
	 * Inicializa as flags de controle de estado
	 */
	g_CC3000DHCP = 0;
	g_CC3000Connected = 0;
	g_Socket = 0;
	g_SmartConfigFinished=0;

	/*
	 * Inicializa o Driver do CC3000
	 */
	wlan_init( 	UServer_cc3000UsynchCallback,
				0,
				0,
				0,
				ReadWlanInterruptPin,
				WlanInterruptEnable,
				WlanInterruptDisable,
				WriteWlanPin);

	/*
	 * Inicializa o CC3000
	 */
	wlan_start(0);

	/*
	 * Configura quais eventos não serão necessários
	 */
	wlan_set_event_mask(HCI_EVNT_WLAN_KEEPALIVE|HCI_EVNT_WLAN_UNSOL_INIT|HCI_EVNT_WLAN_ASYNC_PING_REPORT);

	/*
	 * Configura IP, Subnet Mask e Gateway
	 * Para IP Dinâmico devemos configurar Gateway e IP como 0.0.0.0
	 */
	netapp_dhcp((unsigned long *)g_IP_Addr, (unsigned long *)g_SubnetMask, (unsigned long *)g_IP_DefaultGWAddr, (unsigned long *)g_DNS);
}

 A biblioteca CC3000HostDriver precisa que a aplicação do usuário forneça um callback de eventos do CC3000, no caso implementado na função UServer_cc3000USynchCallback. Quando um evento (que não mascaramos durante a inicialização) ocorrer, esta função vai ser chamada com três argumentos: o tipo do evento, um ponteiro para os dados relacionados ao evento e o comprimento destes dados. No caso implementamos um switch/case básico para lidar com os diferentes tipos de eventos.

Quando o evento HCI_EVNT_WLAN_SIMPLE_CONFIG_DONE é gerado, quer dizer que o procedimento do SmartConfig foi finalizado, logo mudamos o valor da flag g_SmartConfigFinished para 1.

Quando o evento HCI_EVNT_USOL_CONNECT é gerado, quer dizer que o CC3000 se conectou à alguma rede. Logo, neste caso modificamos a flag g_CC3000Connected para 1.

Quando o evento HCI_EVNT_USOL_DISCONNECT é gerado, quer dizer que o CC3000 se desconectou da conexão com a qual ele estava conectado. Este evento pode ser gerado tanto por uma desconexão voluntária, tanto por uma desconexão involuntária (e.g. perda de sinal). Neste caso modificamos as flag g_CC3000Connected e g_CC3000DHCP para 0.

E para finalizar, temos o evento HCI_EVNT_USOL_DHCP, que é gerado quando o IP dinãmico é assinalado ao CC3000 com sucesso. Neste caso neste caso modificamos a flag g_CC3000DHCP para 1 e armazenamos o IP em questão na variável g_IP_Addr.

static void UServer_cc3000UsynchCallback(long lEventType, char * data, unsigned char length)
{
	switch(lEventType)
	{
	/*
	 * Evento gerado quando o SmartConfig é finalizado
	 */
	case HCI_EVNT_WLAN_ASYNC_SIMPLE_CONFIG_DONE:
		g_SmartConfigFinished = 1;
		break;

	/*
	 * Evento gerado quando o CC3000 consegue se conectar à uma WAN
	 */
	case HCI_EVNT_WLAN_UNSOL_CONNECT:
		g_CC3000Connected = 1;
		break;

	/*
	 * Evento gerado quando o CC3000 é desconectado de uma WAN
	 */
	case HCI_EVNT_WLAN_UNSOL_DISCONNECT:
        g_CC3000Connected = 0;
        g_CC3000DHCP      = 0;
		break;

	/*
	 * Evento gerado quando um IP dinâmico é gerado com sucesso.
	 */
	case HCI_EVNT_WLAN_UNSOL_DHCP:
		/*
		 * Seta a flag informando que o DHCP foi finalizado
		 */
		g_CC3000DHCP = 1;
		/*
		 * Salva o número do IP
		 */
		g_IP_Addr[3] = data[0];
		g_IP_Addr[2] = data[1];
		g_IP_Addr[1] = data[2];
		g_IP_Addr[0] = data[3];
		break;
	default:
		break;
	}
}

A função UServer_syncProcess implementa as chamadas síncronas ao timer do servidor. No caso ela implementa um contador para gerar um pedido de envio de temperatura.

void UServer_syncProcess(void)
{
	static uint8_t sendTemperatureCntr = TEMPERATURE_BROADCAST_PERIOD;
	sendTemperatureCntr--;
	if(!sendTemperatureCntr)
	{
		sendTemperatureCntr = TEMPERATURE_BROADCAST_PERIOD;
		UServer_requestSendTemperature();
	}
}

A função Userver_startSmartConfig é utilizada para inicializar o procedimento do SmartConfig. Nela basicamente passamos qual será o prefixo utilizado nas comunicações do SmartConfig (sempre TTT) e chamamos a função que inicializar o processo propriamente dito.

static void UServer_startSmartConfig(void)
{

	/*
	 * Informa o prefixo utilizado pelo SmartConfig
	 */
	wlan_smart_config_set_prefix((char*)g_CC3000_prefix);

	/*
	 * Inicializa o SmartConfig
	 */
	wlan_smart_config_start(0);
}

Para finalizar, a função UServer_asyncProcess implementa a lógica básica do nosso servidor a ser processada ciclicamente, incluindo a máquina de estados projetada. Basicamente inicialmente executamos a função hci_unsolicited_event_handler para eventos do CC3000 pendentes. Depois verificamos se há alguma solicitação de efetuar o procedimento do SmartConfig feita pelo usuário. Se sim, verifica se o procedimento já está sendo executado e se estiver ignora a requisição. Caso não esteja sendo executado, configura a politica de conexão para não se auto-conectar por hora, deleta as redes armazenadas para garantir que o CC3000 sempre ira se conectar na rede desejada (no caso esse passo é dependente da aplicação) e muda o estado do sistema para pre-inicialização do SmartConfig. Depois do processamento das requisições do SmartConfig, temos a máquina de estados do sistema.

No estado USERVER_CONNECTING o servidor está tentando se conectar a alguma rede, logo as variáveis g_CC3000DHCP e g_CC3000Connected são monitoradas para avaliar quando isto acontecer. No caso de uma conexão bem sucedida,  informamos o dispositivo que está realizando o SmartConfig que houve sucesso na conexão através da função mDNSAdvertiser. A função recebe como argumento algum string que queira ser informado, e no caso é o nome do nosso dispositivo “Termometro Wi-Fi”. Em seguida, seguimos nossa máquina de estados descrita anteriormente e mudamos para o estado USERVER_OPEN_SOCKET.

No estado USERVER_PRE_SMART_CONFIG o servidor espera o CC3000 se desconectar de alguma possível rede que ele estivesse conectado. Quando isto acontece, a função UServer_startSmartConfig é chamada e a máquina de estados muda para o estado USERVER_SMART_CONFIG.

No estado USERVER_SMART_CONFIG o servidor monitora a variável g_SmartConfigFinished para avaliar quando o procedimento do SmartConfig finalizou. Quando isto acontece, a função wlan_ioctl_set_connection_policy é chamada configurando o CC3000 para utilizar uma rede armazenada e se conectar na útima rede previamente conectada, se disponível. Depois desta configuração o CC3000 é reinicializado, chamando a função wlan_stop, fazendo um delay de alguns microsegundos e reiniciando o o CC3000 chamando a função wlan_start. No caso foi utilizado um delay gastando ciclos do processador. Esta prática não é recomendada pois basicamente bloqueia o processamento do seu sistema. Além disso foi utilizado um valor muito mair de tempo do que o necessário. Isto foi feito para simplificar a visualização do led na placa piscando com o CC3000 é resetado. Depois disso, novamente configuramos a máscara de eventos que não queremos ser informados com a função wlan_set_event_mask. Para finalizar o estado do servidor é modificado para USERVER_CONNECTING, esperando o CC3000 se conectar na rede configurada.

No estado USERVER_OPEN_SOCKET o servidor abre um Socket UDP chamando a função socket, e armazena um identificador do mesmo na variável g_Socket. Caso a função socket retorne um valor negativo, quer dizer que não houve sucesso na abertura do Socket por um erro de conexão. Caso o Socket tenha sido aberto com sucesso, o servidor muda para o estado USERVER_CONNECTED. Por redundância, é verificado se o servidor continua conectado, e caso negativo o mesmo volta para o estado USERVER_CONNECTING.

No estado USERVER_CONNECTED o servidor fica ciclicamente enviando um string com o valor da temperatura interna do MSP430 lida. Para tal é utilizada um estrutura de dados do tipo sockaddr, que armazena no campo sa_data a porta e o IP a serem endereçados. No caso estamos configurando com a porta 4444 e o IP xxx.xxx.xxx.255, que é o IP de broadcast da maior parte das redes. Os campos com xxx são configurados de acordo com o IP assinalado ao CC3000 pelo DHCP. Com a função sendto, a mensagem é enviada pelo Socket previamente aberto. Caso a função retorne um valor negativo, significa um erro de conexão do Socket e o mesmo tem que ser reaberto, logo  fechamos o Socket e mudamos par ao estado USERVER_OPEN_SOCKET.

void UServer_asyncProcess(void)
{
	static userver_state_e uServer_state = USERVER_CONNECTING;
	sockaddr socketAddr;
	char sendData[10];
	int32_t temperature;
	long dataLenght;

	/*
	 * Gerencia eventos do CC3000 pendentes
	 */
	hci_unsolicited_event_handler();

	/*
	 * Verifica se o SmartConfig foi solicitado
	 */
	HAL_disableInterrupts();
	if(g_RequestSmartConfig)
	{
		g_RequestSmartConfig = 0;
		HAL_enableInterrupts();

		/*
		 * Verifica se o SmartConfig já não está sendo executado
		 */
		if(uServer_state != USERVER_SMART_CONFIG && uServer_state != USERVER_PRE_SMART_CONFIG)
		{
			/*
			 * Coloca o valor das flags como zero.
			 */
		    g_SmartConfigFinished = 0;
		    g_CC3000DHCP = 0;

		    /*
		     * Configura política de conexão para não se auto-conectar
		     */
			wlan_ioctl_set_connection_policy(0, 0, 0);

		    /*
		     * Deleta todos os profiles armazenados para garantir que o servidor sempre
		     * irá se conectar na rede especificada
		     */
			wlan_ioctl_del_profile(255);

			uServer_state = USERVER_PRE_SMART_CONFIG;
		}
	}
	HAL_enableInterrupts();

	switch(uServer_state)
	{
	case USERVER_CONNECTING:
		/*
		 * Espera até o CC3000 se conectar e o DHCP ser finalizado
		 */
		if(g_CC3000DHCP && g_CC3000Connected)
		{
			mdnsAdvertiser(1,g_DeviceName,strlen(g_DeviceName));
			mdnsAdvertiser(1,g_DeviceName,strlen(g_DeviceName));
			mdnsAdvertiser(1,g_DeviceName,strlen(g_DeviceName));

			uServer_state = USERVER_OPEN_SOCKET;
		}
		break;
	case USERVER_PRE_SMART_CONFIG:
		/*
		 * Espera até o C3000 ser desconectado e então inicializa
		 * o SmartConfig.
		 */
		if(!g_CC3000Connected)
		{
			UServer_startSmartConfig();
			uServer_state = USERVER_SMART_CONFIG;
		}
		break;
	case USERVER_SMART_CONFIG:
		/*
		 * Espera até o SmartConfig ser finalizado.
		 */
		if(g_SmartConfigFinished == 1)
		{
			/*
			 * Configura a politica de conexão do CC3000 para utilizar
			 * um profile armazenado e se conectar na última rede conectada,
			 * se disponível
			 */
		    wlan_ioctl_set_connection_policy(0, 1, 1);

		    /*
		     * Reseta o CC3000
		     */
		    wlan_stop();


		    HAL_delayCycles(6000000);

		    /*
		     *  Inicializa o CC3000
		     */
		    wlan_start(0);

		    /*
		     *  Desabilita os eventos que não estamos interessados
		     */
		    wlan_set_event_mask(HCI_EVNT_WLAN_KEEPALIVE|HCI_EVNT_WLAN_UNSOL_INIT|HCI_EVNT_WLAN_ASYNC_PING_REPORT);


		    uServer_state = USERVER_CONNECTING;
		}
		break;
	case USERVER_OPEN_SOCKET:
		/*
		 * Abre um socket UDP
		 */
		g_Socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if(g_Socket > -1)
		{
			uServer_state = USERVER_CONNECTED;
		}
		/*
		 * Verifica se o CC3000 ainda está conectado na WAN
		 */
		if(!g_CC3000DHCP || !g_CC3000Connected)
		{
			uServer_state = USERVER_CONNECTING;
			break;
		}
		break;
	case USERVER_CONNECTED:
		/*
		 * Verifica se o CC3000 ainda está conectado na WAN
		 */
		if(!g_CC3000DHCP || !g_CC3000Connected)
		{
			uServer_state = USERVER_CONNECTING;
			break;
		}
		/*
		 * Se for requisitado enviar a temperatura, é realizada a
		 * leitura. Depois transformamos o valor em um string e
		 * enviamos pelo socket UDP.
		 */
		if(g_RequestSendTemperature)
		{
			int ret;
			g_RequestSendTemperature = 0;
			temperature = HAL_readTemperatureCelcius();
			dataLenght = itoa(temperature, sendData);
			sendData[dataLenght-1] = '\n';
			/*
			 * Configura o socket para a porta 4444 e para realizar broadcast
			 */
			socketAddr.sa_family = AF_INET;
			socketAddr.sa_data[0] = 0x11;
			socketAddr.sa_data[1] = 0x5c;
			socketAddr.sa_data[2] = g_IP_Addr[0];
			socketAddr.sa_data[3] = g_IP_Addr[1];
			socketAddr.sa_data[4] = g_IP_Addr[2];
			socketAddr.sa_data[5] = 0xFF;
			ret = sendto(g_Socket, sendData, dataLenght, 0, &socketAddr, sizeof(sockaddr));
			if(ret < 0)
			{
				closesocket(g_Socket);
				uServer_state = USERVER_OPEN_SOCKET;
			}
		}
		break;
	default: uServer_state = USERVER_CONNECTING;
		break;
	}
}

Colocando para Funcionar

Eu recomendo fortemente atualizar a versão do firmware do CC3000. Para tal, conecte o CC3000 na Launchpad e entre na pasta PatchProgrammer\MSP flashing tools\MSP430Flasher_1.1.3.

Clique em download_cc3000_patch_programmer_driver.bat e espere o procedimento terminar. Clique em download_cc3000_patch_programmer_firmware.bat e espere o procedimento terminar.

Se você não tem prática em como criar um projeto no Code Composer Studio, você pode utilizar o projeto pronto, distribuído neste tópico. Basta selecionar a pasta workspace quando for pedido o workspace, e o projeto já estará pronto para uso. Caso ainda não o tenha feito, conecte o booster pack do CC3000 na LaunchPad e conecte a mesma no computador via porta USB. No CCS clique em debug e espere o firmware ser gravado na placa. Quando o procedimento estiver finalizado, clique em run

No pacote de arquivos, abra a pasta Java Smart Config desktop client\net.betaengine.smartconfig-2013-10-03. Dentro dela clique em repackage.jar. Depois de finalizado clique em net.betaengine.smartconfig-ui.jar. Vai abrir uma janela igual à da figura abaixo.

CC3000SmartConfigSetup

Em tese o Network Name e o Gateway Adress já devem vir preenchidos. Caso não seja o caso, preencha com os dados de sua rede. Preencha também o Password (se houver) e mude o Device Name para Termometro Wi-Fi. Aperte Send e espere o procedimento do SmartConfig terminar. Se tudo der certo, após o procedimento, o CC3000 irá se conectar à rede configurada, abrir o Socket e começar a fazer broadcast da temperatura. 

Para visualizar os dados enviados, abra o hercules_3-2-4.exe. Dentro do programa, navegue para a aba UDP, preencha o campo Local port com 4444 e clique em Listen. Como na imagem abaixo, você deve começar a receber o valor da temperatura enviado pelo MSP430 (o valor muito provavelmente vai apresentar um offset de temperatura, que deve ser calibrado).

Conclusão

Aqui foi demonstrada uma aplicação simplória utilizando o CC3000, porém serve como uma boa referência para começar um projeto. Na minha opinião a ferramenta desenvolvida pela Texas é bastante poderosa pois permite que um microcontrolador consiga se conectar em uma rede Wi-Fi sem muito esforço e custo: a gama de aplicações é muito grande.

Iniciando com CC3000

Iniciando com CC3000: Seu microcontrolador com Wi-Fi – Parte 1
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
2 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Edgar Dos Reis
Edgar Dos Reis
20/07/2014 10:10

Olá,
Eu tenho um MSP430F5529, sabe me dizer se é possível fazer este projeto
Com esta placa?

Obrigado
Edgar dos Reis

Home » Comunicação » Iniciando com CC3000: Seu microcontrolador com Wi-Fi – Parte 2

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: