Léa AubertNicolas DAILLYThéo Didime
Published

Connected pet feeder with LoRa and Android

Discover our connected pet feeder controlled by an Android application, using the LoRa network and made with 3D printed parts.

AdvancedShowcase (no instructions)Over 1 day325
Connected pet feeder with LoRa and Android

Things used in this project

Hardware components

SODAQ ONE V3
SODAQ ONE V3
The SodaQ board has a LoRa built in system.
×1
WunderBar
Relayr WunderBar
The relay to pilot the pump.
×1
EZO-PMP™ Peristaltic Pump
Atlas Scientific EZO-PMP™ Peristaltic Pump
The pump with a little hose to carry the water from the tank to the bowl.
×1
Stepper motor driver board A4988
SparkFun Stepper motor driver board A4988
The stepper motor that rotates the worm screw.
×1
Weight sensor
To get the weight of the two bowls.
×2

Software apps and online services

Fusion 360
Autodesk Fusion 360
To make the 3D printed parts.
Node-RED
Node-RED
Firebase
Google Firebase
To store our datas.
Android Studio
Android Studio
To make our application.
The Things Network
The Things Network
To register our object and to customize our payload format.

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Laser cutter (generic)
Laser cutter (generic)

Story

Read more

Custom parts and enclosures

Worm screw

This worm screw is powered by the stepper motor and its purpose is to push the kibbles toward the bowl.

Kibbles tank

This tank is used to store the kibbles before they are conveyed to the bowl thanks to the motor and the worm screw.

Regular bracket

Used to connect two perpendicular sides.

Thick bracket

We used these brackets to connect perpendicular sides so that one side can be slid open.

Code

Node-RED : example of the downlink branch

JSON
This code shows how to retrieve data from TTN.
[{"id":"69fbc9aa.756748","type":"tab","label":"Flow 6","disabled":false,"info":""},{"id":"b9fbbae3.636768","type":"mqtt in","z":"69fbc9aa.756748","name":"","topic":"+/devices/+/up","qos":"2","datatype":"json","broker":"d71c1575.3ac6e8","x":100,"y":180,"wires":[["5571350b.8ceb9c","a71fee97.830e6","913906c7.1e0f78"]]},{"id":"5571350b.8ceb9c","type":"debug","z":"69fbc9aa.756748","name":"Water","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":140,"wires":[]},{"id":"a71fee97.830e6","type":"debug","z":"69fbc9aa.756748","name":"Kibbles","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.kibbles","targetType":"msg","statusVal":"","statusType":"auto","x":360,"y":340,"wires":[]},{"id":"913906c7.1e0f78","type":"json","z":"69fbc9aa.756748","name":"Payload","property":"payload","action":"obj","pretty":false,"x":500,"y":200,"wires":[["e4d0fbfe.734238","8ac5f97a.3b4e38","4cb24446.02319c"]]},{"id":"8ac5f97a.3b4e38","type":"function","z":"69fbc9aa.756748","name":"decode payload","func":"msg.payload = {\n        device : msg.payload.dev_id,\n        kibbles :msg.payload.payload_fields.kibbles,\n        water: msg.payload.payload_fields.water,\n        tank: msg.payload.payload_fields.tank\n\n    }\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":780,"y":240,"wires":[["8ceab3ff.717bf","fbecf802.b7ebc8"]]},{"id":"e4d0fbfe.734238","type":"debug","z":"69fbc9aa.756748","name":"Water","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.payload_fields.water","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":420,"wires":[]},{"id":"4cb24446.02319c","type":"debug","z":"69fbc9aa.756748","name":"Kibbles","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.payload_fields.kibbles","targetType":"msg","statusVal":"","statusType":"auto","x":600,"y":500,"wires":[]},{"id":"3f27fd2f.4b0562","type":"http request","z":"69fbc9aa.756748","name":"","method":"POST","ret":"txt","paytoqs":"ignore","url":"https://firestore.googleapis.com/v1/projects/kibbles-2a454/databases/(default)/documents/gamelles","tls":"","persist":false,"proxy":"","authType":"","x":1390,"y":260,"wires":[["4d96a1fd.69d73"]]},{"id":"8ceab3ff.717bf","type":"function","z":"69fbc9aa.756748","name":"","func":"msg.payload ={\n    \"fields\": {\n         \"timestamp\": {\n            \"stringValue\": new Date().toISOString() \n        },\n        \"kibbles_tank\": {\n            \"stringValue\": msg.payload.tank.toString()\n        },\n        \"water\": {\n            \"stringValue\": msg.payload.water.toString()\n        },\n        \"kibbles\": {\n            \"stringValue\": msg.payload.kibbles.toString()\n        },\n    },\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1080,"y":240,"wires":[["3f27fd2f.4b0562"]]},{"id":"4d96a1fd.69d73","type":"debug","z":"69fbc9aa.756748","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1570,"y":260,"wires":[]},{"id":"b723c8ed.0850b8","type":"comment","z":"69fbc9aa.756748","name":"Link to cut/re-do","info":"Cut the link between function and http request when not using it","x":1360,"y":420,"wires":[]},{"id":"fbecf802.b7ebc8","type":"debug","z":"69fbc9aa.756748","name":"Kibbles","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.tank","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":360,"wires":[]},{"id":"d71c1575.3ac6e8","type":"mqtt-broker","name":"gamelle","broker":"eu.thethings.network","port":"8883","tls":"e76c8c34.f6e3a","clientid":"","usetls":true,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"e76c8c34.f6e3a","type":"tls-config","name":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"","verifyservercert":true}]

Node-RED : example of an uplink branch

JSON
This code shows how to process the POST query sent by the Android App when the user clicks on a button to refill the water bowl.
[{"id":"69fbc9aa.756748","type":"tab","label":"Flow 6","disabled":false,"info":""},{"id":"a7f4a608.1da528","type":"http in","z":"69fbc9aa.756748","name":"","url":"/refill","method":"post","upload":false,"swaggerDoc":"","x":100,"y":80,"wires":[["e4be7429.ecb408","476b2408.fc444c"]]},{"id":"e4be7429.ecb408","type":"debug","z":"69fbc9aa.756748","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":390,"y":220,"wires":[]},{"id":"476b2408.fc444c","type":"switch","z":"69fbc9aa.756748","name":"switch value","property":"payload.action","propertyType":"msg","rules":[{"t":"eq","v":"refill_kibbles","vt":"str"},{"t":"eq","v":"refill_water","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":450,"y":80,"wires":[["c7521a97.34b238"],["77edf664.3d83a8"]]},{"id":"2acebd13.0caad2","type":"mqtt out","z":"69fbc9aa.756748","name":"","topic":"gamelle/devices/gamelle_board/down","qos":"","retain":"","broker":"d71c1575.3ac6e8","x":990,"y":20,"wires":[]},{"id":"a6e2e396.e7b5c","type":"debug","z":"69fbc9aa.756748","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1010,"y":100,"wires":[]},{"id":"c7521a97.34b238","type":"function","z":"69fbc9aa.756748","name":"","func":"msg.payload =\n \n { \"dev_id\": \"gamelle_board\", // The device ID\n  \"port\": 1,             // LoRaWAN FPort\n  \"confirmed\": false,    // Whether the downlink should be confirmed by the device\n  \"payload_fields\": {    // The JSON object to be encoded by your encoder payload function\n    \"kibbles\" : true,\n  }\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":640,"y":60,"wires":[["a6e2e396.e7b5c","5111b8de.b01358"]]},{"id":"38d43eb3.911a72","type":"mqtt out","z":"69fbc9aa.756748","name":"","topic":"gamelle/devices/gamelle_board/down","qos":"","retain":"","broker":"d71c1575.3ac6e8","x":990,"y":180,"wires":[]},{"id":"7ee35303.4b2e7c","type":"debug","z":"69fbc9aa.756748","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":910,"y":260,"wires":[]},{"id":"77edf664.3d83a8","type":"function","z":"69fbc9aa.756748","name":"","func":"msg.payload =\n \n { \"dev_id\": \"gamelle_board\", // The device ID\n  \"port\": 1,             // LoRaWAN FPort\n  \"confirmed\": false,    // Whether the downlink should be confirmed by the device\n  \"payload_fields\": {    // The JSON object to be encoded by your encoder payload function\n    \"water\" : true,\n  }\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":640,"y":220,"wires":[["38d43eb3.911a72","7ee35303.4b2e7c"]]},{"id":"f66491fe.ddfb7","type":"comment","z":"69fbc9aa.756748","name":"Si signal = remplissage de croquettes","info":"","x":590,"y":20,"wires":[]},{"id":"c875a494.309238","type":"comment","z":"69fbc9aa.756748","name":"Si signal = remplissage d'eau","info":"","x":560,"y":160,"wires":[]},{"id":"5111b8de.b01358","type":"json","z":"69fbc9aa.756748","name":"Payload","property":"payload","action":"obj","pretty":false,"x":780,"y":80,"wires":[["2acebd13.0caad2"]]},{"id":"acd20c6b.bfe6f","type":"debug","z":"69fbc9aa.756748","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":770,"y":340,"wires":[]},{"id":"d71c1575.3ac6e8","type":"mqtt-broker","name":"gamelle","broker":"eu.thethings.network","port":"8883","tls":"e76c8c34.f6e3a","clientid":"","usetls":true,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"e76c8c34.f6e3a","type":"tls-config","name":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"","verifyservercert":true}]

Android Studio : Main Activity source

Java
This is the java code from our main activity, so that you can understand how we made our connections with Cloud Firestore.
package com.example.kibbles;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.QuerySnapshot;

import org.json.JSONException;
import org.json.JSONObject;

public class MainActivity extends AppCompatActivity {
    FirebaseFirestore db;
    String TAG;
    String TB_content;
    String water;
    String kibbles;
    String timestamp;

    Button btn_tanks;
    Button btn_mode_manuel;
    Button btn_mode_auto;
    Button btn_refillKibbles;
    Button btn_refillWater;

    TextView TV_kibbleContent;
    TextView TV_waterContent;
    TextView TV_curentContent;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = FirebaseFirestore.getInstance();

        btn_tanks = findViewById(R.id.btn_tanks);
        btn_mode_manuel = findViewById(R.id.btn_mode_manuel);
        btn_mode_auto = findViewById(R.id.btn_mode_auto);
        btn_refillKibbles = findViewById(R.id.btn_refillKibbles);
        btn_refillWater = findViewById(R.id.btn_refillWater);

        TV_curentContent = findViewById(R.id.TV_curentContent);
        TV_waterContent = findViewById(R.id.TV_waterContent);
        TV_kibbleContent = findViewById(R.id.TV_kibblesContent);

        getDatasFromFirebase();
        btn_tanks.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intentTanksActivity = new Intent(getApplicationContext(), TanksActivity.class);
                startActivity(intentTanksActivity);
            }
        });

        btn_mode_manuel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                modePost("manuel");
            }
        });

        btn_mode_auto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                modePost("auto");
            }
        });

        btn_refillKibbles.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                refillPost("refill_kibbles");
            }
        });

        btn_refillWater.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                refillPost("refill_water");
            }
        });

    }

    public void refillPost(String action){
        String postUrl = "<Node-RED IP>/refill";
        RequestQueue requestQueue = Volley.newRequestQueue(this);

        JSONObject postData = new JSONObject();
        try {
            postData.put("action", action);

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

        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST, postUrl, postData, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                System.out.println(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                error.printStackTrace();
            }
        });

        requestQueue.add(jsonObjectRequest);
    }
    public void modePost(String mode){
        String postUrl = "<Node-RED IP>/mode";
        RequestQueue requestQueue = Volley.newRequestQueue(this);

        JSONObject postData = new JSONObject();
        try {
            postData.put("mode", mode);

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

        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST, postUrl, postData, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                System.out.println(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                error.printStackTrace();
            }
        });

        requestQueue.add(jsonObjectRequest);
    }

    public void getDatasFromFirebase(){
        db.collection("gamelles").orderBy("timestamp", Query.Direction.DESCENDING).limit(1)
                .get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            for (QueryDocumentSnapshot document : task.getResult()) {
                                Log.d(TAG, document.getId() + " => " + document.getData());
                                timestamp = (String)document.getString("timestamp");

                                kibbles = (String)document.getString("kibbles");
                                water = (String)document.getString("water");

                                TV_waterContent.setText(water + " mL");
                                TV_kibbleContent.setText(kibbles + " g");
                                TV_curentContent.setText("Contenu actuel (maj : " +timestamp);
                            }
                        } else {
                            Log.w(TAG, "Error getting documents.", task.getException());
                        }
                    }
                });
    }
}

Credits

Léa Aubert

Léa Aubert

1 project • 1 follower
Engineering student in ESIEE Amiens
Nicolas DAILLY

Nicolas DAILLY

3 projects • 6 followers
Associated Professor at UniLaSalle - Amiens / Head of the Computer Network Department / Teach Computer and Telecommunication Networks
Théo Didime

Théo Didime

1 project • 1 follower

Comments

Add projectSign up / Login