diff --git a/AGENTS.md b/AGENTS.md index bd5c669fa..afa783ade 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ PebbleOS is the operating system running on Pebble smartwatches. ## Code style - clang-format for C code +- ruff for Python code ## Firmware development diff --git a/include/pbl/services/common/analytics/analytics.def b/include/pbl/services/common/analytics/analytics.def new file mode 100644 index 000000000..bf8034d9f --- /dev/null +++ b/include/pbl/services/common/analytics/analytics.def @@ -0,0 +1,80 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +// OS +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(memory_pct_max) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(stack_free_kernel_main_bytes) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(stack_free_kernel_background_bytes) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(stack_free_newtimers_bytes) +PBL_ANALYTICS_METRIC_DEFINE_SIGNED(utc_offset_s) + +// Battery & Power +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(battery_soc_pct) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(battery_soc_pct_drop) +PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(battery_voltage, 1000) +PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(battery_voltage_delta, 1000) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(battery_charge_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(battery_discharge_duration_ms) + +// Hardware I/O +PBL_ANALYTICS_METRIC_DEFINE_TIMER(backlight_on_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(backlight_avg_intensity_pct) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(vibrator_on_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(vibrator_avg_strength_pct) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(hrm_on_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(button_pressed_count) + +// CPU usage +PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(cpu_running_pct, 100) +PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(cpu_sleep0_pct, 100) +PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(cpu_sleep1_pct, 100) +PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(cpu_sleep2_pct, 100) + +// Accelerometer +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(accel_sample_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(accel_shake_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(accel_double_tap_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(accel_peek_count) + +// Notifications, phone calls, etc. +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(notification_received_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(notification_received_dnd_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(phone_call_incoming_count) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(phone_call_time_ms) + +// Modes +PBL_ANALYTICS_METRIC_DEFINE_TIMER(low_power_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(stationary_time_ms) + +// Watchface +PBL_ANALYTICS_METRIC_DEFINE_TIMER(watchface_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_STRING(watchface_name, 32) +PBL_ANALYTICS_METRIC_DEFINE_STRING(watchface_uuid, 39) + +// File system +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(pfs_space_free_kb) + +// NOR Flash +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(flash_spi_write_bytes) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(flash_spi_erase_bytes) + +// BLE +PBL_ANALYTICS_METRIC_DEFINE_TIMER(ble_adv_short_intvl_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(ble_adv_long_intvl_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(ble_conn_itvl_min_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(ble_conn_itvl_mid_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(ble_conn_itvl_max_time_ms) + +// Settings +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(settings_health_tracking_enabled) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(settings_health_hrm_enabled) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(settings_health_hrm_measurement_interval) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(settings_health_hrm_activity_tracking_enabled) + +// Application +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(app_message_sent_count) +PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(app_message_received_count) + +// Connectivity +PBL_ANALYTICS_METRIC_DEFINE_TIMER(connectivity_connected_time_ms) +PBL_ANALYTICS_METRIC_DEFINE_TIMER(connectivity_expected_time_ms) \ No newline at end of file diff --git a/include/pbl/services/common/analytics/analytics.h b/include/pbl/services/common/analytics/analytics.h index 190122c7c..16d1467f0 100644 --- a/include/pbl/services/common/analytics/analytics.h +++ b/include/pbl/services/common/analytics/analytics.h @@ -3,28 +3,61 @@ #pragma once -#if defined(ANALYTICS_IMPL_MEMFAULT) -#include "memfault/analytics_impl.h" -#else -#include "null/analytics_impl.h" -#endif +#include -static inline void analytics_init(void) { - analytics_impl_init(); -} +#define PBL_ANALYTICS_KEY(key_name) PBL_ANALYTICS_KEY__##key_name -void analytics_external_update(void); +enum pbl_analytics_key { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) \ + PBL_ANALYTICS_KEY(key), +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) \ + PBL_ANALYTICS_KEY(key), +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) \ + PBL_ANALYTICS_KEY(key), +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) \ + PBL_ANALYTICS_KEY(key), +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) \ + PBL_ANALYTICS_KEY(key), +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) \ + PBL_ANALYTICS_KEY(key), + #include "analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + PBL_ANALYTICS_KEY_COUNT, +}; + +void pbl_analytics_init(void); + +void pbl_analytics_set_signed(enum pbl_analytics_key key, int32_t signed_value); + +void pbl_analytics_set_unsigned(enum pbl_analytics_key key, uint32_t unsigned_value); + +void pbl_analytics_set_string(enum pbl_analytics_key key, const char *value); + +void pbl_analytics_timer_start(enum pbl_analytics_key key); + +void pbl_analytics_timer_stop(enum pbl_analytics_key key); + +void pbl_analytics_add(enum pbl_analytics_key key, int32_t amount); #define PBL_ANALYTICS_SET_SIGNED(key_name, signed_value) \ - PBL_ANALYTICS_IMPL_SET_SIGNED(key_name, signed_value) + pbl_analytics_set_signed(PBL_ANALYTICS_KEY(key_name), signed_value) #define PBL_ANALYTICS_SET_UNSIGNED(key_name, unsigned_value) \ - PBL_ANALYTICS_IMPL_SET_UNSIGNED(key_name, unsigned_value) + pbl_analytics_set_unsigned(PBL_ANALYTICS_KEY(key_name), unsigned_value) -#define PBL_ANALYTICS_SET_STRING(key_name, value) PBL_ANALYTICS_IMPL_SET_STRING(key_name, value) +#define PBL_ANALYTICS_SET_STRING(key_name, value) \ + pbl_analytics_set_string(PBL_ANALYTICS_KEY(key_name), value) -#define PBL_ANALYTICS_TIMER_START(key_name) PBL_ANALYTICS_IMPL_TIMER_START(key_name) +#define PBL_ANALYTICS_TIMER_START(key_name) \ + pbl_analytics_timer_start(PBL_ANALYTICS_KEY(key_name)) -#define PBL_ANALYTICS_TIMER_STOP(key_name) PBL_ANALYTICS_IMPL_TIMER_STOP(key_name) +#define PBL_ANALYTICS_TIMER_STOP(key_name) \ + pbl_analytics_timer_stop(PBL_ANALYTICS_KEY(key_name)) -#define PBL_ANALYTICS_ADD(key_name, amount) PBL_ANALYTICS_IMPL_ADD(key_name, amount) +#define PBL_ANALYTICS_ADD(key_name, amount) \ + pbl_analytics_add(PBL_ANALYTICS_KEY(key_name), amount) diff --git a/include/pbl/services/common/analytics/backend.h b/include/pbl/services/common/analytics/backend.h new file mode 100644 index 000000000..6db2a6681 --- /dev/null +++ b/include/pbl/services/common/analytics/backend.h @@ -0,0 +1,17 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "analytics.h" + +#include + +struct pbl_analytics_backend_ops { + void (*set_signed)(enum pbl_analytics_key key, int32_t signed_value); + void (*set_unsigned)(enum pbl_analytics_key key, uint32_t unsigned_value); + void (*set_string)(enum pbl_analytics_key key, const char *value); + void (*timer_start)(enum pbl_analytics_key key); + void (*timer_stop)(enum pbl_analytics_key key); + void (*add)(enum pbl_analytics_key key, int32_t amount); +}; \ No newline at end of file diff --git a/include/pbl/services/common/analytics/memfault/analytics_impl.h b/include/pbl/services/common/analytics/memfault/analytics_impl.h deleted file mode 100644 index b2ce66aa9..000000000 --- a/include/pbl/services/common/analytics/memfault/analytics_impl.h +++ /dev/null @@ -1,20 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ -/* SPDX-License-Identifier: Apache-2.0 */ - -#include - -void analytics_impl_init(void); - -#define PBL_ANALYTICS_IMPL_SET_SIGNED(key_name, signed_value) \ - MEMFAULT_METRIC_SET_SIGNED(key_name, signed_value) - -#define PBL_ANALYTICS_IMPL_SET_UNSIGNED(key_name, unsigned_value) \ - MEMFAULT_METRIC_SET_UNSIGNED(key_name, unsigned_value) - -#define PBL_ANALYTICS_IMPL_SET_STRING(key_name, value) MEMFAULT_METRIC_SET_STRING(key_name, value) - -#define PBL_ANALYTICS_IMPL_TIMER_START(key_name) MEMFAULT_METRIC_TIMER_START(key_name) - -#define PBL_ANALYTICS_IMPL_TIMER_STOP(key_name) MEMFAULT_METRIC_TIMER_STOP(key_name) - -#define PBL_ANALYTICS_IMPL_ADD(key_name, amount) MEMFAULT_METRIC_ADD(key_name, amount) \ No newline at end of file diff --git a/include/pbl/services/common/analytics/null/analytics_impl.h b/include/pbl/services/common/analytics/null/analytics_impl.h deleted file mode 100644 index ba8ce3863..000000000 --- a/include/pbl/services/common/analytics/null/analytics_impl.h +++ /dev/null @@ -1,16 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ -/* SPDX-License-Identifier: Apache-2.0 */ - -static inline void analytics_impl_init(void) { -} - -#define PBL_ANALYTICS_IMPL_SET_SIGNED(key_name, signed_value) (void)(signed_value) - -#define PBL_ANALYTICS_IMPL_SET_UNSIGNED(key_name, unsigned_value) (void)(unsigned_value) - -#define PBL_ANALYTICS_IMPL_SET_STRING(key_name, value) (void)(value) -#define PBL_ANALYTICS_IMPL_TIMER_START(key_name) - -#define PBL_ANALYTICS_IMPL_TIMER_STOP(key_name) - -#define PBL_ANALYTICS_IMPL_ADD(key_name, amount) (void)(amount) diff --git a/include/pbl/services/normal/data_logging/data_logging_service.h b/include/pbl/services/normal/data_logging/data_logging_service.h index 82ab842e1..62b70e58c 100644 --- a/include/pbl/services/normal/data_logging/data_logging_service.h +++ b/include/pbl/services/normal/data_logging/data_logging_service.h @@ -26,9 +26,8 @@ typedef enum { DlsSystemTagActivityAccelSamples = 82, DlsSystemTagActivitySession = 84, DlsSystemTagProtobufLogSession = 85, -#ifdef MEMFAULT DlsSystemTagMemfaultChunksSession = 86, -#endif + DlsSystemTagAnalyticsNativeHeartbeat = 87, } DlsSystemTag; //! Init the data logging service. Called by the system at boot time. diff --git a/src/fw/console/prompt_commands.h b/src/fw/console/prompt_commands.h index 905155481..a68f06fb8 100644 --- a/src/fw/console/prompt_commands.h +++ b/src/fw/console/prompt_commands.h @@ -286,6 +286,11 @@ extern void command_mflt_metrics_dump(void); extern void command_mflt_device_info(void); #endif +#ifdef ANALYTICS_NATIVE +extern void command_analytics_native_metrics_dump(void); +extern void command_analytics_native_heartbeat(void); +#endif + extern void command_console_disable_rx(const char *seconds_str); #if MICRO_FAMILY_SF32LB52 @@ -654,6 +659,11 @@ static const Command s_prompt_commands[] = { { "mflt metrics_dump", command_mflt_metrics_dump, 0 }, { "mflt device_info", command_mflt_device_info, 0 }, #endif // MEMFAULT + +#if ANALYTICS_NATIVE + { "analytics native metrics_dump", command_analytics_native_metrics_dump, 0 }, + { "analytics native heartbeat", command_analytics_native_heartbeat, 0 }, +#endif }; #define NUM_PROMPT_COMMANDS ARRAY_LENGTH(s_prompt_commands) diff --git a/src/fw/freertos_application.c b/src/fw/freertos_application.c index 85fcd714e..635c9fe0b 100644 --- a/src/fw/freertos_application.c +++ b/src/fw/freertos_application.c @@ -267,7 +267,7 @@ void dump_current_runtime_stats(void) { prompt_send_response(buf); } -void analytics_external_collect_cpu_stats(void) { +void pbl_analytics_external_collect_cpu_stats(void) { uint32_t stop_ticks = s_analytics_stop_ticks; uint32_t sleep_ticks = s_analytics_sleep_ticks; diff --git a/src/fw/kernel/kernel_heap.c b/src/fw/kernel/kernel_heap.c index 4d436e88b..883109514 100644 --- a/src/fw/kernel/kernel_heap.c +++ b/src/fw/kernel/kernel_heap.c @@ -44,7 +44,7 @@ void kernel_heap_init(void) { }); } -void analytics_external_collect_kernel_heap_stats(void) { +void pbl_analytics_external_collect_kernel_heap_stats(void) { uint32_t headroom = heap_get_minimum_headroom(&s_kernel_heap); size_t total_size = heap_size(&s_kernel_heap); uint32_t headroom_pct = (total_size > 0) ? (headroom * 100) / total_size : 0; diff --git a/src/fw/kernel/pebble_tasks.c b/src/fw/kernel/pebble_tasks.c index b3ed56555..20e39048d 100644 --- a/src/fw/kernel/pebble_tasks.c +++ b/src/fw/kernel/pebble_tasks.c @@ -101,7 +101,7 @@ void pebble_task_suspend(PebbleTask task) { vTaskSuspend(g_task_handles[task]); } -void analytics_external_collect_stack_free(void) { +void pbl_analytics_external_collect_stack_free(void) { PBL_ANALYTICS_SET_UNSIGNED(stack_free_kernel_main_bytes, prv_task_get_stack_free(PebbleTask_KernelMain)); PBL_ANALYTICS_SET_UNSIGNED(stack_free_kernel_background_bytes, prv_task_get_stack_free(PebbleTask_KernelBackground)); PBL_ANALYTICS_SET_UNSIGNED(stack_free_newtimers_bytes, prv_task_get_stack_free(PebbleTask_NewTimers)); diff --git a/src/fw/main.c b/src/fw/main.c index 0502a5ffb..ea8211f90 100644 --- a/src/fw/main.c +++ b/src/fw/main.c @@ -398,7 +398,7 @@ static NOINLINE void prv_main_task_init(void) { task_watchdog_init(); task_watchdog_pause(30); - analytics_init(); + pbl_analytics_init(); register_system_timers(); system_task_timer_init(); diff --git a/src/fw/services/common/analytics/analytics.c b/src/fw/services/common/analytics/analytics.c index 554313815..33b43d2af 100644 --- a/src/fw/services/common/analytics/analytics.c +++ b/src/fw/services/common/analytics/analytics.c @@ -1,22 +1,124 @@ /* SPDX-FileCopyrightText: 2024 Google LLC */ /* SPDX-License-Identifier: Apache-2.0 */ -extern void analytics_external_collect_battery(void); -extern void analytics_external_collect_cpu_stats(void); -extern void analytics_external_collect_stack_free(void); -extern void analytics_external_collect_pfs_stats(void); -extern void analytics_external_collect_kernel_heap_stats(void); -extern void analytics_external_collect_backlight_stats(void); -extern void analytics_external_collect_vibe_stats(void); -extern void analytics_external_collect_settings(void); - -void analytics_external_update(void) { - analytics_external_collect_battery(); - analytics_external_collect_cpu_stats(); - analytics_external_collect_stack_free(); - analytics_external_collect_pfs_stats(); - analytics_external_collect_kernel_heap_stats(); - analytics_external_collect_backlight_stats(); - analytics_external_collect_vibe_stats(); - analytics_external_collect_settings(); +#include + +#include "pbl/services/common/analytics/analytics.h" +#include "pbl/services/common/analytics/backend.h" +#include "pbl/services/common/new_timer/new_timer.h" +#include "util/size.h" + +#define HEARTBEAT_PERIOD_SEC 3600 + +extern void pbl_analytics_external_collect_battery(void); +extern void pbl_analytics_external_collect_cpu_stats(void); +extern void pbl_analytics_external_collect_stack_free(void); +extern void pbl_analytics_external_collect_pfs_stats(void); +extern void pbl_analytics_external_collect_kernel_heap_stats(void); +extern void pbl_analytics_external_collect_backlight_stats(void); +extern void pbl_analytics_external_collect_vibe_stats(void); +extern void pbl_analytics_external_collect_settings(void); + +#ifdef ANALYTICS_NATIVE +extern void pbl_analytics__native_init(void); +extern void pbl_analytics__native_heartbeat(void); + +extern const struct pbl_analytics_backend_ops pbl_analytics__native_ops; +#endif + +#ifdef ANALYTICS_MEMFAULT +extern void pbl_analytics__memfault_init(void); +extern void pbl_analytics__memfault_heartbeat(void); + +extern const struct pbl_analytics_backend_ops pbl_analytics__memfault_ops; +#endif + +static TimerID s_heartbeat_timer; + +static void (*const s_init[])(void) = { +#ifdef ANALYTICS_NATIVE + pbl_analytics__native_init, +#endif +#ifdef ANALYTICS_MEMFAULT + pbl_analytics__memfault_init, +#endif +}; + +static void (*const s_heartbeat[])(void) = { +#ifdef ANALYTICS_NATIVE + pbl_analytics__native_heartbeat, +#endif +#ifdef ANALYTICS_MEMFAULT + pbl_analytics__memfault_heartbeat, +#endif +}; + +static const struct pbl_analytics_backend_ops *s_backend_ops[] = { +#ifdef ANALYTICS_NATIVE + &pbl_analytics__native_ops, +#endif +#ifdef ANALYTICS_MEMFAULT + &pbl_analytics__memfault_ops, +#endif +}; + +static void prv_heartbeat_timer_cb(void *data) { + pbl_analytics_external_collect_battery(); + pbl_analytics_external_collect_cpu_stats(); + pbl_analytics_external_collect_stack_free(); + pbl_analytics_external_collect_pfs_stats(); + pbl_analytics_external_collect_kernel_heap_stats(); + pbl_analytics_external_collect_backlight_stats(); + pbl_analytics_external_collect_vibe_stats(); + pbl_analytics_external_collect_settings(); + + for (size_t i = 0U; i < ARRAY_LENGTH(s_heartbeat); i++) { + s_heartbeat[i](); + } +} + +void pbl_analytics_init(void) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_init); i++) { + s_init[i](); + } + + s_heartbeat_timer = new_timer_create(); + + new_timer_start(s_heartbeat_timer, HEARTBEAT_PERIOD_SEC * 1000, prv_heartbeat_timer_cb, NULL, + TIMER_START_FLAG_REPEATING); +} + +void pbl_analytics_set_signed(enum pbl_analytics_key key, int32_t signed_value) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->set_signed(key, signed_value); + } +} + +void pbl_analytics_set_unsigned(enum pbl_analytics_key key, uint32_t unsigned_value) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->set_unsigned(key, unsigned_value); + } +} + +void pbl_analytics_set_string(enum pbl_analytics_key key, const char *value) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->set_string(key, value); + } +} + +void pbl_analytics_timer_start(enum pbl_analytics_key key) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->timer_start(key); + } +} +void pbl_analytics_timer_stop(enum pbl_analytics_key key) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->timer_stop(key); + } +} + +void pbl_analytics_add(enum pbl_analytics_key key, int32_t amount) { + for (size_t i = 0U; i < ARRAY_LENGTH(s_backend_ops); i++) { + s_backend_ops[i]->add(key, amount); + } } \ No newline at end of file diff --git a/src/fw/services/common/analytics/memfault.c b/src/fw/services/common/analytics/memfault.c new file mode 100644 index 000000000..f56208b45 --- /dev/null +++ b/src/fw/services/common/analytics/memfault.c @@ -0,0 +1,62 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "pbl/services/common/analytics/backend.h" + +#include + +extern void memfault_platform_boot_early(void); +extern void memfault_platform_heartbeat(void); + +static const MemfaultMetricId s_pbl_to_memfault[] = { +#define _PBL_TO_MEMFAULT(key) [PBL_ANALYTICS_KEY(key)] = MEMFAULT_METRICS_KEY(key), + +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) _PBL_TO_MEMFAULT(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) _PBL_TO_MEMFAULT(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) _PBL_TO_MEMFAULT(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) _PBL_TO_MEMFAULT(key) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) _PBL_TO_MEMFAULT(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) _PBL_TO_MEMFAULT(key) + #include "pbl/services/common/analytics/analytics.def" +}; + +void pbl_analytics__memfault_init(void) { + memfault_platform_boot_early(); +} + +void pbl_analytics__memfault_heartbeat(void) { + memfault_platform_heartbeat(); +} + +static void prv_set_signed(enum pbl_analytics_key key, int32_t signed_value) { + memfault_metrics_heartbeat_set_signed(s_pbl_to_memfault[key], signed_value); +} + +static void prv_set_unsigned(enum pbl_analytics_key key, uint32_t unsigned_value) { + memfault_metrics_heartbeat_set_unsigned(s_pbl_to_memfault[key], unsigned_value); +} + +static void prv_set_string(enum pbl_analytics_key key, const char *value) { + memfault_metrics_heartbeat_set_string(s_pbl_to_memfault[key], value); +} + +static void prv_timer_start(enum pbl_analytics_key key) { + memfault_metrics_heartbeat_timer_start(s_pbl_to_memfault[key]); +} + +static void prv_timer_stop(enum pbl_analytics_key key) { + memfault_metrics_heartbeat_timer_stop(s_pbl_to_memfault[key]); +} + +static void prv_add(enum pbl_analytics_key key, int32_t amount) { + memfault_metrics_heartbeat_add(s_pbl_to_memfault[key], amount); +} + +const struct pbl_analytics_backend_ops pbl_analytics__memfault_ops = { + .set_signed = prv_set_signed, + .set_unsigned = prv_set_unsigned, + .set_string = prv_set_string, + .timer_start = prv_timer_start, + .timer_stop = prv_timer_stop, + .add = prv_add, +}; \ No newline at end of file diff --git a/src/fw/services/common/analytics/memfault/analytics_impl.c b/src/fw/services/common/analytics/memfault/analytics_impl.c deleted file mode 100644 index f81f973c6..000000000 --- a/src/fw/services/common/analytics/memfault/analytics_impl.c +++ /dev/null @@ -1,8 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ -/* SPDX-License-Identifier: Apache-2.0 */ - -extern void memfault_platform_boot_early(void); - -void analytics_impl_init(void) { - memfault_platform_boot_early(); -} \ No newline at end of file diff --git a/src/fw/services/common/analytics/native.c b/src/fw/services/common/analytics/native.c new file mode 100644 index 000000000..73bd36538 --- /dev/null +++ b/src/fw/services/common/analytics/native.c @@ -0,0 +1,408 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include + +#include "console/prompt.h" +#include "drivers/rtc.h" +#include "os/mutex.h" +#include "pbl/services/common/analytics/backend.h" +#include "pbl/services/normal/data_logging/data_logging_service.h" +#include "system/logging.h" +#include "system/passert.h" +#include "util/build_id.h" +#include "util/math.h" +#include "util/size.h" +#include "util/uuid.h" + +#define NATIVE_HEARTBEAT_RECORD_VERSION 1 + +/* Heartbeat record logged to DLS */ +__attribute__((packed)) struct native_heartbeat_record { + uint8_t version; + uint64_t timestamp; + uint8_t build_id[BUILD_ID_EXPECTED_LEN]; +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) uint32_t metric_##key; +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) int32_t metric_##key; +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) \ + uint32_t metric_##key; \ + uint16_t metric_##key##_scale; +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) \ + int32_t metric_##key; \ + uint16_t metric_##key##_scale; +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) uint32_t metric_##key; +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) char metric_##key[(len) + 1]; +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +/* Type-specific internal index enums (dense, no gaps) */ + +enum native_integer_index { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + NATIVE_INTEGER_COUNT, +}; + +enum native_timer_index { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) NATIVE_TIMER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + NATIVE_TIMER_COUNT, +}; + +enum native_string_index { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) NATIVE_STRING_IDX_##key, +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + NATIVE_STRING_COUNT, +}; + +/* Mapping tables: global key enum -> type-specific index (-1 if N/A) */ + +static const int8_t s_key_to_integer[] = { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) NATIVE_INTEGER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) -1, +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +static const int8_t s_key_to_timer[] = { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) NATIVE_TIMER_IDX_##key, +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) -1, +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +static const int8_t s_key_to_string[] = { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) -1, +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) NATIVE_STRING_IDX_##key, +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +/* Type-specific storage */ + +static int32_t s_integer_values[NATIVE_INTEGER_COUNT]; + +static struct { + uint32_t value_ms; + bool running; + RtcTicks start_ticks; +} s_timers[NATIVE_TIMER_COUNT]; + +/* Per-string dedicated buffers (sized to declared length) */ +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) static char s_string_##key[(len) + 1]; +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + +/* String pointer and length lookup tables */ + +static char *const s_string_ptrs[] = { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) s_string_##key, +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +static const uint8_t s_string_lens[] = { +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) (len), +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +}; + +static PebbleMutex *s_mutex; +static DataLoggingSession *s_dls_session; + +extern const ElfExternalNote TINTIN_BUILD_ID; + +static void prv_record_metrics(struct native_heartbeat_record *record, bool reset) { + uint32_t timer_value_ms[NATIVE_TIMER_COUNT]; + + memset(record, 0, sizeof(*record)); + + record->version = NATIVE_HEARTBEAT_RECORD_VERSION; + record->timestamp = rtc_get_time(); + memcpy(record->build_id, &TINTIN_BUILD_ID.data[TINTIN_BUILD_ID.name_length], + MIN(BUILD_ID_EXPECTED_LEN, TINTIN_BUILD_ID.data_length)); + + RtcTicks now = rtc_get_ticks(); + for (size_t i = 0; i < NATIVE_TIMER_COUNT; i++) { + timer_value_ms[i] = s_timers[i].value_ms; + if (s_timers[i].running) { + RtcTicks elapsed = now - s_timers[i].start_ticks; + timer_value_ms[i] += (elapsed * 1000) / RTC_TICKS_HZ; + if (reset) { + s_timers[i].value_ms = timer_value_ms[i]; + s_timers[i].start_ticks = now; + } + } + } + + /* Build heartbeat record from type-specific storage */ +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) \ + record->metric_##key = (uint32_t)s_integer_values[NATIVE_INTEGER_IDX_##key]; +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) \ + record->metric_##key = s_integer_values[NATIVE_INTEGER_IDX_##key]; +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) \ + record->metric_##key = (uint32_t)s_integer_values[NATIVE_INTEGER_IDX_##key]; \ + record->metric_##key##_scale = (scale); +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) \ + record->metric_##key = s_integer_values[NATIVE_INTEGER_IDX_##key]; \ + record->metric_##key##_scale = (scale); +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) \ + record->metric_##key = timer_value_ms[NATIVE_TIMER_IDX_##key]; +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) \ + strncpy(record->metric_##key, s_string_##key, (len)); \ + record->metric_##key[(len)] = '\0'; +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING + + if (reset) { + /* Reset storage for next heartbeat period, keeping running timers active */ + memset(s_integer_values, 0, sizeof(s_integer_values)); + for (size_t i = 0; i < NATIVE_TIMER_COUNT; i++) { + s_timers[i].value_ms = 0; + } + for (size_t i = 0; i < NATIVE_STRING_COUNT; i++) { + s_string_ptrs[i][0] = '\0'; + } + } +} + +void pbl_analytics__native_init(void) { + s_mutex = mutex_create(); + PBL_ASSERTN(s_mutex != NULL); +} + +void pbl_analytics__native_heartbeat(void) { + struct native_heartbeat_record record; + + mutex_lock(s_mutex); + prv_record_metrics(&record, true); + mutex_unlock(s_mutex); + + if (s_dls_session == NULL) { + Uuid system_uuid = UUID_SYSTEM; + + s_dls_session = dls_create(DlsSystemTagAnalyticsNativeHeartbeat, DATA_LOGGING_BYTE_ARRAY, + sizeof(struct native_heartbeat_record), false, false, &system_uuid); + PBL_ASSERTN(s_dls_session != NULL); + } + + DataLoggingResult result = dls_log(s_dls_session, &record, 1); + if (result != DATA_LOGGING_SUCCESS) { + PBL_LOG_ERR("Native analytics DLS log failed: %d", result); + } +} + +static void prv_set_signed(enum pbl_analytics_key key, int32_t signed_value) { + int8_t idx = s_key_to_integer[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + s_integer_values[idx] = signed_value; + mutex_unlock(s_mutex); +} + +static void prv_set_unsigned(enum pbl_analytics_key key, uint32_t unsigned_value) { + int8_t idx = s_key_to_integer[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + s_integer_values[idx] = (int32_t)unsigned_value; + mutex_unlock(s_mutex); +} + +static void prv_set_string(enum pbl_analytics_key key, const char *value) { + int8_t idx = s_key_to_string[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + strncpy(s_string_ptrs[idx], value, s_string_lens[idx]); + s_string_ptrs[idx][s_string_lens[idx]] = '\0'; + mutex_unlock(s_mutex); +} + +static void prv_timer_start(enum pbl_analytics_key key) { + int8_t idx = s_key_to_timer[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + if (!s_timers[idx].running) { + s_timers[idx].running = true; + s_timers[idx].start_ticks = rtc_get_ticks(); + } + mutex_unlock(s_mutex); +} + +static void prv_timer_stop(enum pbl_analytics_key key) { + int8_t idx = s_key_to_timer[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + if (s_timers[idx].running) { + RtcTicks elapsed = rtc_get_ticks() - s_timers[idx].start_ticks; + s_timers[idx].value_ms += (int32_t)((elapsed * 1000) / RTC_TICKS_HZ); + s_timers[idx].running = false; + } + mutex_unlock(s_mutex); +} + +static void prv_add(enum pbl_analytics_key key, int32_t amount) { + int8_t idx = s_key_to_integer[key]; + if (idx < 0) { + return; + } + mutex_lock(s_mutex); + s_integer_values[idx] += amount; + mutex_unlock(s_mutex); +} + +const struct pbl_analytics_backend_ops pbl_analytics__native_ops = { + .set_signed = prv_set_signed, + .set_unsigned = prv_set_unsigned, + .set_string = prv_set_string, + .timer_start = prv_timer_start, + .timer_stop = prv_timer_stop, + .add = prv_add, +}; + +void command_analytics_native_metrics_dump(void) { + struct native_heartbeat_record record; + char buffer[64]; + + prv_record_metrics(&record, false); + +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) \ + prompt_send_response_fmt(buffer, sizeof(buffer), STRINGIFY(key) "=%" PRIu32, record.metric_##key); +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) \ + prompt_send_response_fmt(buffer, sizeof(buffer), STRINGIFY(key) "=%" PRId32, record.metric_##key); +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) \ + prompt_send_response_fmt(buffer, sizeof(buffer), STRINGIFY(key) "=%" PRIu32 ".%" PRIu32, \ + record.metric_##key / (scale), \ + record.metric_##key - (record.metric_##key / (scale)) * (scale)); +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) \ + prompt_send_response_fmt( \ + buffer, sizeof(buffer), STRINGIFY(key) "=%" PRId32 ".%" PRIu32, \ + record.metric_##key / (scale), \ + (uint32_t)(record.metric_##key - (record.metric_##key / (scale)) * (scale))); +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) \ + prompt_send_response_fmt(buffer, sizeof(buffer), STRINGIFY(key) "=%" PRId32 " ms", \ + record.metric_##key); +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) \ + prompt_send_response_fmt(buffer, sizeof(buffer), STRINGIFY(key) "=%s", record.metric_##key); +#include "pbl/services/common/analytics/analytics.def" +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING +} + +void command_analytics_native_heartbeat(void) { + pbl_analytics__native_heartbeat(); +} \ No newline at end of file diff --git a/src/fw/services/common/analytics/wscript_build b/src/fw/services/common/analytics/wscript_build index 55f28d903..ec4ca71ac 100644 --- a/src/fw/services/common/analytics/wscript_build +++ b/src/fw/services/common/analytics/wscript_build @@ -1,13 +1,17 @@ # SPDX-Copyright-Text: 2026 Core Devices LLC # SPDX-License-Identifier: Apache-2.0 -use = [] +use = ['fw_includes'] sources = ['analytics.c'] +if bld.variant != 'prf': + sources.append('native.c') + bld.env.DEFINES.append('ANALYTICS_NATIVE') + if bld.env.memfault: use.append('memfault_includes') - sources.append('memfault/analytics_impl.c') - bld.env.DEFINES.append('ANALYTICS_IMPL_MEMFAULT') + sources.append('memfault.c') + bld.env.DEFINES.append('ANALYTICS_MEMFAULT') bld.stlib( source=sources, diff --git a/src/fw/services/common/battery/nrf_fuel_gauge/battery_state.c b/src/fw/services/common/battery/nrf_fuel_gauge/battery_state.c index 97dc1c205..56a32f854 100644 --- a/src/fw/services/common/battery/nrf_fuel_gauge/battery_state.c +++ b/src/fw/services/common/battery/nrf_fuel_gauge/battery_state.c @@ -65,6 +65,7 @@ static uint64_t prv_ref_time; static int32_t s_last_voltage_mv; static int32_t s_last_temp_mc; static int32_t s_analytics_last_voltage_mv; +static uint8_t s_analytics_last_pct; static uint32_t s_last_tte; static uint32_t s_last_ttf; static RtcTicks s_last_log; @@ -330,9 +331,9 @@ static void prv_update_state(void *force_update) { PBL_ASSERTN(ret == 0); s_last_battery_charge_state.is_plugged = is_plugged; if (is_plugged) { - PBL_ANALYTICS_TIMER_START(battery_plugged_time_ms); + PBL_ANALYTICS_TIMER_STOP(battery_discharge_duration_ms); } else { - PBL_ANALYTICS_TIMER_STOP(battery_plugged_time_ms); + PBL_ANALYTICS_TIMER_START(battery_discharge_duration_ms); } update = true; } @@ -491,6 +492,12 @@ void battery_state_init(void) { !(s_last_chg_status == BatteryChargeStatusComplete || s_last_chg_status == BatteryChargeStatusUnknown); + if (s_last_battery_charge_state.is_charging) { + PBL_ANALYTICS_TIMER_START(battery_charge_time_ms); + } else if (!s_last_battery_charge_state.is_plugged) { + PBL_ANALYTICS_TIMER_START(battery_discharge_duration_ms); + } + s_periodic_timer_id = new_timer_create(); battery_state_force_update(); @@ -499,6 +506,9 @@ void battery_state_init(void) { .cb = prv_callback_from_regular_timer }; regular_timer_add_multisecond_callback(&battery_regular_timer, BATTERY_SAMPLE_RATE_S); + + s_analytics_last_voltage_mv = s_last_voltage_mv; + s_analytics_last_pct = s_last_battery_charge_state.pct; } void battery_state_handle_connection_event(bool is_connected) { @@ -554,14 +564,21 @@ void command_print_battery_status(void) { // Analytics // Note that this is run on a different thread than battery_state! -void analytics_external_collect_battery(void) { - // This should not be called for an hour after bootup +void pbl_analytics_external_collect_battery(void) { + int32_t battery_mv = s_last_voltage_mv; + uint8_t battery_soc = s_last_battery_charge_state.pct; int32_t d_mv; + uint8_t d_soc; - d_mv = s_last_voltage_mv - s_analytics_last_voltage_mv; - PBL_ANALYTICS_SET_UNSIGNED(battery_voltage, s_last_voltage_mv); + d_mv = battery_mv - s_analytics_last_voltage_mv; + PBL_ANALYTICS_SET_UNSIGNED(battery_voltage, battery_mv); PBL_ANALYTICS_SET_SIGNED(battery_voltage_delta, d_mv); - s_analytics_last_voltage_mv = s_last_voltage_mv; + s_analytics_last_voltage_mv = battery_mv; + + d_soc = MAX((int8_t)battery_soc - (int8_t)s_analytics_last_pct, 0); + PBL_ANALYTICS_SET_UNSIGNED(battery_soc_pct, battery_soc); + PBL_ANALYTICS_SET_UNSIGNED(battery_soc_pct_drop, d_soc); + s_analytics_last_pct = battery_soc; } static void prv_set_forced_charge_state(bool is_charging) { diff --git a/src/fw/services/common/battery/voltage/battery_state.c b/src/fw/services/common/battery/voltage/battery_state.c index 8a43ee0d5..f5f423375 100644 --- a/src/fw/services/common/battery/voltage/battery_state.c +++ b/src/fw/services/common/battery/voltage/battery_state.c @@ -58,6 +58,7 @@ typedef struct BatteryState { static BatteryState s_last_battery_state; static TimerID s_periodic_timer_id = TIMER_INVALID_ID; static int s_analytics_previous_mv = 0; +static uint8_t s_analytics_last_pct = 0; static void prv_schedule_update(uint32_t delay, bool force_update); PreciseBatteryChargeState prv_get_precise_charge_state(const BatteryState *state); @@ -72,19 +73,21 @@ static void prv_update_plugged_change(void) { // probably switching to a new curve. battery_state_reset_filter(); - bool is_charging = battery_charge_controller_thinks_we_are_charging(); + if (is_charging) { PBL_ANALYTICS_TIMER_START(battery_charge_time_ms); + PBL_ANALYTICS_TIMER_STOP(battery_discharge_duration_ms); } else { + bool is_plugged = battery_is_usb_connected(); + PBL_ANALYTICS_TIMER_STOP(battery_charge_time_ms); - } - bool is_plugged = battery_is_usb_connected(); - if (is_plugged) { - PBL_ANALYTICS_TIMER_START(battery_plugged_time_ms); - } else { - PBL_ANALYTICS_TIMER_STOP(battery_plugged_time_ms); + if (!is_plugged) { + PBL_ANALYTICS_TIMER_START(battery_discharge_duration_ms); + } else { + PBL_ANALYTICS_TIMER_STOP(battery_discharge_duration_ms); + } } } @@ -255,6 +258,7 @@ void battery_state_init(void) { battery_state_force_update(); s_analytics_previous_mv = s_last_battery_state.voltage; + s_analytics_last_pct = ratio32_to_percent(s_last_battery_state.percent); } void battery_state_handle_connection_event(bool is_connected) { @@ -335,16 +339,21 @@ void command_print_battery_status(void) { // Analytics // Note that this is run on a different thread than battery_state! -void analytics_external_collect_battery(void) { - // This should not be called for an hour after bootup +void pbl_analytics_external_collect_battery(void) { + int32_t battery_mv = s_last_battery_state.voltage; + uint8_t battery_soc = ratio32_to_percent(s_last_battery_state.percent); + int32_t d_mv; + uint8_t d_soc; - int battery_mv = s_last_battery_state.voltage; + d_mv = battery_mv - s_analytics_previous_mv; PBL_ANALYTICS_SET_UNSIGNED(battery_voltage, battery_mv); - - int d_mv = battery_mv - s_analytics_previous_mv; PBL_ANALYTICS_SET_SIGNED(battery_voltage_delta, d_mv); - s_analytics_previous_mv = battery_mv; + + d_soc = MAX((int8_t)battery_soc - (int8_t)s_analytics_last_pct, 0); + PBL_ANALYTICS_SET_UNSIGNED(battery_soc_pct, battery_soc); + PBL_ANALYTICS_SET_UNSIGNED(battery_soc_pct_drop, d_soc); + s_analytics_last_pct = battery_soc; } static void prv_set_forced_charge_state(bool is_charging) { diff --git a/src/fw/services/common/bluetooth/bluetooth_ctl.c b/src/fw/services/common/bluetooth/bluetooth_ctl.c index 557f7d0f7..3b0a95f23 100644 --- a/src/fw/services/common/bluetooth/bluetooth_ctl.c +++ b/src/fw/services/common/bluetooth/bluetooth_ctl.c @@ -13,9 +13,6 @@ #include "kernel/events.h" #include "kernel/pbl_malloc.h" #include "kernel/util/stop.h" -#if MEMFAULT -#include "memfault/metrics/connectivity.h" -#endif #include "os/mutex.h" #include "pbl/services/common/analytics/analytics.h" #include "pbl/services/common/bluetooth/ble_bas.h" @@ -137,15 +134,12 @@ static void prv_send_state_change_event(void) { }, }; event_put(&event); -#if MEMFAULT if (s_comm_airplane_mode_on) { - memfault_metrics_connectivity_connected_state_change( - kMemfaultMetricsConnectivityState_Stopped); + PBL_ANALYTICS_TIMER_STOP(connectivity_connected_time_ms); + PBL_ANALYTICS_TIMER_STOP(connectivity_expected_time_ms); } else { - memfault_metrics_connectivity_connected_state_change( - kMemfaultMetricsConnectivityState_Started); + PBL_ANALYTICS_TIMER_START(connectivity_expected_time_ms); } -#endif } static void prv_comm_state_change(void *context) { diff --git a/src/fw/services/common/comm_session/session_analytics.c b/src/fw/services/common/comm_session/session_analytics.c index 5efdf2b78..e14c77be1 100644 --- a/src/fw/services/common/comm_session/session_analytics.c +++ b/src/fw/services/common/comm_session/session_analytics.c @@ -6,7 +6,6 @@ #include "drivers/rtc.h" #if MEMFAULT -#include "memfault/metrics/connectivity.h" #include "memfault_chunk_collector.h" #endif #include "pbl/services/common/comm_session/session_internal.h" @@ -21,9 +20,9 @@ CommSessionTransportType comm_session_analytics_get_transport_type(CommSession * void comm_session_analytics_open_session(CommSession *session) { const bool is_system = (session->destination != TransportDestinationApp); if (is_system) { + PBL_ANALYTICS_TIMER_START(connectivity_expected_time_ms); + PBL_ANALYTICS_TIMER_START(connectivity_connected_time_ms); #if MEMFAULT - memfault_metrics_connectivity_connected_state_change( - kMemfaultMetricsConnectivityState_Connected); // Trigger a delayed Memfault chunk collection so any pending coredump data // gets pushed into datalogging shortly after the phone connects, rather than // waiting for the 15-minute periodic timer. The delay gives the phone time @@ -37,10 +36,8 @@ void comm_session_analytics_open_session(CommSession *session) { void comm_session_analytics_close_session(CommSession *session, CommSessionCloseReason reason) { const bool is_system = (session->destination != TransportDestinationApp); if (is_system) { -#if MEMFAULT - memfault_metrics_connectivity_connected_state_change( - kMemfaultMetricsConnectivityState_ConnectionLost); -#endif + PBL_ANALYTICS_TIMER_START(connectivity_expected_time_ms); + PBL_ANALYTICS_TIMER_STOP(connectivity_connected_time_ms); } } \ No newline at end of file diff --git a/src/fw/services/common/light/service.c b/src/fw/services/common/light/service.c index 4b77a62d7..caacf4c8f 100644 --- a/src/fw/services/common/light/service.c +++ b/src/fw/services/common/light/service.c @@ -516,7 +516,7 @@ uint8_t light_get_current_brightness_percent(void) { return percent; } -void analytics_external_collect_backlight_stats(void) { +void pbl_analytics_external_collect_backlight_stats(void) { mutex_lock(s_mutex); // Capture one final sample to account for time since last brightness change diff --git a/src/fw/services/common/vibe_pattern/service.c b/src/fw/services/common/vibe_pattern/service.c index 697cb2379..3e97366da 100644 --- a/src/fw/services/common/vibe_pattern/service.c +++ b/src/fw/services/common/vibe_pattern/service.c @@ -358,7 +358,7 @@ DEFINE_SYSCALL(void, sys_vibe_pattern_clear, void) { mutex_unlock(s_vibe_pattern_mutex); } -void analytics_external_collect_vibe_stats(void) { +void pbl_analytics_external_collect_vibe_stats(void) { mutex_lock(s_vibe_pattern_mutex); // Capture one final sample to account for time since last strength change diff --git a/src/fw/services/normal/filesystem/pfs.c b/src/fw/services/normal/filesystem/pfs.c index 8fbd08da8..ab3419842 100644 --- a/src/fw/services/normal/filesystem/pfs.c +++ b/src/fw/services/normal/filesystem/pfs.c @@ -2200,7 +2200,7 @@ uint32_t pfs_crc_calculate_file(int fd, uint32_t offset, uint32_t num_bytes) { return (crc); } -void analytics_external_collect_pfs_stats(void) { +void pbl_analytics_external_collect_pfs_stats(void) { uint16_t avail_kilobytes = (uint16_t)(get_available_pfs_space() / 1024); PBL_ANALYTICS_SET_UNSIGNED(pfs_space_free_kb, avail_kilobytes); } diff --git a/src/fw/shell/normal/prefs.c b/src/fw/shell/normal/prefs.c index e0bd15c5c..5568a4865 100644 --- a/src/fw/shell/normal/prefs.c +++ b/src/fw/shell/normal/prefs.c @@ -1735,7 +1735,7 @@ void shell_prefs_set_power_mode(PowerMode mode) { prv_pref_set(PREF_KEY_POWER_MODE, &val, sizeof(val)); } -void analytics_external_collect_settings(void) { +void pbl_analytics_external_collect_settings(void) { PBL_ANALYTICS_SET_UNSIGNED(settings_health_tracking_enabled, activity_prefs_tracking_is_enabled()); #ifdef CONFIG_HRM diff --git a/src/fw/shell/prf/stubs.c b/src/fw/shell/prf/stubs.c index f43b07c11..6569262bb 100644 --- a/src/fw/shell/prf/stubs.c +++ b/src/fw/shell/prf/stubs.c @@ -246,3 +246,9 @@ int16_t timeline_peek_get_obstruction_origin_y(void) { void timeline_peek_handle_process_start(void) { } void timeline_peek_handle_process_kill(void) { } + +void pbl_analytics_external_collect_pfs_stats(void) { +} + +void pbl_analytics_external_collect_settings(void) { +} diff --git a/src/fw/shell/sdk/stubs.c b/src/fw/shell/sdk/stubs.c index 830e7afbe..e504252b0 100644 --- a/src/fw/shell/sdk/stubs.c +++ b/src/fw/shell/sdk/stubs.c @@ -77,8 +77,6 @@ const PebbleProcessMd* alarms_app_get_info(void) { return (const PebbleProcessMd*) &s_alarms_app_info; } -void analytics_external_update(void) { -} bool shell_prefs_get_stationary_enabled(void) { return false; @@ -131,3 +129,6 @@ uint8_t activity_prefs_heart_get_zone2_threshold(void) { uint8_t activity_prefs_heart_get_zone3_threshold(void) { return 172; } + +void pbl_analytics_external_collect_settings(void) { +} diff --git a/src/fw/soc/sf32lb/sf32lb52x/freertos.c b/src/fw/soc/sf32lb/sf32lb52x/freertos.c index 67a6f264e..5bade78ca 100644 --- a/src/fw/soc/sf32lb/sf32lb52x/freertos.c +++ b/src/fw/soc/sf32lb/sf32lb52x/freertos.c @@ -368,7 +368,7 @@ void command_force_deepwfi(const char *arg) { } } -void analytics_external_collect_cpu_stats(void) { +void pbl_analytics_external_collect_cpu_stats(void) { uint32_t wfi_ticks = s_analytics_wfi_ticks; uint32_t deepwfi_ticks = s_analytics_deepwfi_ticks; uint32_t deepsleep_ticks = s_analytics_deepsleep_ticks; diff --git a/tests/fw/services/bluetooth/test_bluetooth_persistent_storage.c b/tests/fw/services/bluetooth/test_bluetooth_persistent_storage.c index 5dbdc5e05..c291f7302 100644 --- a/tests/fw/services/bluetooth/test_bluetooth_persistent_storage.c +++ b/tests/fw/services/bluetooth/test_bluetooth_persistent_storage.c @@ -8,12 +8,9 @@ #include "pbl/services/common/bluetooth/bluetooth_persistent_storage.h" #include "pbl/services/normal/bluetooth/bluetooth_persistent_storage_unittest_impl.h" - #include "pbl/services/normal/settings/settings_file.h" #include "pbl/services/normal/filesystem/pfs.h" #include "pbl/services/common/event_service.h" -#include "pbl/services/common/analytics/analytics.h" -#include "pbl/services/common/analytics/analytics_external.h" #include "flash_region/flash_region.h" // Stubs @@ -34,7 +31,6 @@ typedef struct GAPLEConnection GAPLEConnection; #include "stubs_bluetooth_persistent_storage_debug.h" #include "stubs_bt_lock.h" #include "stubs_gap_le_advert.h" -#include "stubs_bluetooth_analytics.h" #include "stubs_gatt_client_discovery.h" #include "stubs_gatt_client_subscriptions.h" #include "stubs_logging.h" @@ -53,8 +49,6 @@ static int s_ble_bonding_change_add_count; static int s_ble_bonding_change_update_count; static int s_ble_bonding_change_delete_count; -static int s_analytics_ble_pairings_count; - typedef bool (*BondingSyncFilterCb)(const BleBonding *bonding, void *ctx); const BleBonding *bonding_sync_find(BondingSyncFilterCb cb, void *ctx) { return NULL; @@ -112,12 +106,6 @@ void kernel_le_client_handle_bonding_change(BTBondingID bonding, BtPersistBondin return; } -void analytics_set(AnalyticsMetric metric, int64_t val, AnalyticsClient client) { - if (metric == ANALYTICS_DEVICE_METRIC_BLE_PAIRING_RECORDS_COUNT) { - s_analytics_ble_pairings_count = val; - } -} - uint16_t gaps_get_starting_att_handle(void) { return 4; } @@ -140,7 +128,6 @@ void test_bluetooth_persistent_storage__initialize(void) { s_ble_bonding_change_add_count = 0; s_ble_bonding_change_update_count = 0; s_ble_bonding_change_delete_count = 0; - s_analytics_ble_pairings_count = 0; fake_shared_prf_storage_reset_counts(); @@ -480,54 +467,6 @@ void test_bluetooth_persistent_storage__delete_ble_pairing_by_id(void) { cl_assert(!ret); } - -void test_bluetooth_persistent_storage__analytics_external_collect_ble_pairing_info(void) { - // No pairings yet - analytics_external_collect_ble_pairing_info(); - cl_assert_equal_i(s_analytics_ble_pairings_count, 0); - - // Store a pairing - SMPairingInfo pairing = (SMPairingInfo) { - .irk = (SMIdentityResolvingKey) { - .data = { - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, - }, - }, - .identity = (BTDeviceInternal) { - .address = (BTDeviceAddress) { - .octets = { - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, - }, - }, - .is_classic = false, - .is_random_address = false, - }, - .is_remote_identity_info_valid = true, - }; - BleBonding ble_bonding = (BleBonding) { - .is_gateway = true, - .pairing_info = pairing, - }; - bonding_sync_add_bonding(&ble_bonding); - BTBondingID id = bt_persistent_storage_store_ble_pairing(&pairing, true /* is_gateway */, NULL, - false /* requires_address_pinning */, - false /* auto_accept_re_pairing */); - cl_assert(id != BT_BONDING_ID_INVALID); - - // We should now be at 1 - analytics_external_collect_ble_pairing_info(); - cl_assert_equal_i(s_analytics_ble_pairings_count, 1); - - // Delete the Pairing - bt_persistent_storage_delete_ble_pairing_by_id(id); - cl_assert_equal_i(s_ble_bonding_change_delete_count, 1); - - // We should now be at 0 - analytics_external_collect_ble_pairing_info(); - cl_assert_equal_i(s_analytics_ble_pairings_count, 0); -} - void test_bluetooth_persistent_storage__ble_ancs_bonding(void) { bool ret; diff --git a/tests/stubs/stubs_analytics.h b/tests/stubs/stubs_analytics.h index 11f46d2b3..c7f1b9dec 100644 --- a/tests/stubs/stubs_analytics.h +++ b/tests/stubs/stubs_analytics.h @@ -3,5 +3,34 @@ #pragma once -#define ANALYTICS_IMPL_NULL -#include "pbl/services/common/analytics/analytics.h" \ No newline at end of file +#include "pbl/services/common/analytics/analytics.h" + +void pbl_analytics_init(void) {} + +void pbl_analytics_set_signed(enum pbl_analytics_key key, int32_t signed_value) { + (void)key; + (void)signed_value; +} + +void pbl_analytics_set_unsigned(enum pbl_analytics_key key, uint32_t unsigned_value) { + (void)key; + (void)unsigned_value; +} + +void pbl_analytics_set_string(enum pbl_analytics_key key, const char *value) { + (void)key; + (void)value; +} + +void pbl_analytics_timer_start(enum pbl_analytics_key key) { + (void)key; +} + +void pbl_analytics_timer_stop(enum pbl_analytics_key key) { + (void)key; +} + +void pbl_analytics_add(enum pbl_analytics_key key, int32_t amount) { + (void)key; + (void)amount; +} diff --git a/third_party/memfault/port/include/memfault_metrics_heartbeat_config.def b/third_party/memfault/port/include/memfault_metrics_heartbeat_config.def index 5e6e87517..c4d810abf 100644 --- a/third_party/memfault/port/include/memfault_metrics_heartbeat_config.def +++ b/third_party/memfault/port/include/memfault_metrics_heartbeat_config.def @@ -1,73 +1,21 @@ -#include "ports/freertos/config/memfault_metrics_heartbeat_freertos_config.def" - -// OS -MEMFAULT_METRICS_KEY_DEFINE(memory_pct_max, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(stack_free_kernel_main_bytes, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(stack_free_kernel_background_bytes, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(stack_free_newtimers_bytes, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(utc_offset_s, kMemfaultMetricType_Signed) - -// Battery & Power -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(battery_voltage, kMemfaultMetricType_Unsigned, 1000) -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(battery_voltage_delta, kMemfaultMetricType_Signed, 1000) -MEMFAULT_METRICS_KEY_DEFINE(battery_charge_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(battery_plugged_time_ms, kMemfaultMetricType_Timer) - -// Hardware I/O -MEMFAULT_METRICS_KEY_DEFINE(backlight_on_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(backlight_avg_intensity_pct, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(vibrator_on_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(vibrator_avg_strength_pct, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(hrm_on_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(button_pressed_count, kMemfaultMetricType_Unsigned) - -// CPU usage -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(cpu_running_pct, kMemfaultMetricType_Unsigned, 100) -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(cpu_sleep0_pct, kMemfaultMetricType_Unsigned, 100) -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(cpu_sleep1_pct, kMemfaultMetricType_Unsigned, 100) -MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(cpu_sleep2_pct, kMemfaultMetricType_Unsigned, 100) - -// Accelerometer -MEMFAULT_METRICS_KEY_DEFINE(accel_sample_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(accel_shake_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(accel_double_tap_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(accel_peek_count, kMemfaultMetricType_Unsigned) - -// Notifications, phone calls, etc. -MEMFAULT_METRICS_KEY_DEFINE(notification_received_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(notification_received_dnd_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(phone_call_incoming_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(phone_call_time_ms, kMemfaultMetricType_Timer) - -// Modes -MEMFAULT_METRICS_KEY_DEFINE(low_power_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(stationary_time_ms, kMemfaultMetricType_Timer) - -// Watchface -MEMFAULT_METRICS_KEY_DEFINE(watchface_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_STRING_KEY_DEFINE(watchface_name, 32) -MEMFAULT_METRICS_STRING_KEY_DEFINE(watchface_uuid, 39) - -// File system -MEMFAULT_METRICS_KEY_DEFINE(pfs_space_free_kb, kMemfaultMetricType_Unsigned) - -// NOR Flash -MEMFAULT_METRICS_KEY_DEFINE(flash_spi_write_bytes, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(flash_spi_erase_bytes, kMemfaultMetricType_Unsigned) - -// BLE -MEMFAULT_METRICS_KEY_DEFINE(ble_adv_short_intvl_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(ble_adv_long_intvl_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(ble_conn_itvl_min_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(ble_conn_itvl_mid_time_ms, kMemfaultMetricType_Timer) -MEMFAULT_METRICS_KEY_DEFINE(ble_conn_itvl_max_time_ms, kMemfaultMetricType_Timer) - -// Settings -MEMFAULT_METRICS_KEY_DEFINE(settings_health_tracking_enabled, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(settings_health_hrm_enabled, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(settings_health_hrm_measurement_interval, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(settings_health_hrm_activity_tracking_enabled, kMemfaultMetricType_Unsigned) - -// Application -MEMFAULT_METRICS_KEY_DEFINE(app_message_sent_count, kMemfaultMetricType_Unsigned) -MEMFAULT_METRICS_KEY_DEFINE(app_message_received_count, kMemfaultMetricType_Unsigned) +#define PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED(key) \ + MEMFAULT_METRICS_KEY_DEFINE(key, kMemfaultMetricType_Unsigned) +#define PBL_ANALYTICS_METRIC_DEFINE_SIGNED(key) \ + MEMFAULT_METRICS_KEY_DEFINE(key, kMemfaultMetricType_Signed) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED(key, scale) \ + MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(key, kMemfaultMetricType_Unsigned, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED(key, scale) \ + MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(key, kMemfaultMetricType_Signed, scale) +#define PBL_ANALYTICS_METRIC_DEFINE_TIMER(key) \ + MEMFAULT_METRICS_KEY_DEFINE(key, kMemfaultMetricType_Timer) +#define PBL_ANALYTICS_METRIC_DEFINE_STRING(key, len) \ + MEMFAULT_METRICS_STRING_KEY_DEFINE(key, len) + +#include "services/common/analytics/analytics.def" + +#undef PBL_ANALYTICS_METRIC_DEFINE_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_UNSIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_SCALED_SIGNED +#undef PBL_ANALYTICS_METRIC_DEFINE_TIMER +#undef PBL_ANALYTICS_METRIC_DEFINE_STRING \ No newline at end of file diff --git a/third_party/memfault/port/include/memfault_platform_config.h b/third_party/memfault/port/include/memfault_platform_config.h index 059193bb6..605892b23 100644 --- a/third_party/memfault/port/include/memfault_platform_config.h +++ b/third_party/memfault/port/include/memfault_platform_config.h @@ -36,7 +36,4 @@ // slightly shorter length here #define MEMFAULT_DATA_EXPORT_CHUNK_MAX_LEN 50 -#define MEMFAULT_REBOOT_REASON_CUSTOM_ENABLE 1 - -#define MEMFAULT_METRICS_BATTERY_ENABLE 1 -#define MEMFAULT_METRICS_CONNECTIVITY_CONNECTED_TIME 1 \ No newline at end of file +#define MEMFAULT_REBOOT_REASON_CUSTOM_ENABLE 1 \ No newline at end of file diff --git a/third_party/memfault/port/src/memfault_platform_metrics.c b/third_party/memfault/port/src/memfault_platform_metrics.c deleted file mode 100644 index 51b45fea8..000000000 --- a/third_party/memfault/port/src/memfault_platform_metrics.c +++ /dev/null @@ -1,26 +0,0 @@ -/* SPDX-CopyrightText: 2026 Core Devices LLC */ -/* SPDX-License-Identifier: Apache-2.0 */ - -#include "kernel/kernel_heap.h" -#include "memfault/components.h" -#include "drivers/imu/lsm6dso/lsm6dso.h" -#include "pbl/services/common/battery/battery_state.h" -#include "util/heap.h" -#include "pbl/services/common/analytics/analytics.h" -#include "shell/normal/watchface.h" -#include "process_management/app_install_manager.h" - -int memfault_platform_get_stateofcharge(sMfltPlatformBatterySoc *soc) { - BatteryChargeState chargestate = battery_get_charge_state(); - - *soc = (sMfltPlatformBatterySoc){ - .soc = chargestate.charge_percent, - .discharging = !chargestate.is_charging, - }; - - return 0; -} - -void memfault_metrics_heartbeat_collect_data(void) { - analytics_external_update(); -} diff --git a/third_party/memfault/port/src/memfault_platform_port.c b/third_party/memfault/port/src/memfault_platform_port.c index 4f2da36f5..c3bfd9845 100644 --- a/third_party/memfault/port/src/memfault_platform_port.c +++ b/third_party/memfault/port/src/memfault_platform_port.c @@ -17,6 +17,9 @@ #include "system/logging.h" #include "system/version.h" +#include "FreeRTOS.h" +#include "task.h" + // Buffer used to store formatted string for output #define MEMFAULT_DEBUG_LOG_BUFFER_SIZE_BYTES \ (sizeof("2024-11-27T14:19:29Z|123456780 I ") + MEMFAULT_DATA_EXPORT_BASE64_CHUNK_MAX_LEN) @@ -177,8 +180,6 @@ int memfault_platform_boot(void) { }; memfault_metrics_boot(evt_storage, &boot_info); - memfault_metrics_battery_boot(); - memfault_build_info_dump(); memfault_device_info_dump(); @@ -191,8 +192,6 @@ int memfault_platform_boot(void) { return 0; } -#include "FreeRTOS.h" - // [MJ] FIXME: We shouldinstead use the Memfault FreeRTOS port, but it requires // a newer version of FreeRTOS + assumes FreeRTOS timers are available. uint64_t memfault_platform_get_time_since_boot_ms(void) { @@ -213,17 +212,14 @@ uint64_t memfault_platform_get_time_since_boot_ms(void) { return (s_elapsed_ticks * 1000) / configTICK_RATE_HZ; } -static TimerID s_memfault_heartbeat_timer; - -static void prv_memfault_metrics_timer_cb(void *data) { - MemfaultPlatformTimerCallback *callback = (MemfaultPlatformTimerCallback *)data; - callback(); -} +static MemfaultPlatformTimerCallback *s_memfault_heartbeat_callback; bool memfault_platform_metrics_timer_boot(uint32_t period_sec, MemfaultPlatformTimerCallback callback) { - s_memfault_heartbeat_timer = new_timer_create(); - new_timer_start(s_memfault_heartbeat_timer, period_sec * 1000, prv_memfault_metrics_timer_cb, - (void *)callback, TIMER_START_FLAG_REPEATING); + s_memfault_heartbeat_callback = callback; return true; } + +void memfault_platform_heartbeat(void) { + s_memfault_heartbeat_callback(); +} diff --git a/tools/analytics_heartbeat_layout.py b/tools/analytics_heartbeat_layout.py new file mode 100755 index 000000000..995ab4b41 --- /dev/null +++ b/tools/analytics_heartbeat_layout.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Extract the native_heartbeat_record struct layout from an ELF file using DWARF +debug info. Useful for building parsers for the analytics DLS blob. + +Usage: + python3 tools/analytics_heartbeat_layout.py +""" + +import argparse +import struct +import sys + +from elftools.elf.elffile import ELFFile + + +STRUCT_NAME = "native_heartbeat_record" +HEADER_FIELDS = {"version", "timestamp", "build_id"} +METRIC_PREFIX = "metric_" + +# Map DWARF base type encodings to human-readable names +_DW_ATE_NAMES = { + 0x05: "signed", + 0x06: "char", + 0x07: "unsigned", + 0x08: "unsigned char", +} + + +def _get_type_die(die): + """Follow DW_AT_type references until we reach a base or array type.""" + while True: + if "DW_AT_type" not in die.attributes: + return die + ref = die.attributes["DW_AT_type"].value + die = die.get_DIE_from_attribute("DW_AT_type") + return die + + +def _resolve_type_info(die): + """Resolve a member's type to (type_name, is_array, array_count).""" + type_die = ( + die.get_DIE_from_attribute("DW_AT_type") + if "DW_AT_type" in die.attributes + else die + ) + + is_array = False + array_count = 0 + + # Walk through typedefs, const, volatile qualifiers + while type_die.tag in ( + "DW_TAG_typedef", + "DW_TAG_const_type", + "DW_TAG_volatile_type", + ): + if "DW_AT_type" not in type_die.attributes: + break + type_die = type_die.get_DIE_from_attribute("DW_AT_type") + + # Handle arrays + if type_die.tag == "DW_TAG_array_type": + is_array = True + elem_type = type_die.get_DIE_from_attribute("DW_AT_type") + # Get array bound from subrange child + for child in type_die.iter_children(): + if child.tag == "DW_TAG_subrange_type": + if "DW_AT_upper_bound" in child.attributes: + array_count = child.attributes["DW_AT_upper_bound"].value + 1 + elif "DW_AT_count" in child.attributes: + array_count = child.attributes["DW_AT_count"].value + # Resolve element type through typedefs + while elem_type.tag in ( + "DW_TAG_typedef", + "DW_TAG_const_type", + "DW_TAG_volatile_type", + ): + if "DW_AT_type" not in elem_type.attributes: + break + elem_type = elem_type.get_DIE_from_attribute("DW_AT_type") + type_die = elem_type + + # Get base type name + if type_die.tag == "DW_TAG_base_type": + encoding = type_die.attributes.get("DW_AT_encoding") + byte_size = type_die.attributes.get("DW_AT_byte_size") + if encoding and byte_size: + enc_val = encoding.value + size_val = byte_size.value + enc_name = _DW_ATE_NAMES.get(enc_val, f"enc_{enc_val}") + if is_array and enc_val in (0x06, 0x08) and size_val == 1: # char array + return f"char[{array_count}]", True, array_count + else: + type_name = f"{enc_name}{size_val * 8}" + if is_array: + type_name = f"{type_name}[{array_count}]" + return type_name, is_array, array_count + name = type_die.attributes.get("DW_AT_name") + if name: + type_name = ( + name.value.decode() + if isinstance(name.value, bytes) + else str(name.value) + ) + if is_array: + type_name = f"{type_name}[{array_count}]" + return type_name, is_array, array_count + + return "unknown", is_array, array_count + + +def _die_name(die): + """Extract the name string from a DIE, or None.""" + attr = die.attributes.get("DW_AT_name") + if attr is None: + return None + return attr.value.decode() if isinstance(attr.value, bytes) else str(attr.value) + + +def _find_cu_offset(elf_file, dwarf_info): + """Find the CU offset containing STRUCT_NAME using a raw .debug_str + + .debug_info binary search, avoiding full DWARF parsing of every CU. + """ + debug_str = elf_file.get_section_by_name(".debug_str") + debug_info = elf_file.get_section_by_name(".debug_info") + if not debug_str or not debug_info: + return None + + str_offset = debug_str.data().find(STRUCT_NAME.encode() + b"\x00") + if str_offset == -1: + return None + + # Find where this string offset is referenced in .debug_info + needle = struct.pack("6} {'Size':>4} {'Type':<16} {'Field'}") + print(sep) + for name, offset, byte_size, type_name in header_fields: + size_str = str(byte_size) if byte_size is not None else "?" + print(f"{offset:>6} {size_str:>4} {type_name:<16} {name}") + print(sep) + + if metric_fields: + print(f"\nMetrics:") + print(sep) + print(f"{'Offset':>6} {'Size':>4} {'Type':<16} {'Field'}") + print(sep) + prev_display_name = None + for name, offset, byte_size, type_name in metric_fields: + size_str = str(byte_size) if byte_size is not None else "?" + display_name = ( + name[len(METRIC_PREFIX) :] + if name.startswith(METRIC_PREFIX) + else name + ) + if display_name.endswith("_scale"): + scaled_field = prev_display_name or display_name + print( + f"{offset:>6} {size_str:>4} {type_name:<16} ↳ scale for {scaled_field}" + ) + else: + print( + f"{offset:>6} {size_str:>4} {type_name:<16} {display_name}" + ) + prev_display_name = display_name + print(sep) + + +def main(): + parser = argparse.ArgumentParser( + description=f"Extract {STRUCT_NAME} layout from ELF DWARF info" + ) + parser.add_argument("elf_file", help="Path to the ELF file with debug info") + args = parser.parse_args() + + dump_layout(args.elf_file) + + +if __name__ == "__main__": + main()