mirror of
https://github.com/markqvist/RNode_Firmware.git
synced 2026-04-27 14:30:33 +00:00
Add bubble level complication with EMA-filtered accelerometer
Always-on ACCEL_PASSTHROUGH at 10Hz feeds an exponential moving average filter (α=0.15) for smooth, noise-resistant tilt sensing. Watch face shows a circular bubble level below the step counter: - Ring with crosshairs, moving dot shows tilt direction - Dot colour: green (<3°), amber (<15°), red (>15°) - Angle readout in degrees below the ring - Bubble position clamped to ring boundary Accel mapping: ax/ay normalized to 1g (4096 LSB), inverted for natural bubble behaviour (tilt left → bubble moves left).
This commit is contained in:
parent
7208ce348c
commit
3d328d28f4
2 changed files with 106 additions and 0 deletions
88
Gui.h
88
Gui.h
|
|
@ -107,6 +107,16 @@ static lv_obj_t *gui_gps_fix = NULL;
|
|||
static lv_obj_t *gui_gps_alt = NULL;
|
||||
static lv_obj_t *gui_gps_beacon = NULL;
|
||||
|
||||
// Bubble level widgets
|
||||
static lv_obj_t *gui_level_ring = NULL; // outer circle
|
||||
static lv_obj_t *gui_level_dot = NULL; // moving bubble
|
||||
static lv_obj_t *gui_level_cross_h = NULL; // crosshair horizontal
|
||||
static lv_obj_t *gui_level_cross_v = NULL; // crosshair vertical
|
||||
static lv_obj_t *gui_level_angle = NULL; // tilt angle text
|
||||
#define GUI_LEVEL_SIZE 140 // ring diameter
|
||||
#define GUI_LEVEL_DOT 16 // bubble diameter
|
||||
#define GUI_LEVEL_Y 340 // vertical position on watch face
|
||||
|
||||
// Touch input via function pointer (set by .ino after touch init)
|
||||
typedef bool (*gui_touch_fn_t)(int16_t *x, int16_t *y);
|
||||
static gui_touch_fn_t gui_touch_fn = NULL;
|
||||
|
|
@ -171,6 +181,8 @@ extern uint32_t imu_log_samples;
|
|||
extern uint32_t imu_log_start_ms;
|
||||
// Forward declaration for touch logging (defined in IMULogger.h)
|
||||
void sensor_log_touch(int16_t x, int16_t y, bool pressed);
|
||||
// Forward declarations for filtered accel (defined in .ino)
|
||||
extern volatile float imu_ax_f, imu_ay_f, imu_az_f;
|
||||
#ifndef PMU_TEMP_MIN
|
||||
#define PMU_TEMP_MIN -30
|
||||
#endif
|
||||
|
|
@ -341,6 +353,50 @@ static void gui_create_watchface(lv_obj_t *parent) {
|
|||
lv_obj_set_width(gui_step_label, GUI_W);
|
||||
lv_obj_set_style_text_align(gui_step_label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_pos(gui_step_label, 0, GUI_RULE2_Y + 15);
|
||||
|
||||
// Bubble level
|
||||
int lx = (GUI_W - GUI_LEVEL_SIZE) / 2;
|
||||
|
||||
// Outer ring
|
||||
gui_level_ring = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(gui_level_ring);
|
||||
lv_obj_set_size(gui_level_ring, GUI_LEVEL_SIZE, GUI_LEVEL_SIZE);
|
||||
lv_obj_set_pos(gui_level_ring, lx, GUI_LEVEL_Y);
|
||||
lv_obj_set_style_radius(gui_level_ring, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_border_color(gui_level_ring, lv_color_hex(GUI_COL_DIM), 0);
|
||||
lv_obj_set_style_border_width(gui_level_ring, 1, 0);
|
||||
lv_obj_set_style_bg_opa(gui_level_ring, LV_OPA_TRANSP, 0);
|
||||
lv_obj_clear_flag(gui_level_ring, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// Crosshairs
|
||||
static lv_point_precise_t ch_pts[] = {{0,0},{GUI_LEVEL_SIZE,0}};
|
||||
gui_level_cross_h = lv_line_create(gui_level_ring);
|
||||
lv_line_set_points(gui_level_cross_h, ch_pts, 2);
|
||||
lv_obj_set_style_line_color(gui_level_cross_h, lv_color_hex(0x202020), 0);
|
||||
lv_obj_set_style_line_width(gui_level_cross_h, 1, 0);
|
||||
lv_obj_center(gui_level_cross_h);
|
||||
|
||||
static lv_point_precise_t cv_pts[] = {{0,0},{0,GUI_LEVEL_SIZE}};
|
||||
gui_level_cross_v = lv_line_create(gui_level_ring);
|
||||
lv_line_set_points(gui_level_cross_v, cv_pts, 2);
|
||||
lv_obj_set_style_line_color(gui_level_cross_v, lv_color_hex(0x202020), 0);
|
||||
lv_obj_set_style_line_width(gui_level_cross_v, 1, 0);
|
||||
lv_obj_center(gui_level_cross_v);
|
||||
|
||||
// Bubble dot
|
||||
gui_level_dot = lv_obj_create(gui_level_ring);
|
||||
lv_obj_remove_style_all(gui_level_dot);
|
||||
lv_obj_set_size(gui_level_dot, GUI_LEVEL_DOT, GUI_LEVEL_DOT);
|
||||
lv_obj_set_style_radius(gui_level_dot, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(gui_level_dot, lv_color_hex(GUI_COL_GREEN), 0);
|
||||
lv_obj_set_style_bg_opa(gui_level_dot, LV_OPA_COVER, 0);
|
||||
lv_obj_center(gui_level_dot);
|
||||
|
||||
// Angle text below ring
|
||||
gui_level_angle = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "");
|
||||
lv_obj_set_width(gui_level_angle, GUI_W);
|
||||
lv_obj_set_style_text_align(gui_level_angle, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_pos(gui_level_angle, 0, GUI_LEVEL_Y + GUI_LEVEL_SIZE + 5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -729,6 +785,38 @@ static void gui_update_data() {
|
|||
}
|
||||
}
|
||||
|
||||
// Bubble level — map filtered accel to dot position
|
||||
if (gui_level_dot && imu_az_f != 0) {
|
||||
// Tilt as fraction of 1g (4096 LSB = 1g)
|
||||
float tx = imu_ax_f / 4096.0f; // roll
|
||||
float ty = imu_ay_f / 4096.0f; // pitch
|
||||
|
||||
// Map to pixel offset (half ring size = full tilt)
|
||||
float max_r = (GUI_LEVEL_SIZE - GUI_LEVEL_DOT) / 2.0f;
|
||||
float px = -tx * max_r; // invert for natural bubble feel
|
||||
float py = -ty * max_r;
|
||||
|
||||
// Clamp to ring boundary
|
||||
float dist = sqrtf(px * px + py * py);
|
||||
if (dist > max_r) {
|
||||
px = px * max_r / dist;
|
||||
py = py * max_r / dist;
|
||||
}
|
||||
|
||||
lv_obj_set_pos(gui_level_dot,
|
||||
(int)(GUI_LEVEL_SIZE / 2 - GUI_LEVEL_DOT / 2 + px),
|
||||
(int)(GUI_LEVEL_SIZE / 2 - GUI_LEVEL_DOT / 2 + py));
|
||||
|
||||
// Color: green when near level, amber when tilted, red when extreme
|
||||
float tilt_deg = atan2f(sqrtf(tx*tx + ty*ty), fabsf(imu_az_f / 4096.0f)) * 57.2958f;
|
||||
uint32_t dot_col = (tilt_deg < 3.0f) ? GUI_COL_GREEN :
|
||||
(tilt_deg < 15.0f) ? GUI_COL_AMBER : GUI_COL_RED;
|
||||
lv_obj_set_style_bg_color(gui_level_dot, lv_color_hex(dot_col), 0);
|
||||
|
||||
lv_label_set_text_fmt(gui_level_angle, "%.1f\xC2\xB0", tilt_deg);
|
||||
lv_obj_set_style_text_color(gui_level_angle, lv_color_hex(dot_col), 0);
|
||||
}
|
||||
|
||||
// Battery complication — voltage and state
|
||||
if (battery_state == BATTERY_STATE_CHARGING) {
|
||||
lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,20 @@
|
|||
volatile uint32_t imu_step_count = 0;
|
||||
volatile bool imu_wrist_tilt = false;
|
||||
|
||||
// Filtered accelerometer for bubble level (EMA, α=0.15)
|
||||
volatile float imu_ax_f = 0, imu_ay_f = 0, imu_az_f = 4096;
|
||||
#define IMU_EMA_ALPHA 0.15f
|
||||
void imu_accel_live_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
|
||||
if (size >= 6) {
|
||||
float ax = (int16_t)(data[0] | (data[1] << 8));
|
||||
float ay = (int16_t)(data[2] | (data[3] << 8));
|
||||
float az = (int16_t)(data[4] | (data[5] << 8));
|
||||
imu_ax_f += IMU_EMA_ALPHA * (ax - imu_ax_f);
|
||||
imu_ay_f += IMU_EMA_ALPHA * (ay - imu_ay_f);
|
||||
imu_az_f += IMU_EMA_ALPHA * (az - imu_az_f);
|
||||
}
|
||||
}
|
||||
|
||||
// MAX98357A I2S speaker + SPM1423 PDM microphone
|
||||
#include "Speaker.h"
|
||||
#include "Microphone.h"
|
||||
|
|
@ -2239,6 +2253,10 @@ void loop() {
|
|||
bhi260->configure(SensorBHI260AP::WRIST_TILT_GESTURE, 1.0, 0);
|
||||
bhi260->onResultEvent(SensorBHI260AP::WRIST_TILT_GESTURE, imu_wrist_tilt_cb);
|
||||
|
||||
// Always-on accelerometer at 10Hz for bubble level
|
||||
bhi260->configure(SensorBHI260AP::ACCEL_PASSTHROUGH, 10.0, 0);
|
||||
bhi260->onResultEvent(SensorBHI260AP::ACCEL_PASSTHROUGH, imu_accel_live_cb);
|
||||
|
||||
// Register IMU log toggle for remote debug
|
||||
#if HAS_SD && HAS_DISPLAY
|
||||
gui_log_toggle_fn = []() -> bool {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue