Here's a step by step guide to understanding the code for this project. I personally find it helpful for myself when learning new things to explain what I am doing to an audience.

Step 1: Handling Secrets Securely

Hardcoding credentials directly into main application files makes sharing code messy. Following standard MicroPython conventions, I created a standalone secrets.py file stored locally on the board's filesystem:

# secrets.pyADAFRUIT_IO_USERNAME = "your_username_here"
ADAFRUIT_IO_KEY      = "your_aio_key_here"

The main script imports these credentials to construct the Adafruit IO topic hierarchy (username/feeds/feed-name) without exposing sensitive API keys.

Step 2: The Firmware Breakdown

Here is how each section of the project functions, from hardware bring-up to cloud communication.

1. Imports and Configuration

import gc
import time
import network
from machine import I2C, Pin, SPI
from secrets import ADAFRUIT_IO_KEY, ADAFRUIT_IO_USERNAME
from umqtt.simple import MQTTClient
import ahtx0

# MQTT Broker Details
MQTT_HOST = "io.adafruit.com"
MQTT_PORT = 1883

# Adafruit IO Feeds
TEMP_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/temperature"
HUMIDITY_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/humidity"
LED_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/led"
BUZZER_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/beep"

PUBLISH_INTERVAL_MS = 30000
import gc
import time
import network
from machine import I2C, Pin, SPI
from secrets import ADAFRUIT_IO_KEY, ADAFRUIT_IO_USERNAME
from umqtt.simple import MQTTClient
import ahtx0

# MQTT Broker Details
MQTT_HOST = "io.adafruit.com"
MQTT_PORT = 1883

# Adafruit IO Feeds
TEMP_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/temperature"
HUMIDITY_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/humidity"
LED_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/led"
BUZZER_TOPIC = f"{ADAFRUIT_IO_USERNAME}/feeds/beep"

PUBLISH_INTERVAL_MS = 30000

Module selection: machine exposes raw hardware peripherals (Pin, SPI, I2C), while network interfaces with the onboard W5500 MAC/PHY. umqtt.simple provides a low-overhead MQTT client, and ahtx0 handles register communication and calibration for the temperature/humidity sensor.

Topic structure: Adafruit IO expects MQTT topics formatted as username/feeds/feed-name. Defining them dynamically via string interpolation keeps the code clean.

Interval: PUBLISH_INTERVAL_MS is set to 30, 000 ms (30 seconds) to respect the Adafruit IO free-tier rate limits.

2. Bringing Up Ethernet via SPI and DHCP

# Configure SPI 2 for the internal W5500 controller
spi = SPI(2, baudrate=8_000_000)
cs = Pin("PB12", Pin.OUT)
rst = Pin("PD9", Pin.OUT)

# Initialize network driver
nic = network.WIZNET5K(spi, cs, rst)
nic.active(True)
nic.ifconfig("dhcp")

print("Waiting for DHCP lease...")
deadline = time.ticks_add(time.ticks_ms(), 30000)

while nic.ifconfig()[0] == "0.0.0.0":
    if time.ticks_diff(deadline, time.ticks_ms()) <= 0:
        raise RuntimeError("DHCP timeout")
    time.sleep_ms(500)

print("Ethernet Link UP! IP Address:", nic.ifconfig()[0]) 

Internal SPI: The onboard WIZnet chip is physically wired to SPI 2, requiring PB12 for Chip Select and PD9 for Reset.

DHCP lease: Passing "dhcp" to nic.ifconfig() commands the board to request an automatic IP, subnet mask, gateway, and DNS server from the local router.

Non-blocking wait: While negotiating, nic.ifconfig()[0] returns "0.0.0.0". Using time.ticks_add() and time.ticks_diff() establishes a clean 30-second timeout rather than locking up Thonny indefinitely if an Ethernet cable is unplugged.

3. Initializing I2C and Actuator GPIOs

# Initialize I2C bus and environmental sensor
i2c = I2C(1)
print("Detected I2C devices:", [hex(addr) for addr in i2c.scan()])
sensor = ahtx0.AHT20(i2c)

# Configure output pins for feedback
led    = Pin("PD14", Pin.OUT)
buzzer = Pin("PD15", Pin.OUT)

led.off()
buzzer.off()

I2C scanning:i2c.scan() pings the bus and verifies the sensor replies with an ACK at its standard hardware address (0x38). Passing that bus instance into ahtx0.AHT20(i2c) prepares the sensor...

Read more »