ESPHome 2026.6.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
6static const char *const 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}
65 if (this->is_failed())
66 return;
67 MotionData motion_data{};
68 MotionData raw_data{};
69 if (!this->update_data(raw_data))
70 return;
71 this->map_axes_(motion_data.acceleration, raw_data.acceleration);
72 this->map_axes_(motion_data.angular_rate, raw_data.angular_rate);
73 this->motion_data_callback_.call(motion_data);
74
75 ESP_LOGV(TAG, "Accel: [%.3f, %.3f, %.3f] g; Gyro: [%.3f, %.3f, %.3f] °/s", motion_data.acceleration[X_AXIS],
76 motion_data.acceleration[Y_AXIS], motion_data.acceleration[Z_AXIS], motion_data.angular_rate[X_AXIS],
77 motion_data.angular_rate[Y_AXIS], motion_data.angular_rate[Z_AXIS]);
78}
79
82 if (!this->update_data(raw)) {
83 ESP_LOGW(TAG, "calibrate_level: failed to read sensor data");
84 return false;
85 }
86
87 // Apply the current matrix first so any existing axis mapping is preserved.
88 float mapped[3];
89 this->map_axes_(mapped, raw.acceleration);
90
91 float nx = mapped[X_AXIS];
92 float ny = mapped[Y_AXIS];
93 float nz = mapped[Z_AXIS];
94 float mag = std::sqrt(nx * nx + ny * ny + nz * nz);
95 if (mag < 0.1f) {
96 ESP_LOGW(TAG, "calibrate_level: acceleration magnitude too small (%.3f)", mag);
97 return false;
98 }
99
100 // Normalize
101 nx /= mag;
102 ny /= mag;
103 nz /= mag;
104
105 // Compute rotation matrix R such that R * [nx, ny, nz] = [0, 0, 1]
106 // using Rodrigues' rotation formula, then compose with the existing matrix.
107 if (nz > 0.99999f) {
108 // Already aligned with +Z — nothing to compose
109 ESP_LOGI(TAG, "Level calibration: already aligned");
110 log_matrix(this->matrix_);
111 // returning true here will trigger on_success and a save to NVS, but the save will ultimately be a no-op
112 // since the backend sync will not write unchanged values.
113 return true;
114 }
115
116 float r[9];
117 if (nz < -0.9999f) {
118 // Aligned with -Z — 180° rotation about X
119 float m[9] = {1, 0, 0, 0, -1, 0, 0, 0, -1};
120 memcpy(r, m, sizeof(r));
121 } else {
122 float f = 1.0f / (1.0f + nz);
123 r[0] = 1.0f - nx * nx * f;
124 r[1] = -nx * ny * f;
125 r[2] = -nx;
126 r[3] = -nx * ny * f;
127 r[4] = 1.0f - ny * ny * f;
128 r[5] = -ny;
129 r[6] = nx;
130 r[7] = ny;
131 r[8] = nz;
132 }
133
134 // Compose: new_matrix = R * old_matrix
135 float old[9];
136 memcpy(old, this->matrix_, sizeof(old));
137 for (int i = 0; i < 3; i++) {
138 for (int j = 0; j < 3; j++) {
139 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];
140 }
141 }
142
143 ESP_LOGI(TAG, "Level calibration applied (mapped accel: [%.3f, %.3f, %.3f])", mapped[X_AXIS], mapped[Y_AXIS],
144 mapped[Z_AXIS]);
145 log_matrix(this->matrix_);
146 return true;
147}
148
150 MotionData raw{};
151 if (!this->update_data(raw)) {
152 ESP_LOGW(TAG, "calibrate_heading: failed to read sensor data");
153 return false;
154 }
155
156 // Apply current matrix to get the mapped acceleration
157 float mapped[3];
158 this->map_axes_(mapped, raw.acceleration);
159
160 float mx = mapped[X_AXIS];
161 float my = mapped[Y_AXIS];
162 float h = std::sqrt(mx * mx + my * my);
163 if (h < 0.05f) {
164 ESP_LOGW(TAG, "calibrate_heading: device must be tilted (XY magnitude %.3f too small)", h);
165 return false;
166 }
167
168 // Rotation angle in the XY plane: eliminate Y component while preserving X sign.
169 // Without the sign correction, atan2(my,mx) would rotate everything to +X,
170 // flipping the sign when the tilt projects onto -X.
171 float sign_mx = mx >= 0 ? 1.0f : -1.0f;
172 float cos_phi = sign_mx * mx / h; // = |mx| / h
173 float sin_phi = sign_mx * my / h;
174
175 // Compose Rz(-phi) with the current matrix
176 // Rz(-phi) = [[cos_phi, sin_phi, 0], [-sin_phi, cos_phi, 0], [0, 0, 1]]
177 float old[9];
178 memcpy(old, this->matrix_, sizeof(old));
179
180 this->matrix_[0] = cos_phi * old[0] + sin_phi * old[3];
181 this->matrix_[1] = cos_phi * old[1] + sin_phi * old[4];
182 this->matrix_[2] = cos_phi * old[2] + sin_phi * old[5];
183 this->matrix_[3] = -sin_phi * old[0] + cos_phi * old[3];
184 this->matrix_[4] = -sin_phi * old[1] + cos_phi * old[4];
185 this->matrix_[5] = -sin_phi * old[2] + cos_phi * old[5];
186 // Row 2 unchanged
187
188 ESP_LOGI(TAG, "Heading calibration applied (mapped accel: [%.3f, %.3f, %.3f])", mapped[X_AXIS], mapped[Y_AXIS],
189 mapped[Z_AXIS]);
190 log_matrix(this->matrix_);
191 return true;
192}
193
194} // 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:272
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", "2026.2.0") void set_retry(const std uint32_t uint8_t std::function< RetryResult(uint8_t)> && f
Definition component.h:437
LazyCallbackManager< void(MotionData &)> motion_data_callback_
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.
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()
Restore the build-time (axis_map / transform_matrix) base, discarding calibration.
bool save_calibration()
Save the current matrix to NVS.
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:32