You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Settings app: add IMU calibration with check
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
"""Calibrate IMU Activity.
|
||||
|
||||
Guides user through IMU calibration process:
|
||||
1. Check current calibration quality
|
||||
2. Ask if user wants to recalibrate
|
||||
3. Check stationarity
|
||||
4. Perform calibration
|
||||
5. Verify results
|
||||
6. Save to new location
|
||||
"""
|
||||
|
||||
import lvgl as lv
|
||||
import time
|
||||
import _thread
|
||||
import sys
|
||||
from mpos.app.activity import Activity
|
||||
import mpos.ui
|
||||
import mpos.sensor_manager as SensorManager
|
||||
import mpos.apps
|
||||
|
||||
|
||||
class CalibrationState:
|
||||
"""Enum for calibration states."""
|
||||
IDLE = 0
|
||||
CHECKING_QUALITY = 1
|
||||
AWAITING_CONFIRMATION = 2
|
||||
CHECKING_STATIONARITY = 3
|
||||
CALIBRATING = 4
|
||||
VERIFYING = 5
|
||||
COMPLETE = 6
|
||||
ERROR = 7
|
||||
|
||||
|
||||
class CalibrateIMUActivity(Activity):
|
||||
"""Guide user through IMU calibration process."""
|
||||
|
||||
# State
|
||||
current_state = CalibrationState.IDLE
|
||||
calibration_thread = None
|
||||
|
||||
# Widgets
|
||||
title_label = None
|
||||
status_label = None
|
||||
progress_bar = None
|
||||
detail_label = None
|
||||
action_button = None
|
||||
action_button_label = None
|
||||
cancel_button = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_desktop = sys.platform != "esp32"
|
||||
|
||||
def onCreate(self):
|
||||
screen = lv.obj()
|
||||
screen.set_style_pad_all(mpos.ui.pct_of_display_width(3), 0)
|
||||
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
|
||||
screen.set_flex_align(lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.START, lv.FLEX_ALIGN.CENTER)
|
||||
|
||||
# Title
|
||||
self.title_label = lv.label(screen)
|
||||
self.title_label.set_text("IMU Calibration")
|
||||
self.title_label.set_style_text_font(lv.font_montserrat_20, 0)
|
||||
|
||||
# Status label
|
||||
self.status_label = lv.label(screen)
|
||||
self.status_label.set_text("Initializing...")
|
||||
self.status_label.set_style_text_font(lv.font_montserrat_16, 0)
|
||||
self.status_label.set_long_mode(lv.label.LONG_MODE.WRAP)
|
||||
self.status_label.set_width(lv.pct(90))
|
||||
|
||||
# Progress bar (hidden initially)
|
||||
self.progress_bar = lv.bar(screen)
|
||||
self.progress_bar.set_size(lv.pct(90), 20)
|
||||
self.progress_bar.set_value(0, False)
|
||||
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
# Detail label (for additional info)
|
||||
self.detail_label = lv.label(screen)
|
||||
self.detail_label.set_text("")
|
||||
self.detail_label.set_style_text_font(lv.font_montserrat_12, 0)
|
||||
self.detail_label.set_style_text_color(lv.color_hex(0x888888), 0)
|
||||
self.detail_label.set_long_mode(lv.label.LONG_MODE.WRAP)
|
||||
self.detail_label.set_width(lv.pct(90))
|
||||
|
||||
# Button container
|
||||
btn_cont = lv.obj(screen)
|
||||
btn_cont.set_width(lv.pct(100))
|
||||
btn_cont.set_height(lv.SIZE_CONTENT)
|
||||
btn_cont.set_style_border_width(0, 0)
|
||||
btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW)
|
||||
btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0)
|
||||
|
||||
# Action button
|
||||
self.action_button = lv.button(btn_cont)
|
||||
self.action_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
|
||||
self.action_button_label = lv.label(self.action_button)
|
||||
self.action_button_label.set_text("Start")
|
||||
self.action_button_label.center()
|
||||
self.action_button.add_event_cb(self.action_button_clicked, lv.EVENT.CLICKED, None)
|
||||
|
||||
# Cancel button
|
||||
self.cancel_button = lv.button(btn_cont)
|
||||
self.cancel_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
|
||||
cancel_label = lv.label(self.cancel_button)
|
||||
cancel_label.set_text("Cancel")
|
||||
cancel_label.center()
|
||||
self.cancel_button.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None)
|
||||
|
||||
self.setContentView(screen)
|
||||
|
||||
def onResume(self, screen):
|
||||
super().onResume(screen)
|
||||
|
||||
# Check if IMU is available
|
||||
if not self.is_desktop and not SensorManager.is_available():
|
||||
self.set_state(CalibrationState.ERROR)
|
||||
self.status_label.set_text("IMU not available on this device")
|
||||
self.action_button.add_state(lv.STATE.DISABLED)
|
||||
return
|
||||
|
||||
# Start by checking current quality
|
||||
self.set_state(CalibrationState.IDLE)
|
||||
self.action_button_label.set_text("Check Quality")
|
||||
|
||||
def onPause(self, screen):
|
||||
# Stop any running calibration
|
||||
if self.current_state == CalibrationState.CALIBRATING:
|
||||
# Calibration will detect activity is no longer in foreground
|
||||
pass
|
||||
super().onPause(screen)
|
||||
|
||||
def set_state(self, new_state):
|
||||
"""Update state and UI accordingly."""
|
||||
self.current_state = new_state
|
||||
self.update_ui_for_state()
|
||||
|
||||
def update_ui_for_state(self):
|
||||
"""Update UI based on current state."""
|
||||
if self.current_state == CalibrationState.IDLE:
|
||||
self.status_label.set_text("Ready to check calibration quality")
|
||||
self.action_button_label.set_text("Check Quality")
|
||||
self.action_button.remove_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
elif self.current_state == CalibrationState.CHECKING_QUALITY:
|
||||
self.status_label.set_text("Checking current calibration...")
|
||||
self.action_button.add_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN)
|
||||
self.progress_bar.set_value(20, True)
|
||||
|
||||
elif self.current_state == CalibrationState.AWAITING_CONFIRMATION:
|
||||
# Status will be set by quality check result
|
||||
self.action_button_label.set_text("Calibrate Now")
|
||||
self.action_button.remove_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.set_value(30, True)
|
||||
|
||||
elif self.current_state == CalibrationState.CHECKING_STATIONARITY:
|
||||
self.status_label.set_text("Checking if device is stationary...")
|
||||
self.detail_label.set_text("Keep device still on flat surface")
|
||||
self.action_button.add_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.set_value(40, True)
|
||||
|
||||
elif self.current_state == CalibrationState.CALIBRATING:
|
||||
self.status_label.set_text("Calibrating IMU...")
|
||||
self.detail_label.set_text("Do not move device!\nCollecting samples...")
|
||||
self.action_button.add_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.set_value(60, True)
|
||||
|
||||
elif self.current_state == CalibrationState.VERIFYING:
|
||||
self.status_label.set_text("Verifying calibration...")
|
||||
self.action_button.add_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.set_value(90, True)
|
||||
|
||||
elif self.current_state == CalibrationState.COMPLETE:
|
||||
self.status_label.set_text("Calibration complete!")
|
||||
self.action_button_label.set_text("Done")
|
||||
self.action_button.remove_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.set_value(100, True)
|
||||
|
||||
elif self.current_state == CalibrationState.ERROR:
|
||||
self.action_button_label.set_text("Retry")
|
||||
self.action_button.remove_state(lv.STATE.DISABLED)
|
||||
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
def action_button_clicked(self, event):
|
||||
"""Handle action button clicks based on current state."""
|
||||
if self.current_state == CalibrationState.IDLE:
|
||||
self.start_quality_check()
|
||||
elif self.current_state == CalibrationState.AWAITING_CONFIRMATION:
|
||||
self.start_calibration_process()
|
||||
elif self.current_state == CalibrationState.COMPLETE:
|
||||
self.finish()
|
||||
elif self.current_state == CalibrationState.ERROR:
|
||||
self.set_state(CalibrationState.IDLE)
|
||||
|
||||
def start_quality_check(self):
|
||||
"""Check current calibration quality."""
|
||||
self.set_state(CalibrationState.CHECKING_QUALITY)
|
||||
|
||||
# Run in background thread
|
||||
_thread.stack_size(mpos.apps.good_stack_size())
|
||||
_thread.start_new_thread(self.quality_check_thread, ())
|
||||
|
||||
def quality_check_thread(self):
|
||||
"""Background thread for quality check."""
|
||||
try:
|
||||
if self.is_desktop:
|
||||
quality = self.get_mock_quality()
|
||||
else:
|
||||
quality = SensorManager.check_calibration_quality(samples=50)
|
||||
|
||||
if quality is None:
|
||||
self.update_ui_threadsafe_if_foreground(self.handle_quality_error, "Failed to read IMU")
|
||||
return
|
||||
|
||||
# Update UI with results
|
||||
self.update_ui_threadsafe_if_foreground(self.show_quality_results, quality)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CalibrateIMU] Quality check error: {e}")
|
||||
self.update_ui_threadsafe_if_foreground(self.handle_quality_error, str(e))
|
||||
|
||||
def show_quality_results(self, quality):
|
||||
"""Show quality check results and ask for confirmation."""
|
||||
rating = quality['quality_rating']
|
||||
score = quality['quality_score']
|
||||
issues = quality['issues']
|
||||
|
||||
# Build status message
|
||||
if rating == "Good":
|
||||
msg = f"Current calibration: {rating} ({score*100:.0f}%)\n\nCalibration looks good!"
|
||||
else:
|
||||
msg = f"Current calibration: {rating} ({score*100:.0f}%)\n\nRecommend recalibrating."
|
||||
|
||||
if issues:
|
||||
msg += "\n\nIssues found:\n" + "\n".join(f"- {issue}" for issue in issues[:3]) # Show first 3
|
||||
|
||||
self.status_label.set_text(msg)
|
||||
self.set_state(CalibrationState.AWAITING_CONFIRMATION)
|
||||
|
||||
def handle_quality_error(self, error_msg):
|
||||
"""Handle error during quality check."""
|
||||
self.set_state(CalibrationState.ERROR)
|
||||
self.status_label.set_text(f"Error: {error_msg}")
|
||||
self.detail_label.set_text("Check IMU connection and try again")
|
||||
|
||||
def start_calibration_process(self):
|
||||
"""Start the calibration process."""
|
||||
self.set_state(CalibrationState.CHECKING_STATIONARITY)
|
||||
|
||||
# Run in background thread
|
||||
_thread.stack_size(mpos.apps.good_stack_size())
|
||||
_thread.start_new_thread(self.calibration_thread_func, ())
|
||||
|
||||
def calibration_thread_func(self):
|
||||
"""Background thread for calibration process."""
|
||||
try:
|
||||
# Step 1: Check stationarity
|
||||
if self.is_desktop:
|
||||
stationarity = {'is_stationary': True, 'message': 'Mock: Stationary'}
|
||||
else:
|
||||
stationarity = SensorManager.check_stationarity(samples=30)
|
||||
|
||||
if stationarity is None or not stationarity['is_stationary']:
|
||||
msg = stationarity['message'] if stationarity else "Stationarity check failed"
|
||||
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error,
|
||||
f"Device not stationary!\n\n{msg}\n\nPlace on flat surface and try again.")
|
||||
return
|
||||
|
||||
# Step 2: Perform calibration
|
||||
self.update_ui_threadsafe_if_foreground(lambda: self.set_state(CalibrationState.CALIBRATING))
|
||||
time.sleep(0.5) # Brief pause for user to see status change
|
||||
|
||||
if self.is_desktop:
|
||||
# Mock calibration
|
||||
time.sleep(2)
|
||||
accel_offsets = (0.1, -0.05, 0.15)
|
||||
gyro_offsets = (0.2, -0.1, 0.05)
|
||||
else:
|
||||
# Real calibration
|
||||
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
|
||||
|
||||
if accel:
|
||||
accel_offsets = SensorManager.calibrate_sensor(accel, samples=100)
|
||||
else:
|
||||
accel_offsets = None
|
||||
|
||||
if gyro:
|
||||
gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=100)
|
||||
else:
|
||||
gyro_offsets = None
|
||||
|
||||
# Step 3: Verify results
|
||||
self.update_ui_threadsafe_if_foreground(lambda: self.set_state(CalibrationState.VERIFYING))
|
||||
time.sleep(0.5)
|
||||
|
||||
if self.is_desktop:
|
||||
verify_quality = self.get_mock_quality(good=True)
|
||||
else:
|
||||
verify_quality = SensorManager.check_calibration_quality(samples=50)
|
||||
|
||||
if verify_quality is None:
|
||||
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error,
|
||||
"Calibration completed but verification failed")
|
||||
return
|
||||
|
||||
# Step 4: Show results
|
||||
rating = verify_quality['quality_rating']
|
||||
score = verify_quality['quality_score']
|
||||
|
||||
result_msg = f"Calibration successful!\n\nNew quality: {rating} ({score*100:.0f}%)"
|
||||
if accel_offsets:
|
||||
result_msg += f"\n\nAccel offsets:\nX:{accel_offsets[0]:.3f} Y:{accel_offsets[1]:.3f} Z:{accel_offsets[2]:.3f}"
|
||||
if gyro_offsets:
|
||||
result_msg += f"\n\nGyro offsets:\nX:{gyro_offsets[0]:.3f} Y:{gyro_offsets[1]:.3f} Z:{gyro_offsets[2]:.3f}"
|
||||
|
||||
self.update_ui_threadsafe_if_foreground(self.show_calibration_complete, result_msg)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CalibrateIMU] Calibration error: {e}")
|
||||
self.update_ui_threadsafe_if_foreground(self.handle_calibration_error, str(e))
|
||||
|
||||
def show_calibration_complete(self, result_msg):
|
||||
"""Show calibration completion message."""
|
||||
self.status_label.set_text(result_msg)
|
||||
self.detail_label.set_text("Calibration saved to Settings")
|
||||
self.set_state(CalibrationState.COMPLETE)
|
||||
|
||||
def handle_calibration_error(self, error_msg):
|
||||
"""Handle error during calibration."""
|
||||
self.set_state(CalibrationState.ERROR)
|
||||
self.status_label.set_text(f"Calibration failed:\n\n{error_msg}")
|
||||
self.detail_label.set_text("")
|
||||
|
||||
def get_mock_quality(self, good=False):
|
||||
"""Generate mock quality data for desktop testing."""
|
||||
import random
|
||||
|
||||
if good:
|
||||
# Simulate excellent calibration after calibration
|
||||
return {
|
||||
'accel_mean': (random.uniform(-0.05, 0.05), random.uniform(-0.05, 0.05), 9.8 + random.uniform(-0.1, 0.1)),
|
||||
'accel_variance': (random.uniform(0.001, 0.02), random.uniform(0.001, 0.02), random.uniform(0.001, 0.02)),
|
||||
'gyro_mean': (random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1)),
|
||||
'gyro_variance': (random.uniform(0.01, 0.2), random.uniform(0.01, 0.2), random.uniform(0.01, 0.2)),
|
||||
'quality_score': random.uniform(0.90, 0.99),
|
||||
'quality_rating': "Good",
|
||||
'issues': []
|
||||
}
|
||||
else:
|
||||
# Simulate mediocre calibration before calibration
|
||||
return {
|
||||
'accel_mean': (random.uniform(-1.0, 1.0), random.uniform(-1.0, 1.0), 9.8 + random.uniform(-2.0, 2.0)),
|
||||
'accel_variance': (random.uniform(0.2, 0.5), random.uniform(0.2, 0.5), random.uniform(0.2, 0.5)),
|
||||
'gyro_mean': (random.uniform(-3.0, 3.0), random.uniform(-3.0, 3.0), random.uniform(-3.0, 3.0)),
|
||||
'gyro_variance': (random.uniform(2.0, 5.0), random.uniform(2.0, 5.0), random.uniform(2.0, 5.0)),
|
||||
'quality_score': random.uniform(0.4, 0.6),
|
||||
'quality_rating': "Fair",
|
||||
'issues': ["High accelerometer variance", "Gyro not near zero"]
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
"""Check IMU Calibration Activity.
|
||||
|
||||
Shows current IMU calibration quality with real-time sensor values,
|
||||
variance, expected value comparison, and overall quality score.
|
||||
"""
|
||||
|
||||
import lvgl as lv
|
||||
import time
|
||||
import sys
|
||||
from mpos.app.activity import Activity
|
||||
import mpos.ui
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
|
||||
class CheckIMUCalibrationActivity(Activity):
|
||||
"""Display IMU calibration quality with real-time monitoring."""
|
||||
|
||||
# Update interval for real-time display (milliseconds)
|
||||
UPDATE_INTERVAL = 100
|
||||
|
||||
# State
|
||||
updating = False
|
||||
update_timer = None
|
||||
|
||||
# Widgets
|
||||
status_label = None
|
||||
quality_label = None
|
||||
accel_labels = [] # [x_label, y_label, z_label]
|
||||
gyro_labels = []
|
||||
issues_label = None
|
||||
quality_score_label = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_desktop = sys.platform != "esp32"
|
||||
|
||||
def onCreate(self):
|
||||
screen = lv.obj()
|
||||
screen.set_style_pad_all(mpos.ui.pct_of_display_width(2), 0)
|
||||
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
|
||||
|
||||
# Title
|
||||
title = lv.label(screen)
|
||||
title.set_text("IMU Calibration Check")
|
||||
title.set_style_text_font(lv.font_montserrat_20, 0)
|
||||
|
||||
# Status label
|
||||
self.status_label = lv.label(screen)
|
||||
self.status_label.set_text("Checking...")
|
||||
self.status_label.set_style_text_font(lv.font_montserrat_14, 0)
|
||||
|
||||
# Separator
|
||||
sep1 = lv.obj(screen)
|
||||
sep1.set_size(lv.pct(100), 2)
|
||||
sep1.set_style_bg_color(lv.color_hex(0x666666), 0)
|
||||
|
||||
# Quality score (large, prominent)
|
||||
self.quality_score_label = lv.label(screen)
|
||||
self.quality_score_label.set_text("Quality: --")
|
||||
self.quality_score_label.set_style_text_font(lv.font_montserrat_20, 0)
|
||||
|
||||
# Accelerometer section
|
||||
accel_title = lv.label(screen)
|
||||
accel_title.set_text("Accelerometer (m/s²)")
|
||||
accel_title.set_style_text_font(lv.font_montserrat_14, 0)
|
||||
|
||||
for axis in ['X', 'Y', 'Z']:
|
||||
label = lv.label(screen)
|
||||
label.set_text(f"{axis}: --")
|
||||
label.set_style_text_font(lv.font_montserrat_12, 0)
|
||||
self.accel_labels.append(label)
|
||||
|
||||
# Gyroscope section
|
||||
gyro_title = lv.label(screen)
|
||||
gyro_title.set_text("Gyroscope (deg/s)")
|
||||
gyro_title.set_style_text_font(lv.font_montserrat_14, 0)
|
||||
|
||||
for axis in ['X', 'Y', 'Z']:
|
||||
label = lv.label(screen)
|
||||
label.set_text(f"{axis}: --")
|
||||
label.set_style_text_font(lv.font_montserrat_12, 0)
|
||||
self.gyro_labels.append(label)
|
||||
|
||||
# Separator
|
||||
sep2 = lv.obj(screen)
|
||||
sep2.set_size(lv.pct(100), 2)
|
||||
sep2.set_style_bg_color(lv.color_hex(0x666666), 0)
|
||||
|
||||
# Issues label
|
||||
self.issues_label = lv.label(screen)
|
||||
self.issues_label.set_text("Issues: None")
|
||||
self.issues_label.set_style_text_font(lv.font_montserrat_12, 0)
|
||||
self.issues_label.set_style_text_color(lv.color_hex(0xFF6666), 0)
|
||||
self.issues_label.set_long_mode(lv.label.LONG_MODE.WRAP)
|
||||
self.issues_label.set_width(lv.pct(95))
|
||||
|
||||
# Button container
|
||||
btn_cont = lv.obj(screen)
|
||||
btn_cont.set_width(lv.pct(100))
|
||||
btn_cont.set_height(lv.SIZE_CONTENT)
|
||||
btn_cont.set_style_border_width(0, 0)
|
||||
btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW)
|
||||
btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0)
|
||||
|
||||
# Back button
|
||||
back_btn = lv.button(btn_cont)
|
||||
back_btn.set_size(lv.pct(45), lv.SIZE_CONTENT)
|
||||
back_label = lv.label(back_btn)
|
||||
back_label.set_text("Back")
|
||||
back_label.center()
|
||||
back_btn.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None)
|
||||
|
||||
# Calibrate button
|
||||
calibrate_btn = lv.button(btn_cont)
|
||||
calibrate_btn.set_size(lv.pct(45), lv.SIZE_CONTENT)
|
||||
calibrate_label = lv.label(calibrate_btn)
|
||||
calibrate_label.set_text("Calibrate")
|
||||
calibrate_label.center()
|
||||
calibrate_btn.add_event_cb(self.start_calibration, lv.EVENT.CLICKED, None)
|
||||
|
||||
self.setContentView(screen)
|
||||
|
||||
def onResume(self, screen):
|
||||
super().onResume(screen)
|
||||
|
||||
# Check if IMU is available
|
||||
if not self.is_desktop and not SensorManager.is_available():
|
||||
self.status_label.set_text("IMU not available on this device")
|
||||
self.quality_score_label.set_text("N/A")
|
||||
return
|
||||
|
||||
# Start real-time updates
|
||||
self.updating = True
|
||||
self.update_timer = lv.timer_create(self.update_display, self.UPDATE_INTERVAL, None)
|
||||
|
||||
def onPause(self, screen):
|
||||
# Stop updates
|
||||
self.updating = False
|
||||
if self.update_timer:
|
||||
self.update_timer.delete()
|
||||
self.update_timer = None
|
||||
super().onPause(screen)
|
||||
|
||||
def update_display(self, timer=None):
|
||||
"""Update display with current sensor values and quality."""
|
||||
if not self.updating:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get quality check (desktop or hardware)
|
||||
if self.is_desktop:
|
||||
quality = self.get_mock_quality()
|
||||
else:
|
||||
quality = SensorManager.check_calibration_quality(samples=30)
|
||||
|
||||
if quality is None:
|
||||
self.status_label.set_text("Error reading IMU")
|
||||
return
|
||||
|
||||
# Update quality score
|
||||
score = quality['quality_score']
|
||||
rating = quality['quality_rating']
|
||||
self.quality_score_label.set_text(f"Quality: {rating} ({score*100:.0f}%)")
|
||||
|
||||
# Color based on rating
|
||||
if rating == "Good":
|
||||
color = 0x66FF66 # Green
|
||||
elif rating == "Fair":
|
||||
color = 0xFFFF66 # Yellow
|
||||
else:
|
||||
color = 0xFF6666 # Red
|
||||
self.quality_score_label.set_style_text_color(lv.color_hex(color), 0)
|
||||
|
||||
# Update accelerometer values
|
||||
accel_mean = quality['accel_mean']
|
||||
accel_var = quality['accel_variance']
|
||||
for i, (mean, var) in enumerate(zip(accel_mean, accel_var)):
|
||||
axis = ['X', 'Y', 'Z'][i]
|
||||
self.accel_labels[i].set_text(f"{axis}: {mean:6.2f} (var: {var:.3f})")
|
||||
|
||||
# Update gyroscope values
|
||||
gyro_mean = quality['gyro_mean']
|
||||
gyro_var = quality['gyro_variance']
|
||||
for i, (mean, var) in enumerate(zip(gyro_mean, gyro_var)):
|
||||
axis = ['X', 'Y', 'Z'][i]
|
||||
self.gyro_labels[i].set_text(f"{axis}: {mean:6.2f} (var: {var:.3f})")
|
||||
|
||||
# Update issues
|
||||
issues = quality['issues']
|
||||
if issues:
|
||||
issues_text = "Issues:\n" + "\n".join(f"- {issue}" for issue in issues)
|
||||
else:
|
||||
issues_text = "Issues: None - calibration looks good!"
|
||||
self.issues_label.set_text(issues_text)
|
||||
|
||||
self.status_label.set_text("Real-time monitoring (place on flat surface)")
|
||||
except:
|
||||
# Widgets were deleted (activity closed), stop updating
|
||||
self.updating = False
|
||||
|
||||
def get_mock_quality(self):
|
||||
"""Generate mock quality data for desktop testing."""
|
||||
import random
|
||||
|
||||
# Simulate good calibration with small random noise
|
||||
return {
|
||||
'accel_mean': (
|
||||
random.uniform(-0.2, 0.2),
|
||||
random.uniform(-0.2, 0.2),
|
||||
9.8 + random.uniform(-0.3, 0.3)
|
||||
),
|
||||
'accel_variance': (
|
||||
random.uniform(0.01, 0.1),
|
||||
random.uniform(0.01, 0.1),
|
||||
random.uniform(0.01, 0.1)
|
||||
),
|
||||
'gyro_mean': (
|
||||
random.uniform(-0.5, 0.5),
|
||||
random.uniform(-0.5, 0.5),
|
||||
random.uniform(-0.5, 0.5)
|
||||
),
|
||||
'gyro_variance': (
|
||||
random.uniform(0.1, 1.0),
|
||||
random.uniform(0.1, 1.0),
|
||||
random.uniform(0.1, 1.0)
|
||||
),
|
||||
'quality_score': random.uniform(0.75, 0.95),
|
||||
'quality_rating': "Good",
|
||||
'issues': []
|
||||
}
|
||||
|
||||
def start_calibration(self, event):
|
||||
"""Navigate to calibration activity."""
|
||||
from mpos.content.intent import Intent
|
||||
from calibrate_imu import CalibrateIMUActivity
|
||||
|
||||
intent = Intent(activity_class=CalibrateIMUActivity)
|
||||
self.startActivity(intent)
|
||||
@@ -1,3 +1,4 @@
|
||||
import lvgl as lv
|
||||
from mpos.apps import Activity, Intent
|
||||
from mpos.activity_navigator import ActivityNavigator
|
||||
|
||||
@@ -7,6 +8,10 @@ import mpos.config
|
||||
import mpos.ui
|
||||
import mpos.time
|
||||
|
||||
# Import IMU calibration activities
|
||||
from check_imu_calibration import CheckIMUCalibrationActivity
|
||||
from calibrate_imu import CalibrateIMUActivity
|
||||
|
||||
# Used to list and edit all settings:
|
||||
class SettingsActivity(Activity):
|
||||
def __init__(self):
|
||||
@@ -39,6 +44,8 @@ class SettingsActivity(Activity):
|
||||
]
|
||||
self.settings = [
|
||||
# Novice settings, alphabetically:
|
||||
{"title": "Calibrate IMU", "key": "calibrate_imu", "value_label": None, "cont": None, "ui": "activity", "activity_class": "CalibrateIMUActivity"},
|
||||
{"title": "Check IMU Calibration", "key": "check_imu_calibration", "value_label": None, "cont": None, "ui": "activity", "activity_class": "CheckIMUCalibrationActivity"},
|
||||
{"title": "Light/Dark Theme", "key": "theme_light_dark", "value_label": None, "cont": None, "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")]},
|
||||
{"title": "Theme Color", "key": "theme_primary_color", "value_label": None, "cont": None, "placeholder": "HTML hex color, like: EC048C", "ui": "dropdown", "ui_options": theme_colors},
|
||||
{"title": "Timezone", "key": "timezone", "value_label": None, "cont": None, "ui": "dropdown", "ui_options": self.get_timezone_tuples(), "changed_callback": lambda : mpos.time.refresh_timezone_preference()},
|
||||
@@ -104,6 +111,20 @@ class SettingsActivity(Activity):
|
||||
focusgroup.add_obj(setting_cont)
|
||||
|
||||
def startSettingActivity(self, setting):
|
||||
ui_type = setting.get("ui")
|
||||
|
||||
# Handle activity-based settings (NEW)
|
||||
if ui_type == "activity":
|
||||
activity_class_name = setting.get("activity_class")
|
||||
if activity_class_name == "CheckIMUCalibrationActivity":
|
||||
intent = Intent(activity_class=CheckIMUCalibrationActivity)
|
||||
self.startActivity(intent)
|
||||
elif activity_class_name == "CalibrateIMUActivity":
|
||||
intent = Intent(activity_class=CalibrateIMUActivity)
|
||||
self.startActivity(intent)
|
||||
return
|
||||
|
||||
# Handle traditional settings (existing code)
|
||||
intent = Intent(activity_class=SettingActivity)
|
||||
intent.putExtra("setting", setting)
|
||||
self.startActivity(intent)
|
||||
|
||||
@@ -271,6 +271,238 @@ def calibrate_sensor(sensor, samples=100):
|
||||
_lock.release()
|
||||
|
||||
|
||||
# Helper functions for calibration quality checking (module-level to avoid nested def issues)
|
||||
def _calc_mean_variance(samples_list):
|
||||
"""Calculate mean and variance for a list of samples."""
|
||||
if not samples_list:
|
||||
return 0.0, 0.0
|
||||
n = len(samples_list)
|
||||
mean = sum(samples_list) / n
|
||||
variance = sum((x - mean) ** 2 for x in samples_list) / n
|
||||
return mean, variance
|
||||
|
||||
|
||||
def _calc_variance(samples_list):
|
||||
"""Calculate variance for a list of samples."""
|
||||
if not samples_list:
|
||||
return 0.0
|
||||
n = len(samples_list)
|
||||
mean = sum(samples_list) / n
|
||||
return sum((x - mean) ** 2 for x in samples_list) / n
|
||||
|
||||
|
||||
def check_calibration_quality(samples=50):
|
||||
"""Check quality of current calibration.
|
||||
|
||||
Args:
|
||||
samples: Number of samples to collect (default 50)
|
||||
|
||||
Returns:
|
||||
dict with:
|
||||
- accel_mean: (x, y, z) mean values in m/s²
|
||||
- accel_variance: (x, y, z) variance values
|
||||
- gyro_mean: (x, y, z) mean values in deg/s
|
||||
- gyro_variance: (x, y, z) variance values
|
||||
- quality_score: float 0.0-1.0 (1.0 = perfect)
|
||||
- quality_rating: string ("Good", "Fair", "Poor")
|
||||
- issues: list of strings describing problems
|
||||
None if IMU not available
|
||||
"""
|
||||
if not is_available():
|
||||
return None
|
||||
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
accel = get_default_sensor(TYPE_ACCELEROMETER)
|
||||
gyro = get_default_sensor(TYPE_GYROSCOPE)
|
||||
|
||||
# Collect samples
|
||||
accel_samples = [[], [], []] # x, y, z lists
|
||||
gyro_samples = [[], [], []]
|
||||
|
||||
for _ in range(samples):
|
||||
if accel:
|
||||
data = read_sensor(accel)
|
||||
if data:
|
||||
ax, ay, az = data
|
||||
accel_samples[0].append(ax)
|
||||
accel_samples[1].append(ay)
|
||||
accel_samples[2].append(az)
|
||||
if gyro:
|
||||
data = read_sensor(gyro)
|
||||
if data:
|
||||
gx, gy, gz = data
|
||||
gyro_samples[0].append(gx)
|
||||
gyro_samples[1].append(gy)
|
||||
gyro_samples[2].append(gz)
|
||||
time.sleep_ms(10)
|
||||
|
||||
# Calculate statistics using module-level helper
|
||||
accel_stats = [_calc_mean_variance(s) for s in accel_samples]
|
||||
gyro_stats = [_calc_mean_variance(s) for s in gyro_samples]
|
||||
|
||||
accel_mean = tuple(s[0] for s in accel_stats)
|
||||
accel_variance = tuple(s[1] for s in accel_stats)
|
||||
gyro_mean = tuple(s[0] for s in gyro_stats)
|
||||
gyro_variance = tuple(s[1] for s in gyro_stats)
|
||||
|
||||
# Calculate quality score (0.0 - 1.0)
|
||||
issues = []
|
||||
scores = []
|
||||
|
||||
# Check accelerometer
|
||||
if accel:
|
||||
# Variance check (lower is better)
|
||||
accel_max_variance = max(accel_variance)
|
||||
variance_score = max(0.0, 1.0 - (accel_max_variance / 1.0)) # 1.0 m/s² variance threshold
|
||||
scores.append(variance_score)
|
||||
if accel_max_variance > 0.5:
|
||||
issues.append(f"High accelerometer variance: {accel_max_variance:.3f} m/s²")
|
||||
|
||||
# Expected values check (X≈0, Y≈0, Z≈9.8)
|
||||
ax, ay, az = accel_mean
|
||||
xy_error = (abs(ax) + abs(ay)) / 2.0
|
||||
z_error = abs(az - _GRAVITY)
|
||||
expected_score = max(0.0, 1.0 - ((xy_error + z_error) / 5.0)) # 5.0 m/s² error threshold
|
||||
scores.append(expected_score)
|
||||
if xy_error > 1.0:
|
||||
issues.append(f"Accel X/Y not near zero: X={ax:.2f}, Y={ay:.2f} m/s²")
|
||||
if z_error > 1.0:
|
||||
issues.append(f"Accel Z not near 9.8: Z={az:.2f} m/s²")
|
||||
|
||||
# Check gyroscope
|
||||
if gyro:
|
||||
# Variance check
|
||||
gyro_max_variance = max(gyro_variance)
|
||||
variance_score = max(0.0, 1.0 - (gyro_max_variance / 10.0)) # 10 deg/s variance threshold
|
||||
scores.append(variance_score)
|
||||
if gyro_max_variance > 5.0:
|
||||
issues.append(f"High gyroscope variance: {gyro_max_variance:.3f} deg/s")
|
||||
|
||||
# Expected values check (all ≈0)
|
||||
gx, gy, gz = gyro_mean
|
||||
error = (abs(gx) + abs(gy) + abs(gz)) / 3.0
|
||||
expected_score = max(0.0, 1.0 - (error / 10.0)) # 10 deg/s error threshold
|
||||
scores.append(expected_score)
|
||||
if error > 2.0:
|
||||
issues.append(f"Gyro not near zero: X={gx:.2f}, Y={gy:.2f}, Z={gz:.2f} deg/s")
|
||||
|
||||
# Overall quality score
|
||||
quality_score = sum(scores) / len(scores) if scores else 0.0
|
||||
|
||||
# Rating
|
||||
if quality_score >= 0.8:
|
||||
quality_rating = "Good"
|
||||
elif quality_score >= 0.5:
|
||||
quality_rating = "Fair"
|
||||
else:
|
||||
quality_rating = "Poor"
|
||||
|
||||
return {
|
||||
'accel_mean': accel_mean,
|
||||
'accel_variance': accel_variance,
|
||||
'gyro_mean': gyro_mean,
|
||||
'gyro_variance': gyro_variance,
|
||||
'quality_score': quality_score,
|
||||
'quality_rating': quality_rating,
|
||||
'issues': issues
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SensorManager] Error checking calibration quality: {e}")
|
||||
return None
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
def check_stationarity(samples=30, variance_threshold_accel=0.5, variance_threshold_gyro=5.0):
|
||||
"""Check if device is stationary (required for calibration).
|
||||
|
||||
Args:
|
||||
samples: Number of samples to collect (default 30)
|
||||
variance_threshold_accel: Max acceptable accel variance in m/s² (default 0.5)
|
||||
variance_threshold_gyro: Max acceptable gyro variance in deg/s (default 5.0)
|
||||
|
||||
Returns:
|
||||
dict with:
|
||||
- is_stationary: bool
|
||||
- accel_variance: max variance across axes
|
||||
- gyro_variance: max variance across axes
|
||||
- message: string describing result
|
||||
None if IMU not available
|
||||
"""
|
||||
if not is_available():
|
||||
return None
|
||||
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
accel = get_default_sensor(TYPE_ACCELEROMETER)
|
||||
gyro = get_default_sensor(TYPE_GYROSCOPE)
|
||||
|
||||
# Collect samples
|
||||
accel_samples = [[], [], []]
|
||||
gyro_samples = [[], [], []]
|
||||
|
||||
for _ in range(samples):
|
||||
if accel:
|
||||
data = read_sensor(accel)
|
||||
if data:
|
||||
ax, ay, az = data
|
||||
accel_samples[0].append(ax)
|
||||
accel_samples[1].append(ay)
|
||||
accel_samples[2].append(az)
|
||||
if gyro:
|
||||
data = read_sensor(gyro)
|
||||
if data:
|
||||
gx, gy, gz = data
|
||||
gyro_samples[0].append(gx)
|
||||
gyro_samples[1].append(gy)
|
||||
gyro_samples[2].append(gz)
|
||||
time.sleep_ms(10)
|
||||
|
||||
# Calculate variance using module-level helper
|
||||
accel_var = [_calc_variance(s) for s in accel_samples]
|
||||
gyro_var = [_calc_variance(s) for s in gyro_samples]
|
||||
|
||||
max_accel_var = max(accel_var) if accel_var else 0.0
|
||||
max_gyro_var = max(gyro_var) if gyro_var else 0.0
|
||||
|
||||
# Check thresholds
|
||||
accel_stationary = max_accel_var < variance_threshold_accel
|
||||
gyro_stationary = max_gyro_var < variance_threshold_gyro
|
||||
is_stationary = accel_stationary and gyro_stationary
|
||||
|
||||
# Generate message
|
||||
if is_stationary:
|
||||
message = "Device is stationary - ready to calibrate"
|
||||
else:
|
||||
problems = []
|
||||
if not accel_stationary:
|
||||
problems.append(f"movement detected (accel variance: {max_accel_var:.3f})")
|
||||
if not gyro_stationary:
|
||||
problems.append(f"rotation detected (gyro variance: {max_gyro_var:.3f})")
|
||||
message = f"Device NOT stationary: {', '.join(problems)}"
|
||||
|
||||
return {
|
||||
'is_stationary': is_stationary,
|
||||
'accel_variance': max_accel_var,
|
||||
'gyro_variance': max_gyro_var,
|
||||
'message': message
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SensorManager] Error checking stationarity: {e}")
|
||||
return None
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Internal driver abstraction layer
|
||||
# ============================================================================
|
||||
@@ -571,16 +803,34 @@ def _register_mcu_temperature_sensor():
|
||||
# ============================================================================
|
||||
|
||||
def _load_calibration():
|
||||
"""Load calibration from SharedPreferences."""
|
||||
"""Load calibration from SharedPreferences (with migration support)."""
|
||||
if not _imu_driver:
|
||||
return
|
||||
|
||||
try:
|
||||
from mpos.config import SharedPreferences
|
||||
prefs = SharedPreferences("com.micropythonos.sensors")
|
||||
|
||||
accel_offsets = prefs.get_list("accel_offsets")
|
||||
gyro_offsets = prefs.get_list("gyro_offsets")
|
||||
# Try NEW location first
|
||||
prefs_new = SharedPreferences("com.micropythonos.settings", filename="sensors.json")
|
||||
accel_offsets = prefs_new.get_list("accel_offsets")
|
||||
gyro_offsets = prefs_new.get_list("gyro_offsets")
|
||||
|
||||
# If not found, try OLD location and migrate
|
||||
if not accel_offsets and not gyro_offsets:
|
||||
prefs_old = SharedPreferences("com.micropythonos.sensors")
|
||||
accel_offsets = prefs_old.get_list("accel_offsets")
|
||||
gyro_offsets = prefs_old.get_list("gyro_offsets")
|
||||
|
||||
if accel_offsets or gyro_offsets:
|
||||
print("[SensorManager] Migrating calibration from old to new location...")
|
||||
# Save to new location
|
||||
editor = prefs_new.edit()
|
||||
if accel_offsets:
|
||||
editor.put_list("accel_offsets", accel_offsets)
|
||||
if gyro_offsets:
|
||||
editor.put_list("gyro_offsets", gyro_offsets)
|
||||
editor.commit()
|
||||
print("[SensorManager] Migration complete")
|
||||
|
||||
if accel_offsets or gyro_offsets:
|
||||
_imu_driver.set_calibration(accel_offsets, gyro_offsets)
|
||||
@@ -590,13 +840,14 @@ def _load_calibration():
|
||||
|
||||
|
||||
def _save_calibration():
|
||||
"""Save calibration to SharedPreferences."""
|
||||
"""Save calibration to SharedPreferences (new location)."""
|
||||
if not _imu_driver:
|
||||
return
|
||||
|
||||
try:
|
||||
from mpos.config import SharedPreferences
|
||||
prefs = SharedPreferences("com.micropythonos.sensors")
|
||||
# NEW LOCATION: com.micropythonos.settings/sensors.json
|
||||
prefs = SharedPreferences("com.micropythonos.settings", filename="sensors.json")
|
||||
editor = prefs.edit()
|
||||
|
||||
cal = _imu_driver.get_calibration()
|
||||
@@ -604,6 +855,6 @@ def _save_calibration():
|
||||
editor.put_list("gyro_offsets", list(cal['gyro_offsets']))
|
||||
editor.commit()
|
||||
|
||||
print(f"[SensorManager] Saved calibration: accel={cal['accel_offsets']}, gyro={cal['gyro_offsets']}")
|
||||
print(f"[SensorManager] Saved calibration to settings: accel={cal['accel_offsets']}, gyro={cal['gyro_offsets']}")
|
||||
except Exception as e:
|
||||
print(f"[SensorManager] Failed to save calibration: {e}")
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
Graphical test for IMU calibration activities.
|
||||
|
||||
Tests both CheckIMUCalibrationActivity and CalibrateIMUActivity
|
||||
with mock data on desktop.
|
||||
|
||||
Usage:
|
||||
Desktop: ./tests/unittest.sh tests/test_graphical_imu_calibration.py
|
||||
Device: ./tests/unittest.sh tests/test_graphical_imu_calibration.py --ondevice
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import mpos.apps
|
||||
import mpos.ui
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from mpos.ui.testing import (
|
||||
wait_for_render,
|
||||
capture_screenshot,
|
||||
find_label_with_text,
|
||||
verify_text_present,
|
||||
print_screen_labels,
|
||||
simulate_click,
|
||||
get_widget_coords,
|
||||
find_button_with_text
|
||||
)
|
||||
|
||||
|
||||
class TestIMUCalibration(unittest.TestCase):
|
||||
"""Test suite for IMU calibration activities."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Get screenshot directory
|
||||
if sys.platform == "esp32":
|
||||
self.screenshot_dir = "tests/screenshots"
|
||||
else:
|
||||
self.screenshot_dir = "/home/user/MicroPythonOS/tests/screenshots"
|
||||
|
||||
# Ensure directory exists
|
||||
try:
|
||||
os.mkdir(self.screenshot_dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
# Navigate back to launcher
|
||||
try:
|
||||
for _ in range(3): # May need multiple backs
|
||||
mpos.ui.back_screen()
|
||||
wait_for_render(5)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_check_calibration_activity_loads(self):
|
||||
"""Test that CheckIMUCalibrationActivity loads and displays."""
|
||||
print("\n=== Testing CheckIMUCalibrationActivity ===")
|
||||
|
||||
# Navigate: Launcher -> Settings -> Check IMU Calibration
|
||||
result = mpos.apps.start_app("com.micropythonos.settings")
|
||||
self.assertTrue(result, "Failed to start Settings app")
|
||||
wait_for_render(15)
|
||||
|
||||
# Initialize touch device with dummy click
|
||||
simulate_click(10, 10)
|
||||
wait_for_render(10)
|
||||
|
||||
# Find and click "Check IMU Calibration" setting
|
||||
screen = lv.screen_active()
|
||||
check_cal_label = find_label_with_text(screen, "Check IMU Calibration")
|
||||
self.assertIsNotNone(check_cal_label, "Could not find 'Check IMU Calibration' setting")
|
||||
|
||||
# Click on the setting container
|
||||
coords = get_widget_coords(check_cal_label.get_parent())
|
||||
self.assertIsNotNone(coords, "Could not get coordinates of setting")
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(30)
|
||||
|
||||
# Verify CheckIMUCalibrationActivity loaded
|
||||
screen = lv.screen_active()
|
||||
self.assertTrue(verify_text_present(screen, "IMU Calibration Check"),
|
||||
"CheckIMUCalibrationActivity title not found")
|
||||
|
||||
# Wait for real-time updates to populate
|
||||
wait_for_render(20)
|
||||
|
||||
# Verify key elements are present
|
||||
print_screen_labels(screen)
|
||||
self.assertTrue(verify_text_present(screen, "Quality:"),
|
||||
"Quality label not found")
|
||||
self.assertTrue(verify_text_present(screen, "Accelerometer"),
|
||||
"Accelerometer label not found")
|
||||
self.assertTrue(verify_text_present(screen, "Gyroscope"),
|
||||
"Gyroscope label not found")
|
||||
|
||||
# Capture screenshot
|
||||
screenshot_path = f"{self.screenshot_dir}/check_imu_calibration.raw"
|
||||
print(f"Capturing screenshot: {screenshot_path}")
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
# Verify screenshot saved
|
||||
stat = os.stat(screenshot_path)
|
||||
self.assertTrue(stat[6] > 0, "Screenshot file is empty")
|
||||
|
||||
print("=== CheckIMUCalibrationActivity test complete ===")
|
||||
|
||||
def test_calibrate_activity_flow(self):
|
||||
"""Test CalibrateIMUActivity full calibration flow."""
|
||||
print("\n=== Testing CalibrateIMUActivity Flow ===")
|
||||
|
||||
# Navigate: Launcher -> Settings -> Calibrate IMU
|
||||
result = mpos.apps.start_app("com.micropythonos.settings")
|
||||
self.assertTrue(result, "Failed to start Settings app")
|
||||
wait_for_render(15)
|
||||
|
||||
# Initialize touch device with dummy click
|
||||
simulate_click(10, 10)
|
||||
wait_for_render(10)
|
||||
|
||||
# Find and click "Calibrate IMU" setting
|
||||
screen = lv.screen_active()
|
||||
calibrate_label = find_label_with_text(screen, "Calibrate IMU")
|
||||
self.assertIsNotNone(calibrate_label, "Could not find 'Calibrate IMU' setting")
|
||||
|
||||
coords = get_widget_coords(calibrate_label.get_parent())
|
||||
self.assertIsNotNone(coords)
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(30)
|
||||
|
||||
# Verify activity loaded
|
||||
screen = lv.screen_active()
|
||||
self.assertTrue(verify_text_present(screen, "IMU Calibration"),
|
||||
"CalibrateIMUActivity title not found")
|
||||
|
||||
# Capture initial state
|
||||
screenshot_path = f"{self.screenshot_dir}/calibrate_imu_01_initial.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
# Step 1: Click "Check Quality" button
|
||||
check_btn = find_button_with_text(screen, "Check Quality")
|
||||
self.assertIsNotNone(check_btn, "Could not find 'Check Quality' button")
|
||||
coords = get_widget_coords(check_btn)
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(10)
|
||||
|
||||
# Wait for quality check to complete (mock is fast)
|
||||
time.sleep(2.5) # Allow thread to complete
|
||||
wait_for_render(15)
|
||||
|
||||
# Verify quality check completed
|
||||
screen = lv.screen_active()
|
||||
print_screen_labels(screen)
|
||||
self.assertTrue(verify_text_present(screen, "Current calibration:"),
|
||||
"Quality check results not shown")
|
||||
|
||||
# Capture after quality check
|
||||
screenshot_path = f"{self.screenshot_dir}/calibrate_imu_02_quality.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
# Step 2: Click "Calibrate Now" button
|
||||
calibrate_btn = find_button_with_text(screen, "Calibrate Now")
|
||||
self.assertIsNotNone(calibrate_btn, "Could not find 'Calibrate Now' button")
|
||||
coords = get_widget_coords(calibrate_btn)
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(10)
|
||||
|
||||
# Wait for calibration to complete (mock takes ~3 seconds)
|
||||
time.sleep(4.0)
|
||||
wait_for_render(15)
|
||||
|
||||
# Verify calibration completed
|
||||
screen = lv.screen_active()
|
||||
print_screen_labels(screen)
|
||||
self.assertTrue(verify_text_present(screen, "Calibration successful!") or
|
||||
verify_text_present(screen, "Calibration complete!"),
|
||||
"Calibration completion message not found")
|
||||
|
||||
# Capture completion state
|
||||
screenshot_path = f"{self.screenshot_dir}/calibrate_imu_03_complete.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
print("=== CalibrateIMUActivity flow test complete ===")
|
||||
|
||||
def test_navigation_from_check_to_calibrate(self):
|
||||
"""Test navigation from Check to Calibrate activity via button."""
|
||||
print("\n=== Testing Check -> Calibrate Navigation ===")
|
||||
|
||||
# Navigate to Check activity
|
||||
result = mpos.apps.start_app("com.micropythonos.settings")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
# Initialize touch device with dummy click
|
||||
simulate_click(10, 10)
|
||||
wait_for_render(10)
|
||||
|
||||
screen = lv.screen_active()
|
||||
check_cal_label = find_label_with_text(screen, "Check IMU Calibration")
|
||||
coords = get_widget_coords(check_cal_label.get_parent())
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(30) # Wait for real-time updates
|
||||
|
||||
# Click "Calibrate" button
|
||||
screen = lv.screen_active()
|
||||
calibrate_btn = find_button_with_text(screen, "Calibrate")
|
||||
self.assertIsNotNone(calibrate_btn, "Could not find 'Calibrate' button")
|
||||
|
||||
coords = get_widget_coords(calibrate_btn)
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(15)
|
||||
|
||||
# Verify CalibrateIMUActivity loaded
|
||||
screen = lv.screen_active()
|
||||
self.assertTrue(verify_text_present(screen, "Check Quality"),
|
||||
"Did not navigate to CalibrateIMUActivity")
|
||||
|
||||
print("=== Navigation test complete ===")
|
||||
Reference in New Issue
Block a user