0

ArduFarmBot: Part 2 – “Estação Remota” – IoT

Este post é uma continuação do ArduFarmBot: Controlando um tomateiro com a ajuda de um Arduino e Internet das coisas (IoT)

Na primeira parte do projeto, criamos uma estação local de controle e captura de informações, tais como temperatura, humidade relativa do ar, luminosidade e humidade do solo. Com base nesses dados, o ArduFarmBot decidia automaticamente a quantidade certa (e quando) o tomateiro deveria receber calor e água. A estação local de controle desenvolvida na Parte 1, também permitia a intervenção manual de um operador a fim de controlar tanto a bomba de água quanto a lâmpada elétrica. Nesta segunda parte, vamos implementar uma abordagem “IoT” onde a”intervenção manual” também poderá ser feita remotamente via Internet. O diagrama de blocos mostra como faremos isso:

Note-se que os dados capturados serão enviados a um “serviço de armazenamento na nuvem” (no nosso caso Thinkspeak.com). Além disso, um website dedicado, o “Remote Control Page” mostrado no diagrama,  irá monitorar e exibir esses dados em tempo quase real. Esta página também permitirá a ativação remota da bomba e  da lâmpada

Lista de materiais

Arduino Nano
Temperature and Humidity Sensor DHT22 ou DHT11
Luminosity Sensor – AD-018 Photo resistor module. ou equivalente
2X Soil Moisture Sensor –  (Opcional, pode ser do tipo “caseiro -DIY”)
LCD I2C 20X4
LEDs (1X)
Esp8266 Serial Wifi Wireless Transceiver Module Esp-01
Active Buzzer – Ky-12 ou equivalente
2 X 5v Relay Module
Jump wires
Resistor 10KOhms
Resistor 2.2K Ohms
Resistor 1.0K Ohms
Resistor 220 Ohms
Arduino Nano Shield (“Funduino”)
Membrane keyboard (4 Keys)
Caixa Plástica

Completando o Hardware

Para conectar-se o ArduFarmBot à Internet, utilizaremos o ESP8266, um módulo simples, barato e fácil de programar para os projectos envolvendo a Internet das coisas (IoT). A partir da estação local desenvolvida na Parte 1, o único HW adicional necessário é o próprio ESP8266. O diagrama de blocos abaixo, mostra todas as conexões com os pinos do Arduino e dos componentes principais.

O único cuidado que deve ter é relacionado com o nível de tensão do pino RX do ESP8266, pois ele funciona com 3.3V. Assim, o Pin Rx não deve ser conectado diretamente ao pino Tx do Nano (D3). Um nivelador de tensão deve ser usado. No nosso caso, vamos construir um divisor de tensão para ser usado como um “conversor de nível de tensão”.

O esquema eléctrico abaixo, mostra com mais detalhes como se conectar o ESP8266.

Note que estamos usando o ESP8266 conectado aos pinos 2 (Tx) e 3 (Rx) do Nano, utilizando-se da biblioteca “SoftSerial”. Se quiser “livrar” os pinos digitais, poderá alternadamente usar os pinos 0 e 1 do Nano (Serial por HW). Apenas lembre-se que deve desligá-los ao enviar o código para Nano.

NOTA: Se deseja se conectar o “Buzzer”, devera fazê-lo no pino D17 (o mesmo que o pino A3). É bom ter um alarme sonoro quando ocorra um erro de comunicação. Eu usei-o durante a fase de teste, deixando de fora no HW do projecto final (mas o código está preparado para isso). Cabe a você decidir se o insta-la ou não.

O código abaixo pode ser usado para testar e / ou configurar o  ESP8266:

ESP8266_test_Set_up.ino

Conectando o ESP8266 a internet

Uma vez que o módulo ESP8266 está instalado, o próximo passo é aplicar um “Reset” no pino CH-PD.

/***************************************************
* Reset funtion to accept communication
****************************************************/
void reset8266(void)
{
  pinMode(CH_PD, OUTPUT);
  digitalWrite(CH_PD, LOW);

  delay(300);
  digitalWrite(CH_PD, HIGH);
  Serial.print("8266 reset OK");
  lcd.clear();
  lcd.println("8266 reset OK       ");
}

Após o reset, vamos conectá-lo à rede local (alterar o no código: username e password, por suas credenciais ) e iniciar o módulo como “Modo Station” (CWMODE = 1):

/***************************************************
* Connect WiFi
****************************************************/
void connectWiFi(void)
{
  sendData("AT+RST\r\n", 2000, DEBUG); // reset
  sendData("AT+CWJAP=\"USERNAME\",\"PASSWORD\"\r\n", 2000, DEBUG); //Connect network
  delay(3000);
  sendData("AT+CWMODE=1\r\n", 1000, DEBUG);
  sendData("AT+CIFSR\r\n", 1000, DEBUG); // Show IP Adress
  lcd.clear();
  lcd.print("8266 Connected");
  Serial.println("8266 Connected");
}

Para enviar dados ao ESP8266, utilizamos a função sendData():

/***************************************************
* Send AT commands to module
****************************************************/

String sendData(String command, const int timeout, boolean debug)
{
  String response = "";
  esp8266.print(command);
  long int time = millis();
  while ( (time + timeout) > millis())
  {
    while (esp8266.available())
    {
      // The esp has data so display its output to the serial window
      char c = esp8266.read(); // read the next character.
      response += c;
    }
  }
  if (debug)
  {
    Serial.print(response);
  }
  return response;
}

As funções acima serão chamadas durante a “fase de setup” de nosso Código. Se tudo foi feito corretamente, deve ver pelo monitor serial, mensagens semelhantes como as de abaixo:

Data Storage Cloud: O ThinkSpeak.com

Todos os dados capturados pelo ArduFarmBot serão enviados para a nuvem, utilizando-se um serviço gratuito proporcionado pelo “ThinkSpeak.com“.

No “Loop ()”, depois de capturar os dados com readSensors (), chamaremos uma função específica para carregar os dados capturados: updateDataThingSpeak ();

/***************************************************
* Transmit data to thingspeak.com
****************************************************/
void updateDataThingSpeak(void)
{
  startThingSpeakCmd ();
  
  cmd = msg ;
  cmd += "&field1=";     //field 1 for DHT temperature
  cmd += tempDHT;
  cmd += "&field2=";    //field 2 for DHT humidity
  cmd += humDHT;
  cmd += "&field3=";    //field 3 for LDR luminosity
  cmd += lumen;
  cmd += "&field4=";    //field 4 for Soil Moisture data 
  cmd += soilMoist;
  cmd += "&field5=";    //field 5 for PUMP Status
  cmd += pumpStatus;
  cmd += "&field6=";    //field 6 for LAMP Status
  cmd += lampStatus;
  cmd += "\r\n";

  sendThingSpeakCmd();
}

A fim de enviar esses dados, a primeira coisa a fazer é iniciar a comunicação com o ThingSpeak.com. Faremos isso, usando a função: startThingSpeakCmd ();

/***************************************************
* Start communication with ThingSpeak.com
****************************************************/
void startThingSpeakCmd(void)
{
  cmd = "AT+CIPSTART=\"TCP\",\"";
  cmd += IP;
  cmd += "\",80";
  esp8266.println(cmd);
  delay(2000);
  if(esp8266.find("Error"))
  {
    Serial.println("ESP8266 START ERROR");
    return;
  }
  Serial.println("Thinkspeak Comm Started");
  cmd ="";
}

Uma vez que o canal esteja aberto com o ThingSpeak e o string “cmd” esteja montado com os dados, é hora de fazer o upload de tudo isso no canal correspondente do ThingSpeak usando-se da função:sendThingSpeakCmd();

/***************************************************    
* Update channel ThingSpeak.com
****************************************************/
String sendThingSpeakCmd(void)
{
  esp8266.print("AT+CIPSEND=");
  esp8266.println(cmd.length());
  if(esp8266.find(">")){
    esp8266.print(cmd);
    Serial.println("");
    Serial.println("");
    Serial.println(cmd);
    delay(500);
   
    String messageBody = "";
    while (esp8266.available()) 
    {
      String line = esp8266.readStringUntil('\n');
      if (line.length() == 1) 
      { //actual content starts after empty line (that has length 1)
        messageBody = esp8266.readStringUntil('\n');
        Serial.print("Message received: ");
        Serial.println(messageBody);
      }
    }
    return messageBody;
  }
  else{
    esp8266.println("AT+CIPCLOSE");
    Serial.println("ESP8266 CIPSEND ERROR: RESENDING"); //Resend...
    error=1;
    return "error";
  }
}

As funções acima foram baseadas num tutorial muito bom desenvolvido por Michalis Vasilakis.
A foto abaixo mostra o canal ArduFarmBot no site do ThingSpeak.com:

Comandando os atuadores a partir da web
Neste momento, estamos a descarregar (“Upload”) e a armazenar todos os dados recolhidos na nuvem. Isto é muito útil para a monitoração remota, mas o que aconteceria se com base nesses dados gostaríamos de acionar a bomba ou a Lâmpada, independente do programa automático local? Para fazer isso, além do “Upload”, também faremos um “Download” de dados a partir da nuvem, informando ao controlador como agir baseado nesses dados (comandos).Criemos para isso, campos específicos em nosso canal ThinkSpeak para comandar os atuadores:

  • Field 7:
    – Data = 1 ==> PUMP should be Turn ON
    -Data = 0 ==> PUMP should be Turn OFF
  • Field 8:
    -Data = 1 ==> LAMP should be Turn ON
    -Data = 0 ==> LAMP should be Turn OFF

Como configurar esses campos diretamente no ThingSpeak? Podemos fazê-lo, por exemplo, escrevendo um “plugin” diretamente para ThinksPeak, ou podemos usar um site externo para fazê-lo (esta será a nossa escolha). De qualquer forma, em ambos os casos, você deverá usar um comando como o de abaixo:

api.thingspeak.com/update?key=YOUR_WRITE_KEY&field7=1

Com o comando acima, por exemplo (e usando a sua chave de escrita de canal), você vai escrever “1” no Field 7, o que significa que a bomba deve ser ligada. Você pode facilmente testá-lo, escrevendo a linha de comando acima no seu navegador, o campo correspondente no seu canal será alterado. Como um retorno, o navegador irá mostrar uma página em branco com um único número no canto superior esquerdo, correspondente à entrada de dados sequencial no seu canal.

Até agora, 50% do trabalho está feito! Agora você deverá ler este “comando” (conteúdo do campo), na estação local do ArduFarmBot .

O comando para fazer isso é mostrado abaixo. Ele vai obter o último dado que foi escrito no campo específico (que em nosso caso será um “comando”).

api.thingspeak.com/channels/CHANNEL_ID/fields/7/last

Da mesma forma como fizemos antes, poderá testar a linha de comando, usando seu navegador. Neste caso, o navegador irá mostrar-lhe o dado armazenado esse campo específico. Veja abaixo:

Voltando à “terra”, escreveremos uma função que lerá este “último campo”:

/***************************************************
* Read data from field7 of thingspeak.com
****************************************************/
int readLastDataField7()
{
  startThingSpeakCmd ();

  // "GET /channels/CHANNEL_ID/fields/7/last";
  cmd = msgReadLastDataField7; 
  cmd += "\r\n";

  String messageDown = sendThingSpeakCmd();
  Serial.print("Command received: ");
  Serial.println(messageDown[7]);
  
  int command = messageDown[7]-48; 
  return command;
}

A função acima retornará o dado contido no Field 7 (“1” ou “0”). Uma função semelhante deve ser escrita para o Field 8.

Uma vez que já possuímos o conteúdo de ambos os campos, devemos utilizá-los em uma função que comandará os atuadores de maneira semelhante ao que fizemos com a “função de comando manual”:

/***************************************************
* Receive Commands from thingSpeak.com
****************************************************/
void receiveCommands()
{
  field7Data = readLastDataField7();
  if (field7Data == 1) 
  {
    digitalWrite(PUMP_PIN, HIGH);
    pumpStatus = 1;
    showDataLCD();
  }
  if (field7Data == 0) 
  {
    digitalWrite(PUMP_PIN, LOW);
    pumpStatus = 0;
    showDataLCD();
  }

  delay (500); 
  
  field8Data = readLastDataField8();
  if (field8Data == 1) 
  {
    digitalWrite(LAMP_PIN, HIGH);
    lampStatus = 1;
    showDataLCD();
  }
  if (field8Data == 0) 
  {
    digitalWrite(LAMP_PIN, LOW);
    lampStatus = 0;
    showDataLCD();
  }
  delay (500); 
}

Assim, a partir de agora pode usar a linha de comando no seu navegador para ligar / desligar a bomba e a lâmpada remotamente. Abaixo, se pode ver como o comando recebido aparecerá no monitor serial:

Outra consideração importante é a “coordenação” entre o comando local e o remoto. Teremos de alterar a função readLocalCmd () para que também atualizemos os Fields7 e 8 do Thinkspeak respectivamente, como o estado da bomba e da lâmpada:

field7Data = pumpStatus;

field8Data = lampStatus;

Agora “filed7Data” e “field8Data” estão em sincronia com os comandos da página web e também com as ações de comando locais quando pressiona um botão. Então, vamos atualizar a função aplyCmd (), que é a responsável para ligar / desligar os atuadores:

/***************************************************
* Receive Commands and act on actuators
****************************************************/
void aplyCmd()
{
  if (field7Data == 1) digitalWrite(PUMP_PIN, HIGH);
  if (field7Data == 0) digitalWrite(PUMP_PIN, LOW);
  
  if (field8Data == 1) digitalWrite(LAMP_PIN, HIGH);
  if (field8Data == 0) digitalWrite(LAMP_PIN, LOW);
}

Quando começar os testes, perceberá que qualquer comando executado manualmente no local ou via web, será superado pelas ações automáticas definidas pela função autoControlPlantation(). Neste ponto, deve considerar quem será o “chefe” (ou patroa…), o qual terá a última palavra! Em nosso caso definiremos o seguinte:

  • Em cada ciclo de loop, que é quase “sempre”, verificamos se um botão local é pressionado
  • Aproximadamente a cada minuto, faremos um “pooling” no ThinkSpeak e verificamos se existe um nova ordem manual a partir de lá.
  • Aproximadamente  a cada 10 minutos, vamos ler os sensores, atualizando os dados no ThinkSpeak e mais importante,  executamos as medidas automáticas, que serão tomadas independente das que foram selecionados manualmente e será aquela que irá ser mantido.

Pode alterar a lógica da maneira que melhor se adapte ao seu projeto. Essa é a coisa boa sobre o uso de um processador programável, como o Arduino para controlar coisas!

Assim, 2 temporizadores serão utilizados agora, um para agrupar os comandos remotos e outro para ler os sensores (o mesmo que foi utilizado anteriormente):

long sampleTimingSeconds = 75; // ==> ******** Define Sample time in seconds to read sensores *********
int reverseElapsedTimeSeconds = 0;
long startTiming = 0;
long elapsedTime = 0;
long poolingRemoteCmdSeconds = 20; // ==> ******** Define Pooling time in seconds for new ThingSpeak commands *********
long startRemoteCmdTiming = 0;  
long elapsedRemoteCmdTime = 0;

A função loop () deverá ser reescrita da seguinte forma:

void loop() 
{
  elapsedRemoteCmdTime = millis()-startRemoteCmdTiming;   // Start timer for pooling remote commands
  elapsedTime = millis()-startTiming;   // Start timer for measurements
  reverseElapsedTimeSeconds = round (sampleTimingSeconds - elapsedTime/1000);

  readLocalCmd(); //Read local button status
  showDataLCD();

  if (elapsedRemoteCmdTime > (poolingRemoteCmdSeconds*1000)) 
  {
    receiveCommands();
    updateDataThingSpeak();
    startRemoteCmdTiming = millis();
  }
  
  if (elapsedTime > (sampleTimingSeconds*1000)) 
  {
    readSensors();
    autoControlPlantation();
    updateDataThingSpeak();
    startTiming = millis();
  }
}

Implementação de uma página Web dedicada

Neste ponto o nosso ArduFarmBot está operacional e pode ser controlado a partir da web. Pode monitorizar os dados no site do Thinkspeak e também enviar comandos utilizando-se de um navegador, mas é claro que esta “solução web” não pode ser considerada “elegante”! A melhor maneira de implementar uma solução real para IoT, é desenvolver uma página web completa, a qual além de exibir todos os dados, também possuirá botões para ativação dos atuadores.
Para hospedar nossa página web, escolhi o Byethost, um site gratuito, muito fácil e simples para lidar com as suas páginas.

Não entrarei em detalhes como desenvolver esse tipo de página, uma vez que este não é o propósito central deste tutorial, mas incluirei aqui o HTML, CSS e códigos fonte do JavaScript que desenvolvi exclusivamente para minha página. Uma vez que alguém tenha interesse em como cheguei ao projeto final, podemos discuti-lo utilizando-se do Painel de Comentários no final do post.

É importante reforçar que essa página não trabalha diretamente com a Estação de Controle ArduFarmBot Local. O que ela está fazendo realmente é, interagir com o canal do ThinkSpeak como descrito abaixo:

  1. Ler dados de sensores dos Fields 1, 2, 3, 4
  2. Ler estado do atuadores  (“Echo”) dos Fields  5 e 6
  3. Armazenar dados (comandos) nos Fields 7 e 8
  4. Ler dados meteorológicos locais (Yahoo)

O item 4 acima não é realmente crucial para o projeto, mas é importante ter dados adicionais disponíveis no caso de querer tomar algumas ações remotas independente do que está acontecendo localmente com plantação do seu tomate. Outra consideração é que pode, por exemplo, armazenar esses dados em outro canal ThingSpeak e transferir-los para o Arduino, mostrando dados meteorológicos no display LCD local por exemplo (eu desenvolvi isto noutro projeto que funcionou bem! Deixo aqui como uma sugestão).

FQOK9ENIT653YX5

A Matriz Sensor-Atuador

Na primeira parte deste projeto, definimos algumas considerações preliminares sobre como os atuadores devem agir em função da leitura dos sensores. Naquele caso, fizemos unicamente opções simples, mas o que vai acontecer se tivermos uma situação mais complexa? Várias condições diferentes? O que iremos desenvolver aqui é uma nova abordagem que chamaremos de “Matriz Sensor – Atuador”.

Numa tabela foram definidas para cada sensor, as diferentes condições possíveis  e como deveria ser a respectiva saída dos atuadores. O resultado pode ser observado na tabela Excel abaixo.

Na tabela Excel, existem duas Tabs. Uma com filtro como mostrado acima e outra que além da seleção, te permite obter o “código”binário que deverá ser carregado na variável da Matriz.

Uma vez definida, a matriz deverá ser carregada em nosso código. Um array de 18 linhas e 10 colunas foi criado para este propósito:

//                       +---SOIL----+-LIGHT-+---TEMP---+---ACTUAT----+
 //                        SL  SM  SH  LL  LH  TL  TM  TH   Pump   Lamp
boolean SDf [18] [10] = {{  1,  0,  0,  0,  1,  0,  0,  1,    1,    0 }, 
                          { 1,  0,  0,  0,  1,  0,  1,  0,    1,    0 }, 
                          { 1,  0,  0,  0,  1,  1,  0,  0,    1,    1 }, 
                          { 1,  0,  0,  1,  0,  0,  0,  1,    1,    0 }, 
                          { 1,  0,  0,  1,  0,  0,  1,  0,    1,    0 }, 
                          { 1,  0,  0,  1,  0,  1,  0,  0,    0,    1 }, 
                          { 0,  1,  0,  0,  1,  0,  0,  1,    0,    0 }, 
                          { 0,  1,  0,  0,  1,  0,  1,  0,    0,    0 }, 
                          { 0,  1,  0,  0,  1,  1,  0,  0,    0,    1 }, 
                          { 0,  1,  0,  1,  0,  0,  0,  1,    0,    0 }, 
                          { 0,  1,  0,  1,  0,  0,  1,  0,    0,    1 }, 
                          { 0,  1,  0,  1,  0,  1,  0,  0,    0,    1 }, 
                          { 0,  0,  1,  0,  1,  0,  0,  1,    0,    0 }, 
                          { 0,  0,  1,  0,  1,  0,  1,  0,    0,    0 },
                          { 0,  0,  1,  0,  1,  1,  0,  0,    0,    1 },
                          { 0,  0,  1,  1,  0,  0,  0,  1,    0,    0 },
                          { 0,  0,  1,  1,  0,  0,  1,  0,    0,    1 },
                          { 0,  0,  1,  1,  0,  1,  0,  0,    0,    1 },
                         };

Para trabalhar com a Matrix, criou-se a função defSensorStatus (). Esta função testa para cada linha se a condição das 8 primeiras colunas são TRUE. Se sim, as condições definidas nas 2 últimas colunas são executadas.

por exemplo:

if (1 and 0 and 0 and 0 and 1 and 0 and 0 and 1) { pumpStatus = 1; lampStatus = 0}

else if (1 and 0 and 0 and 0 and 1 and 0 and 1 and 0) { pumpStatus = 1; lampStatus = 0}

e assim por diante …

Dentro da função acima outra array é criada com o estado de cada leitura do sensor:

boolean snsSts[8]={0, 0, 0, 0, 0, 0, 0, 0}; // SL, SM, SH, LL, LH, TL, TM, TH

Esta array variável será também utilizado para o registo do LOG.

Otimizando o código

Durante o processo de desenvolvimento do ArduFarmBot percebemos que algumas mudanças na especificação original deveria ser feita:

Display LCD:

O display LCD deve estar apagado por default e a qualquer momento que uma leitura de sensores é necessária, podemos acendê-lo. Esta condição foi implementada no código e o botão de leitura de sensores deve ser utilizado no modo “toggle” para ligar / desligar o LCD a qualquer momento. Acender ou apagar o LCD irá automaticamente atualizar as leituras de sensores para a exibição, mas estas leituras não serão utilizadas pelo ArduFarmBot em suas funções regulares.

Configuração inicial:

Quando o ArduFarmBot é ligado (ou depois de um Reset), o LCD irá exibirá configuração do controlador “Initial Set-up”. Para se iniciar a execução do programa, o botão “Sensores” deve ser pressionado novamante (o LCD se apagará). A informação inicial disponível na tela inicial do LCD será:

/***************************************************
* Storage of Log data at Arduino EEPROM
****************************************************/
void storeDataLogEEPROM(void)
{
  for (int i = 0; i<8; i++) 
  {
    logData = logData + (snsSts[i])<< 1;
  }
  EEPROM.write (memoAddr, logData);
  memoAddr++;
  logData = 0;
  logData = logData + pumpStatus;
  logData = logData << 1;
  logData = logData + lampStatus;
  EEPROM.write (memoAddr, logData);
  EEPROM.write (0, memoAddr+1);
  logData = 0; 
  if ((memoAddr+1) == 1023) memoAddr=1;
  else memoAddr++;
} 

Como comentado na última etapa, o que será armazenada na EEPROM do Arduino é o conteúdo, bit a bit do array snsSts[] adicionado do status da bomba e lâmpada. Você pode ver abaixo, o log no monitor serial:

Como já visto na parte 1, para facilidade de entendimento e desenvolvimento, o código para o ArduFarmBot foi dividido em vários arquivos, criados por sua especialidade. Note que 2 novos arquivos foram adicionados nesta segunda parte:

  • communication.ino (ThingSpeak and ESP8266 specific funtions)
  • stationCredentials.h (ThinkSpeak Channel ID and specific Keys for writing on the channel)

Por último, uma vez que o código acabou por ficar com um tamanho razoável, decidi armazenar as constantes na memória flash (programa), em vez de SRAM. Para isso, usei a palavra-chave PROGMEM que é um modificador de variável. Por exemplo, em vez de utilizar:

#define DHTPIN 5, usaremos:

const PROGMEM byte DHTPIN = 5;

O PROGMEN informa ao compilador para “colocar esta informação na memória flash”, em vez de em SRAM, onde iria normalmente. Também deve incluir a biblioteca avr/pgmspace.h no arquivo principal do seu código.

Outro bom procedimento para reduzir o uso de SRAM é comentar (ou excluir) todas as linhas de Serial.print () que usou para a depuração durante o desenvolvimento. Perceberá que o código usado por exemplo para mostrar o log no monitor serial estará todo “comentado” no código final.

Abaixo pode encontrar o código completo para o ArduFarmBot. Não se esqueça de alterar os dados no file credentials.h com seu Thinkspeak Channel Id e chave de escrita. Também no communication.ino, entre com seu nome de usuário e senha para conectar o ESP 8266 na Internet.

Abaixo testes do ArduFarmBot e instalação definitiva:

 

Preparando o reservatório de água:

Conectando a bomba da água:

Germinação (aproximadamente 45 dias):

Seleção melhores mudas e transplante :

Artigo gentilmente cedido por: MJRoBot.Org
Todos os produtos utilizados neste artigo podem ser encontrados na Loja de Eletrónica e Robótica – ElectroFun.

Gostaram deste artigo? Deixem o vosso comentário no formulário a baixo e partilhem com os vossos amigos.

Não se esqueçam de fazer like na nossa Página no Facebook.

Podem ainda colocar as vossas dúvidas no nosso Forum da Comunidade Arduino em Portugal ou no nosso Grupo no Facebook Arduino Portugal – Qual o teu projeto?

Comments

Comentários

ArduinoPortugal.pt

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *

To use BrandCaptcha you must get an API Key