From 0858ecbb8e425c54638fe2151cce3a2bad5048e8 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:05:12 -0400 Subject: [PATCH] [spa06_base] Add SPA06-003 Temperature and Pressure Sensor (Part 1 of 3) (#14521) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_base/__init__.py | 201 ++++++++++++ esphome/components/spa06_base/spa06_base.cpp | 320 +++++++++++++++++++ esphome/components/spa06_base/spa06_base.h | 257 +++++++++++++++ 4 files changed, 779 insertions(+) create mode 100644 esphome/components/spa06_base/__init__.py create mode 100644 esphome/components/spa06_base/spa06_base.cpp create mode 100644 esphome/components/spa06_base/spa06_base.h diff --git a/CODEOWNERS b/CODEOWNERS index 88f62c3194..5869925d7c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -457,6 +457,7 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt +esphome/components/spa06_base/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py new file mode 100644 index 0000000000..97d09aad81 --- /dev/null +++ b/esphome/components/spa06_base/__init__.py @@ -0,0 +1,201 @@ +import math + +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@danielkent-net"] + +spa06_ns = cg.esphome_ns.namespace("spa06_base") + +SampleRate = spa06_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "1": SampleRate.SAMPLE_RATE_1, + "2": SampleRate.SAMPLE_RATE_2, + "4": SampleRate.SAMPLE_RATE_4, + "8": SampleRate.SAMPLE_RATE_8, + "16": SampleRate.SAMPLE_RATE_16, + "32": SampleRate.SAMPLE_RATE_32, + "64": SampleRate.SAMPLE_RATE_64, + "128": SampleRate.SAMPLE_RATE_128, + "25p16": SampleRate.SAMPLE_RATE_25P16, + "25p8": SampleRate.SAMPLE_RATE_25P8, + "25p4": SampleRate.SAMPLE_RATE_25P4, + "25p2": SampleRate.SAMPLE_RATE_25P2, + "25": SampleRate.SAMPLE_RATE_25, + "50": SampleRate.SAMPLE_RATE_50, + "100": SampleRate.SAMPLE_RATE_100, + "200": SampleRate.SAMPLE_RATE_200, +} + +Oversampling = spa06_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent) + + +def spa_oversample_time(oversample): + # Pressure oversampling conversion times are listed on datasheet Pg. 26 + # Datasheet does not have a table for temperature oversampling; + # assumption is that it is the same as pressure + OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 3.6, + "2X": 5.2, + "4X": 8.4, + "8X": 14.8, + "16X": 27.6, + "32X": 53.2, + "64X": 104.4, + "128X": 206.8, + } + return OVERSAMPLING_CONVERSION_TIMES[oversample] + + +def spa_sample_rate(rate): + SAMPLE_RATE_OPTIONS_HZ = { + "1": 1.0, + "2": 2.0, + "4": 4.0, + "8": 8.0, + "16": 16.0, + "32": 32.0, + "64": 64.0, + "128": 128.0, + "25p16": 25.0 / 16.0, + "25p8": 25.0 / 8.0, + "25p4": 25.0 / 4.0, + "25p2": 25.0 / 2.0, + "25": 25.0, + "50": 50.0, + "100": 100.0, + "200": 200.0, + } + return SAMPLE_RATE_OPTIONS_HZ[rate] + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # No conversion time necessary without a pressure sensor + pressure_conversion_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = spa_oversample_time( + pressure_config.get(CONF_OVERSAMPLING) + ) + # Temperature required in all cases, default to minimum sample time + temperature_conversion_time = 3.6 + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = spa_oversample_time( + temperature_config.get(CONF_OVERSAMPLING) + ) + + # TODO: Read datasheet to find conversion time error + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +def measurement_timing_check(config): + + temp_time = 0.0 + if temperature_config := config.get(CONF_TEMPERATURE): + temp_oss = ( + spa_oversample_time(temperature_config.get(CONF_OVERSAMPLING)) / 1000.0 + ) + temp_hz = spa_sample_rate(temperature_config.get(CONF_SAMPLE_RATE)) + temp_time = temp_oss * temp_hz + + pres_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pres_oss = spa_oversample_time(pressure_config.get(CONF_OVERSAMPLING)) / 1000.0 + pres_hz = spa_sample_rate(pressure_config.get(CONF_SAMPLE_RATE)) + pres_time = pres_oss * pres_hz + + if temp_time + pres_time >= 1: + raise cv.Invalid( + "Combined sample_rate and oversampling for temperature and pressure is too high" + ) + return config + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPA06Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + }, +).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_sample_rate_config(temperature_config[CONF_SAMPLE_RATE]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_sample_rate_config(pressure_config[CONF_SAMPLE_RATE])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) + return var diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp new file mode 100644 index 0000000000..36268aa9a2 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -0,0 +1,320 @@ +#include "spa06_base.h" + +#include "esphome/core/helpers.h" + +namespace esphome::spa06_base { + +static const char *const TAG = "spa06"; + +// Sign extension function for <=16 bit types +inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint16(msb, lsb) << head) >> (16 - bits); +} + +// Sign extension function for <=32 bit types +inline int32_t decode32(uint8_t xmsb, uint8_t msb, uint8_t lsb, uint8_t xlsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint32(xmsb, msb, lsb, xlsb) << head) >> (32 - bits); +} + +void SPA06Component::dump_config() { + ESP_LOGCONFIG(TAG, "SPA06:"); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->temperature_rate_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->pressure_rate_))); + } +} + +void SPA06Component::setup() { + // Startup sequence for SPA06 (Pg. 16, Figure 4.6.4): + // 1. Perform a soft reset + // 2. Verify sensor chip ID matches + // 3. Verify coefficients are ready + // 4. Read coefficients + // 5. Configure temperature and pressure sensors + // 6. Write communication settings + // 7. Write measurement settings (background measurement mode) + + // 1. Soft reset + if (!this->soft_reset_()) { + this->mark_failed(LOG_STR("Reset failed")); + return; + } + + // soft_reset_() internally delays by 3ms to make sure that + // the sensor is in a ready state and coefficients are ready. + + // 2. Read chip ID + // TODO: check ID for consistency? + if (!spa_read_byte(SPA06_ID, &this->prod_id_.reg)) { + this->mark_failed(LOG_STR("Chip ID read failure")); + return; + } + ESP_LOGV(TAG, + "Product Info:\n" + " Prod ID: %u\n" + " Rev ID: %u", + this->prod_id_.bit.prod_id, this->prod_id_.bit.rev_id); + + // 3. Read chip readiness from MEAS_CFG + // First check if the sensor reports ready + if (!spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + this->mark_failed(LOG_STR("Sensor status read failure")); + return; + } + // Check if the sensor reports coefficients are ready + if (!meas_.bit.coef_ready) { + this->mark_failed(LOG_STR("Coefficients not ready")); + return; + } + + // 4. Read coefficients + if (!this->read_coefficients_()) { + this->mark_failed(LOG_STR("Coefficients read error")); + return; + } + + // 5. Configure temperature and pressure sensors + // Default to measuring both temperature and pressure + + // Temperature must be read regardless of configuration to compute pressure + // If temperature is not configured in config: + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->temperature_sensor_) { + this->temperature_rate_ = SAMPLE_RATE_1; + this->temperature_oversampling_ = OVERSAMPLING_NONE; + this->kt_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // If pressure is not configured in config + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->pressure_sensor_) { + this->pressure_rate_ = SAMPLE_RATE_1; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + this->kp_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // Write temperature settings + if (!write_temperature_settings_(this->temperature_oversampling_, this->temperature_rate_)) { + this->mark_failed(LOG_STR("Temperature settings write fail")); + return; + } + + // Write pressure settings + if (!write_pressure_settings_(this->pressure_oversampling_, this->pressure_rate_)) { + this->mark_failed(LOG_STR("Pressure settings write fail")); + return; + } + // 6. Write communication settings + // This call sets the bit shifts for pressure and temperature if + // their respective oversampling config is > X8 + // This call also disables interrupts, FIFO, and specifies SPI 4-wire + if (!write_communication_settings_(this->pressure_oversampling_ > OVERSAMPLING_X8, + this->temperature_oversampling_ > OVERSAMPLING_X8)) { + this->mark_failed(LOG_STR("Comm settings write fail")); + return; + } + + // 7. Write measurement settings + // This function sets background measurement mode without FIFO + if (!write_measurement_settings_(this->pressure_sensor_ ? MeasCrtl::MEASCRTL_BG_BOTH : MeasCrtl::MEASCRTL_BG_TEMP)) { + this->mark_failed(LOG_STR("Measurement settings write fail")); + return; + } +} + +bool SPA06Component::write_temperature_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_TMP_CFG); +} + +bool SPA06Component::write_pressure_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_PSR_CFG); +} + +bool SPA06Component::write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg) { + if (reg != SPA06_PSR_CFG && reg != SPA06_TMP_CFG) { + return false; + } + this->pt_meas_cfg_.bit.rate = rate; + this->pt_meas_cfg_.bit.prc = oversampling; + ESP_LOGD(TAG, "Config write: %02x", this->pt_meas_cfg_.reg); + return spa_write_byte(reg, this->pt_meas_cfg_.reg); +} + +bool SPA06Component::write_measurement_settings_(MeasCrtl crtl) { + this->meas_.bit.meas_crtl = crtl; + return spa_write_byte(SPA06_MEAS_CFG, this->meas_.reg); +} + +bool SPA06Component::write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl, + bool interrupt_fifo, bool interrupt_tmp, bool interrupt_prs, + bool enable_fifo, bool spi_3wire) { + this->cfg_.bit.p_shift = pressure_shift; + this->cfg_.bit.t_shift = temperature_shift; + this->cfg_.bit.int_hl = interrupt_hl; + this->cfg_.bit.int_fifo = interrupt_fifo; + this->cfg_.bit.int_tmp = interrupt_tmp; + this->cfg_.bit.int_prs = interrupt_prs; + this->cfg_.bit.fifo_en = enable_fifo; + this->cfg_.bit.spi_3wire = spi_3wire; + return spa_write_byte(SPA06_CFG_REG, this->cfg_.reg); +} + +bool SPA06Component::read_coefficients_() { + uint8_t coef[SPA06_COEF_LEN]; + if (!spa_read_bytes(SPA06_COEF, coef, SPA06_COEF_LEN)) { + return false; + } + this->c0_ = decode16(coef[0], coef[1], 12); + this->c1_ = decode16(coef[1], coef[2], 12, 4); + this->c00_ = decode32(coef[3], coef[4], coef[5], 0, 20); + this->c10_ = decode32(coef[5], coef[6], coef[7], 0, 20, 4); + this->c01_ = decode16(coef[8], coef[9], 16); + this->c11_ = decode16(coef[10], coef[11], 16); + this->c20_ = decode16(coef[12], coef[13], 16); + this->c21_ = decode16(coef[14], coef[15], 16); + this->c30_ = decode16(coef[16], coef[17], 16); + this->c31_ = decode16(coef[18], coef[19], 12); + this->c40_ = decode16(coef[19], coef[20], 12, 4); + + ESP_LOGV(TAG, + "Coefficients:\n" + " c0: %i, c1: %i,\n" + " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c01: %i, c11: %i, c21: %i, c31: %i", + this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, + this->c21_, this->c31_); + return true; +} + +bool SPA06Component::soft_reset_() { + // Setup steps for SPA06: + // 1. Perform a protocol reset (required to write command for SPI code, noop for I2C) + this->protocol_reset(); + + // 2. Perform the actual reset + this->reset_.bit.fifo_flush = true; + this->reset_.bit.soft_rst = SPA06_SOFT_RESET; + if (!this->spa_write_byte(SPA06_RESET, this->reset_.reg)) { + return false; + } + + // 3. Wait for chip to become ready. Datasheet specifies 2 ms; wait 3 + delay(3); + // 4. Perform another protocol reset (required for SPI code, noop for I2C) + this->protocol_reset(); + return true; +} + +// Temperature conversion formula. See datasheet pg. 14 +float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; } +// Pressure conversion formula. See datasheet pg. 14 +float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) { + float p2_raw_sc = p_raw_sc * p_raw_sc; + float p3_raw_sc = p2_raw_sc * p_raw_sc; + float p4_raw_sc = p3_raw_sc * p_raw_sc; + return this->c00_ + (float) this->c10_ * p_raw_sc + (float) this->c20_ * p2_raw_sc + (float) this->c30_ * p3_raw_sc + + (float) this->c40_ * p4_raw_sc + + t_raw_sc * ((float) this->c01_ + (float) this->c11_ * p_raw_sc + (float) this->c21_ * p2_raw_sc + + (float) this->c31_ * p3_raw_sc); +} + +void SPA06Component::update() { + // Verify either a temperature or pressure sensor is defined before proceeding + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + // Queue a background task for retrieving the measurement + this->set_timeout("measurement", this->conversion_time_, [this]() { + float raw_temperature; + float temperature = 0.0; + float pressure = 0.0; + + // Check measurement register for readiness + if (!this->spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + ESP_LOGW(TAG, "Cannot read meas config"); + this->status_set_warning(); + return; + } + if (this->pressure_sensor_) { + if (!this->meas_.bit.prs_ready || !this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature and pressure not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_and_pressure_(temperature, pressure, raw_temperature)) { + ESP_LOGW(TAG, "Temperature and pressure read failure"); + this->status_set_warning(); + return; + } + } else { + if (!this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_(temperature, raw_temperature)) { + ESP_LOGW(TAG, "Temperature read fail"); + this->status_set_warning(); + return; + } + } + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } else { + ESP_LOGV(TAG, "No temperature sensor configured"); + } + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } else { + ESP_LOGV(TAG, "No pressure sensor configured"); + } + this->status_clear_warning(); + }); +} + +bool SPA06Component::read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc) { + // Temperature read and decode + if (!this->read_temperature_(temperature, t_raw_sc)) { + return false; + } + // Read raw pressure from device + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_PSR, buf, 3)) { + return false; + } + // Calculate raw scaled pressure value + float p_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kp_; + + // Calculate full pressure values + pressure = this->convert_pressure_(p_raw_sc, t_raw_sc); + return true; +} + +bool SPA06Component::read_temperature_(float &temperature, float &t_raw_sc) { + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_TMP, buf, 3)) { + return false; + } + + t_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kt_; + temperature = this->convert_temperature_(t_raw_sc); + return true; +} +} // namespace esphome::spa06_base diff --git a/esphome/components/spa06_base/spa06_base.h b/esphome/components/spa06_base/spa06_base.h new file mode 100644 index 0000000000..5239e80ec5 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.h @@ -0,0 +1,257 @@ +// SPA06 interface code for ESPHome +// All datasheet page references refer to Goermicro SPA06-003 datasheet version 2.0 + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/progmem.h" + +namespace esphome::spa06_base { + +// Read sizes. All other registers are size 1 +constexpr size_t SPA06_MEAS_LEN = 3; +constexpr size_t SPA06_COEF_LEN = 21; + +// Soft reset command (0b1001, 0x9) +constexpr uint8_t SPA06_SOFT_RESET = 0x9; + +// SPA06 Register Addresses +enum Register : uint8_t { + SPA06_PSR = 0x00, // Pressure Reading MSB (or all 3) + SPA06_PSR_B1 = 0x01, // Pressure Reading LSB + SPA06_PSR_B0 = 0x02, // Pressure Reading XLSB (LSB: Pressure flag in FIFO) + SPA06_TMP = 0x03, // Temperature Reading MSB (or all 3) + SPA06_TMP_B1 = 0x04, // Temperature Reading LSB + SPA06_TMP_B0 = 0x05, // Temperature Reading XLSB + SPA06_PSR_CFG = 0x06, // Pressure Configuration + SPA06_TMP_CFG = 0x07, // Temperature Configuration + SPA06_MEAS_CFG = 0x08, // Measurement Configuration (includes readiness) + SPA06_CFG_REG = 0x09, // Configuration Register + SPA06_INT_STS = 0x0A, // Interrupt Status + SPA06_FIFO_STS = 0x0B, // FIFO Status + SPA06_RESET = 0x0C, // Reset + FIFO Flush + SPA06_ID = 0x0D, // Product ID and revision + SPA06_COEF = 0x10, // Coefficients (0x10-0x24) + SPA06_INVALID_CMD = 0x25, // End of enum command +}; + +// Oversampling config. +enum Oversampling : uint8_t { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7, + OVERSAMPLING_COUNT = 0x8, +}; + +// Measuring rate config +enum SampleRate : uint8_t { + SAMPLE_RATE_1 = 0x0, + SAMPLE_RATE_2 = 0x1, + SAMPLE_RATE_4 = 0x2, + SAMPLE_RATE_8 = 0x3, + SAMPLE_RATE_16 = 0x4, + SAMPLE_RATE_32 = 0x5, + SAMPLE_RATE_64 = 0x6, + SAMPLE_RATE_128 = 0x7, + SAMPLE_RATE_25P16 = 0x8, + SAMPLE_RATE_25P8 = 0x9, + SAMPLE_RATE_25P4 = 0xA, + SAMPLE_RATE_25P2 = 0xB, + SAMPLE_RATE_25 = 0xC, + SAMPLE_RATE_50 = 0xD, + SAMPLE_RATE_100 = 0xE, + SAMPLE_RATE_200 = 0xF, +}; + +// Measuring control config, set in MEAS_CFG register. +// See datasheet pages 28-29 +enum MeasCrtl : uint8_t { + MEASCRTL_IDLE = 0x0, + MEASCRTL_PRES = 0x1, + MEASCRTL_TEMP = 0x2, + MEASCRTL_BG_PRES = 0x5, + MEASCRTL_BG_TEMP = 0x6, + MEASCRTL_BG_BOTH = 0x7, +}; + +// Oversampling scale factors. See datasheet page 15. +constexpr uint32_t OVERSAMPLING_K_LUT[8] = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960}; +PROGMEM_STRING_TABLE(MeasRateStrings, "1Hz", "2Hz", "4Hz", "8Hz", "16Hz", "32Hz", "64Hz", "128Hz", "1.5625Hz", + "3.125Hz", "6.25Hz", "12.5Hz", "25Hz", "50Hz", "100Hz", "200Hz"); +PROGMEM_STRING_TABLE(OversamplingStrings, "X1", "X2", "X4", "X8", "X16", "X32", "X64", "X128"); + +inline static const LogString *oversampling_to_str(const Oversampling oversampling) { + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); +} +inline static const LogString *meas_rate_to_str(SampleRate rate) { + return MeasRateStrings::get_log_str(static_cast(rate), MeasRateStrings::LAST_INDEX); +} +inline uint32_t oversampling_to_scale_factor(const Oversampling oversampling) { + return OVERSAMPLING_K_LUT[static_cast(oversampling)]; +}; + +class SPA06Component : public PollingComponent { + public: + //// Standard ESPHome component class functions + void setup() override; + void update() override; + void dump_config() override; + + //// ESPHome-side settings + void set_conversion_time(uint16_t conversion_time) { this->conversion_time_ = conversion_time; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + this->kt_ = oversampling_to_scale_factor(temperature_oversampling); + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + this->kp_ = oversampling_to_scale_factor(pressure_oversampling); + } + void set_pressure_sample_rate_config(SampleRate rate) { this->pressure_rate_ = rate; } + void set_temperature_sample_rate_config(SampleRate rate) { this->temperature_rate_ = rate; } + + protected: + // Virtual read functions. Implemented in SPI/I2C components + virtual bool spa_read_byte(uint8_t reg, uint8_t *data) = 0; + virtual bool spa_write_byte(uint8_t reg, uint8_t data) = 0; + virtual bool spa_read_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + virtual bool spa_write_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + + //// Protocol-specific read functions + // Soft reset + bool soft_reset_(); + // Protocol-specific reset (used for SPI only, implemented as noop for I2C) + virtual void protocol_reset() {} + // Read temperature and calculate Celsius and scaled raw temperatures + bool read_temperature_(float &temperature, float &t_raw_sc); + // No pressure only read! Pressure calculation depends on scaled temperature value + // Read temperature and calculate Celsius temperature, Pascal pressure, and scaled raw temperature + bool read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc); + // Read coefficients. Stores in class variables. + bool read_coefficients_(); + + //// Protocol-specific write functions + // Write temperature settings to TMP_CFG register + bool write_temperature_settings_(Oversampling oversampling, SampleRate rate); + // Write pressure settings to PRS_CFG register + bool write_pressure_settings_(Oversampling oversampling, SampleRate rate); + // Write measurement settings to MEAS_CRTL register + bool write_measurement_settings_(MeasCrtl crtl); + + // Write communication settings to CFG_REG register + // Set pressure_shift to true if pressure oversampling >X8 + // Set temperature_shift to true if temperature oversampling >X8 + bool write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl = false, + bool interrupt_fifo = false, bool interrupt_tmp = false, + bool interrupt_prs = false, bool enable_fifo = false, bool spi_3wire = false); + + //// Protocol helper functions + // Write function for both temperature and pressure (deduplicates code) + bool write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg); + // Convert raw temperature reading into Celsius + float convert_temperature_(const float &t_raw_sc); + // Convert raw pressure and scaled raw temperature into Pascals + float convert_pressure_(const float &p_raw_sc, const float &t_raw_sc); + + //// Protocol-related variables + // Oversampling scale factors. Defaults are for X16 (pressure) and X1 (temp) + uint32_t kp_{253952}, kt_{524288}; + // Coefficients for calculating pressure and temperature from raw values + // Obtained from IC during setup + int32_t c00_{0}, c10_{0}; + int16_t c0_{0}, c1_{0}, c01_{0}, c11_{0}, c20_{0}, c21_{0}, c30_{0}, c31_{0}, c40_{0}; + + //// ESPHome class objects and configuration + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + Oversampling temperature_oversampling_{Oversampling::OVERSAMPLING_NONE}; + Oversampling pressure_oversampling_{Oversampling::OVERSAMPLING_X16}; + SampleRate temperature_rate_{SampleRate::SAMPLE_RATE_1}; + SampleRate pressure_rate_{SampleRate::SAMPLE_RATE_1}; + // Default conversion time: 27.6ms (16x pres) + 3.6ms (1x temp) ~ 32ms + uint16_t conversion_time_{32}; + + union { + struct { + Oversampling prc : 4; + SampleRate rate : 4; + } bit; + uint8_t reg; + } pt_meas_cfg_ = {.reg = 0}; // PRS_CFG and TMP_CFG + + union { + struct { + uint8_t meas_crtl : 3; + bool tmp_ext : 1; + bool prs_ready : 1; + bool tmp_ready : 1; + bool sensor_ready : 1; + bool coef_ready : 1; + } bit; + uint8_t reg; + } meas_ = {.reg = 0}; // MEAS_REG + + union { + struct { + uint8_t _reserved : 5; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo_full : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; // INT_STS + + union { + struct { + bool spi_3wire : 1; + bool fifo_en : 1; + bool p_shift : 1; + bool t_shift : 1; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo : 1; + bool int_hl : 1; + } bit; + uint8_t reg; + } cfg_ = {.reg = 0}; // CFG_REG + + union { + struct { + bool fifo_empty : 1; + bool fifo_full : 1; + uint8_t _reserved : 6; + } bit; + uint8_t reg; + } fifo_sts_ = {.reg = 0}; // FIFO_STS + + union { + struct { + // Set to true to flush FIFO + bool fifo_flush : 1; + // Reserved bits + uint8_t _reserved : 3; + // Soft reset. Set to 1001 (0x9) to perform reset. + uint8_t soft_rst : 4; + } bit; + uint8_t reg = 0; + } reset_ = {.reg = 0}; // RESET + + union { + struct { + uint8_t prod_id : 4; + uint8_t rev_id : 4; + } bit; + uint8_t reg = 0; + } prod_id_ = {.reg = 0}; // ID + +}; // class SPA06Component +} // namespace esphome::spa06_base