Smart Environmental Monitoring Terminal with Beetle ESP32-C3 – Perfect for Your Smart Garden
Introduction
Recently, my wife has been working on setting up a garden, and as a tech enthusiast, I thought about how I could help her using technology. That's when I came across DFRobot's Beetle ESP32-C3. During my previous research, the ESP32 had already made it onto my shortlist of components, so this seemed like the perfect opportunity to give it a try.
Using this small Beetle, I created a compact environmental monitoring terminal to collect data on temperature, humidity, light levels, and later, soil moisture. The data collected is displayed on a small screen and also sent to my server via MQTT. With this information, I can later implement features like automated irrigation.
The Beetle ESP32-C3 is extremely compact, and when it arrived, I received two parts: the core board and an expansion board. The expansion board offers three sets of I2C interfaces, one serial port, and nine GPIOs. For such a small device, having so many interfaces is incredibly convenient for development. It even has an integrated lithium battery charging management chip, allowing for direct power supply from a lithium battery, perfectly covering the needs of my project.
If the sensors and display mentioned above are difficult to obtain, you can also refer to the products in our store. Please note that DFRobot’s products are only functionally similar to the modules mentioned above, and the code in this post is not compatible with DFRobot’s modules.
The following modules are alternative modules in DFRobot store:
- Analog Capacitive Soil Moisture Sensor
- Temperature & Humidity Sensor
Story
Step 1 Soldering and Assembly
I chose to use female headers on the expansion board to make adjustments easier and improve convenience during use.
I didn’t use IO ports 3 and 10 on the expansion board, as their positions were a bit tricky, so I simply left them unsoldered.
Final assembly result:
Step 2 Programming
The programming part of this project was done using CircuitPython. I chose CircuitPython firstly because I wanted to learn a bit of python on the way, but also because it was recommended by someone who said it was easy.
Here's a breakdown of the programming process:
1. Flashing the CircuitPython firmware:
CircuitPython is indeed simple to use, as it doesn’t even require a complex development environment. The Beetle ESP32-C3 is officially supported, so flashing the firmware and writing code can all be done through a browser.
Firmware download and flashing instructions: DFRobot Beetle ESP32-C3 Download
Note: Before flashing, you need to pull IO9 low and short RST to put the ESP32 into download mode.
2. Writing the code:
CircuitPython typically recommends development boards with USB functionality, where after flashing the firmware, you can simply upload code by placing it into the USB drive created by CircuitPython. However, the Beetle ESP32-C3 lacks USB functionality, so code and file uploads need to be done via the web.
After flashing the firmware, you will need to modify the Wi-Fi connection settings via CircuitPython's online installer (see the image below).
Once you've updated the Wi-Fi information, the board should reboot automatically, though for safety, you can manually short RST to restart it.
After rebooting, CircuitPython will connect to the Wi-Fi access point you set up and start the web service. You will need to check the router for the assigned IP address.
Using this IP address, you can access CircuitPython’s web service, where you can upload, edit, run, and debug the code directly online.
Now, let’s dive into the coding part:
import time
import board
import displayio
import wifi
import ssl
import rtc
import socketpool
import terminalio
import analogio
import adafruit_ahtx0
import adafruit_bh1750
import adafruit_minimqtt.adafruit_minimqtt as MQTT
import adafruit_ntp
from adafruit_display_text import label
from adafruit_st7735r import ST7735R
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.line import Line
from adafruit_display_shapes.sparkline import Sparkline
# Create sensor object, communicating over the board's default I2C bus
i2c = board.I2C() # uses board.SCL and board.SDA
tempSensor = adafruit_ahtx0.AHTx0(i2c)
luxSensor = adafruit_bh1750.BH1750(i2c)
loopCounter = 0
spi = board.SPI()
tft_cs = board.D7
tft_dc = board.D1
tft_rst = board.D2
displayio.release_displays()
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
display = ST7735R(display_bus, width=128, height=128, colstart=2, rowstart=1)
# Make the display context
def showSplash():
splash = displayio.Group()
display.show(splash)
color_bitmap = displayio.Bitmap(128, 128, 1)
color_palette = displayio.Palette(1)
color_palette[0] = 0xFF0000
bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0)
splash.append(bg_sprite)
# Draw a smaller inner rectangle
inner_bitmap = displayio.Bitmap(108, 108, 1)
inner_palette = displayio.Palette(1)
inner_palette[0] = 0xAA0088 # Purple
inner_sprite = displayio.TileGrid(inner_bitmap, pixel_shader=inner_palette, x=10, y=10)
splash.append(inner_sprite)
# Draw a label
text = "Hello DFRobot!"
text_area = label.Label(terminalio.FONT, text=text, color=0xFFFF00, x=20, y=64)
splash.append(text_area)
# Make the display context
def initMainUI():
view = displayio.Group()
display.show(view)
# BG
color_bitmap = displayio.Bitmap(128, 128, 1)
color_palette = displayio.Palette(1)
color_palette[0] = 0x7ecef4
bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0)
view.append(bg_sprite)
rect = Rect(4, 4, 120, 120, outline=0x666666)
view.append(rect)
return view
showSplash()
time.sleep(1)
MQTT_HOST = "192.168.99.7"
MQTT_PORT = 1883
MQTT_USER = "gardener"
MQTT_PASSWORD = "53bffe07f84e0c5909ff569bb2a848e7"
MQTT_SUB_TOPIC = "/garden/notify"
MQTT_PUB_TOPIC = "/garden/notify"
# Define callback methods which are called when events occur
# pylint: disable=unused-argument, redefined-outer-name
def connected(client, userdata, flags, rc):
# This function will be called when the client is connected
# successfully to the broker.
print("Connected to Adafruit IO! Listening for topic changes on %s" % MQTT_SUB_TOPIC)
# Subscribe to all changes on the onoff_feed.
client.subscribe(MQTT_SUB_TOPIC)
def disconnected(client, userdata, rc):
# This method is called when the client is disconnected
print("Disconnected from Adafruit IO!")
def message(client, topic, message):
# This method is called when a topic the client is subscribed to
# has a new message.
print("New message on topic {0}: {1}".format(topic, message))
# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)
ssl_context = ssl.create_default_context()
# Set up a MiniMQTT Client
mqtt_client = MQTT.MQTT(
broker=MQTT_HOST,
port=MQTT_PORT,
username=MQTT_USER,
password=MQTT_PASSWORD,
socket_pool=pool,
ssl_context=ssl_context,
)
# Setup the callback methods above
mqtt_client.on_connect = connected
mqtt_client.on_disconnect = disconnected
mqtt_client.on_message = message
# Connect the client to the MQTT broker.
print("Connecting to MQTT ...")
mqtt_client.connect()
ntp = adafruit_ntp.NTP(pool, tz_offset=0, server="ntp1.aliyun.com", socket_timeout=5)
def updateTimeByNTP():
r = rtc.RTC()
try:
r.datetime = ntp.datetime
except Exception as e:
print(f"NTP fetch time failed: {e}")
mainUi = initMainUI()
font = terminalio.FONT
labelIp = label.Label(font, text="255.255.255.255", color=0x333333, x=10, y=12)
labelTemp = label.Label(font, text="TEMP: 00.0C 100%", color=0x333333, x=10, y=24)
labelEarthHumi = label.Label(font, text="EARTH: 00000 3.3V", color=0x333333, x=10, y=36)
labelLight = label.Label(font, text="Light: 9999.99lux", color=0x333333, x=10, y=48)
line_color = 0xffffff
chart_width = 80
chart_height = 50
spkline = Sparkline(width=chart_width, height=chart_height, max_items=chart_width, x=38, y=60, color=line_color)
text_xoffset = -5
text_label1a = label.Label(
font=font, text=str(spkline.y_top), color=line_color
) # yTop label
text_label1a.anchor_point = (1, 0.5) # set the anchorpoint at right-center
text_label1a.anchored_position = (
spkline.x + text_xoffset,
spkline.y,
) # set the text anchored position to the upper right of the graph
text_label1b = label.Label(
font=font, text=str(spkline.y_bottom), color=line_color
) # yTop label
text_label1b.anchor_point = (1, 0.5) # set the anchorpoint at right-center
text_label1b.anchored_position = (
spkline.x + text_xoffset,
spkline.y + chart_height,
) # set the text anchored position to the upper right of the graph
bounding_rectangle = Rect(
spkline.x, spkline.y, chart_width, chart_height, outline=line_color
)
mainUi.append(labelIp)
mainUi.append(labelTemp)
mainUi.append(labelEarthHumi)
mainUi.append(labelLight)
mainUi.append(spkline)
mainUi.append(text_label1a)
mainUi.append(text_label1b)
mainUi.append(bounding_rectangle)
total_ticks = 5
for i in range(total_ticks + 1):
x_start = spkline.x - 2
x_end = spkline.x
y_both = int(round(spkline.y + (i * (chart_height) / (total_ticks))))
if y_both > spkline.y + chart_height - 1:
y_both = spkline.y + chart_height - 1
mainUi.append(Line(x_start, y_both, x_end, y_both, color=line_color))
display.show(mainUi)
adcPin = analogio.AnalogIn(board.A0)
while True:
mqtt_client.loop()
if loopCounter > 86400:
loopCounter = 1
if loopCounter % 120 == 0:
updateTimeByNTP()
clientId = wifi.radio.hostname
ip = wifi.radio.ipv4_address
now = time.time()
json = f'{{"clientId": "{clientId}", "ip": "{ip}", "earthHumi": {adcPin.value}, "airTemp": {tempSensor.temperature}, "airHumi": {tempSensor.relative_humidity}, "time": {now} }}'
print(f"Time: {time.localtime()}")
print("Temperature: %0.1f C" % tempSensor.temperature)
print("Humidity: %0.1f %%" % tempSensor.relative_humidity)
print("Light: %.2f Lux" % luxSensor.lux)
print(f"ADC A0 vlaue: {adcPin.value} {adcPin.reference_voltage}V")
print(json)
spkline.add_value(tempSensor.temperature)
text_label1a.text = "%.1f" % max(spkline.values())
text_label1b.text = "%.1f" % min(spkline.values())
labelIp.text = f'IP: {ip}'
labelTemp.text = "TEMP: %.1fC / %.1f%%" % (tempSensor.temperature, tempSensor.relative_humidity)
labelEarthHumi.text = "EARTH: %d %.2fV" % (adcPin.value, adcPin.value / 65535 * adcPin.reference_voltage)
labelLight.text = "Light: %.3f Lux" % luxSensor.lux
if loopCounter % 60 == 0:
mqtt_client.publish(MQTT_PUB_TOPIC, json)
loopCounter += 1
time.sleep(1)
Code File:
Project Demo
Web interface to view data trends
Power consumption