Skip to content

Commit 56b7cc1

Browse files
Settings app: add IMU calibration with check
1 parent 92c2fcf commit 56b7cc1

File tree

5 files changed

+1099
-7
lines changed

5 files changed

+1099
-7
lines changed
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
"""Calibrate IMU Activity.
2+
3+
Guides user through IMU calibration process:
4+
1. Check current calibration quality
5+
2. Ask if user wants to recalibrate
6+
3. Check stationarity
7+
4. Perform calibration
8+
5. Verify results
9+
6. Save to new location
10+
"""
11+
12+
import lvgl as lv
13+
import time
14+
import _thread
15+
import sys
16+
from mpos.app.activity import Activity
17+
import mpos.ui
18+
import mpos.sensor_manager as SensorManager
19+
import mpos.apps
20+
21+
22+
class CalibrationState:
23+
"""Enum for calibration states."""
24+
IDLE = 0
25+
CHECKING_QUALITY = 1
26+
AWAITING_CONFIRMATION = 2
27+
CHECKING_STATIONARITY = 3
28+
CALIBRATING = 4
29+
VERIFYING = 5
30+
COMPLETE = 6
31+
ERROR = 7
32+
33+
34+
class CalibrateIMUActivity(Activity):
35+
"""Guide user through IMU calibration process."""
36+
37+
# State
38+
current_state = CalibrationState.IDLE
39+
calibration_thread = None
40+
41+
# Widgets
42+
title_label = None
43+
status_label = None
44+
progress_bar = None
45+
detail_label = None
46+
action_button = None
47+
action_button_label = None
48+
cancel_button = None
49+
50+
def __init__(self):
51+
super().__init__()
52+
self.is_desktop = sys.platform != "esp32"
53+
54+
def onCreate(self):
55+
screen = lv.obj()
56+
screen.set_style_pad_all(mpos.ui.pct_of_display_width(3), 0)
57+
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
58+
screen.set_flex_align(lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.START, lv.FLEX_ALIGN.CENTER)
59+
60+
# Title
61+
self.title_label = lv.label(screen)
62+
self.title_label.set_text("IMU Calibration")
63+
self.title_label.set_style_text_font(lv.font_montserrat_20, 0)
64+
65+
# Status label
66+
self.status_label = lv.label(screen)
67+
self.status_label.set_text("Initializing...")
68+
self.status_label.set_style_text_font(lv.font_montserrat_16, 0)
69+
self.status_label.set_long_mode(lv.label.LONG_MODE.WRAP)
70+
self.status_label.set_width(lv.pct(90))
71+
72+
# Progress bar (hidden initially)
73+
self.progress_bar = lv.bar(screen)
74+
self.progress_bar.set_size(lv.pct(90), 20)
75+
self.progress_bar.set_value(0, False)
76+
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
77+
78+
# Detail label (for additional info)
79+
self.detail_label = lv.label(screen)
80+
self.detail_label.set_text("")
81+
self.detail_label.set_style_text_font(lv.font_montserrat_12, 0)
82+
self.detail_label.set_style_text_color(lv.color_hex(0x888888), 0)
83+
self.detail_label.set_long_mode(lv.label.LONG_MODE.WRAP)
84+
self.detail_label.set_width(lv.pct(90))
85+
86+
# Button container
87+
btn_cont = lv.obj(screen)
88+
btn_cont.set_width(lv.pct(100))
89+
btn_cont.set_height(lv.SIZE_CONTENT)
90+
btn_cont.set_style_border_width(0, 0)
91+
btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW)
92+
btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0)
93+
94+
# Action button
95+
self.action_button = lv.button(btn_cont)
96+
self.action_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
97+
self.action_button_label = lv.label(self.action_button)
98+
self.action_button_label.set_text("Start")
99+
self.action_button_label.center()
100+
self.action_button.add_event_cb(self.action_button_clicked, lv.EVENT.CLICKED, None)
101+
102+
# Cancel button
103+
self.cancel_button = lv.button(btn_cont)
104+
self.cancel_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
105+
cancel_label = lv.label(self.cancel_button)
106+
cancel_label.set_text("Cancel")
107+
cancel_label.center()
108+
self.cancel_button.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None)
109+
110+
self.setContentView(screen)
111+
112+
def onResume(self, screen):
113+
super().onResume(screen)
114+
115+
# Check if IMU is available
116+
if not self.is_desktop and not SensorManager.is_available():
117+
self.set_state(CalibrationState.ERROR)
118+
self.status_label.set_text("IMU not available on this device")
119+
self.action_button.add_state(lv.STATE.DISABLED)
120+
return
121+
122+
# Start by checking current quality
123+
self.set_state(CalibrationState.IDLE)
124+
self.action_button_label.set_text("Check Quality")
125+
126+
def onPause(self, screen):
127+
# Stop any running calibration
128+
if self.current_state == CalibrationState.CALIBRATING:
129+
# Calibration will detect activity is no longer in foreground
130+
pass
131+
super().onPause(screen)
132+
133+
def set_state(self, new_state):
134+
"""Update state and UI accordingly."""
135+
self.current_state = new_state
136+
self.update_ui_for_state()
137+
138+
def update_ui_for_state(self):
139+
"""Update UI based on current state."""
140+
if self.current_state == CalibrationState.IDLE:
141+
self.status_label.set_text("Ready to check calibration quality")
142+
self.action_button_label.set_text("Check Quality")
143+
self.action_button.remove_state(lv.STATE.DISABLED)
144+
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
145+
146+
elif self.current_state == CalibrationState.CHECKING_QUALITY:
147+
self.status_label.set_text("Checking current calibration...")
148+
self.action_button.add_state(lv.STATE.DISABLED)
149+
self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN)
150+
self.progress_bar.set_value(20, True)
151+
152+
elif self.current_state == CalibrationState.AWAITING_CONFIRMATION:
153+
# Status will be set by quality check result
154+
self.action_button_label.set_text("Calibrate Now")
155+
self.action_button.remove_state(lv.STATE.DISABLED)
156+
self.progress_bar.set_value(30, True)
157+
158+
elif self.current_state == CalibrationState.CHECKING_STATIONARITY:
159+
self.status_label.set_text("Checking if device is stationary...")
160+
self.detail_label.set_text("Keep device still on flat surface")
161+
self.action_button.add_state(lv.STATE.DISABLED)
162+
self.progress_bar.set_value(40, True)
163+
164+
elif self.current_state == CalibrationState.CALIBRATING:
165+
self.status_label.set_text("Calibrating IMU...")
166+
self.detail_label.set_text("Do not move device!\nCollecting samples...")
167+
self.action_button.add_state(lv.STATE.DISABLED)
168+
self.progress_bar.set_value(60, True)
169+
170+
elif self.current_state == CalibrationState.VERIFYING:
171+
self.status_label.set_text("Verifying calibration...")
172+
self.action_button.add_state(lv.STATE.DISABLED)
173+
self.progress_bar.set_value(90, True)
174+
175+
elif self.current_state == CalibrationState.COMPLETE:
176+
self.status_label.set_text("Calibration complete!")
177+
self.action_button_label.set_text("Done")
178+
self.action_button.remove_state(lv.STATE.DISABLED)
179+
self.progress_bar.set_value(100, True)
180+
181+
elif self.current_state == CalibrationState.ERROR:
182+
self.action_button_label.set_text("Retry")
183+
self.action_button.remove_state(lv.STATE.DISABLED)
184+
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
185+
186+
def action_button_clicked(self, event):
187+
"""Handle action button clicks based on current state."""
188+
if self.current_state == CalibrationState.IDLE:
189+
self.start_quality_check()
190+
elif self.current_state == CalibrationState.AWAITING_CONFIRMATION:
191+
self.start_calibration_process()
192+
elif self.current_state == CalibrationState.COMPLETE:
193+
self.finish()
194+
elif self.current_state == CalibrationState.ERROR:
195+
self.set_state(CalibrationState.IDLE)
196+
197+
def start_quality_check(self):
198+
"""Check current calibration quality."""
199+
self.set_state(CalibrationState.CHECKING_QUALITY)
200+
201+
# Run in background thread
202+
_thread.stack_size(mpos.apps.good_stack_size())
203+
_thread.start_new_thread(self.quality_check_thread, ())
204+
205+
def quality_check_thread(self):
206+
"""Background thread for quality check."""
207+
try:
208+
if self.is_desktop:
209+
quality = self.get_mock_quality()
210+
else:
211+
quality = SensorManager.check_calibration_quality(samples=50)
212+
213+
if quality is None:
214+
self.update_ui_threadsafe_if_foreground(self.handle_quality_error, "Failed to read IMU")
215+
return
216+
217+
# Update UI with results
218+
self.update_ui_threadsafe_if_foreground(self.show_quality_results, quality)
219+
220+
except Exception as e:
221+
print(f"[CalibrateIMU] Quality check error: {e}")
222+
self.update_ui_threadsafe_if_foreground(self.handle_quality_error, str(e))
223+
224+
def show_quality_results(self, quality):
225+
"""Show quality check results and ask for confirmation."""
226+
rating = quality['quality_rating']
227+
score = quality['quality_score']
228+
issues = quality['issues']
229+
230+
# Build status message
231+
if rating == "Good":
232+
msg = f"Current calibration: {rating} ({score*100:.0f}%)\n\nCalibration looks good!"
233+
else:
234+
msg = f"Current calibration: {rating} ({score*100:.0f}%)\n\nRecommend recalibrating."
235+
236+
if issues:
237+
msg += "\n\nIssues found:\n" + "\n".join(f"- {issue}" for issue in issues[:3]) # Show first 3
238+
239+
self.status_label.set_text(msg)
240+
self.set_state(CalibrationState.AWAITING_CONFIRMATION)
241+
242+
def handle_quality_error(self, error_msg):
243+
"""Handle error during quality check."""
244+
self.set_state(CalibrationState.ERROR)
245+
self.status_label.set_text(f"Error: {error_msg}")
246+
self.detail_label.set_text("Check IMU connection and try again")
247+
248+
def start_calibration_process(self):
249+
"""Start the calibration process."""
250+
self.set_state(CalibrationState.CHECKING_STATIONARITY)
251+
252+
# Run in background thread
253+
_thread.stack_size(mpos.apps.good_stack_size())
254+
_thread.start_new_thread(self.calibration_thread_func, ())
255+
256+
def calibration_thread_func(self):
257+
"""Background thread for calibration process."""
258+
try:
259+
# Step 1: Check stationarity
260+
if self.is_desktop:
261+
stationarity = {'is_stationary': True, 'message': 'Mock: Stationary'}
262+
else:
263+
stationarity = SensorManager.check_stationarity(samples=30)
264+
265+
if stationarity is None or not stationarity['is_stationary']:
266+
msg = stationarity['message'] if stationarity else "Stationarity check failed"
267+
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error,
268+
f"Device not stationary!\n\n{msg}\n\nPlace on flat surface and try again.")
269+
return
270+
271+
# Step 2: Perform calibration
272+
self.update_ui_threadsafe_if_foreground(lambda: self.set_state(CalibrationState.CALIBRATING))
273+
time.sleep(0.5) # Brief pause for user to see status change
274+
275+
if self.is_desktop:
276+
# Mock calibration
277+
time.sleep(2)
278+
accel_offsets = (0.1, -0.05, 0.15)
279+
gyro_offsets = (0.2, -0.1, 0.05)
280+
else:
281+
# Real calibration
282+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
283+
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
284+
285+
if accel:
286+
accel_offsets = SensorManager.calibrate_sensor(accel, samples=100)
287+
else:
288+
accel_offsets = None
289+
290+
if gyro:
291+
gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=100)
292+
else:
293+
gyro_offsets = None
294+
295+
# Step 3: Verify results
296+
self.update_ui_threadsafe_if_foreground(lambda: self.set_state(CalibrationState.VERIFYING))
297+
time.sleep(0.5)
298+
299+
if self.is_desktop:
300+
verify_quality = self.get_mock_quality(good=True)
301+
else:
302+
verify_quality = SensorManager.check_calibration_quality(samples=50)
303+
304+
if verify_quality is None:
305+
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error,
306+
"Calibration completed but verification failed")
307+
return
308+
309+
# Step 4: Show results
310+
rating = verify_quality['quality_rating']
311+
score = verify_quality['quality_score']
312+
313+
result_msg = f"Calibration successful!\n\nNew quality: {rating} ({score*100:.0f}%)"
314+
if accel_offsets:
315+
result_msg += f"\n\nAccel offsets:\nX:{accel_offsets[0]:.3f} Y:{accel_offsets[1]:.3f} Z:{accel_offsets[2]:.3f}"
316+
if gyro_offsets:
317+
result_msg += f"\n\nGyro offsets:\nX:{gyro_offsets[0]:.3f} Y:{gyro_offsets[1]:.3f} Z:{gyro_offsets[2]:.3f}"
318+
319+
self.update_ui_threadsafe_if_foreground(self.show_calibration_complete, result_msg)
320+
321+
except Exception as e:
322+
print(f"[CalibrateIMU] Calibration error: {e}")
323+
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error, str(e))
324+
325+
def show_calibration_complete(self, result_msg):
326+
"""Show calibration completion message."""
327+
self.status_label.set_text(result_msg)
328+
self.detail_label.set_text("Calibration saved to Settings")
329+
self.set_state(CalibrationState.COMPLETE)
330+
331+
def handle_calibration_error(self, error_msg):
332+
"""Handle error during calibration."""
333+
self.set_state(CalibrationState.ERROR)
334+
self.status_label.set_text(f"Calibration failed:\n\n{error_msg}")
335+
self.detail_label.set_text("")
336+
337+
def get_mock_quality(self, good=False):
338+
"""Generate mock quality data for desktop testing."""
339+
import random
340+
341+
if good:
342+
# Simulate excellent calibration after calibration
343+
return {
344+
'accel_mean': (random.uniform(-0.05, 0.05), random.uniform(-0.05, 0.05), 9.8 + random.uniform(-0.1, 0.1)),
345+
'accel_variance': (random.uniform(0.001, 0.02), random.uniform(0.001, 0.02), random.uniform(0.001, 0.02)),
346+
'gyro_mean': (random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1)),
347+
'gyro_variance': (random.uniform(0.01, 0.2), random.uniform(0.01, 0.2), random.uniform(0.01, 0.2)),
348+
'quality_score': random.uniform(0.90, 0.99),
349+
'quality_rating': "Good",
350+
'issues': []
351+
}
352+
else:
353+
# Simulate mediocre calibration before calibration
354+
return {
355+
'accel_mean': (random.uniform(-1.0, 1.0), random.uniform(-1.0, 1.0), 9.8 + random.uniform(-2.0, 2.0)),
356+
'accel_variance': (random.uniform(0.2, 0.5), random.uniform(0.2, 0.5), random.uniform(0.2, 0.5)),
357+
'gyro_mean': (random.uniform(-3.0, 3.0), random.uniform(-3.0, 3.0), random.uniform(-3.0, 3.0)),
358+
'gyro_variance': (random.uniform(2.0, 5.0), random.uniform(2.0, 5.0), random.uniform(2.0, 5.0)),
359+
'quality_score': random.uniform(0.4, 0.6),
360+
'quality_rating': "Fair",
361+
'issues': ["High accelerometer variance", "Gyro not near zero"]
362+
}

0 commit comments

Comments
 (0)