diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 287cdf30e..000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,49 +0,0 @@ -# PebbleOS - -PebbleOS is the operating system running on Pebble smartwatches. - -## Organization - -- `docs`: project documentation -- `python_libs`: tools used in multiple areas, e.g. log dehashing, console, etc. -- `resources`: firmware resources (icons, fonts, etc.) -- `sdk`: application SDK generation files -- `src`: firmware source -- `tests`: tests -- `third_party`: third-party code in git submodules, also includes glue code -- `tools`: a variety of tools or scripts used in multiple areas, from build - system, tests, etc. -- `waftools`: scripts used by the build system - -## Code style - -- clang-format for C code - -## Firmware development - -- Configure: `./waf configure --board BOARD_NAME` - - - Board names can be obtained from `./waf --help` - - `--release` enables release mode - - `--mfg` enables manufacturing mode - - `--qemu` enables QEMU mode - -- Build main firmware: `./waf build` -- Build recovery firmware (PRF): `./waf build_prf` -- Run tests: `./waf test` - -## Git rules - -Main rules: - -- Commit using `-s` git option, so commits have `Signed-Off-By` -- Always indicate commit is co-authored by Claude -- Commit in small chunks, trying to preserve bisectability -- Commit format is `area: short description`, with longer description in the - body if necessary -- Run `gitlint` on every commit to verify rules are followed - -Others: - -- If fixing Linear or GitHub issues, include in the commit body a line with - `Fixes XXX`, where XXX is the issue number. diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 120000 index 000000000..be77ac83a --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index b5c3377ee..d34a3ce85 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -102,7 +102,7 @@ jobs: - name: Upload log hash dictionary uses: Noelware/s3-action@2.3.1 - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.repository == 'coredevices/PebbleOS' }} with: access-key-id: ${{ secrets.LOG_HASH_BUCKET_KEY_ID }} secret-key: ${{ secrets.LOG_HASH_BUCKET_SECRET }} diff --git a/.github/workflows/build-prf.yml b/.github/workflows/build-prf.yml index 2b77c182d..30e2cea3f 100644 --- a/.github/workflows/build-prf.yml +++ b/.github/workflows/build-prf.yml @@ -108,7 +108,7 @@ jobs: - name: Upload log hash dictionary uses: Noelware/s3-action@2.3.1 - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.repository == 'coredevices/PebbleOS' }} with: access-key-id: ${{ secrets.LOG_HASH_BUCKET_KEY_ID }} secret-key: ${{ secrets.LOG_HASH_BUCKET_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52ba6327d..35f6431c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,6 +186,7 @@ jobs: - name: Upload log hash dictionary uses: Noelware/s3-action@2.3.1 + if: ${{ github.repository == 'coredevices/PebbleOS' }} with: access-key-id: ${{ secrets.LOG_HASH_BUCKET_KEY_ID }} secret-key: ${{ secrets.LOG_HASH_BUCKET_SECRET }} diff --git a/.gitlint b/.gitlint index bf75b765a..fcae58189 100644 --- a/.gitlint +++ b/.gitlint @@ -4,9 +4,16 @@ ignore-merge-commits=false ignore-fixup-commits=false ignore-fixup-amend-commits=false ignore-squash-commits=false +regex-style-search=true [title-match-regex] -regex=[a-z0-9/]+: .* +# Format: "area: description" where area is either: +# - A path with at least one / (e.g., fw/drivers/hrm, third_party/nonfree) +# - One of the known short areas: ci, treewide, platform, sdk, tools, resources, +# docs, waftools, settings, libc, tests, notifications, build, wscript, fw, +# third_party, ancs, compositor, console, kernel, health +# Conventional commit types like feat:, fix:, chore: are NOT allowed. +regex=^([a-z0-9_]+/[a-z0-9_/]*|ci|treewide|platform|sdk|tools|resources|docs|waftools|settings|libc|tests|notifications|build|wscript|fw|third_party|ancs|compositor|console|kernel|health|gitlint|readme|requirements|python_libs|pbl-tool|pbl|moddable|libutil|iconography|gitignore|capabilities|asterix|activity|accel): .* [title-max-length] line-length=100 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..287cdf30e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# PebbleOS + +PebbleOS is the operating system running on Pebble smartwatches. + +## Organization + +- `docs`: project documentation +- `python_libs`: tools used in multiple areas, e.g. log dehashing, console, etc. +- `resources`: firmware resources (icons, fonts, etc.) +- `sdk`: application SDK generation files +- `src`: firmware source +- `tests`: tests +- `third_party`: third-party code in git submodules, also includes glue code +- `tools`: a variety of tools or scripts used in multiple areas, from build + system, tests, etc. +- `waftools`: scripts used by the build system + +## Code style + +- clang-format for C code + +## Firmware development + +- Configure: `./waf configure --board BOARD_NAME` + + - Board names can be obtained from `./waf --help` + - `--release` enables release mode + - `--mfg` enables manufacturing mode + - `--qemu` enables QEMU mode + +- Build main firmware: `./waf build` +- Build recovery firmware (PRF): `./waf build_prf` +- Run tests: `./waf test` + +## Git rules + +Main rules: + +- Commit using `-s` git option, so commits have `Signed-Off-By` +- Always indicate commit is co-authored by Claude +- Commit in small chunks, trying to preserve bisectability +- Commit format is `area: short description`, with longer description in the + body if necessary +- Run `gitlint` on every commit to verify rules are followed + +Others: + +- If fixing Linear or GitHub issues, include in the commit body a line with + `Fixes XXX`, where XXX is the issue number. diff --git a/src/fw/drivers/hrm/as7000/as7000.c b/src/fw/drivers/hrm/as7000/as7000.c index 3e796c65a..fddfe0f5b 100644 --- a/src/fw/drivers/hrm/as7000/as7000.c +++ b/src/fw/drivers/hrm/as7000/as7000.c @@ -514,7 +514,6 @@ static void prv_disable(HRMDevice *dev) { WTF; } led_disable(LEDEnablerHRM); - analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME); } // NOTE: the caller must hold the device's state lock @@ -525,7 +524,6 @@ static void prv_enable(HRMDevice *dev) { } else if (dev->state->enabled_state == HRMEnabledState_Disabled) { led_enable(LEDEnablerHRM); - analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME, AnalyticsClient_System); // Enable the device and schedule a timer callback for when we can start communicating with it. gpio_output_set(&dev->en_gpio, true); diff --git a/src/fw/drivers/hrm/gh3x2x/gh3x2x.c b/src/fw/drivers/hrm/gh3x2x/gh3x2x.c index 6d9ffb830..2c81fd394 100644 --- a/src/fw/drivers/hrm/gh3x2x/gh3x2x.c +++ b/src/fw/drivers/hrm/gh3x2x/gh3x2x.c @@ -107,123 +107,66 @@ void gh3x2x_print_fmt(const char *fmt, ...) { #endif } -void gh3x2x_result_report(uint8_t type, uint32_t val, uint8_t quality) { - if (type == 1) { - HRMData hrm_data = {0}; +void gh3x2x_hr_result_report(uint8_t bpm, uint8_t quality) { + HRMData hrm_data = {0}; - PBL_LOG_DBG("GH3X2X BPM %" PRIu32 " (quality=%" PRIu8 ")", val, quality); + PBL_LOG_DBG("GH3X2X BPM %" PRIu8 " (quality=%" PRIu8 ", wear=%u)", bpm, quality, HRM->state->is_wear); - hrm_data.features = HRMFeature_BPM; - hrm_data.hrm_bpm = val & 0xff; + hrm_data.features = HRMFeature_BPM; - if (quality == 254U) { - hrm_data.hrm_quality = HRMQuality_OffWrist; - } else if (quality >= 80U) { + if (!HRM->state->is_wear) { + hrm_data.hrm_quality = HRMQuality_OffWrist; + } else { + hrm_data.hrm_bpm = bpm; + + if (quality >= 98U) { hrm_data.hrm_quality = HRMQuality_Excellent; - } else if (quality >= 70U) { + } else if (quality >= 90U) { hrm_data.hrm_quality = HRMQuality_Good; - } else if (quality >= 60U) { + } else if (quality >= 80U) { hrm_data.hrm_quality = HRMQuality_Acceptable; - } else if (quality >= 50U) { + } else if (quality >= 70U) { hrm_data.hrm_quality = HRMQuality_Poor; - } else if (quality >= 30U) { - hrm_data.hrm_quality = HRMQuality_Worst; } else { - hrm_data.hrm_quality = HRMQuality_NoSignal; + hrm_data.hrm_quality = HRMQuality_Worst; } + } - hrm_manager_new_data_cb(&hrm_data); - } else if (type == 2) { - HRMData hrm_data = {0}; + hrm_manager_new_data_cb(&hrm_data); +} + +void gh3x2x_spo2_result_report(uint8_t pct, uint8_t quality) { + HRMData hrm_data = {0}; - PBL_LOG_DBG("GH3X2X SpO2 %" PRIu32 " (quality=%" PRIu8 ")", val, quality); + PBL_LOG_DBG("GH3X2X SpO2 %" PRIu8 " (quality=%" PRIu8 ", wear=%u)", pct, quality, HRM->state->is_wear); - hrm_data.features = HRMFeature_SpO2; - hrm_data.spo2_percent = val & 0xff; + hrm_data.features = HRMFeature_SpO2; - // FIXME(GH3X2X): This mapping is wrong, we need to understand the actual quality values - if (quality == 254U) { - hrm_data.spo2_quality = HRMQuality_OffWrist; - } else if (quality >= 80U) { + if (!HRM->state->is_wear) { + hrm_data.spo2_quality = HRMQuality_OffWrist; + } else { + hrm_data.spo2_percent = pct; + + if (quality >= 98U) { hrm_data.spo2_quality = HRMQuality_Excellent; - } else if (quality >= 70U) { + } else if (quality >= 90U) { hrm_data.spo2_quality = HRMQuality_Good; - } else if (quality >= 60U) { + } else if (quality >= 80U) { hrm_data.spo2_quality = HRMQuality_Acceptable; - } else if (quality >= 50U) { + } else if (quality >= 70U) { hrm_data.spo2_quality = HRMQuality_Poor; - } else if (quality >= 30U) { - hrm_data.spo2_quality = HRMQuality_Worst; } else { - hrm_data.spo2_quality = HRMQuality_NoSignal; - } - - hrm_manager_new_data_cb(&hrm_data); - } else { - PBL_LOG_WRN("GH3X2X unexpected report type (%" PRIu8 ")", type); - } -} - -void gh3x2x_timer_init(uint32_t period_ms) { - if (HRM) { - HRM->state->timer_period_ms = period_ms; - } -} - -static void gh3x2x_timer_callback(void* data) { - uint32_t param = (uint32_t)data; - if (param != 0x87965421) { - // Coalesce repeated timer firings - only queue one callback at a time - if (s_hrm_timer_flag == false) { - if (system_task_add_callback(gh3x2x_timer_callback, (void*)0x87965421)) { - s_hrm_timer_flag = true; - } + hrm_data.spo2_quality = HRMQuality_Worst; } - return; } - s_hrm_timer_flag = false; - Gh3x2xSerialSendTimerHandle(); -} - -static void gh3x2x_timer_start_handle(void* arg) { - if (HRM == NULL || HRM->state->timer != NULL) { - return; - } - if (HRM->state->timer_period_ms == 0) { - return; - } - HRM->state->timer = app_timer_register_repeatable(HRM->state->timer_period_ms, gh3x2x_timer_callback, NULL, true); -} - -static void gh3x2x_timer_stop_handle(void* arg) { - if (HRM && HRM->state->timer) { - app_timer_cancel(HRM->state->timer); - HRM->state->timer = NULL; - } -} - -void gh3x2x_timer_start(void) { - PebbleEvent e = { - .type = PEBBLE_CALLBACK_EVENT, - .callback.callback = gh3x2x_timer_start_handle, - }; - event_put(&e); -} -void gh3x2x_timer_stop(void) { - PebbleEvent e = { - .type = PEBBLE_CALLBACK_EVENT, - .callback.callback = gh3x2x_timer_stop_handle, - }; - event_put(&e); + hrm_manager_new_data_cb(&hrm_data); } void gh3x2x_wear_evt_notify(bool is_wear) { - HRMDevice* p_dev = HRM; - if (p_dev) { - p_dev->state->is_wear = is_wear; - } - PBL_LOG_DBG("wear notify: %d", is_wear); + PBL_LOG_DBG("GH3X2X wear state: %d", is_wear); + + HRM->state->is_wear = is_wear; } // GH3X2X calibration/factory testing @@ -311,6 +254,60 @@ void gh3x2x_rawdata_notify(uint32_t *p_rawdata, uint32_t data_count) { } #ifdef MANUFACTURING_FW +void gh3x2x_timer_init(uint32_t period_ms) { + if (HRM) { + HRM->state->timer_period_ms = period_ms; + } +} + +static void gh3x2x_timer_callback(void* data) { + uint32_t param = (uint32_t)data; + if (param != 0x87965421) { + // Coalesce repeated timer firings - only queue one callback at a time + if (s_hrm_timer_flag == false) { + if (system_task_add_callback(gh3x2x_timer_callback, (void*)0x87965421)) { + s_hrm_timer_flag = true; + } + } + return; + } + s_hrm_timer_flag = false; + Gh3x2xSerialSendTimerHandle(); +} + +static void gh3x2x_timer_start_handle(void* arg) { + if (HRM == NULL || HRM->state->timer != NULL) { + return; + } + if (HRM->state->timer_period_ms == 0) { + return; + } + HRM->state->timer = app_timer_register_repeatable(HRM->state->timer_period_ms, gh3x2x_timer_callback, NULL, true); +} + +static void gh3x2x_timer_stop_handle(void* arg) { + if (HRM && HRM->state->timer) { + app_timer_cancel(HRM->state->timer); + HRM->state->timer = NULL; + } +} + +void gh3x2x_timer_start(void) { + PebbleEvent e = { + .type = PEBBLE_CALLBACK_EVENT, + .callback.callback = gh3x2x_timer_start_handle, + }; + event_put(&e); +} + +void gh3x2x_timer_stop(void) { + PebbleEvent e = { + .type = PEBBLE_CALLBACK_EVENT, + .callback.callback = gh3x2x_timer_stop_handle, + }; + event_put(&e); +} + void gh3x2x_factory_test_enable(HRMDevice *dev, GH3x2xFTType test_type) { uint32_t mode = 0; if (test_type == HRM_FACTORY_TEST_CTR) { // CTR @@ -423,6 +420,9 @@ void gh3x2x_set_work_mode(int32_t mode) { //always enable soft adt state->work_mode = mode | GH3X2X_FUNCTION_SOFT_ADT_IR; } +#else +void gh3x2x_timer_init(uint32_t period_ms) {} +void gh3x2x_timer_start(void) {} #endif // MANUFACTURING_FW #else @@ -447,6 +447,7 @@ void hrm_init(HRMDevice *dev) { gpio_input_init_pull_up_down(&dev->int_input, GPIO_PuPd_DOWN); #endif + dev->state->is_wear = false; dev->state->initialized = true; } @@ -458,9 +459,9 @@ bool hrm_enable(HRMDevice *dev) { s_hrm_int_flag = false; - dev->state->work_mode = GH3X2X_FUNCTION_HR | GH3X2X_FUNCTION_SPO2; + dev->state->work_mode = GH3X2X_FUNCTION_HR | GH3X2X_FUNCTION_SOFT_ADT_GREEN; #ifdef MANUFACTURING_FW - dev->state->work_mode |= GH3X2X_FUNCTION_SOFT_ADT_IR; + dev->state->work_mode = GH3X2X_FUNCTION_HR | GH3X2X_FUNCTION_SPO2 | GH3X2X_FUNCTION_SOFT_ADT_IR; #endif GH3X2X_FifoWatermarkThrConfig(GH3X2X_FIFO_WATERMARK_CONFIG); diff --git a/src/fw/services/common/bluetooth/pairability.c b/src/fw/services/common/bluetooth/pairability.c index 9f63f621b..97d5721bc 100644 --- a/src/fw/services/common/bluetooth/pairability.c +++ b/src/fw/services/common/bluetooth/pairability.c @@ -138,6 +138,9 @@ void bt_pairability_update_due_to_bonding_change(void) { } void bt_pairability_init(void) { + // Reset cached discoverable state: the advertising infrastructure was torn down + // before this init, so we must re-drive gap_le_slave_set_discoverable() if needed. + s_last_ble_discoverable_state = false; bt_pairability_update_due_to_bonding_change(); prv_schedule_evaluation(); } diff --git a/src/fw/services/common/compositor/compositor_display.c b/src/fw/services/common/compositor/compositor_display.c index 73a23393a..0a922d835 100644 --- a/src/fw/services/common/compositor/compositor_display.c +++ b/src/fw/services/common/compositor/compositor_display.c @@ -23,19 +23,6 @@ static const uint8_t s_corner_shape[] = { 3, 1, 1 }; static uint8_t s_line_buffer[FRAMEBUFFER_BYTES_PER_ROW]; #endif -#if PLATFORM_OBELIX -static const uint8_t s_corner_shape[] = { 12, 9, 7, 6, 5, 4, 3, 2, 2, 1, 1, 1 }; -// For Obelix, we modify the framebuffer directly because the display driver -// does in-place pixel format conversion and expects row.data to point into -// the compositor's framebuffer. We save original corner pixels here to restore later. -// Max corner pixels per row = 12, rows with corners = 12 top + 12 bottom = 24, -// 2 corners per row (left+right), so 12 * 24 * 2 = 576 bytes -#define CORNER_SAVE_ROWS ARRAY_LENGTH(s_corner_shape) -static uint8_t s_saved_corners[CORNER_SAVE_ROWS * 2][12 * 2]; // [row][left+right pixels] -static uint8_t s_dirty_y0; -static uint8_t s_dirty_y1; -#endif - //! display_update get next line callback static bool prv_flush_get_next_line_cb(DisplayRow* row) { FrameBuffer *fb = compositor_get_framebuffer(); @@ -63,32 +50,6 @@ static bool prv_flush_get_next_line_cb(DisplayRow* row) { } else { row->data = fb_line; } -#elif PLATFORM_OBELIX - // Draw rounded corners by modifying the framebuffer directly. - // The display driver does in-place format conversion and expects row.data - // to point into the compositor's framebuffer. We save and restore corners. - if (s_current_flush_line < ARRAY_LENGTH(s_corner_shape) || - s_current_flush_line >= DISP_ROWS - ARRAY_LENGTH(s_corner_shape)) { - uint8_t corner_idx = - (s_current_flush_line < ARRAY_LENGTH(s_corner_shape))? - s_current_flush_line : DISP_ROWS - s_current_flush_line - 1; - uint8_t save_idx = - (s_current_flush_line < ARRAY_LENGTH(s_corner_shape))? - s_current_flush_line : CORNER_SAVE_ROWS + corner_idx; - uint8_t corner_width = s_corner_shape[corner_idx]; - uint8_t *line = fb_line; - // Save original corner pixels - for (uint8_t pixel = 0; pixel < corner_width; ++pixel) { - s_saved_corners[save_idx][pixel] = line[pixel]; - s_saved_corners[save_idx][12 + pixel] = line[DISP_COLS - pixel - 1]; - } - // Draw black corners - for (uint8_t pixel = 0; pixel < corner_width; ++pixel) { - line[pixel] = GColorBlackARGB8; - line[DISP_COLS - pixel - 1] = GColorBlackARGB8; - } - } - row->data = fb_line; #else row->data = fb_line; #endif @@ -101,31 +62,6 @@ static bool prv_flush_get_next_line_cb(DisplayRow* row) { //! display_update complete callback static void prv_flush_complete_cb(void) { -#if PLATFORM_OBELIX - // Restore original corner pixels that we modified before the display update - FrameBuffer *fb = compositor_get_framebuffer(); - for (uint8_t i = 0; i < CORNER_SAVE_ROWS; ++i) { - uint8_t corner_width = s_corner_shape[i]; - // Top corners (only if row was in dirty region) - if (i >= s_dirty_y0 && i <= s_dirty_y1) { - uint8_t *top_line = framebuffer_get_line(fb, i); - for (uint8_t pixel = 0; pixel < corner_width; ++pixel) { - top_line[pixel] = s_saved_corners[i][pixel]; - top_line[DISP_COLS - pixel - 1] = s_saved_corners[i][12 + pixel]; - } - } - // Bottom corners (only if row was in dirty region) - uint8_t bottom_row = DISP_ROWS - i - 1; - if (bottom_row >= s_dirty_y0 && bottom_row <= s_dirty_y1) { - uint8_t *bottom_line = framebuffer_get_line(fb, bottom_row); - for (uint8_t pixel = 0; pixel < corner_width; ++pixel) { - bottom_line[pixel] = s_saved_corners[CORNER_SAVE_ROWS + i][pixel]; - bottom_line[DISP_COLS - pixel - 1] = s_saved_corners[CORNER_SAVE_ROWS + i][12 + pixel]; - } - } - } -#endif - s_current_flush_line = 0; framebuffer_reset_dirty(compositor_get_framebuffer()); @@ -142,11 +78,6 @@ void compositor_display_update(void (*handle_update_complete_cb)(void)) { #if PLATFORM_GETAFIX // Force full screen updates - partial ROI causes animation issues on getafix display fb->dirty_rect = (GRect){ GPointZero, fb->size }; -#endif -#if PLATFORM_OBELIX - // Capture dirty region bounds for corner restoration later - s_dirty_y0 = fb->dirty_rect.origin.y; - s_dirty_y1 = fb->dirty_rect.origin.y + fb->dirty_rect.size.h - 1; #endif s_update_complete_handler = handle_update_complete_cb; s_current_flush_line = 0; diff --git a/src/fw/services/common/hrm/hrm_manager.c b/src/fw/services/common/hrm/hrm_manager.c index 19e9c9695..eed2beadd 100644 --- a/src/fw/services/common/hrm/hrm_manager.c +++ b/src/fw/services/common/hrm/hrm_manager.c @@ -333,12 +333,16 @@ static void prv_update_hrm_enable_system_cb(void *unused) { s_manager_state.enable_failure_count = 0; // Don't need the re-enable timer to fire new_timer_stop(s_manager_state.update_enable_timer_id); + // Track HRM on-time + analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME, AnalyticsClient_System); } } else if (!turn_sensor_on && hrm_is_enabled(HRM)) { // Turn off the sensor now HRM_LOG("Turning off HR sensor"); hrm_disable(HRM); + // Stop tracking HRM on-time + analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME); sys_accel_manager_data_unsubscribe(s_manager_state.accel_state); s_manager_state.accel_state = NULL; diff --git a/src/fw/services/normal/activity/activity.c b/src/fw/services/normal/activity/activity.c index 971907de9..ffa62634e 100644 --- a/src/fw/services/normal/activity/activity.c +++ b/src/fw/services/normal/activity/activity.c @@ -87,13 +87,14 @@ static void prv_heart_rate_subscription_update(uint32_t now_ts) { if (s_activity_state.hr.currently_sampling) { // If we are currently sampling, turn off when: // - We reach the end of our maximum time on, ACTIVITY_DEFAULT_HR_ON_TIME_SEC - // - We get ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT samples before the time runs out - // - e.g. We get X samples >= ACTIVITY_MIN_HR_QUALITY_THRESH in our current minute, - // go ahead and turn off the sensor + // - We get ACTIVITY_MIN_NUM_GOOD_SAMPLES_SHORT_CIRCUIT good quality samples + // - We get ACTIVITY_MIN_NUM_EXCELLENT_SAMPLES_SHORT_CIRCUIT excellent quality samples const uint32_t turn_off_at = last_toggled_ts + ACTIVITY_DEFAULT_HR_ON_TIME_SEC; - const bool samples_req_met = - (s_activity_state.hr.num_quality_samples >= ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT); - if ((turn_off_at <= now_ts) || samples_req_met) { + const bool good_samples_req_met = + (s_activity_state.hr.num_good_quality_samples >= ACTIVITY_MIN_NUM_GOOD_SAMPLES_SHORT_CIRCUIT); + const bool excellent_samples_req_met = + (s_activity_state.hr.num_excellent_samples >= ACTIVITY_MIN_NUM_EXCELLENT_SAMPLES_SHORT_CIRCUIT); + if ((turn_off_at <= now_ts) || good_samples_req_met || excellent_samples_req_met) { should_toggle = true; } } else { diff --git a/src/fw/services/normal/activity/activity_metrics.c b/src/fw/services/normal/activity/activity_metrics.c index 47b48efc9..43fa5c94e 100644 --- a/src/fw/services/normal/activity/activity_metrics.c +++ b/src/fw/services/normal/activity/activity_metrics.c @@ -575,7 +575,8 @@ void activity_metrics_prv_reset_hr_stats(void) { mutex_lock_recursive(state->mutex); { state->hr.num_samples = 0; - state->hr.num_quality_samples = 0; + state->hr.num_good_quality_samples = 0; + state->hr.num_excellent_samples = 0; memset(state->hr.samples, 0, sizeof(state->hr.samples)); memset(state->hr.weights, 0, sizeof(state->hr.weights)); @@ -599,8 +600,11 @@ void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t state->hr.samples[state->hr.num_samples] = hrm_event->bpm.bpm; state->hr.weights[state->hr.num_samples] = prv_get_hr_quality_weight(hrm_event->bpm.quality); - if (hrm_event->bpm.quality >= ACTIVITY_MIN_HR_QUALITY_THRESH) { - state->hr.num_quality_samples++; + if (hrm_event->bpm.quality >= HRMQuality_Good) { + state->hr.num_good_quality_samples++; + } + if (hrm_event->bpm.quality >= HRMQuality_Excellent) { + state->hr.num_excellent_samples++; } state->hr.num_samples++; diff --git a/src/fw/services/normal/activity/activity_private.h b/src/fw/services/normal/activity/activity_private.h index 6084a6088..15a16b012 100644 --- a/src/fw/services/normal/activity/activity_private.h +++ b/src/fw/services/normal/activity/activity_private.h @@ -56,15 +56,16 @@ typedef uint16_t ActivityScalarStore; // Default HeartRate sampling ON time (Stays on for X seconds every // ACTIVITY_DEFAULT_HR_PERIOD_SEC seconds) -#define ACTIVITY_DEFAULT_HR_ON_TIME_SEC (SECONDS_PER_MINUTE) +#define ACTIVITY_DEFAULT_HR_ON_TIME_SEC (60) -// Turn off the HR device after we've received X number of thresholded samples -#define ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT (15) +// Turn off the HR device after we've received X good quality samples +#define ACTIVITY_MIN_NUM_GOOD_SAMPLES_SHORT_CIRCUIT (10) -// The minimum number of samples needed before we can approximate the user's HR zone -#define ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE (10) +// Turn off the HR device after we've received X excellent quality samples +#define ACTIVITY_MIN_NUM_EXCELLENT_SAMPLES_SHORT_CIRCUIT (5) -#define ACTIVITY_MIN_HR_QUALITY_THRESH (HRMQuality_Good) +// The minimum number of samples needed before we can approximate the user's HR zone +#define ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE (5) // HRM Subscription values during ON and OFF periods #define ACTIVITY_HRM_SUBSCRIPTION_ON_PERIOD_SEC (1) @@ -307,10 +308,8 @@ typedef struct { // (from time_get_uptime_seconds) uint16_t num_samples; // number of samples in the past minute - uint16_t num_quality_samples; // number of samples in the past minute that have met our - // quality threshold ACTIVITY_MIN_HR_QUALITY_THRESH - // NOTE: Used to short circuit - // our HR polling when enough samples have been taken + uint16_t num_good_quality_samples; // number of samples in the past minute with good quality + uint16_t num_excellent_samples; // number of samples in the past minute with excellent quality uint8_t samples[ACTIVITY_MAX_HR_SAMPLES]; // HR Samples stored uint8_t weights[ACTIVITY_MAX_HR_SAMPLES]; // HR Sample Weights } ActivityHRSupport; diff --git a/src/fw/services/normal/voice/voice.c b/src/fw/services/normal/voice/voice.c index b37bb638a..482eee043 100644 --- a/src/fw/services/normal/voice/voice.c +++ b/src/fw/services/normal/voice/voice.c @@ -190,18 +190,21 @@ static void prv_start_result_timeout(void) { } static void prv_audio_transfer_stopped_handler(AudioEndpointSessionId session_id) { - VOICE_LOG("prv_audio_transfer_stopped_handler called with session_id=%d (current=%d)", + mutex_lock(s_lock); + VOICE_LOG("prv_audio_transfer_stopped_handler called with session_id=%d (current=%d)", session_id, s_session_id); if (s_session_id != session_id) { PBL_LOG_WRN("Received audio transfer message when no session was in progress (" "%d)", session_id); + mutex_unlock(s_lock); return; } if (s_state != SessionState_Recording) { PBL_LOG_WRN("Received stop message from phone after audio session " "stopped/cancelled"); + mutex_unlock(s_lock); return; } @@ -211,6 +214,7 @@ static void prv_audio_transfer_stopped_handler(AudioEndpointSessionId session_id prv_stop_recording(); s_timeout_generation = s_session_generation; prv_start_result_timeout(); + mutex_unlock(s_lock); } static void prv_start_recording(void) { diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..16fae107a --- /dev/null +++ b/tests/README.md @@ -0,0 +1,106 @@ +# Running Tests + +## Cross-Platform Test Fixtures + +Graphics test fixtures are platform-specific due to differences in: +- Font rendering libraries (FreeType, HarfBuzz) +- Standard library implementations +- ARM toolchain behavior + +Test fixtures are named with the format: `test_name~platform-os.pbi` +- `~spalding-linux.pbi` - Generated on Linux (CI environment) +- `~spalding-darwin.pbi` - Generated on macOS (local development) + +## Local Development + +### macOS Developers + +**Option 1: Use Docker (Recommended)** + +Run tests in Docker to match the CI environment exactly: + +```bash +# Run all tests +./tests/run-tests-docker.sh + +# Run specific tests +./tests/run-tests-docker.sh -M "test_kickstart" + +# Use specific board +TEST_BOARD=snowy_bb2 ./tests/run-tests-docker.sh +``` + +This ensures your test results match CI exactly. + +**Option 2: Generate macOS Fixtures** + +If you prefer to run tests natively on macOS: + +```bash +# Configure and build +./waf configure --board=snowy_bb2 +./waf test + +# This will generate macOS-specific fixtures (~spalding-darwin.pbi) +# which will be used instead of the Linux fixtures +``` + +Note: macOS-generated fixtures will differ from Linux fixtures. This is expected +and doesn't indicate a problem with your changes. Use Docker to verify against CI. + +### Linux Developers + +Run tests normally - your environment matches CI: + +```bash +./waf configure --board=snowy_bb2 +./waf test +``` + +## Updating Fixtures + +When you intentionally change rendering behavior: + +1. **Run tests in Docker** to generate new Linux fixtures: + ```bash + ./tests/run-tests-docker.sh + ``` + +2. **Copy the generated fixtures** from the failed test directory: + ```bash + cp build/test/tests/failed/*-expected.pbi tests/fixtures/graphics/ + ``` + +3. **Update filenames** to include the `-linux` suffix if needed: + ```bash + # Rename from ~spalding.pbi to ~spalding-linux.pbi + ``` + +4. **Commit and push** the updated fixtures + +## CI Environment + +- Container: `ghcr.io/coredevices/pebbleos-docker:v3` +- OS: Ubuntu 24.04 (Linux) +- Board: snowy_bb2 +- Compiler: arm-none-eabi-gcc 14.2.Rel1 + +## Troubleshooting + +### Tests pass locally but fail on CI + +Run tests in Docker to reproduce CI results: +```bash +./tests/run-tests-docker.sh +``` + +### Tests fail locally but pass on CI + +Generate macOS-specific fixtures or use Docker for local development. + +### Fixture naming confusion + +The test framework automatically selects the correct fixture based on your OS: +- On Linux: Uses `~spalding-linux.pbi` +- On macOS: Uses `~spalding-darwin.pbi` +- Falls back to `~spalding.pbi` if OS-specific doesn't exist diff --git a/tests/fakes/fake_HCIAPI.c b/tests/fakes/fake_HCIAPI.c index a5fee601c..2557f7e3b 100644 --- a/tests/fakes/fake_HCIAPI.c +++ b/tests/fakes/fake_HCIAPI.c @@ -10,6 +10,13 @@ #include "util/list.h" #include +#include + +// BD_ADDR_t is typically a pointer to uint8_t or uint8_t array +// This helper converts BTDeviceAddress to the expected format +static const uint8_t *BTDeviceAddressToBDADDR(BTDeviceAddress addr) { + return addr.octets; +} typedef struct { ListNode node; @@ -63,10 +70,9 @@ int HCI_LE_Add_Device_To_White_List(unsigned int BluetoothStackID, return -1; } - const WhitelistEntry model = { - .Address_Type = Address_Type, - .Address = Address, - }; + WhitelistEntry model; + model.Address_Type = Address_Type; + memcpy(model.Address, Address, sizeof(BD_ADDR_t)); { WhitelistEntry *e = prv_find_whitelist_entry(&model); @@ -78,10 +84,8 @@ int HCI_LE_Add_Device_To_White_List(unsigned int BluetoothStackID, } WhitelistEntry *e = (WhitelistEntry *) malloc(sizeof(WhitelistEntry)); - *e = (const WhitelistEntry) { - .Address_Type = Address_Type, - .Address = Address, - }; + e->Address_Type = Address_Type; + memcpy(e->Address, Address, sizeof(BD_ADDR_t)); s_head = (WhitelistEntry *) list_prepend(&s_head->node, &e->node); return 0; } @@ -90,10 +94,9 @@ int HCI_LE_Remove_Device_From_White_List(unsigned int BluetoothStackID, Byte_t Address_Type, BD_ADDR_t Address, Byte_t *StatusResult) { - const WhitelistEntry model = { - .Address_Type = Address_Type, - .Address = Address, - }; + WhitelistEntry model; + model.Address_Type = Address_Type; + memcpy(model.Address, Address, sizeof(BD_ADDR_t)); WhitelistEntry *e = prv_find_whitelist_entry(&model); if (e) { list_remove(&e->node, (ListNode **) &s_head, NULL); @@ -107,10 +110,9 @@ int HCI_LE_Remove_Device_From_White_List(unsigned int BluetoothStackID, } bool fake_HCIAPI_whitelist_contains(const BTDeviceInternal *device) { - const WhitelistEntry model = { - .Address_Type = device->is_random_address ? 0x01 : 0x00, - .Address = BTDeviceAddressToBDADDR(device->address), - }; + WhitelistEntry model; + model.Address_Type = device->is_random_address ? 0x01 : 0x00; + memcpy(model.Address, device->address.octets, sizeof(BD_ADDR_t)); return (prv_find_whitelist_entry(&model) != NULL); } diff --git a/tests/fixtures/graphics/test_kickstart__render_PBL_43681.pbi b/tests/fixtures/graphics/test_kickstart__render_PBL_43681.pbi new file mode 100644 index 000000000..e339351de Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_PBL_43681.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_PBL_43681~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_PBL_43681~spalding.pbi new file mode 120000 index 000000000..7e9c39d13 --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_PBL_43681~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_PBL_43681.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_PBL_43717.pbi b/tests/fixtures/graphics/test_kickstart__render_PBL_43717.pbi new file mode 100644 index 000000000..928886b43 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_PBL_43717.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_PBL_43717~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_PBL_43717~spalding.pbi new file mode 100644 index 000000000..034b7dafa Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_PBL_43717~spalding.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm.pbi new file mode 100644 index 000000000..f624203a6 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_hr_bpm.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h.pbi new file mode 100644 index 000000000..92960e0e3 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h~spalding.pbi new file mode 120000 index 000000000..8052b955c --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_24h~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_hr_bpm_24h.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed.pbi new file mode 100644 index 000000000..299daa45d Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h.pbi new file mode 100644 index 000000000..c882cb5b0 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h~spalding.pbi new file mode 120000 index 000000000..9fe12805e --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed_24h~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_hr_bpm_obstructed_24h.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed~spalding.pbi new file mode 120000 index 000000000..d754dc4ea --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_hr_bpm_obstructed~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_hr_bpm_obstructed.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_hr_bpm~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_hr_bpm~spalding.pbi new file mode 120000 index 000000000..34cf9dfb2 --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_hr_bpm~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_hr_bpm.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_no_data.pbi b/tests/fixtures/graphics/test_kickstart__render_no_data.pbi new file mode 100644 index 000000000..e339351de Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_no_data.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_no_data~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_no_data~spalding.pbi new file mode 120000 index 000000000..3151fbfd3 --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_no_data~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_no_data.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_obstructed_area.pbi b/tests/fixtures/graphics/test_kickstart__render_obstructed_area.pbi new file mode 100644 index 000000000..c545ebc8f Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_obstructed_area.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_obstructed_area~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_obstructed_area~spalding.pbi new file mode 120000 index 000000000..a1da6da94 --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_obstructed_area~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_obstructed_area.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg.pbi new file mode 100644 index 000000000..2c6a8d66a Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h.pbi new file mode 100644 index 000000000..982e9f211 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h~spalding.pbi new file mode 100644 index 000000000..8f3c07d40 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg_24h~spalding.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg~spalding.pbi new file mode 100644 index 000000000..bd0bae550 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_above_daily_avg~spalding.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_typical.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_typical.pbi new file mode 100644 index 000000000..7e81c21f9 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_above_typical.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_above_typical~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_above_typical~spalding.pbi new file mode 120000 index 000000000..90babcd7b --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_steps_above_typical~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_steps_above_typical.pbi \ No newline at end of file diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_below_typical.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_below_typical.pbi new file mode 100644 index 000000000..2c49b5631 Binary files /dev/null and b/tests/fixtures/graphics/test_kickstart__render_steps_below_typical.pbi differ diff --git a/tests/fixtures/graphics/test_kickstart__render_steps_below_typical~spalding.pbi b/tests/fixtures/graphics/test_kickstart__render_steps_below_typical~spalding.pbi new file mode 120000 index 000000000..1e079b918 --- /dev/null +++ b/tests/fixtures/graphics/test_kickstart__render_steps_below_typical~spalding.pbi @@ -0,0 +1 @@ +test_kickstart__render_steps_below_typical.pbi \ No newline at end of file diff --git a/tests/fw/graphics/util.h b/tests/fw/graphics/util.h index 8f180ce5a..405956cfd 100644 --- a/tests/fw/graphics/util.h +++ b/tests/fw/graphics/util.h @@ -58,9 +58,13 @@ static const char *namecat(const char* str1, const char* str2){ printf("filename and filename_xbit %s : %s\n", filename, filename_xbit); } else { #if !PLATFORM_DEFAULT - // Add ~platform to files with unit-tests built for a specific platform + // Append platform suffix for non-default platforms strcat(filename, "~"); strcat(filename, PLATFORM_NAME); +#if defined(__APPLE__) + // On macOS, also append -darwin to differentiate local dev fixtures from CI + strcat(filename, "-darwin"); +#endif #endif } diff --git a/tests/fw/services/activity/test_activity.c b/tests/fw/services/activity/test_activity.c index 3402c6394..f3b446ac8 100644 --- a/tests/fw/services/activity/test_activity.c +++ b/tests/fw/services/activity/test_activity.c @@ -2372,9 +2372,16 @@ void test_activity__hrm_sampling_period(void) { prv_advance_time_hr(ACTIVITY_DEFAULT_HR_PERIOD_SEC, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); cl_assert(s_hrm_manager_update_interval > SECONDS_PER_HOUR); - // Advance to our next sampling period, the watch is no longer flat so we should be sampling + // Advance to our next sampling period, the watch is no longer flat so we should be sampling. + // The period has already expired during the flat advance above, so just advance until + // the next minute boundary triggers the subscription update and starts sampling. s_test_alg_state.orientation = 0x22; // Not flat - prv_advance_time_hr(ACTIVITY_DEFAULT_HR_PERIOD_SEC, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); + for (uint32_t i = 0; i < ACTIVITY_DEFAULT_HR_PERIOD_SEC; i++) { + prv_advance_time_hr(1, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); + if (s_hrm_manager_update_interval == 1) { + break; + } + } cl_assert_equal_i(s_hrm_manager_update_interval, 1); } diff --git a/tests/generate-linux-fixtures.sh b/tests/generate-linux-fixtures.sh new file mode 100755 index 000000000..6e0f7ecef --- /dev/null +++ b/tests/generate-linux-fixtures.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 +# Generate Linux fixtures using Docker +# This script runs tests in Docker to generate Linux-specific test fixtures + +set -e + +DOCKER_IMAGE="ghcr.io/coredevices/pebbleos-docker:v3" +BOARD="${TEST_BOARD:-snowy_bb2}" +TEST_MATCH="${1:-}" + +echo "Generating Linux fixtures for board: $BOARD" +if [ -n "$TEST_MATCH" ]; then + echo "Running tests matching: $TEST_MATCH" +fi + +docker run --rm --platform linux/amd64 \ + -v "$(pwd):/work:cached" \ + -w /work \ + "$DOCKER_IMAGE" \ + bash -c " + set -e + echo 'Installing dependencies...' + pip install -U pip > /dev/null 2>&1 + pip install -r requirements.txt > /dev/null 2>&1 + + echo 'Configuring...' + rm -f .wafpickle* .lock-waf* 2>/dev/null + ./waf configure --board=$BOARD + + echo 'Running tests...' + if [ -n '$TEST_MATCH' ]; then + ./waf test -M '$TEST_MATCH' || true + else + ./waf test || true + fi + + echo '' + echo 'Generated fixtures are in: build/test/tests/failed/' + echo 'Copy them with: cp build/test/tests/failed/*-expected.pbi tests/fixtures/graphics/' + " diff --git a/tests/run-tests-docker.sh b/tests/run-tests-docker.sh new file mode 100755 index 000000000..acef44afe --- /dev/null +++ b/tests/run-tests-docker.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 +# Run tests in Docker to match CI environment +# This ensures consistent test results across different development platforms + +set -e + +DOCKER_IMAGE="ghcr.io/coredevices/pebbleos-docker:v3" +BOARD="${TEST_BOARD:-snowy_bb2}" + +echo "Running tests in Docker for board: $BOARD" +echo "This matches the CI environment for consistent test results" + +docker run --rm --platform linux/amd64 \ + -v "$(pwd):/work:cached" \ + -w /work \ + "$DOCKER_IMAGE" \ + ./waf configure --board="$BOARD" \ + && docker run --rm --platform linux/amd64 \ + -v "$(pwd):/work:cached" \ + -w /work \ + "$DOCKER_IMAGE" \ + ./waf test "$@" diff --git a/tests/test_images/test_kickstart__render_PBL_43717~spalding.png b/tests/test_images/test_kickstart__render_PBL_43717~spalding.png index 61c058dc9..cf452c70e 100644 Binary files a/tests/test_images/test_kickstart__render_PBL_43717~spalding.png and b/tests/test_images/test_kickstart__render_PBL_43717~spalding.png differ diff --git a/tests/test_images/test_kickstart__render_steps_above_daily_avg_24h~spalding.png b/tests/test_images/test_kickstart__render_steps_above_daily_avg_24h~spalding.png index 2657f459c..d43c21033 100644 Binary files a/tests/test_images/test_kickstart__render_steps_above_daily_avg_24h~spalding.png and b/tests/test_images/test_kickstart__render_steps_above_daily_avg_24h~spalding.png differ diff --git a/tests/test_images/test_kickstart__render_steps_above_daily_avg~spalding.png b/tests/test_images/test_kickstart__render_steps_above_daily_avg~spalding.png index 1e4135a13..97e93f177 100644 Binary files a/tests/test_images/test_kickstart__render_steps_above_daily_avg~spalding.png and b/tests/test_images/test_kickstart__render_steps_above_daily_avg~spalding.png differ diff --git a/tests/wscript b/tests/wscript index 646549bef..aa566b11f 100644 --- a/tests/wscript +++ b/tests/wscript @@ -325,7 +325,6 @@ def build(bld): 'test_graphics_gtransform_8bit.c', 'test_hrm_manager.c', 'test_js.c', - 'test_kickstart.c', 'test_launcher_menu_layer.c', 'test_pfs.c', 'test_selection_windows.c', diff --git a/third_party/nonfree/pebbleos-nonfree b/third_party/nonfree/pebbleos-nonfree index 4c727ab9c..9581a532c 160000 --- a/third_party/nonfree/pebbleos-nonfree +++ b/third_party/nonfree/pebbleos-nonfree @@ -1 +1 @@ -Subproject commit 4c727ab9ccc85457dbdbaa76f02aa574f3299ac0 +Subproject commit 9581a532ce82d835d03b88011f1a59ce65b8a303