ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
motion_component.cpp
Go to the documentation of this file.
1#include "motion_component.h"
2#include "esphome/core/log.h"
3
4namespace esphome::motion {
5
6ESPHOME_LOG_TAG(TAG, "motion");
7
8static void log_matrix(const float m[9]) {
9 ESP_LOGCONFIG(TAG, " Calibration matrix:");
10 ESP_LOGCONFIG(TAG, " - [%9.6f, %9.6f, %9.6f]", m[0], m[1], m[2]);
11 ESP_LOGCONFIG(TAG, " - [%9.6f, %9.6f, %9.6f]", m[3], m[4], m[5]);
12 ESP_LOGCONFIG(TAG, " - [%9.6f, %9.6f, %9.6f]", m[6], m[7], m[8]);
13}
14
15// FNV-1a over the raw bytes of the matrix. Identical axis maps always yield
16// bit-identical matrices, so this is a stable fingerprint of the build-time base.
17static uint32_t hash_matrix(const float m[9]) {
18 const uint8_t *bytes = reinterpret_cast<const uint8_t *>(m);
19 uint32_t hash = 2166136261UL;
20 for (size_t i = 0; i < sizeof(float) * 9; i++) {
21 hash ^= bytes[i];
22 hash *= 16777619UL;
23 }
24 return hash;
25}
26
28 // matrix_ currently holds the build-time base (set_matrix ran during codegen).
29 this->base_hash_ = hash_matrix(this->base_matrix_);
31 CalibrationPref saved;
32 if (this->pref_.load(&saved) && saved.base_hash == this->base_hash_) {
33 memcpy(this->matrix_, saved.matrix, sizeof(this->matrix_));
34 ESP_LOGI(TAG, "Restored calibration from NVS");
35 } else {
36 ESP_LOGD(TAG, "No matching saved calibration; using build-time matrix");
37 }
38 log_matrix(this->matrix_);
39}
41 LOG_UPDATE_INTERVAL(this);
42 log_matrix(this->matrix_);
43}
45 if (this->pref_key_ == 0) {
46 ESP_LOGW(TAG, "Cannot save calibration: no preference key set");
47 return false;
48 }
49 CalibrationPref pref{this->base_hash_, {}};
50 memcpy(pref.matrix, this->matrix_, sizeof(pref.matrix));
51 if (this->pref_.save(&pref)) {
53 ESP_LOGI(TAG, "Saved calibration to NVS");
54 return true;
55 }
56 ESP_LOGW(TAG, "Calibration save failed");
57 return false;
58}
60 memcpy(this->matrix_, this->base_matrix_, sizeof(this->matrix_));
61 ESP_LOGI(TAG, "Calibration reset to build-time matrix");
62 log_matrix(this->matrix_);
63 if (save) {
64 this->save_calibration();
65 }
66}
68 if (this->is_failed())
69 return;
70 MotionData motion_data{};
71 MotionData raw_data{};
72 if (!this->update_data(raw_data))
73 return;
74 this->map_axes_(motion_data.acceleration, raw_data.acceleration);
75 this->map_axes_(motion_data.angular_rate, raw_data.angular_rate);
76 this->motion_data_callback_.call(motion_data);
77
78 ESP_LOGV(TAG, "Accel: [%.3f, %.3f, %.3f] g; Gyro: [%.3f, %.3f, %.3f] °/s", motion_data.acceleration[X_AXIS],
79 motion_data.acceleration[Y_AXIS], motion_data.acceleration[Z_AXIS], motion_data.angular_rate[X_AXIS],
80 motion_data.angular_rate[Y_AXIS], motion_data.angular_rate[Z_AXIS]);
81}
82
85 if (!this->update_data(raw)) {
86 ESP_LOGW(TAG, "calibrate_level: failed to read sensor data");
87 return false;
88 }
89
90 // Apply the current matrix first so any existing axis mapping is preserved.
91 float mapped[3];
92 this->map_axes_(mapped, raw.acceleration);
93
94 float nx = mapped[X_AXIS];
95 float ny = mapped[Y_AXIS];
96 float nz = mapped[Z_AXIS];
97 float mag = std::sqrt(nx * nx + ny * ny + nz * nz);
98 if (mag < 0.1f) {
99 ESP_LOGW(TAG, "calibrate_level: acceleration magnitude too small (%.3f)", mag);
100 return false;
101 }
102
103 // Normalize
104 nx /= mag;
105 ny /= mag;
106 nz /= mag;
107
108 // Compute rotation matrix R such that R * [nx, ny, nz] = [0, 0, 1]
109 // using Rodrigues' rotation formula, then compose with the existing matrix.
110 if (nz > 0.99999f) {
111 // Already aligned with +Z — nothing to compose
112 ESP_LOGI(TAG, "Level calibration: already aligned");
113 log_matrix(this->matrix_);
114 // returning true here will trigger on_success and a save to NVS, but the save will ultimately be a no-op
115 // since the backend sync will not write unchanged values.
116 return true;
117 }
118
119 float r[9];
120 if (nz < -0.9999f) {
121 // Aligned with -Z — 180° rotation about X
122 float m[9] = {1, 0, 0, 0, -1, 0, 0, 0, -1};
123 memcpy(r, m, sizeof(r));
124 } else {
125 float f = 1.0f / (1.0f + nz);
126 r[0] = 1.0f - nx * nx * f;
127 r[1] = -nx * ny * f;
128 r[2] = -nx;
129 r[3] = -nx * ny * f;
130 r[4] = 1.0f - ny * ny * f;
131 r[5] = -ny;
132 r[6] = nx;
133 r[7] = ny;
134 r[8] = nz;
135 }
136
137 // Compose: new_matrix = R * old_matrix
138 float old[9];
139 memcpy(old, this->matrix_, sizeof(old));
140 for (int i = 0; i < 3; i++) {
141 for (int j = 0; j < 3; j++) {
142 this->matrix_[i * 3 + j] = r[i * 3 + 0] * old[j] + r[i * 3 + 1] * old[3 + j] + r[i * 3 + 2] * old[6 + j];
143 }
144 }
145
146 ESP_LOGI(TAG, "Level calibration applied (mapped accel: [%.3f, %.3f, %.3f])", mapped[X_AXIS], mapped[Y_AXIS],
147 mapped[Z_AXIS]);
148 log_matrix(this->matrix_);
149 return true;
150}
151
153 MotionData raw{};
154 if (!this->update_data(raw)) {
155 ESP_LOGW(TAG, "calibrate_heading: failed to read sensor data");
156 return false;
157 }
158
159 // Apply current matrix to get the mapped acceleration
160 float mapped[3];
161 this->map_axes_(mapped, raw.acceleration);
162
163 float mx = mapped[X_AXIS];
164 float my = mapped[Y_AXIS];
165 float h = std::sqrt(mx * mx + my * my);
166 if (h < 0.05f) {
167 ESP_LOGW(TAG, "calibrate_heading: device must be tilted (XY magnitude %.3f too small)", h);
168 return false;
169 }
170
171 // Rotation angle in the XY plane: eliminate Y component while preserving X sign.
172 // Without the sign correction, atan2(my,mx) would rotate everything to +X,
173 // flipping the sign when the tilt projects onto -X.
174 float sign_mx = mx >= 0 ? 1.0f : -1.0f;
175 float cos_phi = sign_mx * mx / h; // = |mx| / h
176 float sin_phi = sign_mx * my / h;
177
178 // Compose Rz(-phi) with the current matrix
179 // Rz(-phi) = [[cos_phi, sin_phi, 0], [-sin_phi, cos_phi, 0], [0, 0, 1]]
180 float old[9];
181 memcpy(old, this->matrix_, sizeof(old));
182
183 this->matrix_[0] = cos_phi * old[0] + sin_phi * old[3];
184 this->matrix_[1] = cos_phi * old[1] + sin_phi * old[4];
185 this->matrix_[2] = cos_phi * old[2] + sin_phi * old[5];
186 this->matrix_[3] = -sin_phi * old[0] + cos_phi * old[3];
187 this->matrix_[4] = -sin_phi * old[1] + cos_phi * old[4];
188 this->matrix_[5] = -sin_phi * old[2] + cos_phi * old[5];
189 // Row 2 unchanged
190
191 ESP_LOGI(TAG, "Heading calibration applied (mapped accel: [%.3f, %.3f, %.3f])", mapped[X_AXIS], mapped[Y_AXIS],
192 mapped[Z_AXIS]);
193 log_matrix(this->matrix_);
194 return true;
195}
196
197} // namespace esphome::motion
uint8_t m
Definition bl0906.h:1
uint8_t h
Definition bl0906.h:2
uint8_t raw[35]
Definition bl0939.h:0
bool is_failed() const
Definition component.h:274
void map_axes_(float output[3], const float input[3]) const
bool calibrate_heading()
Assuming Y-axis rotation only, correct the heading so X/Y align correctly.
LazyCallbackManager< void(MotionData const &)> motion_data_callback_
virtual bool update_data(MotionData &data)=0
bool calibrate_level()
Calibrate the matrix so the current reading maps to [0, 0, 1] (device flat).
void clear_calibration(bool save=false)
Restore the build-time (axis_map / transform_matrix) base, discarding calibration,...
bool save_calibration()
Save the current matrix to NVS.
ESPHOME_LOG_TAG(TAG, "motion.binary_sensor")
ESPPreferences * global_preferences
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33