This post was last edited by ltpop on 2023-12-28 16:46
1. Introduction Video
2. Project Summary
I am a smart home enthusiast. I have played with HomeAssistant, ESPHome, Tasmota and other platforms, and used ESP8266 and ESP32 to DIY my own devices. Usually I only use existing platforms for configuration and rarely write hardware code directly. I am very grateful to DigiKey and EEWORLD for the Follow me event, which gave me the opportunity to play with the underlying code of smart devices.
Like most of my classmates, I chose CircuitPython as the development platform and language. The community resources are very rich, the development threshold is low, and debugging is convenient, which is very suitable for novices like me. In order to meet the requirements of the activity and realize a complete function, I chose to make a calendar + weather + Pomodoro function. This function can be used in daily work and study to improve efficiency by using the Pomodoro Technique.
This is my first time to use CircuitPython. I learned CircuitPython while completing the project. My approach is to first determine the project goal, that is, the minimum functionality that needs to be implemented. Then, for each function, refer to the official examples and learn the usage of related libraries, and debug the code at the same time. After each function code passes the test, merge it into the final complete code.
There are many functional points in the project. In addition to the WIFI configuration, network request, Chinese display, and Neopixel LED control required by the activity, it also includes the use of screen control, coroutines, and touch sensors. The difficulty lies in screen control and coroutines. As a CircuitPython novice, the project code currently completed only implements the functions, and there is still a lot of room for optimization. I will continue to explore and practice in the future, and everyone is welcome to communicate with me.
Project Objectives:
Complete the activity requirements
Network data request
Chinese display
Neopixel Control
To achieve the tomato calendar function:
Date and time display, and display lunar calendar
Weather display
Pomodoro control function
You can set the length of the Pomodoro
Show Pomodoro Countdown
After completion, LED prompts and confirms
Tomato Count
Implementation
WIFI connection to the Internet
Get the date and time from the NTP service
Get lunar date from OpenCalendar API
Get local weather from the Open Weather API
Use coroutine functions and shared variables to update data and control screen display
Use the ESP32S3's touch sensor for input control to start and confirm the Pomodoro
Use different colors and states of Neopixel LED to indicate the status of the device, such as network status, Pomodoro running, confirmation stage, etc.
Double-click the reset button and copy the .uf2 file to the FTHRS2BOOT disk. It will automatically restart and complete the installation.
Use Mu editor or VSCode+ plug-in to connect to the development board and enter code development. Both are very convenient for debugging in REPL mode. Mu connection is more stable, and VSCode code writing is more powerful.
The development process is generally to find similar official examples, debug the code snippet in REPL mode, and write it into the code.py file after the test is successful for a complete test.
In the normal model, after modifying the content of the code.py file, the development board will automatically restart and load the code. However, if you need to manually reset the development board in REPL mode, in addition to pressing the RST button on the board, you can also use Ctrl+D or execute the following code to reset it.
import microcontroller;
microcontroller.reset();
Install Lib
Some of the libraries used in the code are already built-in and can be imported directly. Otherwise, you need to install them manually. The official Lib link is Libraries (circuitpython.org) or
The installation method is very simple. After decompression, find the corresponding .mpy file in the lib directory and copy it to the lib directory in the CIRCUITPY disk. If you need the source code, you can also download the original py file.
based on the code of netizens . Due to the limited space on the development board, I chose wenquanyi_13px.pcf of appropriate size and saved it in the font directory of CIRCUITPY disk.
The lunar calendar and weather information in the project can display Chinese content normally, as shown in the following figure (upper middle and lower left corner)
This is relatively simple and does not require additional lib. But it is better to retry the connection multiple times to avoid connection failure during power-on.
Core code
import os
import wifi
import time
# 连接wifi
# 事先在settings.toml配置 WIFI信息:CIRCUITPY_WIFI_SSID=<wifi ssid名称>和CIRCUITPY_WIFI_PASSWORD=<wifi密码>
state = wifi.radio.connected
failure_count = 0
while not wifi.radio.connected:
try:
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
except OSError as error:
print("Failed to connect, retrying\n", error)
failure_count += 1
time.sleep(5)
The project can be displayed differently under different network conditions. If it is not connected to the Internet, it will be displayed as follows (the lower left corner reminds you that you are not connected to WIFI)
The use of NeoPixel LED is very flexible, and there are many official special effect libraries that can achieve various effects. Different colors are used in the project to indicate the current state of the development board, such as the Pomodoro timer is displayed as a constant red, the Pomodoro countdown confirmation is displayed as a cyan blue flash, and so on.
Because the development board is not connected to the clock module, it is necessary to obtain the time from the NTP server. The official library is available for calling. For stability, the domestic NTP server can be used. The data returned by NTP contains the date and time. In order to facilitate the acquisition of the lunar calendar date, the API is used to obtain it. Similar to the weather API, you need to register an account and use the API KEY to access it.
Core code
import os
import wifi
import time
import ssl
import socketpool
# 需要导入adafruit_ntp库
import adafruit_ntp
# 需要导入adafruit_requests库
import adafruit_requests as requests
tz_offset=os.getenv("TIME_ZONE")
ntp = None
while True:
# 使用前需要先联网
# 获取当前ntp时间,
if not ntp:
pool = socketpool.SocketPool(wifi.radio)
try:
ntp = adafruit_ntp.NTP(pool, server = "cn.ntp.org.cn", tz_offset = tz_offset)
except OSError as error:
print("NTP failed, retrying\n", error)
pass
# 获取阴历
pool = socketpool.SocketPool(wifi.radio)
https = requests.Session(pool, ssl.create_default_context())
lunar_response = https.get(f'https://v2.alapi.cn/api/lunar?token={os.getenv("LUNAR_API_KEY")}')
lunar_json = lunar_response.json()
if lunar_json['code'] == 200:
lunar_data = lunar_json['data']
week_day = lunar_data['week_no']
week_name = lunar_data['week_name']
lunar_year_ganzhi = lunar_data['week_name']
lunar_month_chinese = lunar_data['lunar_month_chinese']
lunar_day_chinese = lunar_data['lunar_day_chinese']
print(f"当前农历:{lunar_year_ganzhi}年 {lunar_month_chinese}{lunar_day_chinese}")
print(f"当前星期:{week_name}")
# 仅在第二天时再更新
dt = ntp.datetime
print(f"当前时间:{dt.tm_year}-{dt.tm_mon}-{dt.tm_mday} {dt.tm_hour:02d}:{dt.tm_min:02d}")
wait_seconds = (24-dt.tm_hour)*60+(60 - dt.tm_sec)
time.sleep(wait_seconds)
The top of the screen in the project displays the date, lunar calendar, and time in sequence, as shown below
Official example: EEWORLDLINKTK4. You need to register an account and obtain an API KEY before you can use it.
Password information can be saved in the settings.toml file in the root directory in the format of <variable name> = <value>, and then used in the code to obtain it using os.getenv("<variable name>"), which is safe and convenient.
In order to facilitate the modification of city information, the weather city is also set in the toml file, or the local city is obtained through the external IP of the current network. The relevant API can be accessed to obtain it. The coordinates can be referred to the code.
Core code:
import os
import wifi
import time
import ssl
import socketpool
# 需要导入adafruit_requests库
import adafruit_requests as requests
# 获取天气信息
# 获取天气API KEY
weather_api_key = os.getenv("WEATHER_API_KEY")
# 获取天气城市:从配置文件中读取城市设置
weather_city = os.getenv("WEATHER_CITY")
while True:
# 创建https
pool = socketpool.SocketPool(wifi.radio)
https = requests.Session(pool, ssl.create_default_context())
# 如果读取不到配置的城市,则获取当前IP城市
if not weather_city:
# 获取当前外网IP和城市
ip_city_response = https.get("https://myip.ipip.net/json")
ip_city_json = ip_city_response.json()
if ip_city_json["ret"] == "ok":
weather_city = ip_city_json['data']['location'][2]
print(f"当前IP城市:{weather_city}")
# 当前天气
weather_now_url = f"https://api.seniverse.com/v3/weather/now.json?key={weather_api_key}&location={weather_city}&language=zh-Hans&unit=c&start=-1&days=5"
weather_now_response = https.get(weather_now_url)
weather_json = weather_now_response.json()
if weather_json["results"]:
now_weather = weather_json["results"][0]["now"]["text"]
now_temperature = weather_json["results"][0]["now"]["temperature"]
print(f"当前天气:{now_weather},气温:{now_temperature}℃")
time.sleep(300) #5分钟更新一次
The weather and temperature information in the project is displayed at the bottom of the screen, as shown below
Screen control is divided into two parts. The first part is to draw the display layout of the screen, which can be implemented using the official displayio library; the second part is to control the display content. The basic idea is to update the display content of each element in each loop, such as text, numbers, pictures, etc.
This part involves the processing of wifi connection, network time, weather, etc. in the project, and it needs to control the screen display synchronously. There are many lines of code, because the countdown updates the screen content every second. The screen display is not very smooth when the current code is executed, and it will occasionally get stuck for 1-2 seconds. The code still needs to be optimized.
The subordinate relationship of each component in the screen layout is shown in the figure below. Simply put, Display contains Group, and Group contains Bitmap.
Screen coordinates start from the upper left corner
The anchor_point in bitmap_label is the reference position of the element. The value has the following meanings. For example, the element placed in the upper left corner of the screen can be set to (0,0), and the element placed in the lower right corner can be set to (1,1). For details, please refer to the core code:
In the project, different network requests need to be controlled and different contents need to be displayed on the screen. It will be very complicated to implement them in the same loop. Concurrency control is needed to decouple various events to facilitate code writing. That is, the asynchronous IO (asyncio) in Python. Referring to the official documentation, the implementation can be summarized into the following steps:
Add the async keyword before a normal function to convert it into a coroutine function
Use await asyncio.sleep instead of time.sleep to delay and release the exclusive control of the controller
After defining each coroutine function, call it using asyncio.create_task(some_coroutine(arg1, arg2, ...))
Use await asyncio.gather(task1, task2, ...) to wait for each coroutine task to complete
Once again, don’t forget to use await
Core code: This is the official example. For the actual code, please refer to the complete project source code below.
# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import asyncio
import board
import digitalio
import keypad
class Interval:
"""Simple class to hold an interval value. Use .value to to read or write."""
def __init__(self, initial_interval):
self.value = initial_interval
async def monitor_interval_buttons(pin_slower, pin_faster, interval):
"""Monitor two buttons: one lengthens the interval, the other shortens it.
Change interval.value as appropriate.
"""
# Assume buttons are active low.
with keypad.Keys(
(pin_slower, pin_faster), value_when_pressed=False, pull=True
) as keys:
while True:
key_event = keys.events.get()
if key_event and key_event.pressed:
if key_event.key_number == 0:
# Lengthen the interval.
interval.value += 0.1
else:
# Shorten the interval.
interval.value = max(0.1, interval.value - 0.1)
print("interval is now", interval.value)
# Let another task run.
await asyncio.sleep(0)
async def blink(pin, interval):
"""Blink the given pin forever.
The blinking rate is controlled by the supplied Interval object.
"""
with digitalio.DigitalInOut(pin) as led:
led.switch_to_output()
while True:
led.value = not led.value
await asyncio.sleep(interval.value)
async def main():
interval1 = Interval(0.5)
interval2 = Interval(1.0)
led1_task = asyncio.create_task(blink(board.D1, interval1))
led2_task = asyncio.create_task(blink(board.D2, interval2))
interval1_task = asyncio.create_task(
monitor_interval_buttons(board.D3, board.D4, interval1)
)
interval2_task = asyncio.create_task(
monitor_interval_buttons(board.D5, board.D6, interval2)
)
await asyncio.gather(led1_task, led2_task, interval1_task, interval2_task)
asyncio.run(main())
ESP32S3 has multiple touch sensors built in, which can realize input control without external devices. In this project, they are used for the start trigger and confirmation function of the Pomodoro timer. When using, you can connect the GPIO that supports the touch sensor to a wire or directly touch the PCB board. You can get a list of which GPIO supports touch by executing the following official code.
# 只显示支持触摸的GPIO
Touch on: A4
Touch on: A5
Touch on: TX
Touch on: D10
Touch on: D11
Touch on: D12
Touch on: LED
Touch on: RX
Touch on: D5
Touch on: D6
Touch on: D9
The test results are 11, but not all of them can be used. For example, A4 and A5 are always in the trigger state. The reason is unknown. Friends who know are welcome to comment. D10 is used in the project, located in the middle of the upper part of the screen. After use and testing, the touch response is very sensitive.
Core code
import board
import touchio
import time
touch = touchio.TouchIn(board.D10)
while True:
if touch.value:
# 按钮开始番茄计时
print("start")
time.sleep(0.1)
Multi-device MQTT communication
Also ordered in this event were two other ESP32 microcontrollers, which are used to simulate multi-device communication.
This experiment uses MQTT to achieve communication between multiple devices. The specific equipment list is as follows:
Device 1: The main control of this project, Adafruit ESP32-S3 TFT Feather , is used to simulate the central control panel to display the switch status of the light.
Device 2: Another ESP32S3 development board, AtomS3 Lite ESP32S3 , used to simulate smart lights
Device 3: ESP32C3 module, esp32-c3-wroom-02 , which can be used to simulate a smart switch after simple peripheral circuit welding
Show results:
Device 1: Displays the status of the light, on and off in the lower right corner
Device 3: Simulates a smart switch. The module adds a touch button to simulate a switch.
Let's briefly talk about the wiring of the module. Because this is an ESP32C3 module, it cannot be used directly. The necessary peripherals and switches are welded here and connected through the USB2TTL module, as shown in the figure below. In addition to 3V3, GND, TXD, and RXD directly connected to USB2TTL, EN, IO2, and IO8 need to be connected to a high level. Here, three 10K resistors are connected in series to 3V3, and the two ends of the key switch are connected to IO9 and GND. When powered on, press it to flash the firmware, otherwise it will start normally. (Limited conditions, only used for experiments)
To build an MQTT server locally, mosquitto is used. The process is relatively simple and there are many tutorials, so I will not go into details.
The design of MQTT is as follows:
Two MQTT topics are defined: cmd/d2/led1 and state/d2/led1, where cmd is the command to turn on the light and state is the status update of the light. d2 is the device. This experiment only involves device 2, which is defined as d2. led1 is the specific module in the device, which is led1 in this example.
Device 1 subscribes to the state/d2/led1 topic. When the received message is 1, it means the light is on, and when it is 0, it means the light is off. The corresponding screen displays on and off.
Device 2 subscribes to the cmd/d2/led1 topic. When the received message is 1, it indicates the light is turned on (0 indicates the light is turned off, and 2 indicates the light is switched on). At this time, the device's led1 is turned on and a message is sent to the topic state/d2/led1. The message content is the specific state of the light, 0 or 1. At the same time, the device can also turn the light on and off by pressing the buttons on the device, and the corresponding mqtt message is also sent.
Device 3 monitors the local key operation. When the key is pressed, it sends a message to the topic cmd/d2/led1. The message content is 2, indicating that the light is switched on.
Core code
Device 1 and Device 2 are both ESP32S3 chips, and use the CircuitPython environment directly; Device 3 is ESP32C3, and uses the microPython environment.
Device 1 (central control terminal, responsible for displaying the light status)