1
0
mirror of https://github.com/esphome/esphome.git synced 2025-02-27 15:28:29 +00:00

MSA311 and MSA301 accelerometer support (#6795)

Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
Anton Viktorov 2025-02-27 04:48:47 +01:00 committed by GitHub
parent bc96eb9d52
commit c19621e238
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1134 additions and 13 deletions

View File

@ -299,6 +299,7 @@ esphome/components/mopeka_std_check/* @Fabian-Schmidt
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
esphome/components/msa3xx/* @latonita
esphome/components/nau7802/* @cujomalainey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @edwardtfn @senexcrenshaw

View File

@ -462,8 +462,6 @@ CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_PAUSE = "on_pause"

View File

@ -1,11 +1,9 @@
import esphome.config_validation as cv
from esphome.const import CONF_ANGLE, CONF_MODE
from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y
from ..defines import (
CONF_ANTIALIAS,
CONF_MAIN,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_SRC,

View File

@ -0,0 +1,189 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_CALIBRATION,
CONF_ID,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_OFFSET_Z,
CONF_RANGE,
CONF_RESOLUTION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_TYPE,
)
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_MSA3XX_ID = "msa3xx_id"
CONF_MIRROR_Z = "mirror_z"
CONF_ON_ACTIVE = "on_active"
CONF_ON_DOUBLE_TAP = "on_double_tap"
CONF_ON_FREEFALL = "on_freefall"
CONF_ON_ORIENTATION = "on_orientation"
CONF_ON_TAP = "on_tap"
MODEL_MSA301 = "MSA301"
MODEL_MSA311 = "MSA311"
msa3xx_ns = cg.esphome_ns.namespace("msa3xx")
MSA3xxComponent = msa3xx_ns.class_(
"MSA3xxComponent", cg.PollingComponent, i2c.I2CDevice
)
MSAModels = msa3xx_ns.enum("Model", True)
MSA_MODELS = {
MODEL_MSA301: MSAModels.MSA301,
MODEL_MSA311: MSAModels.MSA311,
}
MSARange = msa3xx_ns.enum("Range", True)
MSA_RANGES = {
"2G": MSARange.RANGE_2G,
"4G": MSARange.RANGE_4G,
"8G": MSARange.RANGE_8G,
"16G": MSARange.RANGE_16G,
}
MSAResolution = msa3xx_ns.enum("Resolution", True)
RESOLUTIONS_MSA301 = {
14: MSAResolution.RES_14BIT,
12: MSAResolution.RES_12BIT,
10: MSAResolution.RES_10BIT,
8: MSAResolution.RES_8BIT,
}
RESOLUTIONS_MSA311 = {
12: MSAResolution.RES_12BIT,
}
_COMMON_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MSA3xxComponent),
cv.Optional(CONF_RANGE, default="2G"): cv.enum(MSA_RANGES, upper=True),
cv.Optional(CONF_CALIBRATION): cv.Schema(
{
cv.Optional(CONF_OFFSET_X, default=0): cv.float_range(
min=-4.5, max=4.5
),
cv.Optional(CONF_OFFSET_Y, default=0): cv.float_range(
min=-4.5, max=4.5
),
cv.Optional(CONF_OFFSET_Z, default=0): cv.float_range(
min=-4.5, max=4.5
),
}
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Z, default=False): cv.boolean,
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
}
),
cv.Optional(CONF_ON_ACTIVE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_TAP): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DOUBLE_TAP): automation.validate_automation(single=True),
cv.Optional(CONF_ON_FREEFALL): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ORIENTATION): automation.validate_automation(single=True),
}
).extend(cv.polling_component_schema("10s"))
CONFIG_SCHEMA = cv.typed_schema(
{
MODEL_MSA301: _COMMON_SCHEMA.extend(
{
cv.Optional(CONF_RESOLUTION, default=14): cv.enum(RESOLUTIONS_MSA301),
}
).extend(i2c.i2c_device_schema(0x26)),
MODEL_MSA311: _COMMON_SCHEMA.extend(
{
cv.Optional(CONF_RESOLUTION, default=12): cv.enum(RESOLUTIONS_MSA311),
}
).extend(i2c.i2c_device_schema(0x62)),
},
upper=True,
enum=MSA_MODELS,
)
MSA_SENSOR_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MSA3XX_ID): cv.use_id(MSA3xxComponent),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_model(config[CONF_TYPE]))
cg.add(var.set_range(MSA_RANGES[config[CONF_RANGE]]))
cg.add(var.set_resolution(RESOLUTIONS_MSA301[config[CONF_RESOLUTION]]))
if transform := config.get(CONF_TRANSFORM):
cg.add(
var.set_transform(
transform[CONF_MIRROR_X],
transform[CONF_MIRROR_Y],
transform[CONF_MIRROR_Z],
transform[CONF_SWAP_XY],
)
)
if calibration_config := config.get(CONF_CALIBRATION):
cg.add(
var.set_offset(
calibration_config[CONF_OFFSET_X],
calibration_config[CONF_OFFSET_Y],
calibration_config[CONF_OFFSET_Z],
)
)
# Triggers secton
if CONF_ON_ORIENTATION in config:
await automation.build_automation(
var.get_orientation_trigger(),
[],
config[CONF_ON_ORIENTATION],
)
if CONF_ON_TAP in config:
await automation.build_automation(
var.get_tap_trigger(),
[],
config[CONF_ON_TAP],
)
if CONF_ON_DOUBLE_TAP in config:
await automation.build_automation(
var.get_double_tap_trigger(),
[],
config[CONF_ON_DOUBLE_TAP],
)
if CONF_ON_ACTIVE in config:
await automation.build_automation(
var.get_active_trigger(),
[],
config[CONF_ON_ACTIVE],
)
if CONF_ON_FREEFALL in config:
await automation.build_automation(
var.get_freefall_trigger(),
[],
config[CONF_ON_FREEFALL],
)

View File

@ -0,0 +1,40 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_NAME, DEVICE_CLASS_VIBRATION, ICON_VIBRATE
from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["msa3xx"]
CONF_TAP = "tap"
CONF_DOUBLE_TAP = "double_tap"
ICON_TAP = "mdi:gesture-tap"
ICON_DOUBLE_TAP = "mdi:gesture-double-tap"
EVENT_SENSORS = (CONF_TAP, CONF_DOUBLE_TAP, CONF_ACTIVE)
ICONS = (ICON_TAP, ICON_DOUBLE_TAP, ICON_VIBRATE)
CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend(
{
cv.Optional(event): cv.maybe_simple_value(
binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_VIBRATION,
icon=icon,
),
key=CONF_NAME,
)
for event, icon in zip(EVENT_SENSORS, ICONS)
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_MSA3XX_ID])
for sensor in EVENT_SENSORS:
if sensor in config:
sens = await binary_sensor.new_binary_sensor(config[sensor])
cg.add(getattr(hub, f"set_{sensor}_binary_sensor")(sens))

View File

@ -0,0 +1,417 @@
#include "msa3xx.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace msa3xx {
static const char *const TAG = "msa3xx";
const uint8_t MSA_3XX_PART_ID = 0x13;
const float GRAVITY_EARTH = 9.80665f;
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // LSB to 1 LSB = 3.9mg = 0.0039g
const float G_OFFSET_MIN = -4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
const float G_OFFSET_MAX = 4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
const uint8_t RESOLUTION[] = {14, 12, 10, 8};
const uint32_t TAP_COOLDOWN_MS = 500;
const uint32_t DOUBLE_TAP_COOLDOWN_MS = 500;
const uint32_t ACTIVITY_COOLDOWN_MS = 500;
const char *model_to_string(Model model) {
switch (model) {
case Model::MSA301:
return "MSA301";
case Model::MSA311:
return "MSA311";
default:
return "Unknown";
}
}
const char *power_mode_to_string(PowerMode power_mode) {
switch (power_mode) {
case PowerMode::NORMAL:
return "Normal";
case PowerMode::LOW_POWER:
return "Low Power";
case PowerMode::SUSPEND:
return "Suspend";
default:
return "Unknown";
}
}
const char *res_to_string(Resolution resolution) {
switch (resolution) {
case Resolution::RES_14BIT:
return "14-bit";
case Resolution::RES_12BIT:
return "12-bit";
case Resolution::RES_10BIT:
return "10-bit";
case Resolution::RES_8BIT:
return "8-bit";
default:
return "Unknown";
}
}
const char *range_to_string(Range range) {
switch (range) {
case Range::RANGE_2G:
return "±2g";
case Range::RANGE_4G:
return "±4g";
case Range::RANGE_8G:
return "±8g";
case Range::RANGE_16G:
return "±16g";
default:
return "Unknown";
}
}
const char *bandwidth_to_string(Bandwidth bandwidth) {
switch (bandwidth) {
case Bandwidth::BW_1_95HZ:
return "1.95 Hz";
case Bandwidth::BW_3_9HZ:
return "3.9 Hz";
case Bandwidth::BW_7_81HZ:
return "7.81 Hz";
case Bandwidth::BW_15_63HZ:
return "15.63 Hz";
case Bandwidth::BW_31_25HZ:
return "31.25 Hz";
case Bandwidth::BW_62_5HZ:
return "62.5 Hz";
case Bandwidth::BW_125HZ:
return "125 Hz";
case Bandwidth::BW_250HZ:
return "250 Hz";
case Bandwidth::BW_500HZ:
return "500 Hz";
default:
return "Unknown";
}
}
const char *orientation_xy_to_string(OrientationXY orientation) {
switch (orientation) {
case OrientationXY::PORTRAIT_UPRIGHT:
return "Portrait Upright";
case OrientationXY::PORTRAIT_UPSIDE_DOWN:
return "Portrait Upside Down";
case OrientationXY::LANDSCAPE_LEFT:
return "Landscape Left";
case OrientationXY::LANDSCAPE_RIGHT:
return "Landscape Right";
default:
return "Unknown";
}
}
const char *orientation_z_to_string(bool orientation) { return orientation ? "Downwards looking" : "Upwards looking"; }
void MSA3xxComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up MSA3xx...");
uint8_t part_id{0xff};
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::PART_ID), &part_id) || (part_id != MSA_3XX_PART_ID)) {
ESP_LOGE(TAG, "Part ID is wrong or missing. Got 0x%02X", part_id);
this->mark_failed();
return;
}
// Resolution LSB/g
// Range : MSA301 : MSA311
// S2g : 1024 (2^10) : 4096 (2^12)
// S4g : 512 (2^9) : 2048 (2^11)
// S8g : 256 (2^8) : 1024 (2^10)
// S16g : 128 (2^7) : 512 (2^9)
if (this->model_ == Model::MSA301) {
this->device_params_.accel_data_width = 14;
this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 12;
} else if (this->model_ == Model::MSA311) {
this->device_params_.accel_data_width = 12;
this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 10;
} else {
ESP_LOGE(TAG, "Unknown model");
this->mark_failed();
return;
}
this->setup_odr_(this->data_rate_);
this->setup_power_mode_bandwidth_(this->power_mode_, this->bandwidth_);
this->setup_range_resolution_(this->range_, this->resolution_); // 2g...16g, 14...8 bit
this->setup_offset_(this->offset_x_, this->offset_y_, this->offset_z_); // calibration offsets
this->write_byte(static_cast<uint8_t>(RegisterMap::TAP_DURATION), 0b11000100); // set tap duration 250ms
this->write_byte(static_cast<uint8_t>(RegisterMap::SWAP_POLARITY), this->swap_.raw); // set axes polarity
this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_0), 0b01110111); // enable all interrupts
this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_1), 0b00011000); // including orientation
}
void MSA3xxComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MSA3xx:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with MSA3xx failed!");
}
ESP_LOGCONFIG(TAG, " Model: %s", model_to_string(this->model_));
ESP_LOGCONFIG(TAG, " Power Mode: %s", power_mode_to_string(this->power_mode_));
ESP_LOGCONFIG(TAG, " Bandwidth: %s", bandwidth_to_string(this->bandwidth_));
ESP_LOGCONFIG(TAG, " Range: %s", range_to_string(this->range_));
ESP_LOGCONFIG(TAG, " Resolution: %s", res_to_string(this->resolution_));
ESP_LOGCONFIG(TAG, " Offsets: {%.3f m/s², %.3f m/s², %.3f m/s²}", this->offset_x_, this->offset_y_, this->offset_z_);
ESP_LOGCONFIG(TAG, " Transform: {mirror_x=%s, mirror_y=%s, mirror_z=%s, swap_xy=%s}", YESNO(this->swap_.x_polarity),
YESNO(this->swap_.y_polarity), YESNO(this->swap_.z_polarity), YESNO(this->swap_.x_y_swap));
LOG_UPDATE_INTERVAL(this);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Tap", this->tap_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Double Tap", this->double_tap_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Active", this->active_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Acceleration X", this->acceleration_x_sensor_);
LOG_SENSOR(" ", "Acceleration Y", this->acceleration_y_sensor_);
LOG_SENSOR(" ", "Acceleration Z", this->acceleration_z_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Orientation XY", this->orientation_xy_text_sensor_);
LOG_TEXT_SENSOR(" ", "Orientation Z", this->orientation_z_text_sensor_);
#endif
}
bool MSA3xxComponent::read_data_() {
uint8_t accel_data[6];
if (!this->read_bytes(static_cast<uint8_t>(RegisterMap::ACC_X_LSB), accel_data, 6)) {
return false;
}
auto raw_to_x_bit = [](uint16_t lsb, uint16_t msb, uint8_t data_bits) -> uint16_t {
return ((msb << 8) | lsb) >> (16 - data_bits);
};
auto lpf = [](float new_value, float old_value, float alpha = 0.5f) {
return alpha * new_value + (1.0f - alpha) * old_value;
};
this->data_.lsb_x =
this->twos_complement_(raw_to_x_bit(accel_data[0], accel_data[1], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.lsb_y =
this->twos_complement_(raw_to_x_bit(accel_data[2], accel_data[3], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.lsb_z =
this->twos_complement_(raw_to_x_bit(accel_data[4], accel_data[5], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.x = lpf(ldexp(this->data_.lsb_x, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.x);
this->data_.y = lpf(ldexp(this->data_.lsb_y, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.y);
this->data_.z = lpf(ldexp(this->data_.lsb_z, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.z);
return true;
}
bool MSA3xxComponent::read_motion_status_() {
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::MOTION_INTERRUPT), &this->status_.motion_int.raw)) {
return false;
}
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::ORIENTATION_STATUS), &this->status_.orientation.raw)) {
return false;
}
return true;
}
void MSA3xxComponent::loop() {
if (!this->is_ready()) {
return;
}
RegMotionInterrupt old_motion_int = this->status_.motion_int;
if (!this->read_data_() || !this->read_motion_status_()) {
this->status_set_warning();
return;
}
this->process_motions_(old_motion_int);
}
void MSA3xxComponent::update() {
ESP_LOGV(TAG, "Updating MSA3xx...");
if (!this->is_ready()) {
ESP_LOGV(TAG, "Component MSA3xx not ready for update");
return;
}
ESP_LOGV(TAG, "Acceleration: {x = %+1.3f m/s², y = %+1.3f m/s², z = %+1.3f m/s²}; ", this->data_.x, this->data_.y,
this->data_.z);
ESP_LOGV(TAG, "Orientation: {XY = %s, Z = %s}", orientation_xy_to_string(this->status_.orientation.orient_xy),
orientation_z_to_string(this->status_.orientation.orient_z));
#ifdef USE_SENSOR
if (this->acceleration_x_sensor_ != nullptr)
this->acceleration_x_sensor_->publish_state(this->data_.x);
if (this->acceleration_y_sensor_ != nullptr)
this->acceleration_y_sensor_->publish_state(this->data_.y);
if (this->acceleration_z_sensor_ != nullptr)
this->acceleration_z_sensor_->publish_state(this->data_.z);
#endif
#ifdef USE_TEXT_SENSOR
if (this->orientation_xy_text_sensor_ != nullptr &&
(this->status_.orientation.orient_xy != this->status_.orientation_old.orient_xy ||
this->status_.never_published)) {
this->orientation_xy_text_sensor_->publish_state(orientation_xy_to_string(this->status_.orientation.orient_xy));
}
if (this->orientation_z_text_sensor_ != nullptr &&
(this->status_.orientation.orient_z != this->status_.orientation_old.orient_z || this->status_.never_published)) {
this->orientation_z_text_sensor_->publish_state(orientation_z_to_string(this->status_.orientation.orient_z));
}
this->status_.orientation_old = this->status_.orientation;
#endif
this->status_.never_published = false;
this->status_clear_warning();
}
float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; }
void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) {
this->offset_x_ = offset_x;
this->offset_y_ = offset_y;
this->offset_z_ = offset_z;
}
void MSA3xxComponent::set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy) {
this->swap_.x_polarity = mirror_x;
this->swap_.y_polarity = mirror_y;
this->swap_.z_polarity = mirror_z;
this->swap_.x_y_swap = swap_xy;
}
void MSA3xxComponent::setup_odr_(DataRate rate) {
RegOutputDataRate reg_odr;
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::ODR));
if (reg.has_value()) {
reg_odr.raw = reg.value();
} else {
reg_odr.raw = 0x0F; // defaut from datasheet
}
reg_odr.x_axis_disable = false;
reg_odr.y_axis_disable = false;
reg_odr.z_axis_disable = false;
reg_odr.odr = rate;
this->write_byte(static_cast<uint8_t>(RegisterMap::ODR), reg_odr.raw);
}
void MSA3xxComponent::setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth) {
// 0x11 POWER_MODE_BANDWIDTH
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH));
RegPowerModeBandwidth power_mode_bandwidth;
if (reg.has_value()) {
power_mode_bandwidth.raw = reg.value();
} else {
power_mode_bandwidth.raw = 0xde; // defaut from datasheet
}
power_mode_bandwidth.power_mode = power_mode;
power_mode_bandwidth.low_power_bandwidth = bandwidth;
this->write_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH), power_mode_bandwidth.raw);
}
void MSA3xxComponent::setup_range_resolution_(Range range, Resolution resolution) {
RegRangeResolution reg;
reg.raw = this->read_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION)).value_or(0x00);
reg.range = range;
if (this->model_ == Model::MSA301) {
reg.resolution = resolution;
}
this->write_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION), reg.raw);
}
void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset_z) {
uint8_t offset[3];
auto offset_g_to_lsb = [](float accel) -> int8_t {
float acccel_clamped = clamp(accel, G_OFFSET_MIN, G_OFFSET_MAX);
return static_cast<int8_t>(acccel_clamped * LSB_COEFF);
};
offset[0] = offset_g_to_lsb(offset_x);
offset[1] = offset_g_to_lsb(offset_y);
offset[2] = offset_g_to_lsb(offset_z);
ESP_LOGV(TAG, "Offset (%.3f, %.3f, %.3f)=>LSB(%d, %d, %d)", offset_x, offset_y, offset_z, offset[0], offset[1],
offset[2]);
this->write_bytes(static_cast<uint8_t>(RegisterMap::OFFSET_COMP_X), (uint8_t *) &offset, 3);
}
int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) {
if (value > (1ULL << (bits - 1))) {
return (int64_t) (value - (1ULL << bits));
} else {
return (int64_t) value;
}
}
void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger,
uint32_t cooldown_ms, void *bs, const char *desc) {
if (state && now - last_ms > cooldown_ms) {
ESP_LOGV(TAG, "%s detected", desc);
trigger.trigger();
last_ms = now;
#ifdef USE_BINARY_SENSOR
if (bs != nullptr) {
static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(true);
}
#endif
} else if (!state && now - last_ms > cooldown_ms && bs != nullptr) {
#ifdef USE_BINARY_SENSOR
static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(false);
#endif
}
}
#ifdef USE_BINARY_SENSOR
#define BS_OPTIONAL_PTR(x) ((void *) (x))
#else
#define BS_OPTIONAL_PTR(x) (nullptr)
#endif
void MSA3xxComponent::process_motions_(RegMotionInterrupt old) {
uint32_t now = millis();
binary_event_debounce(this->status_.motion_int.single_tap_interrupt, old.single_tap_interrupt, now,
this->status_.last_tap_ms, this->tap_trigger_, TAP_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->tap_binary_sensor_), "Tap");
binary_event_debounce(this->status_.motion_int.double_tap_interrupt, old.double_tap_interrupt, now,
this->status_.last_double_tap_ms, this->double_tap_trigger_, DOUBLE_TAP_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->double_tap_binary_sensor_), "Double Tap");
binary_event_debounce(this->status_.motion_int.active_interrupt, old.active_interrupt, now,
this->status_.last_action_ms, this->active_trigger_, ACTIVITY_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->active_binary_sensor_), "Activity");
if (this->status_.motion_int.orientation_interrupt) {
ESP_LOGVV(TAG, "Orientation changed");
this->orientation_trigger_.trigger();
}
}
} // namespace msa3xx
} // namespace esphome

View File

@ -0,0 +1,311 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/automation.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome {
namespace msa3xx {
// Combined register map of MSA301 and MSA311
// Differences
// What | MSA301 | MSA11 |
// - Resolution | 14-bit | 12-bit |
//
// I2c address
enum class Model : uint8_t {
MSA301 = 0x26,
MSA311 = 0x62,
};
// Combined MSA301 and MSA311 register map
enum class RegisterMap : uint8_t {
SOFT_RESET = 0x00,
PART_ID = 0x01,
ACC_X_LSB = 0x02,
ACC_X_MSB = 0x03,
ACC_Y_LSB = 0x04,
ACC_Y_MSB = 0x05,
ACC_Z_LSB = 0x06,
ACC_Z_MSB = 0x07,
MOTION_INTERRUPT = 0x09,
DATA_INTERRUPT = 0x0A,
TAP_ACTIVE_STATUS = 0x0B,
ORIENTATION_STATUS = 0x0C,
RESOLUTION_RANGE_CONFIG = 0x0D,
RANGE_RESOLUTION = 0x0F,
ODR = 0x10,
POWER_MODE_BANDWIDTH = 0x11,
SWAP_POLARITY = 0x12,
INT_SET_0 = 0x16,
INT_SET_1 = 0x17,
INT_MAP_0 = 0x19,
INT_MAP_1 = 0x1A,
INT_CONFIG = 0x20,
INT_LATCH = 0x21,
FREEFALL_DURATION = 0x22,
FREEFALL_THRESHOLD = 0x23,
FREEFALL_HYSTERESIS = 0x24,
ACTIVE_DURATION = 0x27,
ACTIVE_THRESHOLD = 0x28,
TAP_DURATION = 0x2A,
TAP_THRESHOLD = 0x2B,
ORIENTATION_CONFIG = 0x2C,
Z_BLOCK = 0x2D,
OFFSET_COMP_X = 0x38,
OFFSET_COMP_Y = 0x39,
OFFSET_COMP_Z = 0x3A,
};
enum class Range : uint8_t {
RANGE_2G = 0b00,
RANGE_4G = 0b01,
RANGE_8G = 0b10,
RANGE_16G = 0b11,
};
enum class Resolution : uint8_t {
RES_14BIT = 0b00,
RES_12BIT = 0b01,
RES_10BIT = 0b10,
RES_8BIT = 0b11,
};
enum class PowerMode : uint8_t {
NORMAL = 0b00,
LOW_POWER = 0b01,
SUSPEND = 0b11,
};
enum class Bandwidth : uint8_t {
BW_1_95HZ = 0b0000,
BW_3_9HZ = 0b0011,
BW_7_81HZ = 0b0100,
BW_15_63HZ = 0b0101,
BW_31_25HZ = 0b0110,
BW_62_5HZ = 0b0111,
BW_125HZ = 0b1000,
BW_250HZ = 0b1001,
BW_500HZ = 0b1010,
};
enum class DataRate : uint8_t {
ODR_1HZ = 0b0000, // not available in normal mode
ODR_1_95HZ = 0b0001, // not available in normal mode
ODR_3_9HZ = 0b0010,
ODR_7_81HZ = 0b0011,
ODR_15_63HZ = 0b0100,
ODR_31_25HZ = 0b0101,
ODR_62_5HZ = 0b0110,
ODR_125HZ = 0b0111,
ODR_250HZ = 0b1000,
ODR_500HZ = 0b1001, // not available in low power mode
ODR_1000HZ = 0b1010, // not available in low power mode
};
enum class OrientationXY : uint8_t {
PORTRAIT_UPRIGHT = 0b00,
PORTRAIT_UPSIDE_DOWN = 0b01,
LANDSCAPE_LEFT = 0b10,
LANDSCAPE_RIGHT = 0b11,
};
union Orientation {
struct {
OrientationXY xy : 2;
bool z : 1;
uint8_t reserved : 5;
} __attribute__((packed));
uint8_t raw;
};
// 0x09
union RegMotionInterrupt {
struct {
bool freefall_interrupt : 1;
bool reserved_1 : 1;
bool active_interrupt : 1;
bool reserved_3 : 1;
bool double_tap_interrupt : 1;
bool single_tap_interrupt : 1;
bool orientation_interrupt : 1;
bool reserved_7 : 1;
} __attribute__((packed));
uint8_t raw;
};
// 0x0C
union RegOrientationStatus {
struct {
uint8_t reserved_0_3 : 4;
OrientationXY orient_xy : 2;
bool orient_z : 1;
uint8_t reserved_7 : 1;
} __attribute__((packed));
uint8_t raw{0x00};
};
// 0x0f
union RegRangeResolution {
struct {
Range range : 2;
Resolution resolution : 2;
uint8_t reserved_2 : 4;
} __attribute__((packed));
uint8_t raw{0x00};
};
// 0x10
union RegOutputDataRate {
struct {
DataRate odr : 4;
uint8_t reserved_4 : 1;
bool z_axis_disable : 1;
bool y_axis_disable : 1;
bool x_axis_disable : 1;
} __attribute__((packed));
uint8_t raw{0xde};
};
// 0x11
union RegPowerModeBandwidth {
struct {
uint8_t reserved_0 : 1;
Bandwidth low_power_bandwidth : 4;
uint8_t reserved_5 : 1;
PowerMode power_mode : 2;
} __attribute__((packed));
uint8_t raw{0xde};
};
// 0x12
union RegSwapPolarity {
struct {
bool x_y_swap : 1;
bool z_polarity : 1;
bool y_polarity : 1;
bool x_polarity : 1;
uint8_t reserved : 4;
} __attribute__((packed));
uint8_t raw{0};
};
// 0x2a
union RegTapDuration {
struct {
uint8_t duration : 3;
uint8_t reserved : 3;
bool tap_shock : 1;
bool tap_quiet : 1;
} __attribute__((packed));
uint8_t raw{0x04};
};
class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void loop() override;
void update() override;
float get_setup_priority() const override;
void set_model(Model model) { this->model_ = model; }
void set_offset(float offset_x, float offset_y, float offset_z);
void set_range(Range range) { this->range_ = range; }
void set_bandwidth(Bandwidth bandwidth) { this->bandwidth_ = bandwidth; }
void set_resolution(Resolution resolution) { this->resolution_ = resolution; }
void set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy);
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(tap)
SUB_BINARY_SENSOR(double_tap)
SUB_BINARY_SENSOR(active)
#endif
#ifdef USE_SENSOR
SUB_SENSOR(acceleration_x)
SUB_SENSOR(acceleration_y)
SUB_SENSOR(acceleration_z)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(orientation_xy)
SUB_TEXT_SENSOR(orientation_z)
#endif
Trigger<> *get_tap_trigger() { return &this->tap_trigger_; }
Trigger<> *get_double_tap_trigger() { return &this->double_tap_trigger_; }
Trigger<> *get_orientation_trigger() { return &this->orientation_trigger_; }
Trigger<> *get_freefall_trigger() { return &this->freefall_trigger_; }
Trigger<> *get_active_trigger() { return &this->active_trigger_; }
protected:
Model model_{Model::MSA311};
PowerMode power_mode_{PowerMode::NORMAL};
DataRate data_rate_{DataRate::ODR_250HZ};
Bandwidth bandwidth_{Bandwidth::BW_250HZ};
Range range_{Range::RANGE_2G};
Resolution resolution_{Resolution::RES_14BIT};
float offset_x_, offset_y_, offset_z_; // in m/s²
RegSwapPolarity swap_;
struct {
int scale_factor_exp;
uint8_t accel_data_width;
} device_params_{};
struct {
int16_t lsb_x, lsb_y, lsb_z;
float x, y, z;
} data_{};
struct {
RegMotionInterrupt motion_int;
RegOrientationStatus orientation;
RegOrientationStatus orientation_old;
uint32_t last_tap_ms{0};
uint32_t last_double_tap_ms{0};
uint32_t last_action_ms{0};
bool never_published{true};
} status_{};
void setup_odr_(DataRate rate);
void setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth);
void setup_range_resolution_(Range range, Resolution resolution);
void setup_offset_(float offset_x, float offset_y, float offset_z);
bool read_data_();
bool read_motion_status_();
int64_t twos_complement_(uint64_t value, uint8_t bits);
//
// Actons / Triggers
//
Trigger<> tap_trigger_;
Trigger<> double_tap_trigger_;
Trigger<> orientation_trigger_;
Trigger<> freefall_trigger_;
Trigger<> active_trigger_;
void process_motions_(RegMotionInterrupt old);
};
} // namespace msa3xx
} // namespace esphome

View File

@ -0,0 +1,42 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCELERATION_X,
CONF_ACCELERATION_Y,
CONF_ACCELERATION_Z,
CONF_NAME,
ICON_BRIEFCASE_DOWNLOAD,
STATE_CLASS_MEASUREMENT,
UNIT_METER_PER_SECOND_SQUARED,
)
from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["msa3xx"]
ACCELERATION_SENSORS = (CONF_ACCELERATION_X, CONF_ACCELERATION_Y, CONF_ACCELERATION_Z)
accel_schema = cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED,
icon=ICON_BRIEFCASE_DOWNLOAD,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
)
CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend(
{cv.Optional(sensor): accel_schema for sensor in ACCELERATION_SENSORS}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_MSA3XX_ID])
for accel_key in ACCELERATION_SENSORS:
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(hub, f"set_{accel_key}_sensor")(sens))

View File

@ -0,0 +1,38 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_NAME
from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["msa3xx"]
CONF_ORIENTATION_XY = "orientation_xy"
CONF_ORIENTATION_Z = "orientation_z"
ICON_SCREEN_ROTATION = "mdi:screen-rotation"
ORIENTATION_SENSORS = (CONF_ORIENTATION_XY, CONF_ORIENTATION_Z)
CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend(
{
cv.Optional(sensor): cv.maybe_simple_value(
text_sensor.text_sensor_schema(icon=ICON_SCREEN_ROTATION),
key=CONF_NAME,
)
for sensor in ORIENTATION_SENSORS
}
)
async def setup_conf(config, key, hub):
if sensor_config := config.get(key):
var = await text_sensor.new_text_sensor(sensor_config)
cg.add(getattr(hub, f"set_{key}_text_sensor")(var))
async def to_code(config):
hub = await cg.get_variable(config[CONF_MSA3XX_ID])
for key in ORIENTATION_SENSORS:
await setup_conf(config, key, hub)

View File

@ -1,15 +1,17 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import (
CONF_EXTERNAL_VCC,
CONF_LAMBDA,
CONF_MODEL,
CONF_RESET_PIN,
CONF_BRIGHTNESS,
CONF_CONTRAST,
CONF_EXTERNAL_VCC,
CONF_INVERT,
CONF_LAMBDA,
CONF_MODEL,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_RESET_PIN,
)
ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base")
@ -18,8 +20,6 @@ SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
CONF_FLIP_X = "flip_x"
CONF_FLIP_Y = "flip_y"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
MODELS = {
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,

View File

@ -546,6 +546,9 @@ CONF_OFF_SPEED_CYCLE = "off_speed_cycle"
CONF_OFFSET = "offset"
CONF_OFFSET_HEIGHT = "offset_height"
CONF_OFFSET_WIDTH = "offset_width"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_OFFSET_Z = "offset_z"
CONF_ON = "on"
CONF_ON_BLE_ADVERTISE = "on_ble_advertise"
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise"

View File

@ -0,0 +1,48 @@
msa3xx:
i2c_id: i2c_msa3xx
type: msa301
range: 4G
resolution: 14
update_interval: 10s
calibration:
offset_x: -0.250
offset_y: -0.400
offset_z: -0.800
transform:
mirror_x: false
mirror_y: true
mirror_z: true
swap_xy: false
on_tap:
- then:
- logger.log: "Tapped"
on_double_tap:
- then:
- logger.log: "Double tapped"
on_active:
- then:
- logger.log: "Activity detected"
on_orientation:
- then:
- logger.log: "Orientation changed"
sensor:
- platform: msa3xx
acceleration_x: Accel X
acceleration_y: Accel Y
acceleration_z: Accel Z
text_sensor:
- platform: msa3xx
orientation_xy: Orientation XY
orientation_z: Orientation Z
binary_sensor:
- platform: msa3xx
tap: Single tap
double_tap:
name: Double tap
active:
name: Active
filters:
- delayed_off: 5000ms

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO16
sda: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO5
sda: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO5
sda: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO16
sda: GPIO17
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO5
sda: GPIO4
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
- id: i2c_msa3xx
scl: GPIO5
sda: GPIO4
<<: !include common.yaml