No mundo atual, a logística e o transporte de cargas desempenham um papel fundamental na economia global. As empresas que gerenciam frotas de caminhões enfrentam desafios diários, desde otimizar rotas até garantir a segurança dos motoristas e a entrega pontual das mercadorias. Nesse contexto, a utilização de dispositivos de GPS (Sistema de Posicionamento Global) para rastreamento em tempo real tornou-se uma ferramenta indispensável.O objetivo deste projeto é desenvolver um dispositivo de GPS especialmente projetado para frotas de caminhões, permitindo o monitoramento e o rastreamento em tempo real da localização dos veículos. Com base nas coordenadas geográficas transmitidas pelo dispositivo, será possível obter informações precisas sobre a posição exata dos caminhões ao longo de suas rotas.
É possível ver o marcador no mapa indicando o local real naquele exato momento de um caminhão. São usados os dados de latitude e longitude, vindos do firebase, para inserir esse marcador no mapa.
O primeiro campo do lado direito indica a longitude, e o segundo a latitude. O dado pode ser enviado para o firebase manualmente colocando os dados nos campos e clicando em PUSH, e assim atualizando o marcador, ou vindo através do sensor de GPS. É possível também ligar e desligar esse dispositivo clicando no botão “Turn Off GPS”, então ele será alterado no firebase e lido na placa.
Placas
Neste projeto, utilizamos uma placa NUCLEO, conectada a uma NodeMCU, para implementar um sistema de rastreamento de GPS em tempo real. A placa NUCLEO desempenha um papel fundamental ao atuar como a interface principal entre o sensor de GPS e a NodeMCU.
A NodeMCU é uma placa de desenvolvimento que incorpora um microcontrolador WiFi ESP8266, fornecendo a capacidade de comunicação sem fio. Ela é conectada à placa NUCLEO por meio de uma interface UART (Universal Asynchronous Receiver/Transmitter), que permite a transmissão de dados entre os dispositivos.
O sensor de GPS é responsável por capturar as coordenadas de latitude e longitude, fornecendo informações precisas sobre a localização do caminhão. Esses dados são enviados para a placa NUCLEO, que atua como um controlador central.
Ao receber os dados de GPS, a placa NUCLEO realiza o tratamento necessário para garantir a precisão e a integridade das informações. Isso pode envolver a validação dos dados, a filtragem de ruídos ou a aplicação de algoritmos para correção de erros.
Uma vez que os dados de GPS são tratados pela placa núcleo, a NodeMCU entra em ação para enviar essas informações para o Firebase. O Firebase é uma plataforma de desenvolvimento de aplicativos que oferece serviços em nuvem, incluindo um banco de dados em tempo real. A NodeMCU estabelece uma conexão com o Firebase e envia os dados de localização do caminhão, que são armazenados de forma segura e imediata.
Ao utilizar o Firebase como destino para os dados tratados de GPS, é possível acessar as informações remotamente de forma fácil e eficiente.
Frontend
No desenvolvimento deste projeto, implementamos um frontend utilizando a biblioteca React para criar uma interface interativa e intuitiva. Essa interface tem como objetivo exibir os dados de coordenadas dos caminhões em tempo real, obtidos a partir do Firebase.
O Firebase é um banco de dados em tempo real que nos permite armazenar e atualizar informações de forma dinâmica. Neste caso, utilizamos o Firebase como fonte dos dados de localização dos caminhões, que são atualizados continuamente à medida que os veículos se deslocam.
Com o auxílio da biblioteca Mapbox, integramos um mapa interativo na interface do frontend. O Mapbox é uma poderosa ferramenta que fornece serviços de mapas, permitindo a visualização de informações geográficas de forma personalizada e altamente interativa.
No frontend, implementamos a lógica necessária para buscar os dados de coordenadas do Firebase e atualizá-los em tempo real. A medida que os dados são atualizados, a interface reage e atualiza a posição do caminhão no mapa. Para representar visualmente a localização atual do caminhão, utilizamos um marcador no mapa, que se move de acordo com as atualizações das coordenadas.
Dessa forma, os usuários podem visualizar de maneira clara e instantânea a localização em tempo real dos caminhões em um mapa interativo. Isso proporciona uma visão geral do deslocamento das frotas, facilitando o acompanhamento das rotas e o planejamento logístico.
Além disso, a utilização do React como framework de frontend permite criar uma interface responsiva e dinâmica, possibilitando uma experiência de usuário agradável e intuitiva. Os dados são atualizados de forma automática e contínua, garantindo a precisão e a atualização em tempo real da localização dos caminhões.
const push = () => {
    const longitudeNum = parseFloat(longitude);
    const latitudeNum = parseFloat(latitude);
    database.ref('locais').set({
      longitude: longitudeNum,
      latitude: latitudeNum,
      status: status // Adiciona o status aos dados enviados para o Firebase
    }).then(() => {
      updateMap(longitudeNum, latitudeNum); // Atualiza o marcador com as coordenadas enviadas
    }).catch(alert);
  };Essa função é responsável por enviar os dados de latitude, longitude e status para o Firebase. Ela converte as coordenadas de strings para números de ponto flutuante, usa o método set() para enviar os dados para a referência 'locais' no Firebase e, em seguida, chama a função updateMap() para atualizar o mapa com as novas coordenadas.
// Função para alternar o status entre ligado (1) e desligado (0)
  const toggleStatus = () => {
    const newStatus = status === 1 ? 0 : 1;
    setStatus(newStatus);
    // Atualiza o valor do campo "status" no Firebase
    database.ref('locais/status').set(newStatus).catch(alert);
  };Essa função é chamada quando o botão "Turn On GPS" ou "Turn Off GPS" é clicado. Ela alterna o valor do estado status entre 1 e 0. Em seguida, atualiza o valor do campo "status" na referência 'locais/status' do Firebase.
// Função para buscar os dados do Firebase e adicionar o listener
  const fetchData = () => {
    const locaisRef = database.ref('locais');
 
    locaisRef.on('value', (snapshot) => {
      const locais = snapshot.val();
 if (locais) {
        setData(locais);
        setLongitude(locais.longitude.toString());
        setLatitude(locais.latitude.toString());
 if (locais.status !== undefined) {
          setStatus(locais.status); // Define o estado do status com base nos dados do Firebase
        }
        updateMap(locais.longitude, locais.latitude);
      }
    }, (error) => {
      console.error(error);
    });
 
    // Adicionar listener para atualizar o marcador quando os dados mudarem
    locaisRef.child('longitude').on('value', (snapshot) => {
      const newLongitude = snapshot.val();
 if (newLongitude !== null) {
        setLongitude(newLongitude.toString());
        updateMap(newLongitude, latitude);
      }
    });
 
    locaisRef.child('latitude').on('value', (snapshot) => {
      const newLatitude = snapshot.val();
 if (newLatitude !== null) {
        setLatitude(newLatitude.toString());
        updateMap(longitude, newLatitude);
      }
    });
  };Essa função é responsável por buscar os dados iniciais do Firebase e adicionar um listener para atualizações em tempo real. Ela consulta a referência 'locais' e, sempre que os dados mudam, o listener é acionado. Os dados são então armazenados no estado data, as coordenadas são atualizadas e a função updateMap() é chamada.
useEffect(() => {
    fetchData();
  }, []);Esse trecho de código utiliza o hook useEffect para chamar a função fetchData() uma vez quando o componente é carregado.
// Inicializa o mapa
  useEffect(() => {
    const initializeMap = () => {
      const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [longitude, latitude],
        zoom: 15,
      });
      const marker = new mapboxgl.Marker().setLngLat([longitude, latitude]).addTo(map);
      setMarker(marker);
      setMap(map);
      // Event listener para clicar no mapa e atualizar as coordenadas
      map.on('click', (e) => {
        const { lng, lat } = e.lngLat;
        setLongitude(lng.toFixed(6));
        setLatitude(lat.toFixed(6));
      });
      // Atualiza o mapa e o marcador com as coordenadas iniciais
      updateMap(longitude, latitude);
    };
 if (!map && longitude !== '' && latitude !== '') {
      initializeMap();
    }
  }, [map, longitude, latitude]);Esse trecho utiliza outro useEffect para inicializar o mapa quando as variáveis map, longitude e latitude mudam. Ele cria uma nova instância de mapboxgl.Map, configura o estilo do mapa, o centro, o zoom e adiciona um marcador na posição das coordenadas atuais. O mapa e o marcador são atualizados nos estados map e marker, respectivamente.
const updateMap = (newLongitude, newLatitude) => {
 if (map && marker) {
      marker.setLngLat([newLongitude, newLatitude]);
      // Verifica o valor do status para mostrar ou ocultar o marcador
 if (status === 0) {
        marker.remove(); // Remove o marcador do mapa
      } else {
        marker.addTo(map); // Adiciona o marcador ao mapa
      }
      map.flyTo({
        center: [newLongitude, newLatitude],
        zoom: 1,
        maxBounds: [
          [-180, -85],
          [180, 85]
        ],
      });
    }
  };Essa função é chamada para atualizar o mapa e o marcador com novas coordenadas. Ela recebe as novas coordenadas de longitude e latitude como parâmetros. Primeiro, atualiza a posição do marcador com as novas coordenadas. Em seguida, verifica o valor do estado status e mostra ou oculta o marcador com base nesse valor. Por fim, utiliza o método flyTo() para mover o centro do mapa para as novas coordenadas.
return (
    <div className="App">
      <div id="map" style={{ width: '100%', height: '100vh' }}></div>
      <div className="input-container">
        <input
          placeholder="Enter your longitude"
          value={longitude}
          onChange={(e) => setLongitude(e.target.value)}
        />
        <br /><br />
        <input
          placeholder="Enter your latitude"
          value={latitude}
          onChange={(e) => setLatitude(e.target.value)}
        />
        <br /><br />
        <button onClick={push}>PUSH</button>
        <button onClick={toggleStatus}>{status === 1 ? 'Turn Off GPS' : 'Turn On GPS'}</button>
      </div>
    </div>
  );
}Esse bloco de código representa a renderização do componente App. Ele retorna o JSX que será exibido na página. Inclui uma div para exibir o mapa, um formulário com campos de entrada para longitude e latitude, botões para enviar os dados e alternar o status, respectivamente.
Sobre os hardwaresAcesse o código completo no github:
- Nucleo: https://github.com/bernardoqueiroz/nucleoGPS
 - NodeMCU: https://github.com/bernardoqueiroz/nodeMCUGPS
 
O módulo gps envia uma série de strings pelo pino TX, esses dados possuem diversas informações além das coordenadas geográficas como: velocidade, altitude, data e hora. Para auxiliar na interpretação dessas strings, utilizamos duas libs que encontramos no github:
https://github.com/controllerstech/stm32-uart-ring-buffer/tree/master
https://github.com/leech001/gps
A utilização dessas bibliotecas é necessária pois o gps envia as informações em strings diferentes e cada string tem muitos dados, logo, após recebê-las, precisamos parsear e armazenar as informações na memória, para isso utilizamos algumas structs. Segue abaixo um exemplo de como as strings chegam pela porta serial:
$GPGGA,183227.00,,,,,0,00,99.99,,,,,,*6B
$GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30
$GPGSV,1,1,01,07,,,28*75
$GPGLL,,,,,183227.00,V,N*47
$GPRMC,183228.00,V,,,,,,,020623,,,N*7A
$GPVTG,,,,,,,,,N*30
$GPGGA,183228.00,,,,,0,00,99.99,,,,,,*64
$GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30
$GPGSV,1,1,01,07,,,28*75
$GPGLL,,,,,183228.00,V,N*48
$GPRMC,183229.00,V,,,,,,,020623,,,N*7B
$GPVTG,,,,,,,,,N*30
$GPGGA,183229.00,,,,,0,00,99.99,,,,,,*65Código NUCLEO
Utilizamos as funções para decodificação das strings da primeira, a decodeGGA e a decodeRMC, e da segunda lib utilizamos a implementação do buffer para armazenar e validar os dados enviados pelo gps.
Criamos uma task chamada "cli", ela é responsável por receber as informações do gps pela huart1, armazenar as strings no buffer e chamar as funções de validação e parse. Além disso, caso as flags das strings GGA e RMC sejam acionadas com o valor 2 e a flag gpsEnabled estiver ativa, a nucleo deve enviar as coordenadas para a nodeMCU pela huart3.
void cli(void * vParam)
{
 while(1)
 {
  while(HAL_UART_Receive(&huart1, &rx_data, 1, HAL_MAX_DELAY) != HAL_OK);
  if (rx_data != '\n' && rx_index < sizeof(rx_buffer)) {
   rx_buffer[rx_index++] = rx_data;
  } else {
   #if (GPS_DEBUG == 1)
   GPS_print((char*)rx_buffer);
   #endif
   if(GPS_validate((char*) rx_buffer)) {
    GPS_parse((char*) rx_buffer);
   }
   rx_index = 0;
   memset(rx_buffer, 0, sizeof(rx_buffer));
  }
  if ((flagGGA == 2) | (flagRMC == 2))
  {
   if (gpsEnabled == 1) {
    sprintf(nodeMCUBuffer,"%f,%f,\n", gpsData.ggastruct.lcation.latitude, gpsData.ggastruct.lcation.longitude);
    sendString(nodeMCUBuffer, 3);
   }
  }
  else if ((flagGGA == 1) | (flagRMC == 1))
  {
    sendString("   NO FIX YET   \n\r", 2);
    sendString("   Please wait  \n\r", 2);
  }
  if (VCCTimeout <= 0)
  {
    VCCTimeout = 5000;  // Reset the timeout
    flagGGA =flagRMC =0;
    sendString("    VCC Issue   \n\r",2);
    sendString("Check Connection\n\r",2);
  }
 }
}Na função de parse basicamente checamos se as strings são do tipo GPGGA ou GPRMC, e caso passemos por essa verificação, vamos chamar as funções decodeGGA ou decodeRMC, elas que vão de fato parsear as strings e salvá-las numa struct.
void GPS_parse(char *GPSstrParse){
 if(!strncmp(GPSstrParse, "$GPGGA", 6)){
  VCCTimeout = 5000;  // Reset the VCC Timeout indicating the GGA is being received
  if (decodeGGA(GPSstrParse, &gpsData.ggastruct) == 0) flagGGA = 2;  // 2 indicates the data is valid
  else flagGGA = 1;  // 1 indicates the data is invalid
    }
 else if (!strncmp(GPSstrParse, "$GPRMC", 6)){
  VCCTimeout = 5000;  // Reset the VCC Timeout indicating the RMC is being received
  if (decodeRMC(GPSstrParse, &gpsData.rmcstruct) == 0) flagRMC = 2;  // 2 indicates the data is valid
  else flagRMC = 1;  // 1 indicates the data is invalid
    }
}Como o nosso GPS não consegue se conectar com satélites em ambientes fechados, criamos a test_task, ela basicamente simula a task "cli", enviando coordenadas mockadas para a nodeMCU a cada 3 segundos.
void test_task( void *vParam){
 char *places[4] = {"-20.7000000,-45.3666667,\n", "-5.9500000,-43.0000000,\n",
   "-17.4333333,-44.8500000,\n", "3.7500000,-61.3500000,\n"};
 while(1){
  for (int i = 0; i < 4; i++) {
   if (gpsEnabled == 1) {
    sendString(places[i], 3);
   }
   vTaskDelay(3000);
  }
 }
}Criamos também uma task chamada "gpsEnabled", ela é responsável por receber da nodeMCU uma flag que indica se o gps deve ou não enviar as coordenadas.
void gpsEnabled_task(void * vParam)
{
 unsigned char flag;
 vTaskDelay(100);
 while(1){
  //Aguarda receber um caracter da UART3 que esta conectada ao Node MCU
  while(HAL_UART_Receive(&huart3, &flag, 1, HAL_MAX_DELAY) != HAL_OK);
  if (flag == '1') {
   gpsEnabled = 1;
   sendChar('v', 2);
  } else {
   gpsEnabled = 0;
   sendChar('f', 2);
  }
  vTaskDelay(1);
 }
}Código Node MCUDo lado da nodeMCU temos apenas um loop, nele, a cada dois segundos, buscamos do firebase o valor da flag "status", ela indica se o gps está ativo ou não, logo em seguida enviamos o valor dessa flag para a núcleo através da serial de software. Além disso, caso a SSerial esteja available, pegamos as coordenadas enviadas com o auxílio de um buffer e as enviamos para o firebase.
void loop()
{
 if (millis() - dataMillis > 2000)
   {
       dataMillis = millis();
       int gpsEnabled = 0;
       Serial.printf("Get int ref... %s\n\r", Firebase.RTDB.getInt(&fbdo, "/locais/status", &gpsEnabled) ? String(gpsEnabled).c_str() : fbdo.errorReason().c_str());  
       digitalWrite(BUILTIN_LED, !gpsEnabled);
       SSerial.write(String(gpsEnabled).c_str());
   }
 if (SSerial.available()){  
     char c = SSerial.read();
 if (c == '\n') {
         gps_buffer[buffer_index] = '\0';
         char* latitude = strtok(gps_buffer, ",");
         char* longitude = strtok(NULL, ","); 
 if (latitude != NULL && longitude != NULL) {
             Serial.printf("Set lat... %s\n\r", Firebase.RTDB.setString(&fbdo, "/locais/latitude", latitude) ? "ok" : fbdo.errorReason().c_str());
             Serial.printf("Set long... %s\n\r", Firebase.RTDB.setString(&fbdo, "/locais/longitude", longitude) ? "ok" : fbdo.errorReason().c_str());
         } else {
             Serial.printf("Invalid coordinates format\n\r");
         }
         buffer_index = 0;
     } else {
         gps_buffer[buffer_index] = c;
         buffer_index++;
 if (buffer_index >= 512) {
             Serial.printf("Buffer overflow\n\r");
             buffer_index = 0;
         }
     }
     delay(1);
   } 
}







Comments