Introduction: SmartBin

Este é um projeto para um sistema inteligente de coletas, no qual os caminhões de lixo recebem dados das lixeiras, identificando a quantidade de lixo presente em cada uma delas, e uma rota de coleta traçada, com base nas informações recuperadas.

Para montar este projeto, é necessário:

  • NodeMCU
  • Sensor Ultrassônico de Distancia
  • Caixa de papelão
  • Protoboard
  • Cabos
  • Dispositivo Android

Step 1: Conectando O Sensor

Primeiramente, vamos efetuar a conexão entre o sensor ultrassônico e o NODEMCU. Para tanto, vamos conectar as portas trigger e echo do sensor nas portas D4 e D3 do NodeMCU:

----------------------

// defines pins numbers
#define pino_trigger 2 //D4

#define pino_echo 0 //D3

----------------------

Para efetuar a leitura dos dados do sensor, foi seguido o tutorial elaborado pelo FilipeFlop , disponível aqui.

----------------------

float cmMsec, inMsec;

long microsec = ultrasonic.timing();

cmMsec = ultrasonic.convert(microsec, Ultrasonic::CM);

inMsec = ultrasonic.convert(microsec, Ultrasonic::IN);

//Exibe informacoes no serial monitor

Serial.print("Distancia em cm: ");

Serial.print(cmMsec);

Serial.print(" - Distancia em polegadas: ");

Serial.println(inMsec);

String data = String(cmMsec);

Serial.println(data);

----------------------

Step 2: Montando a Lixeira

Agora, vamos montar a lixeira inteligente. Precisaremos conectar o sensor ultrassônico no “teto” da lixeira. Para o exemplo, utilizei um cabo e fita isolante. Em seguida, temos que medir a distância inicial, para saber o valor para a lixeira vazia. No meu caso, foi de 26,3cm. Esse é o valor que considerarmos para uma lixeira vazia.

Para simulação, visto que não possuo mais de um sensor ultrassônico, foi feito um algoritmo para salvar randomicamente a distancia lida em 4 lixeiras diferentes.

----------------------

//Simulando 4 lixeiras

long lixeiraID;

void loop() {

...

lixeiraID = random(1, 5);

}

----------------------

Step 3: Upload Para a Nuvem

Agora, precisamos enviar estes dados para a nuvem. Eu escolhi o ThingSpeak, por familiaridade com o mesmo. Primeiramente, é necessário criar um novo canal, recebendo 4 parâmetros, referentes ao volume de cada lixeira.

Pará conectar a aplicação com o ThingSpeak, é necessário salvar o número da API do canal criado. Siga os passos descritos no site oficial.

De volta à aplicação, vamos utilizar a biblioteca ESP8266WiFi.h para efetuar conexão com o ThingSpeak, e transferir os dados.

Primeiramente, uma função para efetuar conexão com a rede (defina previamente duas variáveis, ssid[] e pass[], contendo o identificador e a senha de sua rede).

----------------------

void connectWifi(){

Serial.print("Connecting to "+ *ssid);

WiFi.begin(ssid, pass);

while (WiFi.status() != WL_CONNECTED) {

delay(500);

Serial.print(".");

}

Serial.println("");

Serial.print("Conectado na rede ");

Serial.println(ssid);

Serial.print("IP: ");

Serial.println(WiFi.localIP());

}

----------------------

Durante o setup, tentamos efetuar a conexão com a rede.

----------------------

void setup() {

Serial.begin(9600);

Serial.println("Lendo dados do sensor...");

//Conectando ao Wi-Fi

connectWifi();

}

----------------------

E, para enviar os dados para o ThingSpeak, basta abrir uma conexão HTTP padrão, passando o número da API e os parâmetros.

----------------------

void sendDataTS(float cmMsec, long id){

if (client.connect(server, 80)) {

Serial.println("Enviando dados para o ThingSpeak ");

String postStr = apiKey;

postStr += "&field";

postStr += id;

postStr += "=";

postStr += String(cmMsec);

postStr += "\r\n\r\n";

Serial.println(postStr);

client.print("POST /update HTTP/1.1\n");

client.print("Host: api.thingspeak.com\n");

client.print("Connection: close\n");

client.print("X-THINGSPEAKAPIKEY: " + apiKey + "\n");

client.print("Content-Type: application/x-www-form-urlencoded\n");

client.print("Content-Length: ");

client.print(postStr.length());

client.print("\n\n");

client.print(postStr);

delay(1000);

}

client.stop();

}

----------------------

O primeiro parâmetro corresponde à distância em centímetros encontrada pelo sensor ultrassônico. O segundo parâmetro é o ID da lixeira que foi lida (que foi gerado randomicamente, um número de 1 a 4).

O ID da lixeira serve também para identificar para qual campo será feito o upload do valor lido.

Step 4: Recuperando Dados Do ThingSpeak

O ThingSpeak permite efetuar leitura dos dados do seu canal, através de um serviço retornando um JSON. As diferentes opções para leitura do feed do seu canal estão descritas aqui:

https://www.mathworks.com/help/thingspeak/get-a-ch...

Neste projeto, optou-se por ler diretamente os dados de cada campo. O padrão de URL para este cenário é:

https://api.thingspeak.com/channels/CHANNEL_ID/fields/FIELD_NUMBER/last.json?api_key=API_KEY&status=true

Cada campo está descrito no link informado previamente. Os mais importantes para o projeto são:

  • CHANNEL_ID: número do seu canal
  • FIELD_NUMBER: o número do campo
  • API_KEY: a chave de API do seu canal

Esta é a URL que será lida do aplicativa Android, para recuperar os dados do ThingSpeak.

Step 5: Criando a Aplicação Android

No Android Studio, crie um novo projeto Android. Para o correto funcionamento da aplicação, é necessário configurar as permissões abaixo no AndroidManifest.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<uses-permission android:name="com.androidtutorialpoint.mymapsappsdirection.permission.MAPS_RECEIVE"/>

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>

Para utilizar o Google Maps, será necessário pegar uma chave junto ao Google. Siga os passos descritos no link Obter chave de API.

Uma vez com a chave, você deve também configurá-la na aplicação.

<!--The API key for Google Maps-based APIs is defined as a string resource.

(See the file "res/values/google_maps_api.xml"). Note that the API key is linked to the encryption key used to sign the APK. You need a different API key for each encryption key, including the release key that is used to sign the APK for publishing. You can define the keys for the debug and release targets in src/debug/ and src/release/. -->

<meta-data

android:name="com.google.android.geo.API_KEY" android:value="@string/google_maps_key" />

>


A configuração completa está mo arquivo AndroidManifest anexado ao projeto.



Step 6: Recuperando O Feed No Android

Na atividade principal no Android, MainActivity, crie 4 variáveis para identificar cada um dos canais do ThingSpeak a serem lidos:

private String url_a = "https://api.thingspeak.com/channels/429823/fields/1/last.json?api_key="+API_THINGSPEAK_KEY+"&status=true";
private String url_b = "https://api.thingspeak.com/channels/429823/fields/2/last.json?api_key="+API_THINGSPEAK_KEY+"&status=true"; private String url_c = "https://api.thingspeak.com/channels/429823/fields/3/last.json?api_key="+API_THINGSPEAK_KEY+"&status=true"; private String url_d = "https://api.thingspeak.com/channels/429823/fields/4/last.json?api_key="+API_THINGSPEAK_KEY+"&status=true";

Para efetuar a leitura dos dados, iremos utilizar uma classe do Android específica, chamada JSONObject. Mais uma vez, vamos criar um objeto para cada URL:

JSONObject responseLixeiraA;
JSONObject responseLixeiraB; JSONObject responseLixeiraC; JSONObject responseLixeiraD;

Para abrir a conexão com as urls, vamos usar criar uma classe auxiliar, chamada HttpJsonParser. Esta classe será responsável por abrir uma conexão com um URL, efetuar leitura dos dados encontrados, e retornar o objeto JSON montado.

public JSONObject makeHttpRequest(String url, String method,
Map params) {

try { Uri.Builder builder = new Uri.Builder(); URL urlObj; String encodedParams = ""; if (params != null) { for (Map.Entry entry : params.entrySet()) { builder.appendQueryParameter(entry.getKey(), entry.getValue()); } } if (builder.build().getEncodedQuery() != null) { encodedParams = builder.build().getEncodedQuery();

} if ("GET".equals(method)) { url = url + "?" + encodedParams; urlObj = new URL(url); urlConnection = (HttpURLConnection) urlObj.openConnection(); urlConnection.setRequestMethod(method);

} else { urlObj = new URL(url); urlConnection = (HttpURLConnection) urlObj.openConnection(); urlConnection.setRequestMethod(method); urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); urlConnection.setRequestProperty("Content-Length", String.valueOf(encodedParams.getBytes().length)); urlConnection.getOutputStream().write(encodedParams.getBytes()); } //Connect to the server urlConnection.connect(); //Read the response is = urlConnection.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder sb = new StringBuilder(); String line;

//Parse the response while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } is.close(); json = sb.toString(); //Convert the response to JSON Object jObj = new JSONObject(json);

} catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (ProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { Log.e("JSON Parser", "Error parsing data " + e.toString()); } catch (Exception e) { Log.e("Exception", "Error parsing data " + e.toString()); }

// return JSON Object return jObj;

}}

De volta a atividade principal, vamos efetuar a chamada às urls de forma assíncrona, escrevendo este código dentro do método doInBackground.

@Override
protected String doInBackground(String... params) { HttpJsonParser jsonParser = new HttpJsonParser();

responseLixeiraA = jsonParser.makeHttpRequest(url_a,"GET",null); responseLixeiraB = jsonParser.makeHttpRequest(url_b,"GET",null); responseLixeiraC = jsonParser.makeHttpRequest(url_c,"GET",null); responseLixeiraD = jsonParser.makeHttpRequest(url_d,"GET",null);

return null;
}

Quando o método doInBackgroundé encerrado, o controle de execução do Android passa para o método onPostExecute. Neste método, vamos criar os objetos Lixeira, e popular com os dados recuperados do ThingSpeak:

protected void onPostExecute(String result) {
pDialog.dismiss(); runOnUiThread(new Runnable() { public void run() {

//ListView listView =(ListView)findViewById(R.id.feedList); View mainView =(View)findViewById(R.id.activity_main); if (success == 1) { try { //Cria feedDetail para cada lixeira Lixeira feedDetails1 = new Lixeira(); Lixeira feedDetails2 = new Lixeira(); Lixeira feedDetails3 = new Lixeira(); Lixeira feedDetails4 = new Lixeira();

feedDetails1.setId('A'); feedDetails1.setPesoLixo(Double.parseDouble(responseLixeiraA.getString(KEY_FIELD1))); feedDetails1.setVolumeLixo(Double.parseDouble(responseLixeiraA.getString(KEY_FIELD1)));

feedDetails2.setId('B'); feedDetails2.setPesoLixo(Double.parseDouble(responseLixeiraB.getString(KEY_FIELD2))); feedDetails2.setVolumeLixo(Double.parseDouble(responseLixeiraB.getString(KEY_FIELD2)));

feedDetails3.setId('C'); feedDetails3.setPesoLixo(Double.parseDouble(responseLixeiraC.getString(KEY_FIELD3))); feedDetails3.setVolumeLixo(Double.parseDouble(responseLixeiraC.getString(KEY_FIELD3)));

feedDetails4.setId('D'); feedDetails4.setPesoLixo(Double.parseDouble(responseLixeiraD.getString(KEY_FIELD4))); feedDetails4.setVolumeLixo(Double.parseDouble(responseLixeiraD.getString(KEY_FIELD4)));

feedList.add(feedDetails1); feedList.add(feedDetails2); feedList.add(feedDetails3); feedList.add(feedDetails4);

//Calcula dados das lixeiras SmartBinService calculator = new SmartBinService(); calculator.montaListaLixeiras(feedList);

//Recupera componentes TextView createDate = (TextView) mainView.findViewById(R.id.date); ListView listaDeLixeiras = (ListView) findViewById(R.id.lista); adapter.addAll(feedList);

//Data atual Date currentTime = Calendar.getInstance().getTime(); SimpleDateFormat simpleDate = new SimpleDateFormat("dd/MM/yyyy"); String currentDate = simpleDate.format(currentTime); createDate.setText(KEY_DATE + currentDate + " "); listaDeLixeiras.setAdapter(adapter);

} catch (JSONException e) { e.printStackTrace(); }

} else { Toast.makeText(MainActivity.this, "Some error occurred while loading data", Toast.LENGTH_LONG).show();

} } }); }

Agora, na tela inicial do aplicativo, serão listados os dados de cada lixeira.

Step 7: Mostrando No Mapa

Ainda na atividade principal, vamos adicionar uma ação a ser relacionada ao botão Mapa, na tela inicial.

/** Called when the user taps the Mapa button */
public void openMaps(View view) { Intent intent = new Intent(this, LixeiraMapsActivity.class);

//Passa a lista de lixeiras Bundle bundle = new Bundle(); bundle.putParcelableArrayList("lixeiras", feedList); intent.putExtras(bundle);

startActivity(intent); }

No mapa, temos três atividades a executar:

  1. marcar a posição atual do caminha de lixo
  2. marcar os pontos correspondentes a cada lixeira no mapa
  3. traçar a rota entre os pontos

Para executar os passos acima, vamos usar a API Google Directions. Para desenhar as rotas, foram seguidos os passos do tutorial Drawing driving route directions between two locations using Google Directions in Google Map Android API V2

Primeiro, vamos criar localidades para cada um dos pontos que desejamos marcar:

//Locations

    private LatLng current;    private LatLng lixeiraA;    private LatLng lixeiraB;    private LatLng lixeiraC;    private LatLng lixeiraD;.

Para adicionar a posição atual no mapa, foi criado o método:

private void checkLocationandAddToMap() {
//Checking if the user has granted the permission if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { //Requesting the Location permission ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_REQUEST_CODE); return; }

//Fetching the last known location using the Fus Location location = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);

//MarkerOptions are used to create a new Marker.You can specify location, title etc with MarkerOptions this.current = new LatLng(location.getLatitude(), location.getLongitude()); MarkerOptions markerOptions = new MarkerOptions().position(current).title("Posição atual");

//Adding the created the marker on the map, moving camera to position markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)); System.out.println("+++++++++++++ Passei aqui! +++++++++++++"); mMap.addMarker(markerOptions);

// Move the camera instantly to location with a zoom of 15. mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(current, 15));

// Zoom in, animating the camera. mMap.animateCamera(CameraUpdateFactory.zoomTo(14), 2000, null);

}

Em seguida, para cada lixeira, foram criados métodos similares ao abaixo:

private void addBinALocation() {
//Checking if the user has granted the permission if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { //Requesting the Location permission ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_REQUEST_CODE); return; }

//Praça da Estação double latitude = -19.9159578; double longitude = -43.9387856; this.lixeiraA = new LatLng(latitude, longitude);

MarkerOptions markerOptions = new MarkerOptions().position(lixeiraA).title("Lixeira A"); markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)); mMap.addMarker(markerOptions); }

As posições de latitude e longitude de cada lixeira foram recuperadas através do próprio Google Maps, e deixadas fixas no código. Idealmente, estes valores ficariam salvos em um banco de dados (por exemplo Firebase). Será a primeira evolução deste projeto!

O último passo agora é traçar as rotas entre os pontos. Para tal, um conceito muito importante, e que será utilizado neste projeto, são os Waypoints!

Foi criado um método para traçar a rota entre dois dados pontos:

private String getDirectionsUrl(LatLng origin,LatLng dest, List waypointsList){

// Origin of route String str_origin = "origin="+origin.latitude+","+origin.longitude;

// Destination of route String str_dest = "destination="+dest.latitude+","+dest.longitude;

//Waypoints along the route //waypoints=optimize:true|-19.9227365,-43.9473546|-19.9168006,-43.9361124 String waypoints = "waypoints=optimize:true"; for (LatLng point : waypointsList){ waypoints += "|" + point.latitude + "," + point.longitude; }

// Sensor enabled String sensor = "sensor=false";

// Building the parameters to the web service String parameters = str_origin+"&"+str_dest+"&"+sensor + "&" + waypoints;

// Output format String output = "json";

// Building the url to the web service String url = "https://maps.googleapis.com/maps/api/directions/"+output+"?"+parameters; System.out.println("++++++++++++++ "+url);

return url; }

E, por fim, juntando tudo no método principal da classe, onMapReady:

@Override
public void onMapReady(GoogleMap googleMap) { mMap = googleMap;

checkLocationandAddToMap();

if (lixeirasList.get(0).getVolumeLixo() > Lixeira.MIN_VOLUME_GARBAGE || lixeirasList.get(0).getPesoLixo()-10 > Lixeira.MIN_SIZE_GARBAGE){ addBinALocation(); } if (lixeirasList.get(1).getVolumeLixo() > Lixeira.MIN_VOLUME_GARBAGE || lixeirasList.get(1).getPesoLixo() > Lixeira.MIN_SIZE_GARBAGE){ addBinBLocation(); } if (lixeirasList.get(2).getVolumeLixo() > Lixeira.MIN_VOLUME_GARBAGE || lixeirasList.get(2).getPesoLixo() > Lixeira.MIN_SIZE_GARBAGE){ addBinCLocation(); } if (lixeirasList.get(3).getVolumeLixo() > Lixeira.MIN_VOLUME_GARBAGE || lixeirasList.get(3).getPesoLixo() > Lixeira.MIN_SIZE_GARBAGE){ addBinDLocation(); }

//Draw routes

// Getting URL to the Google Directions API List points = new ArrayList<>(); points.add(lixeiraB); points.add(lixeiraC); points.add(lixeiraD);

String url = getDirectionsUrl(current, lixeiraA, points); DownloadTask downloadTask = new DownloadTask(); // Start downloading json data from Google Directions API downloadTask.execute(url); }

Aqui passamos apenas pelos pontos principais. O código completo do projeto será disponibilizado para consulta.

Step 8: Conclusão

Este foi um projeto trabalhando conceitos de IoT, mostrando uma das várias opções de conectar dispositivos através da nuvem, e efetuar tomada de decisões sem interferência humana direta. Em anexo, segue um vídeo do projeto completo, para ilustração, e os fontes das atividades criadas no Android.