---
# System prepended metadata

title: Smart Litter Box

---

*Created and written by Gustav Kånåhols (gk222kj).*

**Table of Contents**

[TOC]


I've created a system which gathers and stores data about a cat litter box and reminds its owner (me) to clean the box when needed. The system consists of an IoT device with sensors which attaches to the box, hereafter refered to as the Box Unit (BU), and a server running a number of services including a MQTT-broker.

The project will take anywhere from 4-24+ hours depending on your experience with docker and Linux in general as well as on whether you decide to 3D-print the box and solder the final circuit or not.

Code, schematics and docker files can be found in the [GitHub repository](https://github.com/Kurbitz/litter-box-detector)

### Objective
I chose this project because I wanted to create something to help me with a chore I don't like doing.  The data gathered by the system will be used to remind me to clean the box when needed and to keep track of how often I clean it. I want to use the data in order to get a better understanding of the cat's behavior and how it affects the frequency of cleaning. I also want to see if there are any patterns in the data that can be used to predict when the box needs to be cleaned.

One of my goals was to host everything locally, meaning I wouldn't have to rely on any third-party services. I also wanted to use as much open-source software as possible. This would allow me to customize the system to my liking and to add new features if I wanted to. I would also be able to host the system on my LAN wihtout transmitting any data over the internet. This would allow me to keep the data private and to avoid any potential security issues.

### Material
Below is a list of all the components used in the project. The prices are in SEK. Items marked with "*" are optional.
| Component | Price | Link|
| -------- | -------- | -------- |
| NodeMCU ESP8266   | 149.00 | [ElectroKit](https://www.electrokit.com/produkt/nodemcu-v3/) |
| PIR-sensor | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/pir-rorelsedetektor-hc-sr501/) |
| DHT11 | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/digital-temperatur-och-fuktsensor-dht11/) |
| *LCD 1602 display (any HD44780-compatible display will work) | 99.00 | [ElectroKit](https://www.electrokit.com/produkt/lcd-2x16-tecken-jhd162a-stn-bla-vit-led/) |
| *I2C LCD backpack | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/i2c-interface-for-lcd/) |
| *Button (any push-button will work) | 69.90 | [Kjell & Company](https://www.kjell.com/se/produkter/el-verktyg/elektronik/elektromekanik/strombrytare-for-elektronik/tryckstrombrytare/strombrytare-1-pol-frantill-p36134) |

In addition to the components listed above you'll also need a breadboard, a bunch of jumper wires and a USB-cable to power the NodeMCU. I chose not to include these in the list since they're not really components. The breadboard and jumper wires can be bought from ElectroKit. The USB-cable can be bought from pretty much anywhere. I used a USB-cable from an old phone charger. I chose to solder the final circuit together on a perfboard but you could just as well use a breadboard. The perfboard and soldering equipment can be bought from basically any electronics store (like ElectroKit) or borrowed at a local makerspace.

#### Microcontroller
The microcontroller used is a NodeMCU ESP8266. It was chosen over the Pico (which was recommended by the course) because it's far more popular and has a lot more support. I also had a couple of them lying around from previous projects. The NodeMCU is a development board with a ESP8266 microcontroller, a USB port for power and programming, and a bunch of GPIO pins. It's also got a built-in WiFi module which is what I'll be using to send data to the server.
![](https://i.imgur.com/B7FEXUm.jpg =500x)

#### Sensors
The sensors used are a DHT11 and a PIR-sensor. 
##### DHT11
The DHT11 is a temperature and humidity sensor. It's not very accurate but it's cheap and easy to use.
![](https://i.imgur.com/XeiU8xs.jpg =400x)
##### PIR-sensor
The PIR-sensor is a motion sensor which is used to detect when the cat is in the box. It's also quite cheap and readily available.
![](https://i.imgur.com/qgbTDfC.jpg =400x)

#### Input and output
In addition to the sensors I added a button, LCD-display and a status-LED. All of this is not strictly necessary but it makes the system a lot more stylish and user-friendly. One could make do with just the sensors and the microcontroller and control the system through something like a web interface instead. But overkill is the best kind of kill.

##### Button
The button is a generic panel-mount push-button. The size and shape doesn't really matter as long as it fits in the box. Modifications to the box might be necessary depending on the button you choose. The button is used to register when the box is cleaned. 
![](https://i.imgur.com/UtMr9Ze.png =200x200)

##### LCD-display
The LCD is a 1602 display with a HD44780-compatible controller. It's attached to the NodeMCU using a I2C LCD backpack which makes it a lot easier to wire up and use. Again, modifications to the box might be necessary depending on the display you choose. 
![](https://i.imgur.com/QuWDt0n.jpg =400x)
![](https://i.imgur.com/hAFIsXy.jpg =400x)

##### Case
The box unit was assembled using a 3D-printed case. The case was designed in Fusion 360 and printed on a Prusa mini. The STL and STEP-files can be found in the [GitHub repository](https://https://github.com/Kurbitz/litter-box-detector/tree/main/models). The case consists of two parts, the bottom and the lid. The bottom part has four standoffs for mounting the PCB and lugs in the corners for mounting the case to the litter box. The lid has slots for the LCD, button and status-LED. The lid is attached to the bottom using four M3 screws. The case was printed using PLA with 20% infill and 0.2mm layer height. The print took about 4 hours to complete. If you don't own a 3D-printer you can probably find someone who does at a local makerspace. Alternatively you could just use a cardboard box or something similar.

Rendering of the case:
Top part
![](https://i.imgur.com/uBWZvG5.png)
Bottom part
![](https://i.imgur.com/gp8Wcuo.png)

### Computer setup
To set up the development environment you'll need to install a few dependencies. I will give a brief overview of the steps needed to set up the environment on Linux. If you're using Windows or MacOS some of the steps will be different. I will not go into detail on how to set up the environment on those platforms but I will try to provide links to the proper documentation.

When finished you should have the following installed:
- Python 3
- VSCode - *(The IDE used to write code)*
- PyMakr plugin for VSCode - *(Used to upload code to the NodeMCU)*
- Node.js - *(Used by the PyMakr plugin)*
- esptool - *(Used to flash the NodeMCU)*

You will also need to download the MicroPython firmware for the NodeMCU. You can download it from [here](https://micropython.org/download/esp8266/). Make sure to download the latest stable version. The firmware is a binary file which you will need to flash to the NodeMCU. More on that later.

Note that if you want linting and code completion you'll also need a few more things. I will not go into detail on how to set that up but you can find information about it [here](https://micropython-stubber.readthedocs.io/en/latest/intro.html).

#### Python 3
The first thing you'll need is Python 3. If you're using Linux you probably already have it installed. If not you can install it using your package manager. On Ubuntu you can install it using the following command:

```sudo apt install python3```

#### VSCode
The next thing you'll need is VSCode. You can download it from [here](https://code.visualstudio.com/). Once you've downloaded it you can install it by following the instructions on the download page.

#### PyMakr plugin
The PyMakr plugin is used to upload code to the NodeMCU. You can install it by opening VSCode and clicking on the extensions tab in the sidebar. Search for PyMakr and click install. Once it's installed you'll need to restart VSCode.

#### Node.js
To get the PyMakr plugin to work you'll need to install Node.js. On Ubuntu you can install it using the following command:

```sudo apt install nodejs```

#### esptool
The last thing you'll need is esptool. It's used to flash the NodeMCU with MicroPython. You can install it from PyPI by running the following command:

```pip install esptool```

More information on how to install esptool can be found [here](https://docs.espressif.com/projects/esptool/en/latest/esp8266/installation.html)

#### Flashing the NodeMCU

Once you've installed all the dependencies you're ready to flash the NodeMCU with MicroPython. To do this you'll need to connect the NodeMCU to your computer using a USB-cable. Once it's connected you'll need to find out which port it's connected to. On Linux you can do this by running the following command:

```ls /dev/ttyUSB*```

The output should be something like ```/dev/ttyUSB0```. If you're using Windows or MacOS you can find instructions on how to find the port [here](https://docs.micropython.org/en/latest/esp8266/tutorial/intro.html#deploying-the-firmware). The port will probably be something like ```COM1``` on Windows and ```/dev/cu.usbserial-0001``` on MacOS.

Now that you know which port the NodeMCU is connected to you can flash it with MicroPython. To do this you'll need to run the following command:

```esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect -fm dout 0 esp8266-20230426-v1.20.0.bin```

Replace ```/dev/ttyUSB0``` with the port you found earlier and ```esp8266-20230426-v1.20.0.bin``` with the name of the firmware file you downloaded from [micropython.org](https://micropython.org/download/esp8266/) earlier (make sure you're using the correct path).


### Putting everything together
#### Overview
The circuit consists of the NodeMCU, the DHT11, the PIR-sensor, the button, the status-LED and the LCD-display. The NodeMCU is connected to the DHT11, the PIR-sensor, the button and the status-LED using jumper wires. The LCD-display is connected to the NodeMCU using a I2C LCD backpack. The NodeMCU is powered using a USB-cable connected to a USB-charger.

This is what the circuit looks like:
![](https://i.imgur.com/VEB1x16.png)
It could all be conected like this on a breadboard:
![](https://i.imgur.com/Jd8w56T.png)

You can find a Fritzing file of the circuit in the [GitHub repository](https://github.com/Kurbitz/litter-box-detector/blob/main/schematics/Litterbox.fzz).

#### Resistor values
The R1 resistor is used to regulate the current-draw of the LED. The NodeMCU's GPIO pins can only supply a maximum of 12mA of current. The LED I used has a forward voltage of 1.8V (Vf in the datasheet) and a forward current (If) of 25mA. Applying some Ohms we can calculate the ideal resistor value as follows:

Voltage drop (assuming 3.3V from the NodeMCU) =
3.3V - 1.8V = 1.5V

Resistor value =
1.5V / 25mA = 60Ω

The closest resistor value I had was 100Ω. But I found the LED to be a bit too bright so I ended up using a 220Ω resistor instead. This is well within the NodeMCU's maximum current-draw of 12mA. If you're interested in learning more about how to calculate resistor values you can read about it [here](https://www.evilmadscientist.com/2012/resistors-for-leds/).

The R2 resistor is used to pull the PIR-sensor's output pin high. This is because the PIR-sensor I used has an open-collector output. I won't go into detail on how open-collector outputs work but if you want to learn more you can read about it [here](https://en.wikipedia.org/wiki/Open_collector). If you're using a PIR-sensor with a push-pull output you can remove the R2 resistor. The R3 resistor is used to pull the button's input pin low. When the button is pressed the input circuit is closed and the input pin is pulled high. Both the R2 and R3 resistors should be between 1kΩ and 10kΩ. I used 10kΩ resistors for both.


#### Power draw
Estimating the power draw of the circuit is a bit tricky. We would have to look at the power draw for each individual component and add them all together. But since the LCD-display and LED are only on for certain periods of time we can't just add them together. We would have to calculate the average power draw for each component and then add them together or measure the power draw of the entire circuit using a multimeter or usage monitor (like Kill A Watt) over a longer period of time. I'll update this section if I get around to it.

### Platform
I chose to self host everything on a dedicated Ubuntu VM running on my home Proxmox VE (an open-source hypervisor) server. But everything could easily be hosted on a Raspberry Pi or any other Linux machine.

Every application in the stack is running in a separate [Docker](https://www.docker.com/) container. The containers are managed using [Docker Compose](https://docs.docker.com/compose/). This makes admin and maintenance a lot easier since I can tear down and rebuild the entire stack with a single command. 

The server stack consists of the following:
* [Eclipse Mosquitto](https://mosquitto.org/) - (MQTT-broker)
* [Node-RED](https://nodered.org/) - (Flow-based programming)
* [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/) - (Data collector)
* [InfluxDB](https://www.influxdata.com/) - (Time-series database)
* [Grafana](https://grafana.com/) - (Dashboard)

The last three form what's commonly referred to as the TIG-stack (Telegraf, InfluxDB, Grafana).


#### Eclipse Mosquitto
Mosquitto is an open-source MQTT-broker. It's used to route MQTT messages between the Box Unit and the other applications in the stack. Mosquitto is a very easy to set up and use and it's very well documented. It's also very lightweight and doesn't require a lot of resources. This makes it a great choice for a small project like this.

#### Node-RED
Node-RED is a flow-based programming tool. It allows for easy integration between different applications and services. It's a very popular automation-tool for self hosted projects and IoT. I use it to integrate with Telegram to send notifications to my phone when the box needs to be cleaned.


#### Telegraf
Telegraf is a server-based agent used to collect and send data to a database. I use it to collect data from the MQTT-broker and store it in a database. It can also be used to collect system metrics from the host machine among other things. It's a very powerful tool with a lot of plugins.

#### InfluxDB
InfluxDB is a time-series database. I use it to store the data collected by Telegraf. Since the data is tracked over time it makes sense to use a time-series database. InfluxDB is one of the most popular time-series databases and plays nicely with other tools in the TIG-stack. 

#### Grafana
Grafana is a data visualization tool. It can be used to create dashboards and graphs from the data stored in the database. I use it to visualize the data from the box unit. 
### Setting up the server
Setting up the server is quite easy if you have some experience with Docker and Linux. If you don't have any experience with Docker or Linux it might be a bit tricky. I will not go into detail on how to set up each application but I will give a brief overview of the steps needed to set up the server. If you're interested in learning more about how to set up each application you can find more information in the GitHub repository.

The process of setting up the server can be split up into the following steps:
1. Install Docker and Docker Compose
2. Clone the GitHub-repository
3. Edit the configuration files
4. Run `docker-compose up -d` to start the containers
5. Configure the applications using the web interfaces


### The code
The code is split up into two files and a library directory. The main file is called ```main.py``` and contains the main loop. The other file is called ```boot.py``` and contains code that runs when the NodeMCU boots up. The library directory contains the MicroPython libraries for the I2C LCD backpack and configuration file. The libraries are not written by me and can be found [here](https://github.com/dhylands/python_lcd/).

I won't go into detail on how every part of the code works since it's quite long and complicated. But I will give a brief overview of the different parts of the code and a few examples of interesting problems I encountered.

On boot the board connects to the WiFi and performs a connectivity test. If the test is successful it will synchronise the time with an NTP-server, this is important since the events are compared with data in the database. When the time has been synchronised it will start the main function. The main loop runs every `x` seconds and performs the following steps:
1. Read the temperature and humidity from the DHT11
2. Handle any IRQs from the PIR-sensor or button
3. Send the data to the server over MQTT
4. Update the LCD-display
5. Sleep for `x` seconds

The period of sleep is configurable and can be changed in the configuration file. The default value is 30 seconds, but could be set to something longer if operating on battery power.

#### IRQs
IRQs are interrupts that are triggered when a certain event occurs. In this case the events are either the PIR-sensor detecting motion or the button being pressed. When an IRQ is triggered the corresponding function is called. The function then sets a flag which is checked in the main loop. This allows for "asynchronous" handling of events. The main loop doesn't have to wait for the event to occur, it just checks if the flag has been set.

#### Handling global state
To avoid defining global variables I used a class to store the global state. This allows me to access the global state from anywhere in the code without having to define global variables just by passing the class as an argument to the function. Since classes are passed by reference any changes made to the class will be reflected in the global state. This was enormously helpful when handling IRQs. I could just pass the state class to the IRQ handler and set the flag in the class. The main loop would then check the flag and perform the necessary actions. 

Example of how the state class is used:
```python
pir.irq(trigger=Pin.IRQ_FALLING, handler=lambda _: pir_handler(state))
```

Notice how the handler funciton is actually a lambda function. This is because the handler function of the IRQ takes the pin as an argument. Since we don't need the pin we can just ignore it by using a lambda function and passing the state class as an argument instead.

The `pir_handler` function could then set the flag in the state class like this:
```python
def pir_handler(state: SensorState):
    state.motion_detected = True
```

#### Long and short press detection
To detect both long and short presses I used a timer. When the button a function is called which starts a timer. This function can determine whether the button was pressed for a short or long time by checking how much time has passed since the timer was started. Depending on whether the button was pressed for a short or long time a different callback function is called. 

This also handles debouncing of the button, preventing multiple events from being triggered by a single button press. This is by far the most complicated part of the code, but the end result is quite elegant. If you're interested in learning more about how it works you can check out the code in the GitHub repository. 

Here's an example of how the timer is used:
```python
timer.init(
        period=100,
        mode=Timer.ONE_SHOT,
        callback=lambda x: button_handler(
            timer=x,
            state=state,
            button=button,
            long_press_time=2000,
            short_press_callback=short_press_handler,
            long_press_callback=long_press_handler,
        ),
    )
```

#### Getting the last event from the database
When the box is powered off it loses track of time. This means that when it's powered on again it doesn't know when the last event occurred. This is a problem since the box needs to know when the last event occurred in order to determine whether it needs to be cleaned or not. To solve this problem I added a function that gets the last event from the database. This involves running a manual HTTP query against the database. Since the response from the database is in CSV format I had to parse the response and convert it to a timestamp. This was a bit tricky but I managed to get it working. The function returns a timestamp which is then used to determine whether the box needs to be cleaned or not.

### Transmitting the data / connectivity
The data is transmitted to the server over MQTT at regular intervals defined in the configuration file. The data is sent as a JSON object containing the temperature, humidity, motion and button state. All the data is sent in a single message. This is done to reduce the number of messages sent to the server. The messsage looks something like this:
```json
{
  "humidity": 36,
  "reset_pressed": 0,
  "motion_detected": 0,
  "temperature": 26
}
```

Using WiFi was an obvious choice since the NodeMCU has a built-in WiFi module and the device is always indoors, in range of my WiFi network. I chose to use MQTT since it's a very lightweight protocol and is well suited for IoT. If running on battery power it's possible I'd change strategy but since the box is powered by a USB-cable I don't have to worry too much energy consumption.

### Presenting the data
InfluxDB has a built-in retention policy which allows you to specify how long data should be preserved in the database. I've set the retention policy to 1 year. This means that data older than 1 year will be deleted from the database. This is more than enough for my use case.

All the data is presented in a single dashboard. It plots the temperature and humidity over time and shows the current state of the box (last cleaned and last used).
![](https://i.imgur.com/sjT6cUr.png)

I've also created a flow in Node-RED which allows me to get the current status of the litterbox via Telegram.
![](https://i.imgur.com/FsFBfDF.png)
![](https://i.imgur.com/eJ7rwbU.png)

Additional monitoring could easily be added now that everything is in place, such as alerts when the box needs to be cleaned or when the box hasn't been used for a long time.

### Finalizing the design
#### Results
The system works as intended. The box unit gathers data about the litter box and sends it to the server. The server stores the data in a database and visualizes it using a dashboard. 

Here are some pictures of the box unit
![](https://i.imgur.com/hfOKo66.jpg)
Mounted on the litter box
![](https://i.imgur.com/s45rup1.jpg)

#### Video
{%youtube -1Permz3gXA%}

#### Circuit
I'm in the proccess of soldering the final circuit together on a perfboard. In the meantime I'm using a breadboard. I have some pictures of an earlier version of the circuit soldered together on a perfboard, the final version will look similar to this:

![](https://i.imgur.com/rDJXYCe.jpg)

#### Reflections
The project was a success. I managed to create a system that gathers and stores data about a cat litter box and reminds me to clean the box when needed. I also managed to create a dashboard to visualize the data. I'm very happy with the result and I'm looking forward to using the system. Some of the more advanced insights I wanted to get from the data will have to wait until I've gathered more data. But I'm confident that I'll be able to get some interesting insights from the data in the future.

I managed to fry my microcontroller the second to last week of the project. I'm not sure what happened but I think I might have shorted something when I was soldering the circuit together. I had to order a new one which took a few days to arrive. This meant that I had to rush the last week of the project. I didn't have time to implement all the features I wanted to and only got to building the dashboard/visualizations at the very end. But I managed to get everything working in time for the deadline by working a few late nights.

#### Future work
If I had more time I would have designed and printed a sensor unit as well, some sort of enclosure which bundles the DHT11 and PIR-sensor together and provides an easy way to mount the sensors to the litter box. The PIR-sensor is currently attached by only electrical tape, which is not ideal.

I also have a load cell and HX711 amplifier which I wanted to use to measure the weight of the litter box. I would have used this data to determine how much litter is left in the box. This would allow me to predict when I need to refill the box. Unfortunately I didn't have time to implement this feature. I might add it in the future.

UPDATE: I modeled a sensor unit and changed to a DHT22
https://i.imgur.com/ezSjrKY.mp4