Skip to content

Commit 1302f71

Browse files
authored
feat(qtpy): Add QtPy hardware abstraction component for QtPy ESP32 Pico and QtPy ESP32-S3 (#376)
* feat(qtpy): Add QtPy hardware abstraction component for QtPy ESP32 Pico and QtPy ESP32-S3. * readme: update
1 parent c31688c commit 1302f71

File tree

14 files changed

+497
-2
lines changed

14 files changed

+497
-2
lines changed

.github/workflows/build.yml

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ jobs:
113113
target: esp32s3
114114
- path: 'components/pid/example'
115115
target: esp32
116+
- path: 'components/qtpy/example'
117+
target: esp32
118+
- path: 'components/qtpy/example'
119+
target: esp32s3
116120
- path: 'components/qwiicnes/example'
117121
target: esp32
118122
- path: 'components/rmt/example'

components/qtpy/CMakeLists.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# only register the component if the target is esp32s3 or esp32
2+
idf_component_register(
3+
INCLUDE_DIRS "include"
4+
SRC_DIRS "src"
5+
REQUIRES base_component i2c neopixel task interrupt
6+
REQUIRED_IDF_TARGETS "esp32s3" "esp32"
7+
)

components/qtpy/Kconfig

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
menu "QtPy Configuration"
2+
config QTPY_INTERRUPT_STACK_SIZE
3+
int "Interrupt stack size"
4+
default 4096
5+
help
6+
Size of the stack used for the interrupt handler. Used by the
7+
button callback.
8+
endmenu
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# The following lines of boilerplate have to be in your project's CMakeLists
2+
# in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.5)
4+
5+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
6+
7+
# add the component directories that we want to use
8+
set(EXTRA_COMPONENT_DIRS
9+
"../../../components/"
10+
)
11+
12+
set(
13+
COMPONENTS
14+
"main esptool_py logger task qtpy"
15+
CACHE STRING
16+
"List of components to include"
17+
)
18+
19+
project(qtpy_example)
20+
21+
set(CMAKE_CXX_STANDARD 20)

components/qtpy/example/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# QtPy Example
2+
3+
This example shows the use of the `QtPy` class to perform hardware
4+
initialization for the QtPy ESP32 Pico and the QtPy ESP32-S3.
5+
6+
## How to use example
7+
8+
### Hardware Required
9+
10+
This example is designed to be run on either a [QtPy ESP32
11+
Pico](https://www.adafruit.com/product/5395) or a [QtPy
12+
ESP32-S3](https://www.adafruit.com/product/5426). It uses the QtPy's on-board
13+
button and NeoPixel. It will also scan for any devices that are present on the
14+
QtPy's QWIIC I2C port.
15+
16+
### Build and Flash
17+
18+
Build the project and flash it to the board, then run monitor tool to view serial output:
19+
20+
```
21+
idf.py -p PORT flash monitor
22+
```
23+
24+
(Replace PORT with the name of the serial port to use.)
25+
26+
(To exit the serial monitor, type ``Ctrl-]``.)
27+
28+
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
29+
30+
## Example Output
31+
32+
ESP32 Pico:
33+
![CleanShot 2025-02-18 at 15 54 25](https://github.com/user-attachments/assets/c670a804-2415-4a86-aa14-3c0e6f7763bc)
34+
35+
S3:
36+
37+
![CleanShot 2025-02-18 at 15 52 37](https://github.com/user-attachments/assets/948440ed-4d57-47e6-8a28-a0d6b4d8c418)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
idf_component_register(SRC_DIRS "."
2+
INCLUDE_DIRS ".")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#include <chrono>
2+
#include <vector>
3+
4+
#include "qtpy.hpp"
5+
#include "task.hpp"
6+
7+
using namespace std::chrono_literals;
8+
9+
extern "C" void app_main(void) {
10+
// create a logger
11+
espp::Logger logger({.tag = "Qtpy example", .level = espp::Logger::Verbosity::INFO});
12+
{
13+
logger.info("starting example");
14+
15+
//! [qtpy ex1]
16+
auto& qtpy = espp::QtPy::get();
17+
18+
// Initialize the Button
19+
logger.info("Initializing the button");
20+
auto on_button_pressed = [&](const auto &event) {
21+
if (event.active) {
22+
logger.info("Button pressed!");
23+
} else {
24+
logger.info("Button released!");
25+
}
26+
};
27+
qtpy.initialize_button(on_button_pressed);
28+
29+
// Initialize and test the LED
30+
logger.info("Initializing the LED");
31+
qtpy.initialize_led();
32+
33+
logger.info("Turning LED off for 1 second");
34+
qtpy.led(espp::Rgb(0.0f, 0.0f, 0.0f));
35+
std::this_thread::sleep_for(1s);
36+
37+
logger.info("Setting LED to red for 1 second");
38+
qtpy.led(espp::Rgb(1.0f, 0.0f, 0.0f));
39+
std::this_thread::sleep_for(1s);
40+
41+
logger.info("Setting LED to green for 1 second");
42+
qtpy.led(espp::Rgb(0.0f, 1.0f, 0.0f));
43+
std::this_thread::sleep_for(1s);
44+
45+
logger.info("Setting LED to blue for 1 second");
46+
qtpy.led(espp::Rgb(0.0f, 0.0f, 1.0f));
47+
std::this_thread::sleep_for(1s);
48+
49+
// Use a task to rotate the LED through the rainbow using HSV
50+
auto task_fn = [&qtpy](std::mutex &m, std::condition_variable &cv) {
51+
static auto start = std::chrono::high_resolution_clock::now();
52+
auto now = std::chrono::high_resolution_clock::now();
53+
float t = std::chrono::duration<float>(now - start).count();
54+
// rotate through rainbow colors in hsv based on time, hue is 0-360
55+
float hue = (cos(t) * 0.5f + 0.5f) * 360.0f;
56+
espp::Hsv hsv(hue, 1.0f, 1.0f);
57+
// full brightness (1.0, default) is _really_ bright, so tone it down
58+
qtpy.led(hsv);
59+
// NOTE: sleeping in this way allows the sleep to exit early when the
60+
// task is being stopped / destroyed
61+
{
62+
std::unique_lock<std::mutex> lk(m);
63+
cv.wait_for(lk, 50ms);
64+
}
65+
// don't want to stop the task
66+
return false;
67+
};
68+
69+
logger.info("Starting task to rotate LED through rainbow colors");
70+
auto task = espp::Task({.callback = task_fn,
71+
.task_config =
72+
{
73+
.name = "Neopixel Task",
74+
.stack_size_bytes = 5 * 1024,
75+
},
76+
.log_level = espp::Logger::Verbosity::WARN});
77+
task.start();
78+
79+
// Now initialize and test the QWIIC I2C bus
80+
logger.info("Initializing the QWIIC I2C bus");
81+
qtpy.initialize_qwiic_i2c(); // NOTE: using default i2c config here (400khz)
82+
auto i2c = qtpy.qwiic_i2c();
83+
84+
// probe the bus for all addresses and store the ones that were found /
85+
// responded.
86+
logger.info("Probing the I2C bus for devices, this may take a while...");
87+
std::vector<uint8_t> found_addresses;
88+
for (uint8_t address = 0; address < 128; address++) {
89+
if (i2c->probe_device(address)) {
90+
found_addresses.push_back(address);
91+
}
92+
}
93+
// print out the addresses that were found
94+
if (found_addresses.empty()) {
95+
logger.info("No devices found on the I2C bus");
96+
} else {
97+
logger.info("Found devices at addresses: {::#02x}", found_addresses);
98+
}
99+
100+
//! [qtpy ex1]
101+
102+
while (true) {
103+
std::this_thread::sleep_for(100ms);
104+
}
105+
}
106+
107+
logger.info("example complete!");
108+
109+
while (true) {
110+
std::this_thread::sleep_for(1s);
111+
}
112+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CONFIG_IDF_TARGET="esp32s3"
2+
3+
CONFIG_FREERTOS_HZ=1000
4+
5+
# ESP32-specific
6+
#
7+
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
8+
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
9+
10+
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
11+
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
12+
13+
# Common ESP-related
14+
#
15+
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
16+
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

components/qtpy/include/qtpy.hpp

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <string>
5+
#include <vector>
6+
7+
#include "base_component.hpp"
8+
#include "i2c.hpp"
9+
#include "interrupt.hpp"
10+
#include "neopixel.hpp"
11+
12+
namespace espp {
13+
/// The QtPy class provides an interface to the Adafruit QtPy ESP32 and QtPy
14+
/// ESP32-S3 development boards.
15+
///
16+
/// The class provides access to the following features:
17+
/// - RGB LED
18+
/// - I2C (qwiic)
19+
///
20+
/// The class is a singleton and can be accessed using the get() method.
21+
///
22+
/// \section qtpy_example Example
23+
/// \snippet qtpy_example.cpp qtpy example
24+
class QtPy : public BaseComponent {
25+
public:
26+
using button_callback_t = espp::Interrupt::event_callback_fn;
27+
28+
/// @brief Access the singleton instance of the QtPy class
29+
/// @return Reference to the singleton instance of the QtPy class
30+
static QtPy &get() {
31+
static QtPy instance;
32+
return instance;
33+
}
34+
35+
QtPy(const QtPy &) = delete;
36+
QtPy &operator=(const QtPy &) = delete;
37+
QtPy(QtPy &&) = delete;
38+
QtPy &operator=(QtPy &&) = delete;
39+
40+
/// Get a reference to the interrupts
41+
/// \return A reference to the interrupts
42+
espp::Interrupt &interrupts();
43+
44+
/////////////////////////////////////////////////////////////////////////////
45+
// Button
46+
/////////////////////////////////////////////////////////////////////////////
47+
48+
/// Initialize the button
49+
/// \param callback The callback function to call when the button is pressed
50+
/// \return true if the button was successfully initialized, false otherwise
51+
bool initialize_button(const button_callback_t &callback = nullptr);
52+
53+
/// Get the button state
54+
/// \return The button state (true = button pressed, false = button released)
55+
bool button_state() const;
56+
57+
/////////////////////////////////////////////////////////////////////////////
58+
// I2C
59+
/////////////////////////////////////////////////////////////////////////////
60+
61+
/// Initialize the qwiic I2C bus
62+
/// \param i2c_config The I2C configuration to use
63+
/// \return true if the qwiic I2C bus was successfully initialized, false
64+
/// otherwise
65+
/// \note The SDA/SCL pins in the config will be ignored - overwritten with
66+
/// the default values for the QtPy board.
67+
bool initialize_qwiic_i2c(const espp::I2c::Config &i2c_config = {});
68+
69+
/// Get a shared pointer to the qwiic I2C bus
70+
/// \return A shared pointer to the qwiic I2C bus
71+
/// \note The qwiic I2C bus is only available if it was successfully
72+
/// initialized, otherwise the shared pointer will be nullptr.
73+
std::shared_ptr<I2c> qwiic_i2c();
74+
75+
/////////////////////////////////////////////////////////////////////////////
76+
// RGB LED
77+
/////////////////////////////////////////////////////////////////////////////
78+
79+
/// Initialize the RGB LED
80+
/// \return true if the RGB LED was successfully initialized, false otherwise
81+
bool initialize_led();
82+
83+
/// Get the number of LEDs in the strip
84+
/// \return The number of LEDs in the strip
85+
static constexpr size_t num_leds() { return num_leds_; }
86+
87+
/// Get a shared pointer to the RGB LED
88+
/// \return A shared pointer to the RGB LED
89+
std::shared_ptr<Neopixel> led() const { return led_; }
90+
91+
/// Set the color of the LED
92+
/// \param hsv The color of the LED in HSV format
93+
/// \return true if the color was successfully set, false otherwise
94+
bool led(const Hsv &hsv);
95+
96+
/// Set the color of the LED
97+
/// \param rgb The color of the LED in RGB format
98+
/// \return true if the color was successfully set, false otherwise
99+
bool led(const Rgb &rgb);
100+
101+
protected:
102+
QtPy();
103+
104+
#if CONFIG_IDF_TARGET_ESP32
105+
static constexpr bool IS_ESP32 = true;
106+
static constexpr bool IS_ESP32_S3 = false;
107+
108+
// LED
109+
static constexpr auto led_data_io = GPIO_NUM_5;
110+
static constexpr auto led_power_io = GPIO_NUM_8;
111+
112+
// I2C
113+
static constexpr auto qwiic_sda_io = GPIO_NUM_22;
114+
static constexpr auto qwiic_scl_io = GPIO_NUM_19;
115+
#elif CONFIG_IDF_TARGET_ESP32S3
116+
static constexpr bool IS_ESP32 = false;
117+
static constexpr bool IS_ESP32_S3 = true;
118+
119+
// LED
120+
static constexpr auto led_data_io = GPIO_NUM_39;
121+
static constexpr auto led_power_io = GPIO_NUM_38;
122+
123+
// I2C
124+
static constexpr auto qwiic_sda_io = GPIO_NUM_41;
125+
static constexpr auto qwiic_scl_io = GPIO_NUM_40;
126+
#else
127+
#error "Unsupported target"
128+
#endif
129+
130+
// button (boot button)
131+
static constexpr gpio_num_t button_io = GPIO_NUM_0; // active low
132+
133+
// common:
134+
// WS2812 LED
135+
static constexpr auto led_clock_speed = 10 * 1000 * 1000; // 10 MHz
136+
static constexpr auto num_leds_ = 1;
137+
138+
// Interrupts
139+
espp::Interrupt::PinConfig button_interrupt_pin_{
140+
.gpio_num = button_io,
141+
.callback =
142+
[this](const auto &event) {
143+
if (button_callback_) {
144+
button_callback_(event);
145+
}
146+
},
147+
.active_level = espp::Interrupt::ActiveLevel::LOW,
148+
.interrupt_type = espp::Interrupt::Type::ANY_EDGE,
149+
.pullup_enabled = true};
150+
151+
// we'll only add each interrupt pin if the initialize method is called
152+
espp::Interrupt interrupts_{
153+
{.interrupts = {},
154+
.task_config = {.name = "qtpy interrupts",
155+
.stack_size_bytes = CONFIG_QTPY_INTERRUPT_STACK_SIZE}}};
156+
157+
// button
158+
std::atomic<bool> button_initialized_{false};
159+
button_callback_t button_callback_{nullptr};
160+
161+
// led
162+
std::shared_ptr<Neopixel> led_;
163+
164+
// I2C
165+
std::shared_ptr<I2c> qwiic_i2c_;
166+
}; // class QtPy
167+
} // namespace espp

0 commit comments

Comments
 (0)