Merge branch 'dev' into jesserockz-2026-375

This commit is contained in:
Jonathan Swoboda
2026-06-30 07:53:04 -04:00
committed by GitHub
35 changed files with 4068 additions and 2519 deletions
+1 -1
View File
@@ -820,7 +820,7 @@ jobs:
run: echo ${{ matrix.components }}
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@5513791f75b039e2a79653b1a92238d3fb8d99b4 # v1.6.2
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: libsdl2-dev ccache
version: 1.1
+6
View File
@@ -709,3 +709,9 @@ This document provides essential context for AI models interacting with this pro
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```
## 9. English Language
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
audience, including non-native English speakers.
+3
View File
@@ -123,6 +123,7 @@ esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/cst226/* @clydebarrow
esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
@@ -386,6 +387,7 @@ esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pixoo/* @jesserockz
esphome/components/pm1006/* @habbie
esphome/components/pm2005/* @andrewjswan
esphome/components/pmsa003i/* @sjtrny
@@ -405,6 +407,7 @@ esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmi8658/* @clydebarrow
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.21
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.22
RUN \
platformio settings set enable_telemetry No \
+1 -1
View File
@@ -2,6 +2,6 @@ esphome:
name: docker-test-ln882x-arduino
ln882x:
board: generic-ln882hki
board: generic-ln882h
logger:
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c"]
cst9220_ns = cg.esphome_ns.namespace("cst9220")
@@ -0,0 +1,36 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from .. import cst9220_ns
CST9220Touchscreen = cst9220_ns.class_(
"CST9220Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x5A))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
@@ -0,0 +1,141 @@
#include "cst9220_touchscreen.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::cst9220 {
void CST9220Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
void CST9220Touchscreen::continue_setup_() {
uint8_t buffer[4];
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
// Enter command mode so the configuration registers can be read.
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to enter command mode"));
this->mark_failed();
return;
}
delay(10);
// The firmware check code confirms that valid firmware is loaded.
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read check code"));
this->mark_failed();
return;
}
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
this->status_set_error(LOG_STR("Invalid firmware check code"));
this->mark_failed();
return;
}
// Read the panel resolution unless the user supplied calibration values.
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = encode_uint16(buffer[1], buffer[0]);
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = encode_uint16(buffer[3], buffer[2]);
}
// Read the chip type and project id and validate the controller.
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read chip ID"));
this->mark_failed();
return;
}
this->chip_id_ = encode_uint16(buffer[3], buffer[2]);
this->project_id_ = encode_uint16(buffer[1], buffer[0]);
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
this->status_set_error(LOG_STR("Unknown chip ID"));
this->mark_failed();
return;
}
// Fall back to the display dimensions if the resolution read failed.
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = this->display_->get_native_width();
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = this->display_->get_native_height();
this->setup_complete_ = true;
}
void CST9220Touchscreen::update_touches() {
if (!this->setup_complete_)
return;
uint8_t data[CST9220_DATA_LENGTH];
// Only an actual I2C failure should skip the update; a successful read with no
// touches is a real "all fingers lifted" state that must flow through so the
// base class can generate the release event.
if (this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) != i2c::ERROR_OK) {
this->status_set_warning();
this->skip_update_ = true;
return;
}
this->status_clear_warning();
// Acknowledge the report so the controller can prepare the next one.
uint8_t ack = TOUCH_ACK;
this->write_register16(REG_TOUCH_DATA, &ack, 1);
// A valid report carries the ACK marker at offset 6; offset 0 holds the first
// point and must be neither the ACK marker nor empty. Anything else means no
// valid touch data this cycle, which we report as zero touches (not a skip).
if (data[0] == TOUCH_ACK || data[0] == 0x00 || data[6] != TOUCH_ACK)
return;
uint8_t num_touches = data[5] & 0x7F;
if (num_touches > CST9220_MAX_TOUCHES)
num_touches = CST9220_MAX_TOUCHES;
for (uint8_t i = 0; i < num_touches; i++) {
// The first point starts at offset 0; subsequent points are offset by the
// two status bytes that follow it.
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
uint8_t id = p[0] >> 4;
uint8_t event = p[0] & 0x0F;
if (event != TOUCH_EVENT_DOWN)
continue;
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
uint16_t x = (p[1] << 4) | (p[3] >> 4);
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
this->add_raw_touch_position_(id, x, y);
}
}
void CST9220Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG,
"CST9220 Touchscreen:\n"
" Chip ID: 0x%04X\n"
" Project ID: 0x%04X\n"
" X Raw Min: %d, X Raw Max: %d\n"
" Y Raw Min: %d, Y Raw Max: %d",
this->chip_id_, this->project_id_, this->x_raw_min_, this->x_raw_max_, this->y_raw_min_,
this->y_raw_max_);
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
} // namespace esphome::cst9220
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::cst9220 {
static const char *const TAG = "cst9220.touchscreen";
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
static const uint8_t TOUCH_ACK = 0xAB;
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
static const uint16_t CST9220_CHIP_ID = 0x9220;
static const uint16_t CST9217_CHIP_ID = 0x9217;
// Maximum simultaneous touch points reported by the family.
static const uint8_t CST9220_MAX_TOUCHES = 5;
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void continue_setup_();
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint16_t chip_id_{};
uint16_t project_id_{};
bool setup_complete_{};
};
} // namespace esphome::cst9220
+5 -5
View File
@@ -211,14 +211,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 13, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
}
@@ -359,7 +359,9 @@ if __name__ == "__main__":
check_base_code(BASE_CODE_INIT)
# list all boards from ltchiptool
components_dir = Path(__file__).parent.parent
boards = [Board(b) for b in Board.get_list()]
# Board.get_list() returns glob (filesystem) order, which is non-deterministic
# and produces noisy diffs on regeneration; sort by board id for stable output.
boards = sorted((Board(b) for b in Board.get_list()), key=lambda b: b.name)
# keep track of all supported root- and chip-families
components = set()
families = {}
+412 -85
View File
@@ -15,26 +15,38 @@ Any manual changes WILL BE LOST on regeneration.
from esphome.components.libretiny.const import FAMILY_LN882H
LN882X_BOARDS = {
"generic-ln882hki": {
"name": "Generic - LN882HKI",
"generic-ln882h": {
"name": "Generic - LN882H",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"generic-ln882h-tuya": {
"name": "Generic - LN882H (Tuya)",
"family": FAMILY_LN882H,
},
"ln-02": {
"name": "LN-02 Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"ln-cb3s-v1.0": {
"name": "LN-CB3S V1.0",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2h-u": {
"name": "WL2H-U Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
}
LN882X_BOARD_PINS = {
"generic-ln882hki": {
"generic-ln882h": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
@@ -153,27 +165,292 @@ LN882X_BOARD_PINS = {
"A6": 20,
"A7": 21,
},
"generic-ln882h-tuya": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 8,
"WIRE0_SCL_9": 9,
"WIRE0_SCL_10": 10,
"WIRE0_SCL_11": 11,
"WIRE0_SCL_12": 12,
"WIRE0_SCL_13": 19,
"WIRE0_SCL_14": 20,
"WIRE0_SCL_15": 21,
"WIRE0_SCL_16": 22,
"WIRE0_SCL_17": 23,
"WIRE0_SCL_18": 24,
"WIRE0_SCL_19": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 8,
"WIRE0_SDA_9": 9,
"WIRE0_SDA_10": 10,
"WIRE0_SDA_11": 11,
"WIRE0_SDA_12": 12,
"WIRE0_SDA_13": 19,
"WIRE0_SDA_14": 20,
"WIRE0_SDA_15": 21,
"WIRE0_SDA_16": 22,
"WIRE0_SDA_17": 23,
"WIRE0_SDA_18": 24,
"WIRE0_SDA_19": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 5,
"D6": 6,
"D7": 7,
"D8": 8,
"D9": 9,
"D10": 10,
"D11": 11,
"D12": 12,
"D13": 19,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"D18": 24,
"D19": 25,
"A2": 0,
"A3": 1,
"A4": 4,
"A5": 19,
"A6": 20,
"A7": 21,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
"ln-cb3s-v1.0": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 20,
"WIRE0_SCL_10": 21,
"WIRE0_SCL_11": 22,
"WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 20,
"WIRE0_SDA_10": 21,
"WIRE0_SDA_11": 22,
"WIRE0_SDA_12": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 4,
"D3": 5,
"D4": 6,
"D5": 20,
"D6": 25,
"D7": 9,
"D8": 21,
"D9": 22,
"D10": 3,
"D11": 2,
"D12": 11,
"A0": 0,
"A1": 1,
"A2": 4,
"A3": 20,
"A4": 21,
},
"wb02a": {
"WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 25,
"WIRE0_SCL_5": 6,
"WIRE0_SCL_6": 7,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SDA_0": 1,
"WIRE0_SDA_1": 2,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 25,
"WIRE0_SDA_5": 6,
"WIRE0_SDA_6": 7,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -190,6 +467,8 @@ LN882X_BOARD_PINS = {
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA09": 9,
@@ -206,18 +485,128 @@ LN882X_BOARD_PINS = {
"TX0": 2,
"TX1": 25,
"D0": 7,
"D1": 5,
"D1": 6,
"D2": 3,
"D3": 10,
"D4": 2,
"D5": 1,
"D6": 4,
"D7": 9,
"D8": 24,
"D9": 25,
"D7": 5,
"D8": 9,
"D9": 24,
"D10": 25,
"A0": 1,
"A1": 4,
},
"wl2h-u": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 11,
"WIRE0_SCL_10": 12,
"WIRE0_SCL_11": 19,
"WIRE0_SCL_12": 20,
"WIRE0_SCL_13": 21,
"WIRE0_SCL_14": 22,
"WIRE0_SCL_15": 23,
"WIRE0_SCL_16": 24,
"WIRE0_SCL_17": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 11,
"WIRE0_SDA_10": 12,
"WIRE0_SDA_11": 19,
"WIRE0_SDA_12": 20,
"WIRE0_SDA_13": 21,
"WIRE0_SDA_14": 22,
"WIRE0_SDA_15": 23,
"WIRE0_SDA_16": 24,
"WIRE0_SDA_17": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 5,
"D1": 6,
"D2": 4,
"D3": 1,
"D4": 0,
"D5": 24,
"D6": 25,
"D7": 7,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 19,
"D12": 2,
"D13": 3,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"A0": 4,
"A1": 1,
"A2": 0,
"A3": 19,
"A4": 20,
"A5": 21,
},
"wl2s": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
@@ -298,68 +687,6 @@ LN882X_BOARD_PINS = {
"A1": 19,
"A2": 1,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
}
BOARDS = LN882X_BOARDS
+15
View File
@@ -227,6 +227,21 @@ async def to_code(config):
# TCP links; Zephyr falls back to sys_rand32_get() for the ISN (randomized, but not the
# RFC 6528 keyed hash).
zephyr_add_prj_conf("NET_TCP_ISN_RFC6528", False)
# Enlarge the Zephyr network buffer pool and TCP windows for the Thread path.
# Zephyr's defaults are tiny: NET_BUF_TX_COUNT=16 * NET_BUF_DATA_SIZE=128 is only
# ~2 KB of TX data -- barely one 1280-byte IPv6 packet once 6LoWPAN fragments it.
# The ESPHome API entity-sync burst overruns that instantly, so socket writes fail
# with ENOBUFS ("Buffer full") and the connection is dropped. ESP32 sidesteps this
# by enlarging the lwIP TCP window (CONFIG_LWIP_TCP_* above); give Zephyr the
# equivalent headroom, sized to RAM and the Thread 1280-byte MTU (not ESP32's 64 KB).
# The bounded send window also provides flow control so TCP stops queueing past
# what the buffer pool can hold instead of erroring.
zephyr_add_prj_conf("NET_PKT_RX_COUNT", 24)
zephyr_add_prj_conf("NET_PKT_TX_COUNT", 24)
zephyr_add_prj_conf("NET_BUF_RX_COUNT", 48)
zephyr_add_prj_conf("NET_BUF_TX_COUNT", 48)
zephyr_add_prj_conf("NET_TCP_MAX_RECV_WINDOW_SIZE", 2280)
zephyr_add_prj_conf("NET_TCP_MAX_SEND_WINDOW_SIZE", 2280)
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@jesserockz"]
+43
View File
@@ -0,0 +1,43 @@
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_MODEL
from esphome.types import ConfigType
DEPENDENCIES = ["spi"]
AUTO_LOAD = ["split_buffer"]
CONF_PIXOO_ID = "pixoo_id"
pixoo_ns = cg.esphome_ns.namespace("pixoo")
Pixoo = pixoo_ns.class_("Pixoo", cg.PollingComponent, display.Display, spi.SPIDevice)
PixooModel = pixoo_ns.enum("PixooModel")
# Only the 64x64 panel is hardware-verified. Smaller Pixoo panels are assumed to share the
# same protocol; add them here once confirmed.
MODELS = {
"64X64": PixooModel.PIXOO_64,
}
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Pixoo),
cv.Optional(CONF_MODEL, default="64X64"): cv.enum(MODELS, upper=True),
}
).extend(spi.spi_device_schema(cs_pin_required=True, default_data_rate=8e6))
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"pixoo", require_miso=False, require_mosi=True
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_MODEL])
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
if (lambda_config := config.get(CONF_LAMBDA)) is not None:
lambda_ = await cg.process_lambda(
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
@@ -0,0 +1,24 @@
import esphome.codegen as cg
from esphome.components import light
import esphome.config_validation as cv
from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
from esphome.types import ConfigType
from ..display import CONF_PIXOO_ID, Pixoo, pixoo_ns
PixooLight = pixoo_ns.class_("PixooLight", light.LightOutput)
CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(PixooLight),
cv.GenerateID(CONF_PIXOO_ID): cv.use_id(Pixoo),
# The LED board applies its own gamma, so default to no gamma correction here.
cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
await cg.register_parented(var, config[CONF_PIXOO_ID])
@@ -0,0 +1,26 @@
#pragma once
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
#include "esphome/components/pixoo/pixoo.h"
#include "esphome/core/helpers.h"
namespace esphome::pixoo {
// Brightness-only light that drives the Pixoo panel's LIGHT command.
class PixooLight : public light::LightOutput, public Parented<Pixoo> {
public:
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
return traits;
}
void write_state(light::LightState *state) override {
float brightness;
state->current_values_as_brightness(&brightness);
this->parent_->set_panel_brightness(brightness);
}
};
} // namespace esphome::pixoo
+201
View File
@@ -0,0 +1,201 @@
#include "pixoo.h"
#include "esphome/core/log.h"
#include <cmath>
#include <cstring>
#include <utility>
namespace esphome::pixoo {
static const char *const TAG = "pixoo";
// Divoom LED-board packet protocol.
static constexpr uint8_t PACKET_HEAD = 0xAA;
static constexpr uint8_t PACKET_TAIL = 0xBB;
static constexpr uint8_t CMD_DATA = 0x00;
static constexpr uint8_t CMD_LIGHT = 0x01;
static constexpr uint8_t CMD_UNUSED = 0x21;
static constexpr uint8_t CMD_SET_RGB_IOUT = 0x22;
static constexpr size_t PACKET_HEADER_LEN = 4; // head + len(2) + cmd
static constexpr size_t PACKET_STATIC_LEN = 5; // header + tail
static constexpr uint8_t DEFAULT_IOUT = 75; // per-channel LED current / white balance default
// Pack a `0xAA len cmd data 0xBB` packet into buf; returns the packet length.
static inline size_t build_packet(uint8_t *buf, uint8_t cmd, const uint8_t *data, uint16_t len) {
buf[0] = PACKET_HEAD;
buf[1] = static_cast<uint8_t>(len & 0xFF);
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
buf[3] = cmd;
if (data != nullptr && len > 0)
std::memcpy(buf + PACKET_HEADER_LEN, data, len);
buf[PACKET_HEADER_LEN + len] = PACKET_TAIL;
return len + PACKET_STATIC_LEN;
}
// Fill `total` bytes at buf with a single UNUSED padding packet.
static inline void pad_unused(uint8_t *buf, size_t total) {
const uint16_t len = static_cast<uint16_t>(total - PACKET_STATIC_LEN);
buf[0] = PACKET_HEAD;
buf[1] = static_cast<uint8_t>(len & 0xFF);
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
buf[3] = CMD_UNUSED;
buf[total - 1] = PACKET_TAIL;
}
float Pixoo::get_setup_priority() const { return setup_priority::PROCESSOR; }
void Pixoo::setup() {
const uint32_t num_pixels = static_cast<uint32_t>(this->model_) * this->model_;
this->data_size_ = num_pixels * 3;
// The frame is a DATA packet (header + RGB888 + tail) followed by a DMA-chunk-sized UNUSED
// packet, so the LED board completes its final DMA block.
this->frame_size_ = this->data_size_ + PACKET_STATIC_LEN + DMA_CHUNK;
if (!this->buffer_.init(this->data_size_)) {
this->mark_failed(LOG_STR("Failed to allocate draw buffer"));
return;
}
// The frame is shipped in one SPI transfer, so keep it in DMA-capable internal RAM.
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
this->frame_buffer_ = allocator.allocate(this->frame_size_);
if (this->frame_buffer_ == nullptr) {
this->buffer_.free();
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
return;
}
std::memset(this->frame_buffer_, 0, this->frame_size_);
// Pre-build the constant DATA-packet framing; only the RGB888 payload changes per frame.
this->frame_buffer_[0] = PACKET_HEAD;
this->frame_buffer_[1] = static_cast<uint8_t>(this->data_size_ & 0xFF);
this->frame_buffer_[2] = static_cast<uint8_t>((this->data_size_ >> 8) & 0xFF);
this->frame_buffer_[3] = CMD_DATA;
this->frame_buffer_[PACKET_HEADER_LEN + this->data_size_] = PACKET_TAIL;
pad_unused(this->frame_buffer_ + this->data_size_ + PACKET_STATIC_LEN, DMA_CHUNK);
this->spi_setup();
this->buffer_.fill(0x00);
// Set the per-channel LED current. Brightness is controlled separately via the light platform.
const uint8_t iout[3] = {DEFAULT_IOUT, DEFAULT_IOUT, DEFAULT_IOUT};
this->send_command_(CMD_SET_RGB_IOUT, iout, 3);
// Frames are pushed synchronously inside update(), so there is no loop() work to do and the
// component is idle between updates. Marking it done (LOOP_DONE) lets LVGL's
// update_when_display_idle option treat the panel as idle and drive frames on demand.
this->disable_loop();
}
void Pixoo::send_command_(uint8_t cmd, const uint8_t *data, uint16_t len) {
std::memset(this->cmd_buffer_, 0, DMA_CHUNK);
const size_t used = build_packet(this->cmd_buffer_, cmd, data, len);
if (DMA_CHUNK - used >= PACKET_STATIC_LEN)
pad_unused(this->cmd_buffer_ + used, DMA_CHUNK - used);
this->enable();
this->write_array(this->cmd_buffer_, DMA_CHUNK);
this->disable();
}
void Pixoo::set_panel_brightness(float brightness) {
const uint8_t pct = static_cast<uint8_t>(lroundf(clamp(brightness, 0.0f, 1.0f) * 100.0f));
this->send_command_(CMD_LIGHT, &pct, 1);
}
void Pixoo::update() {
this->do_update_();
for (size_t i = 0; i < this->data_size_; i++)
this->frame_buffer_[PACKET_HEADER_LEN + i] = this->buffer_[i];
this->enable();
this->write_array(this->frame_buffer_, this->frame_size_);
this->disable();
}
void Pixoo::set_pixel_(uint32_t index, Color color) {
const size_t off = static_cast<size_t>(index) * 3;
this->buffer_[off] = color.r;
this->buffer_[off + 1] = color.g;
this->buffer_[off + 2] = color.b;
}
void HOT Pixoo::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
const int side = static_cast<int>(this->model_);
switch (this->rotation_) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
std::swap(x, y);
x = side - x - 1;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
x = side - x - 1;
y = side - y - 1;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
std::swap(x, y);
y = side - y - 1;
break;
}
if (x < 0 || x >= side || y < 0 || y >= side)
return;
this->set_pixel_(static_cast<uint32_t>(y) * side + x, color);
}
void Pixoo::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
// Fast path for the common LVGL/image blit: RGB565, RGB order, no rotation, no active clipping.
// Anything else defers to the base implementation, which decodes per pixel and routes through
// draw_pixel_at() so rotation, clipping and other color formats stay correct.
// NOTE: the stride/index math and 565->888 expansion below mirror Display::draw_pixels_at (the
// source of truth) -- keep them in sync if the base ever changes its source layout or decoding.
if (bitness != display::COLOR_BITNESS_565 || order != display::COLOR_ORDER_RGB ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
return;
}
const int side = static_cast<int>(this->model_);
const size_t line_stride = static_cast<size_t>(x_offset) + w + x_pad;
for (int y = 0; y != h; y++) {
const int dst_y = y_start + y;
if (dst_y < 0 || dst_y >= side)
continue;
size_t source_idx = (static_cast<size_t>(y_offset) + y) * line_stride + x_offset;
for (int x = 0; x != w; x++, source_idx++) {
const int dst_x = x_start + x;
if (dst_x < 0 || dst_x >= side)
continue;
const size_t byte_idx = source_idx * 2;
const uint16_t rgb565 =
big_endian ? (ptr[byte_idx] << 8) | ptr[byte_idx + 1] : ptr[byte_idx] | (ptr[byte_idx + 1] << 8);
const uint8_t r5 = (rgb565 >> 11) & 0x1F;
const uint8_t g6 = (rgb565 >> 5) & 0x3F;
const uint8_t b5 = rgb565 & 0x1F;
this->set_pixel_(static_cast<uint32_t>(dst_y) * side + dst_x,
Color((r5 << 3) | (r5 >> 2), (g6 << 2) | (g6 >> 4), (b5 << 3) | (b5 >> 2)));
}
}
}
void Pixoo::fill(Color color) {
if (this->is_clipping()) {
display::Display::fill(color);
return;
}
for (size_t i = 0; i < this->data_size_; i += 3) {
this->buffer_[i] = color.r;
this->buffer_[i + 1] = color.g;
this->buffer_[i + 2] = color.b;
}
}
void Pixoo::dump_config() {
LOG_DISPLAY("", "Divoom Pixoo", this);
ESP_LOGCONFIG(TAG, " Model: %ux%u", (unsigned) this->model_, (unsigned) this->model_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::pixoo
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "esphome/components/display/display.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::pixoo {
// The Pixoo's main board (where ESPHome runs) talks to a separate LED-driver board (a GD32/AT32
// MCU) over SPI using Divoom's packet protocol:
// 0xAA, len_lo, len_hi, cmd, <data...>, 0xBB
// The image is sent as a DATA (0x00) packet carrying width*height*3 bytes of RGB888; brightness is
// a separate LIGHT (0x01) command; the LED current is set once via SET_RGB_IOUT (0x22). Command
// packets are padded out to the LED board's 240-byte DMA chunk with an UNUSED (0x21) packet.
// The model selects the (square) panel side length.
enum PixooModel : uint8_t {
PIXOO_64 = 64,
};
class Pixoo : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
explicit Pixoo(PixooModel model) : model_(model) {}
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override;
// Brightness is controlled exclusively via the light platform: send a LIGHT command to the LED
// board (brightness 0..1 -> 0..100%).
void set_panel_brightness(float brightness);
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void fill(Color color) override;
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
protected:
int get_width_internal() override { return static_cast<int>(this->model_); }
int get_height_internal() override { return static_cast<int>(this->model_); }
void set_pixel_(uint32_t index, Color color);
void send_command_(uint8_t cmd, const uint8_t *data, uint16_t len);
// Size of the LED board's SPI DMA chunk; the command scratch buffer is one chunk.
static constexpr size_t DMA_CHUNK = 240;
PixooModel model_;
size_t data_size_{0}; // RGB888 image bytes: model^2 * 3
size_t frame_size_{0}; // full SPI frame: DATA packet + trailing UNUSED packet
split_buffer::SplitBuffer buffer_{};
uint8_t *frame_buffer_{nullptr};
uint8_t cmd_buffer_[DMA_CHUNK]{};
};
} // namespace esphome::pixoo
+13
View File
@@ -0,0 +1,13 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c", "motion"]
CONF_QMI8658_ID = "qmi8658_id"
# C++ namespace / class
qmi8658_ns = cg.esphome_ns.namespace("qmi8658")
QMI8658Component = qmi8658_ns.class_("QMI8658Component", MotionComponent, i2c.I2CDevice)
CONFIG_SCHEMA = {}
+93
View File
@@ -0,0 +1,93 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import (
CONF_ACCELEROMETER_ODR,
CONF_ACCELEROMETER_RANGE,
CONF_GYROSCOPE_ODR,
CONF_GYROSCOPE_RANGE,
)
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from . import QMI8658Component, qmi8658_ns
# Enum proxies (must match the C++ enum values exactly)
QMI8658AccelRange = qmi8658_ns.enum("QMI8658AccelRange")
ACCEL_RANGE_OPTIONS = {
"2G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_2G,
"4G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_4G,
"8G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_8G,
"16G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_16G,
}
QMI8658GyroRange = qmi8658_ns.enum("QMI8658GyroRange")
GYRO_RANGE_OPTIONS = {
"16DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_16,
"32DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_32,
"64DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_64,
"128DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_128,
"256DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_256,
"512DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_512,
"1024DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_1024,
"2048DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_2048,
}
QMI8658AccelODR = qmi8658_ns.enum("QMI8658AccelODR")
ACCEL_ODR_OPTIONS = {
"31_25HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_31_25,
"62_5HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_62_5,
"125HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_125,
"250HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_250,
"500HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_500,
"1000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_1000,
"2000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_2000,
"4000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_4000,
"8000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_8000,
}
QMI8658GyroODR = qmi8658_ns.enum("QMI8658GyroODR")
GYRO_ODR_OPTIONS = {
"31_25HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_31_25,
"62_5HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_62_5,
"125HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_125,
"250HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_250,
"500HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_500,
"1000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_1000,
"2000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_2000,
"4000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_4000,
"8000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_8000,
}
# Top-level CONFIG_SCHEMA
CONFIG_SCHEMA = (
motion_schema(QMI8658Component, has_accel=True, has_gyro=True)
.extend(
{
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
ACCEL_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_ACCELEROMETER_ODR, default="1000HZ"): cv.enum(
ACCEL_ODR_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_RANGE, default="2048DPS"): cv.enum(
GYRO_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_ODR, default="1000HZ"): cv.enum(
GYRO_ODR_OPTIONS, upper=True
),
}
)
.extend(i2c.i2c_device_schema(0x6B))
)
# Code generation
async def to_code(config):
var = await new_motion_component(config)
await i2c.register_i2c_device(var, config)
# Hardware configuration
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
+136
View File
@@ -0,0 +1,136 @@
#include "qmi8658.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::qmi8658 {
static const char *const TAG = "qmi8658";
// Acceleration scale (g per LSB), indexed by accel_range_ >> 4.
// Full-scale = range_g, mapped over a signed 16-bit value (2^15 counts).
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
16.0f / 32768.0f,
};
// Angular rate scale (°/s per LSB), indexed by gyro_range_ >> 4.
static constexpr float GYRO_SCALE[] = {
16.0f / 32768.0f, 32.0f / 32768.0f, 64.0f / 32768.0f, 128.0f / 32768.0f,
256.0f / 32768.0f, 512.0f / 32768.0f, 1024.0f / 32768.0f, 2048.0f / 32768.0f,
};
void QMI8658Component::setup() {
MotionComponent::setup();
// 1. Verify chip ID
uint8_t who_am_i = 0;
if (!this->read_byte(QMI8658_REG_WHO_AM_I, &who_am_i)) {
ESP_LOGE(TAG, "Failed to read chip ID - check wiring / address");
this->mark_failed();
return;
}
if (who_am_i != QMI8658_WHO_AM_I_VALUE) {
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", who_am_i, QMI8658_WHO_AM_I_VALUE);
this->mark_failed();
return;
}
// 2. Soft reset
if (!this->write_byte(QMI8658_REG_RESET, QMI8658_RESET_CMD)) {
this->mark_failed();
return;
}
delay(15); // spec: wait for reset to complete
// 3. Serial interface: enable register address auto-increment
if (!this->write_byte(QMI8658_REG_CTRL1, QMI8658_CTRL1_VALUE)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL1"));
return;
}
// 4. Configure accelerometer (CTRL2 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL2, (uint8_t) (this->accel_range_) | (uint8_t) (this->accel_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL2"));
return;
}
// 5. Configure gyroscope (CTRL3 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL3, (uint8_t) (this->gyro_range_) | (uint8_t) (this->gyro_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL3"));
return;
}
// 6. Disable the built-in low-pass filters (leave raw data to the motion pipeline)
if (!this->write_byte(QMI8658_REG_CTRL5, 0x00)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL5"));
this->mark_failed();
return;
}
// 7. Enable accelerometer and gyroscope
if (!this->write_byte(QMI8658_REG_CTRL7, QMI8658_CTRL7_ACC_EN | QMI8658_CTRL7_GYR_EN)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL7"));
return;
}
ESP_LOGCONFIG(TAG, "QMI8658 initialised successfully");
}
void QMI8658Component::dump_config() {
ESP_LOGCONFIG(TAG, "QMI8658 IMU:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, " Communication failed!");
return;
}
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±16°/s", "±32°/s", "±64°/s", "±128°/s",
"±256°/s", "±512°/s", "±1024°/s", "±2048°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[this->accel_range_ >> 4]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[this->gyro_range_ >> 4]);
MotionComponent::dump_config();
}
bool QMI8658Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
// Read temperature + accel + gyro in one contiguous block starting at TEMP_L.
uint8_t raw_data[REG_READ_LEN];
if (!this->read_bytes(QMI8658_REG_TEMP_L, raw_data, REG_READ_LEN)) {
ESP_LOGW(TAG, "Failed to read IMU data");
return false;
}
// Data is little-endian (low byte first).
float scale = ACCEL_SCALE[this->accel_range_ >> 4];
int16_t raw_x = encode_uint16(raw_data[ACC_OFFS + 1], raw_data[ACC_OFFS + 0]);
int16_t raw_y = encode_uint16(raw_data[ACC_OFFS + 3], raw_data[ACC_OFFS + 2]);
int16_t raw_z = encode_uint16(raw_data[ACC_OFFS + 5], raw_data[ACC_OFFS + 4]);
ESP_LOGV(TAG, "Read raw accel data: %d, %d, %d", raw_x, raw_y, raw_z);
data.acceleration[motion::X_AXIS] = raw_x * scale;
data.acceleration[motion::Y_AXIS] = raw_y * scale;
data.acceleration[motion::Z_AXIS] = raw_z * scale;
scale = GYRO_SCALE[this->gyro_range_ >> 4];
raw_x = encode_uint16(raw_data[GYR_OFFS + 1], raw_data[GYR_OFFS + 0]);
raw_y = encode_uint16(raw_data[GYR_OFFS + 3], raw_data[GYR_OFFS + 2]);
raw_z = encode_uint16(raw_data[GYR_OFFS + 5], raw_data[GYR_OFFS + 4]);
ESP_LOGV(TAG, "Read raw gyro data: %d, %d, %d", raw_x, raw_y, raw_z);
data.angular_rate[motion::X_AXIS] = raw_x * scale;
data.angular_rate[motion::Y_AXIS] = raw_y * scale;
data.angular_rate[motion::Z_AXIS] = raw_z * scale;
if (this->temperature_callback_.empty())
return true;
// Temperature: signed 16-bit, °C = raw / 256
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
this->temperature_callback_.call(raw_t / 256.0f);
return true;
}
} // namespace esphome::qmi8658
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include "esphome/components/motion/motion_component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::qmi8658 {
// Register map
static constexpr uint8_t QMI8658_REG_WHO_AM_I = 0x00;
static constexpr uint8_t QMI8658_REG_REVISION = 0x01;
static constexpr uint8_t QMI8658_REG_CTRL1 = 0x02; // serial interface / auto-increment
static constexpr uint8_t QMI8658_REG_CTRL2 = 0x03; // accelerometer ODR / range
static constexpr uint8_t QMI8658_REG_CTRL3 = 0x04; // gyroscope ODR / range
static constexpr uint8_t QMI8658_REG_CTRL5 = 0x06; // low-pass filter
static constexpr uint8_t QMI8658_REG_CTRL7 = 0x08; // sensor enable
static constexpr uint8_t QMI8658_REG_STATUS0 = 0x2E;
static constexpr uint8_t QMI8658_REG_TEMP_BASE = 0x33; // start of the data block
static constexpr uint8_t QMI8658_REG_TEMP_L = 0x33; // Low byte of temperature
static constexpr uint8_t QMI8658_REG_AX_L = 0x35;
static constexpr uint8_t QMI8658_REG_GX_L = 0x3B;
static constexpr uint8_t QMI8658_REG_RESET = 0x60;
// One contiguous read covers temperature (2) + accel (6) + gyro (6) starting at TEMP_L.
static constexpr uint8_t REG_READ_LEN = QMI8658_REG_GX_L + 6 - QMI8658_REG_TEMP_BASE; // 0x41 - 0x33 = 14
static constexpr uint8_t TEMP_OFFS = QMI8658_REG_TEMP_L - QMI8658_REG_TEMP_BASE; // 0
static constexpr uint8_t ACC_OFFS = QMI8658_REG_AX_L - QMI8658_REG_TEMP_BASE; // 2
static constexpr uint8_t GYR_OFFS = QMI8658_REG_GX_L - QMI8658_REG_TEMP_BASE; // 8
static constexpr uint8_t QMI8658_WHO_AM_I_VALUE = 0x05;
static constexpr uint8_t QMI8658_RESET_CMD = 0xB0;
// CTRL1: bit6 ADDR_AI (register address auto-increment); little-endian, 4-wire SPI
static constexpr uint8_t QMI8658_CTRL1_VALUE = 0x40;
// CTRL7: aEN (bit0) | gEN (bit1)
static constexpr uint8_t QMI8658_CTRL7_ACC_EN = 0x01;
static constexpr uint8_t QMI8658_CTRL7_GYR_EN = 0x02;
// Accelerometer range options (CTRL2 bits 6:4)
enum QMI8658AccelRange : uint8_t {
QMI8658_ACCEL_RANGE_2G = 0x00,
QMI8658_ACCEL_RANGE_4G = 0x10,
QMI8658_ACCEL_RANGE_8G = 0x20,
QMI8658_ACCEL_RANGE_16G = 0x30,
};
// Accelerometer ODR options (CTRL2 bits 3:0)
enum QMI8658AccelODR : uint8_t {
QMI8658_ACCEL_ODR_8000 = 0x00,
QMI8658_ACCEL_ODR_4000 = 0x01,
QMI8658_ACCEL_ODR_2000 = 0x02,
QMI8658_ACCEL_ODR_1000 = 0x03,
QMI8658_ACCEL_ODR_500 = 0x04,
QMI8658_ACCEL_ODR_250 = 0x05,
QMI8658_ACCEL_ODR_125 = 0x06,
QMI8658_ACCEL_ODR_62_5 = 0x07,
QMI8658_ACCEL_ODR_31_25 = 0x08,
};
// Gyroscope range options (CTRL3 bits 6:4)
enum QMI8658GyroRange : uint8_t {
QMI8658_GYRO_RANGE_16 = 0x00,
QMI8658_GYRO_RANGE_32 = 0x10,
QMI8658_GYRO_RANGE_64 = 0x20,
QMI8658_GYRO_RANGE_128 = 0x30,
QMI8658_GYRO_RANGE_256 = 0x40,
QMI8658_GYRO_RANGE_512 = 0x50,
QMI8658_GYRO_RANGE_1024 = 0x60,
QMI8658_GYRO_RANGE_2048 = 0x70,
};
// Gyroscope ODR options (CTRL3 bits 3:0)
enum QMI8658GyroODR : uint8_t {
QMI8658_GYRO_ODR_8000 = 0x00,
QMI8658_GYRO_ODR_4000 = 0x01,
QMI8658_GYRO_ODR_2000 = 0x02,
QMI8658_GYRO_ODR_1000 = 0x03,
QMI8658_GYRO_ODR_500 = 0x04,
QMI8658_GYRO_ODR_250 = 0x05,
QMI8658_GYRO_ODR_125 = 0x06,
QMI8658_GYRO_ODR_62_5 = 0x07,
QMI8658_GYRO_ODR_31_25 = 0x08,
};
// Main component class
class QMI8658Component : public motion::MotionComponent, public i2c::I2CDevice {
public:
// Lifecycle
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Configuration setters
void set_accel_range(QMI8658AccelRange r) { this->accel_range_ = r; }
void set_accel_odr(QMI8658AccelODR o) { this->accel_odr_ = o; }
void set_gyro_range(QMI8658GyroRange r) { this->gyro_range_ = r; }
void set_gyro_odr(QMI8658GyroODR o) { this->gyro_odr_ = o; }
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
protected:
bool update_data(motion::MotionData &data) override;
// Config
QMI8658AccelRange accel_range_{QMI8658_ACCEL_RANGE_4G};
QMI8658AccelODR accel_odr_{QMI8658_ACCEL_ODR_1000};
QMI8658GyroRange gyro_range_{QMI8658_GYRO_RANGE_2048};
QMI8658GyroODR gyro_odr_{QMI8658_GYRO_ODR_1000};
LazyCallbackManager<void(float)> temperature_callback_{};
};
} // namespace esphome::qmi8658
+39
View File
@@ -0,0 +1,39 @@
# YAML config keys
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.cpp_generator import MockObj
from . import CONF_QMI8658_ID, QMI8658Component
CONFIG_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_TEMPERATURE,
).extend(
{
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
cv.GenerateID(CONF_QMI8658_ID): cv.use_id(QMI8658Component),
}
)
async def to_code(config):
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_QMI8658_ID])
data = MockObj("data")
value_lambda = await cg.process_lambda(
var.publish_state(data),
[(cg.float_, str(data))],
)
cg.add(parent.add_temperature_listener(value_lambda))
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -224,7 +224,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino]
extends = common:arduino
platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1
platform = https://github.com/libretiny-eu/libretiny.git#v1.13.0
framework = arduino
lib_compat_mode = soft
lib_deps =
@@ -525,7 +525,7 @@ build_unflags =
[env:ln882h-arduino]
extends = common:libretiny-arduino
board = generic-ln882hki
board = generic-ln882h
build_flags =
${common:libretiny-arduino.build_flags}
${flags:runtime.build_flags}
+16
View File
@@ -0,0 +1,16 @@
display:
- id: cst9220_display
platform: ili9xxx
model: ili9342
cs_pin: ${cs_pin}
dc_pin: ${dc_pin}
reset_pin: ${disp_reset_pin}
invert_colors: false
touchscreen:
- id: ts_cst9220
i2c_id: i2c_bus
platform: cst9220
display: cst9220_display
interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin}
@@ -0,0 +1,12 @@
substitutions:
cs_pin: GPIO4
dc_pin: GPIO5
disp_reset_pin: GPIO12
interrupt_pin: GPIO15
reset_pin: GPIO25
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
+26
View File
@@ -0,0 +1,26 @@
display:
- platform: pixoo
id: pixoo_display
model: 64x64
cs_pin: GPIO5
data_rate: 10MHz
update_interval: 1s
lambda: |-
it.fill(Color(0, 0, 0));
it.filled_rectangle(0, 0, 16, 16, Color(255, 0, 0));
it.line(0, 0, 63, 63, Color(0, 255, 0));
- platform: pixoo
id: pixoo_display_pages
model: 64x64
cs_pin: GPIO21
rotation: 90
pages:
- id: pixoo_page
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height(), Color(0, 0, 255));
light:
- platform: pixoo
pixoo_id: pixoo_display
name: Pixoo Brightness
@@ -0,0 +1,4 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
+69
View File
@@ -0,0 +1,69 @@
sensor:
- platform: qmi8658
name: "QMI8658 Temperature"
- platform: motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
type: pitch
name: "Pitch"
- platform: motion
type: roll
name: "Roll"
motion:
- platform: qmi8658
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Accelerometer output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
accelerometer_odr: 1000HZ
# Gyroscope full-scale range: 16DPS | 32DPS | 64DPS | 128DPS |
# 256DPS | 512DPS | 1024DPS | 2048DPS
gyroscope_range: 2048DPS
# Gyroscope output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
gyroscope_odr: 1000HZ
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,7 +3,7 @@ esphome:
friendly_name: $component_name
ln882x:
board: generic-ln882hki
board: generic-ln882h
logger:
level: VERY_VERBOSE