Author: Mateusz Sowinski-Smetny (ms226yg)
## Project overview

The project combines an array of different climate related sensors to collect data about the immediate microclimate, as well as the condition of plant soil. In particular, a soil hygrometer, a barometer, and a light sensor are used to monitor the air temperature, humidity, and pressure, as well as the illuminance and soil hydration. The data is reported to a custom-made backend and stored in a relational database, to be then presented in the frontend. Additionally, an LCD screen has been employed to display the current sensor data.
#### Time estimate: it might take a couple days to do, using this tutorial.
## Objective
I chose this project because I wanted to track the hydration of my plant's soil, to let me take better care of it. In addition, I believed measuring metrics such as air temperature, humidity, pressure, and illuminance, might provide me with a better understanding of its growth.
I think the collected datapoints will provide many surprising insights. For instance, it might be the case that irregular microclimate has a negative effect on the plant's growth. Perhaps, one side of the plant is getting less sunlight than the other. And so on.
## Material
I chose the ESP32 as my microcontroller, as I believed it to be more versatily than the course-suggested choice.
- [ESP32 Starter Kit](https://www.amazon.se/dp/B09BC5B4H6?psc=1&ref=ppx_yo2ov_dt_b_product_details) - 399 SEK
- x1 ESP32-WROVER (the microcontroller programmer with micropython)
- x1 USB Cable (for connecting to my machine)
- Jump Wire F-F (to connect the peripherals)
- Jump Wire F-M (to connect the peripherals)
- Jump Wire M-M (to connect the peripherals)
- LCD Module (to display the current status of the sensors)
- Project Board (to prototype)

- [BH1750 Light Sensor](https://www.amazon.se/dp/B07TKWNGZ4?psc=1&ref=ppx_yo2ov_dt_b_product_details) (to measure illuminance; over I2C) - 74 SEK

- [BME280 Barometric Sensor](https://www.amazon.se/dp/B07D8T4HP6?psc=1&ref=ppx_yo2ov_dt_b_product_details) (to measure air temperature, humidity, pressure; over I2C) - 179 SEK

- [Capacitive Soil Hygrometer](https://www.amazon.se/dp/B07HJ6N1S4?psc=1&ref=ppx_yo2ov_dt_b_product_details) (to measure soil hydration; analog output - usage with ADC) - 67 SEK

## Computer Setup
I'm using VS Code as my IDE of choice. I did not install any plugins apart from the python language server for syntax highlighting. To interact with the microcontroller, I used CLI programs.
### Flashing
I downloaded the appropriate [micropython image](https://micropython.org/download/esp32spiram/) for ESP32-based boards with external SPIRAM.
After connecting the ESP32 to my Macbook, I needed to know the serial port that it was using. I ran the following command to see active USB ports:
```bash
$ ls /dev/cu.*
/dev/cu.Bluetooth-Incoming-Port /dev/cu.usbserial-110
```
I verified that `/dev/cu.usbserial-110` by checking that disconnecting the microcontroller also removed that port from that command's output.
Next, I flashed the image using the following command (I had to download `esptool.py` first)
```bash
$ esptool.py --chip esp32 --port /dev/cu.usbserial-110 --baud 460800 write_flash -z 0x1000 esp32-20230426-v1.20.bin
```
This was successful without any issues.
### Running micropython scripts
To upload and run scripts on the microcontroller, I used the popular `ampy` package from the `pip` repository.
```bash
pip install ampy
```
#### Run a script
I used this command to run my main script:
```bash
ampy --port /dev/cu.usbserial-110 run src/index.py
```
#### Uploading files
To use more than one file in my program, I had to upload them first. For instance,
```bash
ampy --port /dev/cu.usbserial-110 put src/BME280.py BME280.py
```
## Circuit Design
All peripherals are rated for 3.3V or 5V, therefore nothing special, such as resistors, was necessary. I simply connected the sensors and the LCD Module to ground, power supply (5V or 3.3V), and capable pins. On the ESP32, most pins can be used for I2C, and many pins (Including the chosen pin 34) can be used for ADC.


## Platform
Having consulted this with a TA, I chose to implement my own platform from the ground-up, without using any third-party platforms or anything of sorts.
The custom platform is comprised of three parts:
- The HTTP backend: receives POST requests from ESP32 with sensor data
- Node.js - language runtime
- TypeScript - language
- Prisma - ORM
- koa.js - HTTP engine library
- The database: PostgreSQL was used
- The web frontend: displays the data visually, using line plots
- Node.js - server-side language runtime
- TypeScript - language
- React & Next.js - SPA framework
- nivo & d3 - visualizations
- tailwindcss - styling
The platform can capture the five defined sensor types (easily extendable), to be then displayed in the form of line plots for a given timespan. Specifically, the user can choose the last 5 minutes, 1 hour, 1 day, or 1 week by clicking on the respective buttons.
I chose to go this way because I'd already had extensive knowledge about the web platform. This way gave me more control over what I wanted to build.
## Code
Depending on the peripheral, I used a combination of:
- open-source driver implementations
- manufacturer-provided driver implementations
- my own driver implementations based on peripheral specification and experimentation
(here, by "driver", I mean micropython code that handles interactions with the peripheral, such as reading and writing)
I used the `urequests` micropython library to handle HTTP requests to my API server.
First, I configure the drivers (pins, i2c, etc.):
```python!
bme280 = BME280(i2c=SoftI2C(freq=100000, sda=Pin(18), scl=Pin(19)))
bh1750 = BH1750(i2c=SoftI2C(freq=100000, sda=Pin(21), scl=Pin(22)))
soil_hygrometer = SoilHygrometer(Pin(34))
lcd = I2cLcd(i2c=SoftI2C(freq=400000, sda=Pin(13), scl=Pin(14)), i2c_addr=63, num_lines=2, num_columns=16)
```
Then, I connect to my Wi-Fi using this standard code:
```python!
sta_if = network.WLAN(network.STA_IF)
network.WLAN(network.AP_IF).active(False)
def wifi_connect():
if not sta_if.isconnected():
print('connecting to network...')
sta_if.active(True)
sta_if.connect('<name>', '<pass>')
while not sta_if.isconnected():
pass
print('network config:', sta_if.ifconfig())
wifi_connect()
```
I define this function to submit a given set of sensor data measurements to the API server:
```python!
def send_to_api(temperature, pressure, humidity, light, hydration):
data = {
'sensors': {
'temperature': temperature,
'pressure': pressure,
'humidity': humidity,
'light': light,
'hydration': hydration
}
}
try:
x = post(
'http://192.168.0.123:8000/readings',
json=data,
headers={'Content-Type': 'application/json'}
)
x.close() # close the socket, so the GC doesn't have to
except Exception as e:
print(e)
```
Additionally, the following function displays some of the current sensor data on the LCD display:
```python!
def print_to_lcd(temperature, pressure, humidity, light):
lcd.move_to(0, 0)
first_line = "{:.02f}C".format(temperature) + ' ' + "{:.02f}hPa".format(pressure)
first_line += ' ' * (16 - len(first_line))
lcd.putstr(first_line)
lcd.move_to(0, 1)
second_line = "{:.02f}%".format(humidity) + ' ' + "{:.02f}lx".format(light)
second_line += ' ' * (16 - len(second_line))
lcd.putstr(second_line)
```
Lastly, the pièce de résistance --- the measurement loop:
```python!
# I'm measuring every about 500ms for the lcd printout,
# but I don't want to send that much data to the API.
# So, I only send every 10th measurement.
measurement_count = 0
while True:
temperature = bme280.read()[0]
pressure = bme280.read()[1]
humidity = bme280.read()[2]
light = bh1750.read()
hydration = soil_hygrometer.read()
print_to_lcd(temperature, pressure, humidity, light)
if measurement_count >= 10:
send_to_api(
temperature,
pressure,
humidity,
light,
hydration
)
measurement_count = 0
measurement_count += 1
sleep(0.5)
```
## Connectivity
As mentioned, a Wi-Fi connection is used. Since the device is supposed to be used in a home environment, this is a fitting choice.
The microcontroller sends data to the API server using HTTP requests (via the `urequests` micropython library). Due to my experience with web development, that was the easiest to adapt.
Data is sent approximately every 5 seconds. This can be longer due to factors such as the Garbage Collector pauses. The API server makes no assumptions about the frequency of incoming data.
I believe the largest factor of power consumption would be how often the data is transmitted. For this project, one could easily set it to transmit much less frequently than every 5 seconds --- I'd imagine once every 5-20 minutes would be satisfiable. The device could disconnect from the network between transmission.
## Presenting the data
The following figure shows the main view of the dashboard. The user can choose timespans ranging from 5 minutes to 1 week.

To recap, the dashboard part is a custom solution built using web technologies, including Node.js, TypeScript, React & Next.js, nivo & d3, and tailwindcss.
I chose PostgreSQL due to my familiarity with it. However, I recognize that sensor data isn't inherently relational, and as such, another database might have been more fitting. In the end, I went the way of least resistance and chose a solution that's good enough.
I did not think any automatic purging of the data was necessary, and as such, all historical data is preserved.
There is no additional automation or triggers beyond that.
## Finalizing the design
A video presentation and pictures:

<video width="400" controls src="https://i.imgur.com/pnJvLPc.mp4">
</video>

