diff --git a/src/fw/applib/pressure_service.c b/src/fw/applib/pressure_service.c new file mode 100644 index 000000000..0f5e7f28c --- /dev/null +++ b/src/fw/applib/pressure_service.c @@ -0,0 +1,199 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "pressure_service.h" +#include "pressure_service_private.h" + +#include "drivers/pressure.h" +#include "process_state/app_state/app_state.h" +#include "syscall/syscall_internal.h" +#include "system/logging.h" + +#include + +#define STANDARD_SEA_LEVEL_PA 101325 + +// Timer intervals in ms for each ODR preset +static uint32_t prv_odr_to_interval_ms(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return 1000; + case PRESSURE_ODR_5HZ: return 200; + case PRESSURE_ODR_10HZ: return 100; + case PRESSURE_ODR_25HZ: return 40; + case PRESSURE_ODR_50HZ: return 20; + default: return 1000; + } +} + +static PressureServiceState *prv_get_state(void) { + return app_state_get_pressure_state(); +} + +static void prv_compute_altitude(PressureServiceState *state, PressureData *data) { + int32_t ref = state->ref_pressure_pa; + if (ref <= 0) { + ref = STANDARD_SEA_LEVEL_PA; + } + + if (state->use_full_formula) { + data->altitude_cm = pressure_get_altitude_full_cm(data->pressure_pa, ref); + } else { + data->altitude_cm = pressure_get_altitude_cm(data->pressure_pa, ref); + } +} + +// Syscall wrapper for hardware access from the timer callback, which runs in +// unprivileged app context. +DEFINE_SYSCALL(bool, sys_pressure_read_and_compute, PressureData *data) { + PressureServiceState *state = prv_get_state(); + + if (!pressure_read(&data->pressure_pa, &data->temperature_centideg)) { + return false; + } + + prv_compute_altitude(state, data); + return true; +} + +static void prv_timer_callback(void *context) { + PressureServiceState *state = prv_get_state(); + if (!state->handler) { + return; + } + + PressureData data = { 0 }; + if (!sys_pressure_read_and_compute(&data)) { + return; + } + + state->handler(&data); +} + +// All exported functions use DEFINE_SYSCALL to escalate privileges, since they +// are called from unprivileged app code via the SDK jump table but need to +// access hardware drivers (pressure_read, pressure_set_odr). + +DEFINE_SYSCALL(void, pressure_service_subscribe, PressureDataHandler handler, PressureODR odr) { + PressureServiceState *state = prv_get_state(); + + // Clean up any existing subscription + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + + state->handler = handler; + state->odr = odr; + + if (!handler) { + return; + } + + // Configure hardware for the requested rate + pressure_set_odr(odr); + + uint32_t interval_ms = prv_odr_to_interval_ms(odr); + state->poll_timer = app_timer_register_repeatable(interval_ms, + prv_timer_callback, + NULL, + true /* repeating */); +} + +DEFINE_SYSCALL(void, pressure_service_unsubscribe, void) { + PressureServiceState *state = prv_get_state(); + + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + state->handler = NULL; + + // Drop hardware back to low-power mode + pressure_set_odr(PRESSURE_ODR_1HZ); +} + +DEFINE_SYSCALL(bool, pressure_service_set_data_rate, PressureODR odr) { + PressureServiceState *state = prv_get_state(); + + if (!state->handler || !state->poll_timer) { + return false; + } + + if (!pressure_set_odr(odr)) { + return false; + } + + state->odr = odr; + uint32_t interval_ms = prv_odr_to_interval_ms(odr); + app_timer_reschedule(state->poll_timer, interval_ms); + return true; +} + +DEFINE_SYSCALL(bool, pressure_service_set_reference, void) { + PressureServiceState *state = prv_get_state(); + + int32_t pressure_pa; + if (!pressure_read(&pressure_pa, NULL)) { + return false; + } + + state->ref_pressure_pa = pressure_pa; + return true; +} + +DEFINE_SYSCALL(void, pressure_service_set_reference_pressure, int32_t ref_pressure_pa) { + PressureServiceState *state = prv_get_state(); + state->ref_pressure_pa = ref_pressure_pa; +} + +DEFINE_SYSCALL(void, pressure_service_use_full_formula, bool enable) { + PressureServiceState *state = prv_get_state(); + state->use_full_formula = enable; +} + +DEFINE_SYSCALL(bool, pressure_service_peek, PressureData *data) { + if (!data) { + return false; + } + + PressureServiceState *state = prv_get_state(); + + if (!pressure_read(&data->pressure_pa, &data->temperature_centideg)) { + return false; + } + + prv_compute_altitude(state, data); + return true; +} + +DEFINE_SYSCALL(bool, pressure_service_set_filter_mode, PressureFilterMode mode) { + return pressure_set_filter_mode(mode); +} + +DEFINE_SYSCALL(int32_t, pressure_service_get_altitude_cm, int32_t pressure_pa, + int32_t ref_pressure_pa) { + PressureServiceState *state = prv_get_state(); + if (state->use_full_formula) { + return pressure_get_altitude_full_cm(pressure_pa, ref_pressure_pa); + } else { + return pressure_get_altitude_cm(pressure_pa, ref_pressure_pa); + } +} + +void pressure_service_state_init(PressureServiceState *state) { + *state = (PressureServiceState) { + .handler = NULL, + .poll_timer = NULL, + .ref_pressure_pa = 0, + .odr = PRESSURE_ODR_1HZ, + .use_full_formula = false, + }; +} + +void pressure_service_state_deinit(PressureServiceState *state) { + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + state->handler = NULL; +} diff --git a/src/fw/applib/pressure_service.h b/src/fw/applib/pressure_service.h new file mode 100644 index 000000000..5855e6a30 --- /dev/null +++ b/src/fw/applib/pressure_service.h @@ -0,0 +1,104 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "drivers/pressure.h" + +#include +#include + +//! @addtogroup Foundation +//! @{ +//! @addtogroup EventService +//! @{ +//! @addtogroup PressureService +//! +//! \brief Using the Pebble barometric pressure sensor +//! +//! The PressureService provides access to the barometric pressure sensor for +//! altitude tracking. Apps can subscribe to periodic pressure/altitude readings, +//! configure the data rate, set a reference altitude, and choose between a fast +//! linear approximation or the full barometric formula for altitude calculation. +//! +//! Typical usage for a skydiving altimeter: +//! \code +//! // On the ground, before jump: +//! pressure_service_subscribe(my_handler, PRESSURE_ODR_1HZ); +//! pressure_service_set_reference(); // zero altitude here +//! pressure_service_use_full_formula(true); +//! +//! // When altitude > 30m, switch to high rate: +//! pressure_service_set_data_rate(PRESSURE_ODR_25HZ); +//! +//! // On landing, drop back: +//! pressure_service_set_data_rate(PRESSURE_ODR_1HZ); +//! \endcode +//! @{ + +//! Pressure/altitude data delivered to the app handler +typedef struct { + int32_t pressure_pa; //!< Absolute pressure in Pascals + int32_t temperature_centideg; //!< Temperature in 0.01 degrees C + int32_t altitude_cm; //!< Altitude in centimeters relative to reference +} PressureData; + +//! Callback type for pressure data events +//! @param data Pointer to the latest pressure reading +typedef void (*PressureDataHandler)(PressureData *data); + +//! Subscribe to the pressure data service. Once subscribed, the handler is +//! called at the rate specified by \p odr. +//! @param handler Callback to receive pressure data +//! @param odr The desired output data rate +void pressure_service_subscribe(PressureDataHandler handler, PressureODR odr); + +//! Unsubscribe from the pressure data service. +void pressure_service_unsubscribe(void); + +//! Change the data rate while subscribed. +//! This reconfigures the sensor hardware for the new rate. +//! @param odr The desired output data rate +//! @return true if the rate was changed successfully +bool pressure_service_set_data_rate(PressureODR odr); + +//! Capture the current pressure as the reference (altitude = 0). +//! Subsequent altitude readings will be relative to this reference. +//! If no reference is set, altitude is computed relative to standard +//! sea-level pressure (101325 Pa). +//! @return true if the reference was captured successfully +bool pressure_service_set_reference(void); + +//! Set the reference pressure to an explicit value. +//! @param ref_pressure_pa Reference pressure in Pascals +void pressure_service_set_reference_pressure(int32_t ref_pressure_pa); + +//! Enable or disable the full barometric formula for altitude calculation. +//! When disabled, a faster linear approximation is used (accurate within ~1000m). +//! When enabled, a lookup-table-based hypsometric formula is used (accurate to ~7500m). +//! @param enable true to use the full formula, false for linear approximation +void pressure_service_use_full_formula(bool enable); + +//! Read the current pressure data without a subscription (one-shot). +//! @param[out] data Pointer to a PressureData struct to fill +//! @return true if the read succeeded +bool pressure_service_peek(PressureData *data); + +//! Calculate altitude relative to a given reference pressure. +//! Uses the full hypsometric formula or linear approximation depending on +//! the current formula setting (see \ref pressure_service_use_full_formula). +//! @param pressure_pa Current pressure in Pascals +//! @param ref_pressure_pa Reference pressure in Pascals (e.g. 101325 for MSL) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_service_get_altitude_cm(int32_t pressure_pa, int32_t ref_pressure_pa); + +//! Set the hardware IIR filter mode on the pressure sensor. +//! Use PRESSURE_FILTER_NONE for fastest response (e.g. skydiving altimeters). +//! Use PRESSURE_FILTER_SMOOTH for smoother readings (e.g. weather monitoring). +//! @param mode The desired filter mode +//! @return true if the mode was set successfully +bool pressure_service_set_filter_mode(PressureFilterMode mode); + +//! @} // end addtogroup PressureService +//! @} // end addtogroup EventService +//! @} // end addtogroup Foundation diff --git a/src/fw/applib/pressure_service_private.h b/src/fw/applib/pressure_service_private.h new file mode 100644 index 000000000..d000dc5eb --- /dev/null +++ b/src/fw/applib/pressure_service_private.h @@ -0,0 +1,21 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "pressure_service.h" +#include "app_timer.h" + +typedef struct PressureServiceState { + PressureDataHandler handler; + AppTimer *poll_timer; + int32_t ref_pressure_pa; //!< Reference pressure for altitude (0 = use standard 101325 Pa) + PressureODR odr; + bool use_full_formula; +} PressureServiceState; + +//! Initialize the pressure service state for a new app. Called during app_state_init. +void pressure_service_state_init(PressureServiceState *state); + +//! Clean up any active subscriptions. Called during app teardown. +void pressure_service_state_deinit(PressureServiceState *state); diff --git a/src/fw/console/prompt_commands.h b/src/fw/console/prompt_commands.h index 45da26b41..57e39827c 100644 --- a/src/fw/console/prompt_commands.h +++ b/src/fw/console/prompt_commands.h @@ -64,6 +64,10 @@ extern void command_accel_status(void); extern void command_accel_selftest(void); extern void command_accel_softreset(void); +extern void command_pressure_read(void); +extern void command_pressure_debug(void); +extern void command_pressure_reinit(void); + extern void command_dump_flash(const char*, const char*); extern void command_crc_flash(const char*, const char*); extern void command_format_flash(void); @@ -297,6 +301,9 @@ extern void command_force_deepwfi(const char *arg); static const Command s_prompt_commands[] = { // PULSE entry point, needed for anything PULSE-related to work { "PULSEv1", pulse_start, 0 }, + { "baroreinit", command_pressure_reinit, 0 }, + { "barodbg", command_pressure_debug, 0 }, + { "baro", command_pressure_read, 0 }, #if KEEP_NON_ESSENTIAL_COMMANDS == 1 // ==================================================================================== // NOTE: The following commands are used by test automation. diff --git a/src/fw/drivers/i2c/definitions.h b/src/fw/drivers/i2c/definitions.h index 01d0be457..ff41beba0 100644 --- a/src/fw/drivers/i2c/definitions.h +++ b/src/fw/drivers/i2c/definitions.h @@ -64,6 +64,14 @@ typedef struct I2CBusState { int user_count; #if MICRO_FAMILY_STM32F4 RtcTicks last_rail_stop_ticks; +#endif +#if MICRO_FAMILY_NRF5 + // Per-bus write buffer for nRF52840 TWIM errata #219 workaround. + // DMA reads from this buffer during the transfer, so it must outlive the call + // to nrfx_twim_xfer(). Currently safe because transfers are synchronous. + // Will NOT work if transfers become async without additional lifetime guarantees. +#define I2C_WRITE_BUF_MAX 32 + uint8_t write_buf[I2C_WRITE_BUF_MAX]; #endif SemaphoreHandle_t event_semaphore; PebbleMutex *bus_mutex; diff --git a/src/fw/drivers/i2c/nrf5.c b/src/fw/drivers/i2c/nrf5.c index 08e59c7ba..fb82d3037 100644 --- a/src/fw/drivers/i2c/nrf5.c +++ b/src/fw/drivers/i2c/nrf5.c @@ -8,6 +8,8 @@ #include "system/passert.h" #include "drivers/periph_config.h" + +#include #include "FreeRTOS.h" #include @@ -63,19 +65,27 @@ void i2c_hal_init_transfer(I2CBus *bus) { void i2c_hal_start_transfer(I2CBus *bus) { nrfx_twim_xfer_desc_t desc; - + desc.address = bus->state->transfer.device_address >> 1; if (bus->state->transfer.type == I2CTransferType_SendRegisterAddress) { if (bus->state->transfer.direction == I2CTransferDirection_Read) { desc.type = NRFX_TWIM_XFER_TXRX; + desc.primary_length = 1; + desc.p_primary_buf = &bus->state->transfer.register_address; + desc.secondary_length = bus->state->transfer.size; + desc.p_secondary_buf = bus->state->transfer.data; } else { - desc.type = NRFX_TWIM_XFER_TXTX; + // Combine register address + data into one TX (nRF52840 TWIM errata #219) + PBL_ASSERTN(bus->state->transfer.size + 1 <= I2C_WRITE_BUF_MAX); + bus->state->write_buf[0] = bus->state->transfer.register_address; + memcpy(&bus->state->write_buf[1], bus->state->transfer.data, + bus->state->transfer.size); + desc.type = NRFX_TWIM_XFER_TX; + desc.primary_length = bus->state->transfer.size + 1; + desc.p_primary_buf = bus->state->write_buf; + desc.secondary_length = 0; + desc.p_secondary_buf = NULL; } - desc.primary_length = 1; - desc.p_primary_buf = &bus->state->transfer.register_address; - - desc.secondary_length = bus->state->transfer.size; - desc.p_secondary_buf = bus->state->transfer.data; } else { if (bus->state->transfer.direction == I2CTransferDirection_Read) { desc.type = NRFX_TWIM_XFER_RX; @@ -85,8 +95,9 @@ void i2c_hal_start_transfer(I2CBus *bus) { desc.primary_length = bus->state->transfer.size; desc.p_primary_buf = bus->state->transfer.data; desc.secondary_length = 0; + desc.p_secondary_buf = NULL; } - + nrfx_err_t rv = nrfx_twim_xfer(&bus->hal->twim, &desc, 0); PBL_ASSERTN(rv == NRFX_SUCCESS); } diff --git a/src/fw/drivers/pressure.h b/src/fw/drivers/pressure.h index 05a583748..ad253c8b7 100644 --- a/src/fw/drivers/pressure.h +++ b/src/fw/drivers/pressure.h @@ -4,6 +4,55 @@ #pragma once #include +#include //! Initialize the pressure sensor driver. Call this once at startup. void pressure_init(void); + +//! Read the current pressure and temperature from the sensor. +//! @param pressure_pa Output: pressure in Pascals (NULL to skip) +//! @param temperature_centideg Output: temperature in 0.01 degrees C (NULL to skip) +//! @return true if read succeeded, false on error or if sensor not initialized +bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg); + +//! Calculate altitude from pressure using a linear approximation of the +//! barometric formula. Accurate within ~1000m of the reference pressure altitude. +//! @param pressure_pa Current pressure in Pascals +//! @param sea_level_pa Reference sea-level pressure in Pascals (e.g. 101325) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_get_altitude_cm(int32_t pressure_pa, int32_t sea_level_pa); + +//! Calculate altitude using the full hypsometric barometric formula with a +//! lookup table. Accurate across the full range 0–7500m. +//! @param pressure_pa Current pressure in Pascals +//! @param ref_pressure_pa Reference pressure in Pascals (e.g. ground-level reading) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_get_altitude_full_cm(int32_t pressure_pa, int32_t ref_pressure_pa); + +//! BMP390 output data rate presets +typedef enum { + PRESSURE_ODR_1HZ = 0, //!< 1 Hz — low power, good for background monitoring + PRESSURE_ODR_5HZ, //!< 5 Hz — moderate rate + PRESSURE_ODR_10HZ, //!< 10 Hz — responsive + PRESSURE_ODR_25HZ, //!< 25 Hz — high rate, suitable for skydiving + PRESSURE_ODR_50HZ, //!< 50 Hz — very high rate +} PressureODR; + +//! Configure the sensor output data rate and adjust oversampling/filter +//! to match. Higher rates reduce oversampling for faster response. +//! @param odr The desired output data rate +//! @return true if configuration succeeded +bool pressure_set_odr(PressureODR odr); + +//! IIR filter modes for the pressure sensor +typedef enum { + PRESSURE_FILTER_NONE = 0, //!< No IIR filtering — fastest response + PRESSURE_FILTER_SMOOTH, //!< IIR filtering — smoother readings, adds latency +} PressureFilterMode; + +//! Set the hardware IIR filter mode. Takes effect on the next +//! pressure_set_odr() call or immediately if already in normal mode. +//! Defaults to PRESSURE_FILTER_NONE. +//! @param mode The desired filter mode +//! @return true if the mode was set successfully +bool pressure_set_filter_mode(PressureFilterMode mode); diff --git a/src/fw/drivers/pressure/bmp390.c b/src/fw/drivers/pressure/bmp390.c index 7d5446a18..25d819bd9 100644 --- a/src/fw/drivers/pressure/bmp390.c +++ b/src/fw/drivers/pressure/bmp390.c @@ -4,39 +4,579 @@ #include "board/board.h" #include "drivers/pressure.h" #include "drivers/i2c.h" +#include "console/prompt.h" +#include "os/mutex.h" #include "system/logging.h" -#define BMP390_CHIP_ID 0x00 -#define BMP390_CHIP_ID_VALUE 0x60 -#define BMP390_PWR_CTRL 0x1B +#include "kernel/util/delay.h" +#include +#include -static bool prv_read_register(I2CSlavePort *i2c, uint8_t register_address, uint8_t *result) { - i2c_use(i2c); - bool rv = i2c_write_block(i2c, 1, ®ister_address); - if (rv) - rv = i2c_read_block(i2c, 1, result); - i2c_release(i2c); +// BMP390 Register Map +#define BMP390_REG_CHIP_ID 0x00 +#define BMP390_REG_DATA 0x04 // 6 bytes: press[0:2], temp[0:2] +#define BMP390_REG_INT_STATUS 0x11 +#define BMP390_REG_PWR_CTRL 0x1B +#define BMP390_REG_OSR 0x1C +#define BMP390_REG_ODR 0x1D +#define BMP390_REG_CONFIG 0x1F +#define BMP390_REG_CALIB_DATA 0x31 // 21 bytes of calibration coefficients +#define BMP390_REG_CMD 0x7E + +#define BMP390_CHIP_ID_VALUE 0x60 +#define BMP390_CALIB_DATA_LEN 21 +#define BMP390_DATA_LEN 6 + +// PWR_CTRL bits +#define BMP390_PRESS_EN (1 << 0) +#define BMP390_TEMP_EN (1 << 1) +#define BMP390_MODE_NORMAL (3 << 4) +#define BMP390_MODE_FORCED (1 << 4) + +// Oversampling settings (for OSR register) +// pressure osr[2:0], temperature osr[5:3] +#define BMP390_OSR_P_1X 0x00 // 1x pressure oversampling +#define BMP390_OSR_P_2X 0x01 // 2x pressure oversampling +#define BMP390_OSR_P_4X 0x02 // 4x pressure oversampling +#define BMP390_OSR_P_8X 0x03 // 8x pressure oversampling +#define BMP390_OSR_T_1X (0x00 << 3) // 1x temperature oversampling + +// IIR filter coefficients (for CONFIG register, bits [3:1]) +#define BMP390_IIR_COEFF_0 (0x00 << 1) // Off +#define BMP390_IIR_COEFF_1 (0x01 << 1) +#define BMP390_IIR_COEFF_3 (0x02 << 1) +#define BMP390_IIR_COEFF_7 (0x03 << 1) +#define BMP390_IIR_COEFF_15 (0x04 << 1) +#define BMP390_IIR_COEFF_31 (0x05 << 1) + +// ODR register values (from BMP390 datasheet Table 22) +#define BMP390_ODR_200HZ 0x00 +#define BMP390_ODR_100HZ 0x01 +#define BMP390_ODR_50HZ 0x02 +#define BMP390_ODR_25HZ 0x03 +#define BMP390_ODR_12P5HZ 0x04 +#define BMP390_ODR_6P25HZ 0x05 +#define BMP390_ODR_3P125HZ 0x06 +#define BMP390_ODR_1P5625HZ 0x07 +#define BMP390_ODR_0P78125HZ 0x08 +#define BMP390_ODR_0P390HZ 0x09 + +// INT_STATUS bits +#define BMP390_DRDY (1 << 3) + +// Calibration coefficient storage +typedef struct { + uint16_t par_t1; + uint16_t par_t2; + int8_t par_t3; + int16_t par_p1; + int16_t par_p2; + int8_t par_p3; + int8_t par_p4; + uint16_t par_p5; + uint16_t par_p6; + int8_t par_p7; + int8_t par_p8; + int16_t par_p9; + int8_t par_p10; + int8_t par_p11; + int64_t t_lin; // linearized temperature for pressure compensation +} BMP390CalibData; + +static BMP390CalibData s_calib; +static bool s_initialized; +static PebbleMutex *s_mutex; +static PressureFilterMode s_filter_mode; + +// I2C helpers using the PebbleOS I2C API +static bool prv_read_register(uint8_t reg, uint8_t *result) { + i2c_use(I2C_BMP390); + bool rv = i2c_read_register(I2C_BMP390, reg, result); + i2c_release(I2C_BMP390); + return rv; +} + +static bool prv_read_register_block(uint8_t reg, uint32_t len, uint8_t *buf) { + i2c_use(I2C_BMP390); + bool rv = i2c_read_register_block(I2C_BMP390, reg, len, buf); + i2c_release(I2C_BMP390); return rv; } -static bool prv_write_register(I2CSlavePort *i2c, uint8_t register_address, uint8_t datum) { - i2c_use(i2c); - uint8_t d[2] = { register_address, datum }; - bool rv = i2c_write_block(i2c, 2, d); - i2c_release(i2c); +static bool prv_write_register(uint8_t reg, uint8_t value) { + i2c_use(I2C_BMP390); + bool rv = i2c_write_register(I2C_BMP390, reg, value); + i2c_release(I2C_BMP390); return rv; } +static void prv_parse_calib_data(const uint8_t *reg_data) { + s_calib.par_t1 = (uint16_t)(reg_data[1] << 8 | reg_data[0]); + s_calib.par_t2 = (uint16_t)(reg_data[3] << 8 | reg_data[2]); + s_calib.par_t3 = (int8_t)reg_data[4]; + s_calib.par_p1 = (int16_t)(reg_data[6] << 8 | reg_data[5]); + s_calib.par_p2 = (int16_t)(reg_data[8] << 8 | reg_data[7]); + s_calib.par_p3 = (int8_t)reg_data[9]; + s_calib.par_p4 = (int8_t)reg_data[10]; + s_calib.par_p5 = (uint16_t)(reg_data[12] << 8 | reg_data[11]); + s_calib.par_p6 = (uint16_t)(reg_data[14] << 8 | reg_data[13]); + s_calib.par_p7 = (int8_t)reg_data[15]; + s_calib.par_p8 = (int8_t)reg_data[16]; + s_calib.par_p9 = (int16_t)(reg_data[18] << 8 | reg_data[17]); + s_calib.par_p10 = (int8_t)reg_data[19]; + s_calib.par_p11 = (int8_t)reg_data[20]; +} + +// Integer compensation from Bosch BMP3 Sensor API (BST-BMP390-DS002) +// Returns temperature in units of 0.01 degrees C +static int64_t prv_compensate_temperature(uint32_t uncomp_temp) { + int64_t partial_data1 = (int64_t)(uncomp_temp - ((int64_t)256 * s_calib.par_t1)); + int64_t partial_data2 = (int64_t)(s_calib.par_t2 * partial_data1); + int64_t partial_data3 = (int64_t)(partial_data1 * partial_data1); + int64_t partial_data4 = (int64_t)partial_data3 * s_calib.par_t3; + int64_t partial_data5 = (int64_t)((int64_t)(partial_data2 * 262144) + partial_data4); + int64_t partial_data6 = (int64_t)(partial_data5 / 4294967296); + + // Store t_lin for pressure compensation + s_calib.t_lin = partial_data6; + + return (int64_t)((partial_data6 * 25) / 16384); +} + +// Integer compensation from Bosch BMP3 Sensor API (BST-BMP390-DS002) +// Returns pressure in units of Pa * 100 (i.e. 1/100 Pa) +static uint64_t prv_compensate_pressure(uint32_t uncomp_press) { + int64_t partial_data1 = s_calib.t_lin * s_calib.t_lin; + int64_t partial_data2 = partial_data1 / 64; + int64_t partial_data3 = (partial_data2 * s_calib.t_lin) / 256; + int64_t partial_data4 = (s_calib.par_p8 * partial_data3) / 32; + int64_t partial_data5 = (s_calib.par_p7 * partial_data1) * 16; + int64_t partial_data6 = (s_calib.par_p6 * s_calib.t_lin) * 4194304; + int64_t offset = (s_calib.par_p5 * (int64_t)140737488355328) + + partial_data4 + partial_data5 + partial_data6; + + partial_data2 = (s_calib.par_p4 * partial_data3) / 32; + partial_data4 = (s_calib.par_p3 * partial_data1) * 4; + partial_data5 = ((int64_t)s_calib.par_p2 - (int64_t)16384) * s_calib.t_lin * 2097152; + int64_t sensitivity = (((int64_t)s_calib.par_p1 - (int64_t)16384) * (int64_t)70368744177664) + + partial_data2 + partial_data4 + partial_data5; + + partial_data1 = (sensitivity / 16777216) * uncomp_press; + partial_data2 = s_calib.par_p10 * s_calib.t_lin; + partial_data3 = partial_data2 + ((int64_t)65536 * s_calib.par_p9); + partial_data4 = (partial_data3 * uncomp_press) / 8192; + // Split to avoid overflow + partial_data5 = (uncomp_press * (partial_data4 / 10)) / 512; + partial_data5 = partial_data5 * 10; + partial_data6 = (int64_t)uncomp_press * (int64_t)uncomp_press; + partial_data2 = (s_calib.par_p11 * partial_data6) / 65536; + partial_data3 = (partial_data2 * uncomp_press) / 128; + partial_data4 = (offset / 4) + partial_data1 + partial_data5 + partial_data3; + + return (uint64_t)((uint64_t)partial_data4 * 25) / (uint64_t)1099511627776; +} + void pressure_init(void) { - bool rv; - uint8_t result; + uint8_t chip_id; + s_initialized = false; + s_filter_mode = PRESSURE_FILTER_NONE; + + if (!s_mutex) { + s_mutex = mutex_create(); + } + + if (!prv_read_register(BMP390_REG_CHIP_ID, &chip_id) || + chip_id != BMP390_CHIP_ID_VALUE) { + PBL_LOG_DBG("BMP390 probe failed; chip_id 0x%02x", chip_id); + return; + } + + PBL_LOG_DBG("BMP390 found, chip_id=0x%02x", chip_id); + + // Soft reset to known state + if (!prv_write_register(BMP390_REG_CMD, 0xB6)) { + PBL_LOG_DBG("BMP390 soft reset FAILED"); + } + + // Poll status register for cmd_rdy (bit 4) after reset, with timeout + for (int i = 0; i < 50; i++) { + delay_us(1000); + uint8_t status; + if (prv_read_register(0x03, &status) && (status & 0x10)) { + PBL_LOG_DBG("BMP390 ready after %d ms", i + 1); + break; + } + } + + // Read factory calibration data + uint8_t calib_raw[BMP390_CALIB_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_CALIB_DATA, BMP390_CALIB_DATA_LEN, calib_raw)) { + PBL_LOG_DBG("BMP390 calibration read failed"); + return; + } + prv_parse_calib_data(calib_raw); + + // Configure: 8x pressure oversampling, 1x temperature, IIR off by default + prv_write_register(BMP390_REG_OSR, BMP390_OSR_P_8X | BMP390_OSR_T_1X); + prv_write_register(BMP390_REG_CONFIG, BMP390_IIR_COEFF_0); + + // Enable pressure + temperature sensors (stay in sleep — reads trigger forced mode) + prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); + + s_initialized = true; + PBL_LOG_DBG("BMP390 initialized"); +} + +// Trigger a forced-mode measurement and wait for data ready +static bool prv_trigger_measurement(void) { + // Trigger forced measurement + prv_write_register(BMP390_REG_PWR_CTRL, + BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_FORCED); + + // Wait for data ready (8x oversampling @ ~5ms conversion = ~40ms typical) + for (int i = 0; i < 100; i++) { + delay_us(1000); + uint8_t int_status; + if (prv_read_register(BMP390_REG_INT_STATUS, &int_status) && + (int_status & BMP390_DRDY)) { + return true; + } + } + return false; +} + +bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg) { + if (!s_initialized) { + return false; + } + + mutex_lock(s_mutex); + + // Trigger forced-mode measurement and wait for completion + if (!prv_trigger_measurement()) { + mutex_unlock(s_mutex); + return false; + } + + // Read 6 bytes of sensor data: pressure[0:2], temperature[0:2] + uint8_t data[BMP390_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data)) { + mutex_unlock(s_mutex); + return false; + } + + uint32_t uncomp_press = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t uncomp_temp = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + + // Temperature must be compensated first — it sets t_lin used by pressure compensation + int64_t temp = prv_compensate_temperature(uncomp_temp); + uint64_t press = prv_compensate_pressure(uncomp_press); + + mutex_unlock(s_mutex); + + // temp is in 0.01 degC, press is in Pa * 100 + if (temperature_centideg) { + *temperature_centideg = (int32_t)temp; + } + if (pressure_pa) { + *pressure_pa = (int32_t)(press / 100); // Convert to Pa + } + + return true; +} + +int32_t pressure_get_altitude_cm(int32_t pressure_pa, int32_t sea_level_pa) { + // Barometric formula approximation using integer math: + // altitude = 44330 * (1 - (P/P0)^0.1903) + // + // For integer approximation, use a first-order Taylor expansion around P0: + // altitude ≈ (P0 - P) * 8435 / P0 (in cm, valid within ~1000m of sea level) + // + // 8435 comes from: 44330 * 0.1903 * 100 (cm conversion) ≈ 843500 / 100 + // More precisely: dh/dP at sea level = -RT/(Mg) ≈ -8.43m/hPa = -0.0843m/Pa + // In cm: 8.43 cm per Pa of pressure difference + + if (sea_level_pa <= 0) { + return 0; + } + + int64_t delta = (int64_t)(sea_level_pa - pressure_pa); + return (int32_t)((delta * 843500) / sea_level_pa); +} + +// Lookup table for full barometric formula: h = 44330 * (1 - (P/P0)^0.190263) +// Index i: altitude_cm for pressure ratio (1024 - i*5) / 1024 +// 129 entries covering 0–7547m +static const int32_t s_altitude_table[] = { + 0, 4127, 8269, 12429, 16605, 20798, 25008, 29236, + 33480, 37743, 42022, 46320, 50636, 54969, 59322, 63692, + 68082, 72490, 76917, 81364, 85829, 90315, 94820, 99346, + 103891, 108457, 113044, 117651, 122279, 126929, 131600, 136293, + 141008, 145744, 150504, 155286, 160090, 164918, 169769, 174644, + 179543, 184465, 189412, 194384, 199381, 204403, 209451, 214524, + 219623, 224749, 229902, 235081, 240288, 245523, 250785, 256076, + 261396, 266744, 272122, 277530, 282968, 288436, 293935, 299466, + 305028, 310622, 316249, 321909, 327602, 333328, 339089, 344885, + 350716, 356583, 362486, 368425, 374402, 380416, 386468, 392560, + 398690, 404861, 411072, 417324, 423618, 429954, 436333, 442756, + 449223, 455735, 462293, 468897, 475548, 482247, 488995, 495793, + 502640, 509539, 516490, 523494, 530552, 537664, 544832, 552057, + 559339, 566681, 574082, 581543, 589067, 596655, 604306, 612023, + 619807, 627660, 635582, 643574, 651640, 659779, 667993, 676285, + 684655, 693106, 701638, 710255, 718957, 727746, 736626, 745597, + 754662, +}; + +#define ALTITUDE_TABLE_ENTRIES 129 +#define ALTITUDE_TABLE_STEP 5 // ratio step per index (at 1024 scale) + +// Use 18-bit precision for the pressure ratio instead of 10-bit. +// At 10 bits, 1 LSB ≈ 98 Pa ≈ 8.3m (27ft) — way too coarse. +// At 18 bits, 1 LSB ≈ 0.38 Pa ≈ 3cm — smooth enough for an altimeter. +#define RATIO_SHIFT 18 +#define RATIO_SCALE (1 << RATIO_SHIFT) // 262144 +#define PRECISION_MULT (RATIO_SCALE / 1024) // 256 +#define TABLE_STEP_SCALED (ALTITUDE_TABLE_STEP * PRECISION_MULT) // 1280 + +int32_t pressure_get_altitude_full_cm(int32_t pressure_pa, int32_t ref_pressure_pa) { + if (ref_pressure_pa <= 0 || pressure_pa <= 0) { + return 0; + } + + // Compute pressure ratio in 1/262144 units: ratio = P * 2^18 / P0 + uint32_t ratio = (uint32_t)(((uint64_t)pressure_pa << RATIO_SHIFT) / ref_pressure_pa); + + // Determine sign: if pressure > reference, altitude is negative (below reference) + bool negative = (pressure_pa > ref_pressure_pa); + if (negative) { + // Mirror: compute altitude for the inverse ratio + ratio = (uint32_t)(((uint64_t)ref_pressure_pa << RATIO_SHIFT) / pressure_pa); + } + + // Clamp to table range + if (ratio >= RATIO_SCALE) { + return 0; + } + uint32_t inv = RATIO_SCALE - ratio; // 0 at reference, increases with altitude + + // Table lookup with linear interpolation + uint32_t idx = inv / TABLE_STEP_SCALED; + uint32_t frac = inv % TABLE_STEP_SCALED; + + if (idx >= ALTITUDE_TABLE_ENTRIES - 1) { + return negative ? -s_altitude_table[ALTITUDE_TABLE_ENTRIES - 1] + : s_altitude_table[ALTITUDE_TABLE_ENTRIES - 1]; + } + + int32_t alt_low = s_altitude_table[idx]; + int32_t alt_high = s_altitude_table[idx + 1]; + int32_t altitude = alt_low + ((int64_t)(alt_high - alt_low) * (int32_t)frac) / TABLE_STEP_SCALED; + + return negative ? -altitude : altitude; +} - rv = prv_read_register(I2C_BMP390, BMP390_CHIP_ID, &result); - if (!rv || result != BMP390_CHIP_ID_VALUE) { - PBL_LOG_DBG("BMP390 probe failed; rv %d, result 0x%02x", rv, result); +// IIR coefficients for each ODR when filtering is enabled +static uint8_t prv_iir_for_odr(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_5HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_10HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_25HZ: return BMP390_IIR_COEFF_15; + case PRESSURE_ODR_50HZ: return BMP390_IIR_COEFF_31; + default: return BMP390_IIR_COEFF_0; + } +} + +bool pressure_set_odr(PressureODR odr) { + if (!s_initialized) { + return false; + } + + mutex_lock(s_mutex); + + // Each ODR preset configures: ODR register, oversampling, and IIR filter. + // Higher rates need lower oversampling to keep up with the output data rate. + uint8_t odr_reg, osr_reg; + switch (odr) { + case PRESSURE_ODR_1HZ: + odr_reg = BMP390_ODR_1P5625HZ; + osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; + break; + case PRESSURE_ODR_5HZ: + odr_reg = BMP390_ODR_6P25HZ; + osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; + break; + case PRESSURE_ODR_10HZ: + odr_reg = BMP390_ODR_12P5HZ; + osr_reg = BMP390_OSR_P_4X | BMP390_OSR_T_1X; + break; + case PRESSURE_ODR_25HZ: + odr_reg = BMP390_ODR_25HZ; + osr_reg = BMP390_OSR_P_2X | BMP390_OSR_T_1X; + break; + case PRESSURE_ODR_50HZ: + odr_reg = BMP390_ODR_50HZ; + osr_reg = BMP390_OSR_P_1X | BMP390_OSR_T_1X; + break; + default: + mutex_unlock(s_mutex); + return false; + } + + uint8_t config_reg = (s_filter_mode == PRESSURE_FILTER_SMOOTH) + ? prv_iir_for_odr(odr) : BMP390_IIR_COEFF_0; + + // Switch to sleep to apply config cleanly, then back to normal + prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); + prv_write_register(BMP390_REG_ODR, odr_reg); + prv_write_register(BMP390_REG_OSR, osr_reg); + prv_write_register(BMP390_REG_CONFIG, config_reg); + prv_write_register(BMP390_REG_PWR_CTRL, + BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_NORMAL); + + mutex_unlock(s_mutex); + return true; +} + +bool pressure_set_filter_mode(PressureFilterMode mode) { + if (!s_initialized) { + return false; + } + + mutex_lock(s_mutex); + s_filter_mode = mode; + + // Apply immediately: update the CONFIG register + uint8_t config_reg = (mode == PRESSURE_FILTER_SMOOTH) + ? BMP390_IIR_COEFF_7 : BMP390_IIR_COEFF_0; + prv_write_register(BMP390_REG_CONFIG, config_reg); + + mutex_unlock(s_mutex); + return true; +} + +void command_pressure_read(void) { + int32_t pressure_pa = 0; + int32_t temperature_centideg = 0; + + if (pressure_read(&pressure_pa, &temperature_centideg)) { + char buffer[64]; + prompt_send_response_fmt(buffer, sizeof(buffer), + "Pressure: %" PRId32 " Pa", pressure_pa); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Temp: %" PRId32 ".%02" PRId32 " C", + temperature_centideg / 100, + temperature_centideg >= 0 + ? temperature_centideg % 100 + : (-temperature_centideg) % 100); + int32_t alt_cm = pressure_get_altitude_cm(pressure_pa, 101325); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Alt (est): %" PRId32 ".%02" PRId32 " m", + alt_cm / 100, + (alt_cm >= 0 ? alt_cm : -alt_cm) % 100); } else { - PBL_LOG_DBG("found the BMP390, setting to low power"); - (void) prv_write_register(I2C_BMP390, BMP390_PWR_CTRL, 0); + prompt_send_response("Pressure read FAILED"); } } + +void command_pressure_reinit(void) { + char buffer[80]; + + // Read chip ID + uint8_t chip_id; + bool ok = prv_read_register(BMP390_REG_CHIP_ID, &chip_id); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Chip ID: 0x%02x (read %s)", chip_id, ok ? "ok" : "FAIL"); + + // Write PWR_CTRL: enable sensors + normal mode + uint8_t pwr = BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_NORMAL; + prv_write_register(BMP390_REG_PWR_CTRL, pwr); + uint8_t readback; + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + prompt_send_response_fmt(buffer, sizeof(buffer), + "PWR_CTRL: wrote 0x%02x read 0x%02x", pwr, readback); + + // If mode bits didn't stick, try forced mode + if ((readback & 0x30) == 0) { + uint8_t forced = BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_FORCED; + prv_write_register(BMP390_REG_PWR_CTRL, forced); + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Forced: wrote 0x%02x read 0x%02x", forced, readback); + + // Wait for measurement in forced mode + delay_us(100000); + + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + uint8_t int_status; + prv_read_register(BMP390_REG_INT_STATUS, &int_status); + prompt_send_response_fmt(buffer, sizeof(buffer), + "After wait: PWR_CTRL=0x%02x DRDY=%d", + readback, (int_status & BMP390_DRDY) ? 1 : 0); + } + + // Read data + uint8_t data[BMP390_DATA_LEN]; + prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data); + uint32_t p = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t t = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Raw P: %" PRIu32 " T: %" PRIu32, p, t); + + // OSR readback + uint8_t osr_rb; + prv_read_register(BMP390_REG_OSR, &osr_rb); + prompt_send_response_fmt(buffer, sizeof(buffer), "OSR: 0x%02x", osr_rb); +} + +void command_pressure_debug(void) { + if (!s_initialized) { + prompt_send_response("BMP390 not initialized"); + return; + } + + char buffer[80]; + + // Read raw data + uint8_t data[BMP390_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data)) { + prompt_send_response("Raw read failed"); + return; + } + + uint32_t uncomp_press = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t uncomp_temp = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + + prompt_send_response_fmt(buffer, sizeof(buffer), + "Raw P: %" PRIu32 " T: %" PRIu32, uncomp_press, uncomp_temp); + + // Read PWR_CTRL to verify sensor mode + uint8_t pwr_ctrl; + prv_read_register(BMP390_REG_PWR_CTRL, &pwr_ctrl); + prompt_send_response_fmt(buffer, sizeof(buffer), + "PWR_CTRL: 0x%02x", pwr_ctrl); + + // Read INT_STATUS to check data ready + uint8_t int_status; + prv_read_register(BMP390_REG_INT_STATUS, &int_status); + prompt_send_response_fmt(buffer, sizeof(buffer), + "INT_STATUS: 0x%02x (DRDY=%d)", int_status, + (int_status & BMP390_DRDY) ? 1 : 0); + + // Dump key calibration coefficients + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_t1=%u par_t2=%u par_t3=%d", + s_calib.par_t1, s_calib.par_t2, s_calib.par_t3); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p1=%d par_p2=%d par_p3=%d par_p4=%d", + s_calib.par_p1, s_calib.par_p2, s_calib.par_p3, s_calib.par_p4); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p5=%u par_p6=%u par_p7=%d par_p8=%d", + s_calib.par_p5, s_calib.par_p6, s_calib.par_p7, s_calib.par_p8); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p9=%d par_p10=%d par_p11=%d", + s_calib.par_p9, s_calib.par_p10, s_calib.par_p11); + prompt_send_response_fmt(buffer, sizeof(buffer), + "t_lin=%" PRId64, s_calib.t_lin); +} diff --git a/src/fw/process_management/pebble_process_info.h b/src/fw/process_management/pebble_process_info.h index 1cf391130..a701cfd30 100644 --- a/src/fw/process_management/pebble_process_info.h +++ b/src/fw/process_management/pebble_process_info.h @@ -155,9 +155,10 @@ typedef enum { // sdk.major:0x5 .minor:0x57 -- Add moddable_createMachine (rev 90) // sdk.major:0x5 .minor:0x58 -- Add size 60 LECO font (rev 91) // sdk.major:0x5 .minor:0x59 -- Add flags to ModdableCreationRecord (rev 92) +// sdk.major:0x5 .minor:0x5a -- Add PressureService API (rev 93) #define PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR 0x5 -#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x59 +#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x5a // The first SDK to ship with 2.x APIs #define PROCESS_INFO_FIRST_2X_SDK_VERSION_MAJOR 0x4 diff --git a/src/fw/process_state/app_state/app_state.c b/src/fw/process_state/app_state/app_state.c index 858b81f2e..5f67db835 100644 --- a/src/fw/process_state/app_state/app_state.c +++ b/src/fw/process_state/app_state/app_state.c @@ -77,6 +77,8 @@ typedef struct { HealthServiceState health_service_state; + PressureServiceState pressure_service_state; + LocaleInfo locale_info; ContentIndicatorsBuffer content_indicators_buffer; @@ -200,6 +202,8 @@ NOINLINE void app_state_init(void) { health_service_state_init(app_state_get_health_service_state()); + pressure_service_state_init(app_state_get_pressure_state()); + locale_init_app_locale(app_state_get_locale_info()); content_indicator_init_buffer(app_state_get_content_indicators_buffer()); @@ -217,6 +221,7 @@ NOINLINE void app_state_init(void) { NOINLINE void app_state_deinit(void) { animation_private_state_deinit(&s_app_state_ptr->animation_state); health_service_state_deinit(app_state_get_health_service_state()); + pressure_service_state_deinit(app_state_get_pressure_state()); unobstructed_area_service_deinit(app_state_get_unobstructed_area_state()); } @@ -317,6 +322,10 @@ HealthServiceState *app_state_get_health_service_state(void) { return &s_app_state_ptr->health_service_state; } +PressureServiceState *app_state_get_pressure_state(void) { + return &s_app_state_ptr->pressure_service_state; +} + ContentIndicatorsBuffer *app_state_get_content_indicators_buffer(void) { return &s_app_state_ptr->content_indicators_buffer; } diff --git a/src/fw/process_state/app_state/app_state.h b/src/fw/process_state/app_state/app_state.h index cffd85a6a..7bb7c78ea 100644 --- a/src/fw/process_state/app_state/app_state.h +++ b/src/fw/process_state/app_state/app_state.h @@ -18,6 +18,7 @@ #include "applib/graphics/text_render.h" #include "applib/health_service.h" #include "applib/health_service_private.h" +#include "applib/pressure_service_private.h" #include "applib/pbl_std/locale.h" #include "applib/plugin_service_private.h" #include "applib/tick_timer_service.h" @@ -123,6 +124,8 @@ ContentIndicatorsBuffer *app_state_get_content_indicators_buffer(void); HealthServiceState *app_state_get_health_service_state(void); +PressureServiceState *app_state_get_pressure_state(void); + RecognizerList *app_state_get_recognizer_list(void); JsRuntimeContext *app_state_get_js_runtime_context(void); diff --git a/src/fw/shell/normal/system_app_registry_list.json b/src/fw/shell/normal/system_app_registry_list.json index 64fb65bbb..19d139142 100644 --- a/src/fw/shell/normal/system_app_registry_list.json +++ b/src/fw/shell/normal/system_app_registry_list.json @@ -707,6 +707,14 @@ "uuid": "cf1e816a-9db0-4511-bbb8-f60c48ca8fac", "bin_resource_id": "RESOURCE_ID_STORED_APP_GOLF", "icon_resource_id": "DEFAULT_MENU_ICON" + }, + { + "id": -100, + "enum": "ALTIMETER", + "name": "Altimeter", + "uuid": "a1e0e4c3-7b2d-4f8a-9c01-d3f5a6b7e8f9", + "bin_resource_id": "RESOURCE_ID_STORED_APP_ALTIMETER", + "icon_resource_id": "DEFAULT_MENU_ICON" } ] } diff --git a/stored_apps/altimeter/appinfo.json b/stored_apps/altimeter/appinfo.json new file mode 100644 index 000000000..019336dee --- /dev/null +++ b/stored_apps/altimeter/appinfo.json @@ -0,0 +1,18 @@ +{ + "uuid": "a1e0e4c3-7b2d-4f8a-9c01-d3f5a6b7e8f9", + "shortName": "Altimeter", + "longName": "Altimeter", + "companyName": "PebbleOS", + "versionCode": 1.0, + "versionLabel": "1.0", + "watchapp": { + "watchface": false, + "onlyShownOnCommunication": false + }, + "appKeys": { + "dummy": 0 + }, + "resources": { + "media": [] + } +} diff --git a/stored_apps/altimeter/src/altimeter.c b/stored_apps/altimeter/src/altimeter.c new file mode 100644 index 000000000..5c0829dc4 --- /dev/null +++ b/stored_apps/altimeter/src/altimeter.c @@ -0,0 +1,195 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include + +typedef struct { + Window *window; + StatusBarLayer *status_layer; + TextLayer *altitude_label; + TextLayer *altitude_value; + TextLayer *altitude_unit; + TextLayer *pressure_label; + TextLayer *pressure_value; + char altitude_buf[16]; + char pressure_buf[32]; + PressureODR current_odr; + bool ref_set; + bool auto_high_rate; +} AppData; + +static AppData s_data; + +static const char *odr_name(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return "1 Hz"; + case PRESSURE_ODR_5HZ: return "5 Hz"; + case PRESSURE_ODR_10HZ: return "10 Hz"; + case PRESSURE_ODR_25HZ: return "25 Hz"; + case PRESSURE_ODR_50HZ: return "50 Hz"; + default: return "?"; + } +} + +static void update_pressure_label(AppData *data, int32_t pressure_pa) { + int32_t hpa = pressure_pa / 100; + int32_t hpa_frac = (pressure_pa % 100) / 10; + snprintf(data->pressure_buf, sizeof(data->pressure_buf), "%ld.%ld hPa (%s)", + (long)hpa, (long)hpa_frac, odr_name(data->current_odr)); + text_layer_set_text(data->pressure_value, data->pressure_buf); +} + +static void pressure_handler(PressureData *pdata) { + AppData *data = &s_data; + + // Altitude in feet (1 cm = 0.0328084 ft, use integer math: ft = cm * 328084 / 10000000) + int32_t alt_cm = pdata->altitude_cm; + int32_t alt_ft_x10 = (int32_t)((int64_t)alt_cm * 328084 / 1000000); + int32_t alt_ft = alt_ft_x10 / 10; + int32_t alt_frac = (alt_ft_x10 < 0 ? -alt_ft_x10 : alt_ft_x10) % 10; + snprintf(data->altitude_buf, sizeof(data->altitude_buf), "%ld.%ld", + (long)alt_ft, (long)alt_frac); + text_layer_set_text(data->altitude_value, data->altitude_buf); + + // Auto-switch to 25Hz when above 50ft relative altitude + int32_t abs_ft = alt_ft < 0 ? -alt_ft : alt_ft; + if (!data->auto_high_rate && abs_ft > 50) { + data->auto_high_rate = true; + data->current_odr = PRESSURE_ODR_25HZ; + pressure_service_set_data_rate(data->current_odr); + } else if (data->auto_high_rate && abs_ft <= 50) { + data->auto_high_rate = false; + data->current_odr = PRESSURE_ODR_1HZ; + pressure_service_set_data_rate(data->current_odr); + } + + // Pressure in hPa with rate in parentheses + update_pressure_label(data, pdata->pressure_pa); +} + +static void up_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + if (data->current_odr < PRESSURE_ODR_50HZ) { + data->current_odr++; + pressure_service_set_data_rate(data->current_odr); + } +} + +static void down_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + if (data->current_odr > PRESSURE_ODR_1HZ) { + data->current_odr--; + pressure_service_set_data_rate(data->current_odr); + } +} + +static void select_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + pressure_service_set_reference(); + data->ref_set = true; + vibes_short_pulse(); +} + +static void config_provider(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, up_click_handler); + window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler); + window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler); +} + +static void window_load(Window *window) { + AppData *data = &s_data; + Layer *root = window_get_root_layer(window); + GRect bounds = layer_get_bounds(root); + int16_t w = bounds.size.w; + + // Status bar — white on black + data->status_layer = status_bar_layer_create(); + status_bar_layer_set_colors(data->status_layer, GColorBlack, GColorWhite); + layer_add_child(root, status_bar_layer_get_layer(data->status_layer)); + + int16_t y = STATUS_BAR_LAYER_HEIGHT + 2; + + // "REL ALT" label + data->altitude_label = text_layer_create(GRect(0, y, w, 18)); + text_layer_set_font(data->altitude_label, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->altitude_label, "REL ALTI"); + text_layer_set_text_alignment(data->altitude_label, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_label, GColorWhite); + text_layer_set_background_color(data->altitude_label, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_label)); + y += 18; + + // Altitude value — big numbers + data->altitude_value = text_layer_create(GRect(0, y, w, 58)); + text_layer_set_font(data->altitude_value, + fonts_get_system_font(FONT_KEY_LECO_42_NUMBERS)); + text_layer_set_text(data->altitude_value, "--.-"); + text_layer_set_text_alignment(data->altitude_value, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_value, GColorWhite); + text_layer_set_background_color(data->altitude_value, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_value)); + y += 50; + + // Unit label + data->altitude_unit = text_layer_create(GRect(0, y, w, 18)); + text_layer_set_font(data->altitude_unit, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->altitude_unit, "feet"); + text_layer_set_text_alignment(data->altitude_unit, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_unit, GColorWhite); + text_layer_set_background_color(data->altitude_unit, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_unit)); + y += 20; + + // Pressure label + data->pressure_label = text_layer_create(GRect(0, y, w, 14)); + text_layer_set_font(data->pressure_label, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->pressure_label, "PRESSURE"); + text_layer_set_text_alignment(data->pressure_label, GTextAlignmentCenter); + text_layer_set_text_color(data->pressure_label, GColorWhite); + text_layer_set_background_color(data->pressure_label, GColorClear); + layer_add_child(root, text_layer_get_layer(data->pressure_label)); + y += 14; + + // Pressure value + rate + data->pressure_value = text_layer_create(GRect(0, y, w, 24)); + text_layer_set_font(data->pressure_value, + fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); + text_layer_set_text(data->pressure_value, "--.- hPa (1 Hz)"); + text_layer_set_text_alignment(data->pressure_value, GTextAlignmentCenter); + text_layer_set_text_color(data->pressure_value, GColorWhite); + text_layer_set_background_color(data->pressure_value, GColorClear); + layer_add_child(root, text_layer_get_layer(data->pressure_value)); + + data->current_odr = PRESSURE_ODR_1HZ; + + // Keep backlight on while altimeter is running + light_enable(true); + + // Subscribe to pressure service and zero on launch + pressure_service_subscribe(pressure_handler, data->current_odr); + pressure_service_use_full_formula(true); + pressure_service_set_reference(); + data->ref_set = true; +} + +static void window_unload(Window *window) { + light_enable(false); + pressure_service_unsubscribe(); +} + +static void handle_init(void) { + AppData *data = &s_data; + data->window = window_create(); + window_set_background_color(data->window, GColorBlack); + window_set_window_handlers(data->window, (WindowHandlers) { + .load = window_load, + .unload = window_unload, + }); + window_set_click_config_provider(data->window, config_provider); + window_stack_push(data->window, true); +} + +int main(void) { + handle_init(); + app_event_loop(); +} diff --git a/tests/fakes/fake_i2c.c b/tests/fakes/fake_i2c.c new file mode 100644 index 000000000..1d39c5b1c --- /dev/null +++ b/tests/fakes/fake_i2c.c @@ -0,0 +1,121 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "fake_i2c.h" +#include "drivers/i2c.h" + +#include + +#define REG_COUNT 256 +#define WRITE_LOG_MAX 256 + +typedef struct { + uint8_t reg; + uint8_t value; +} WriteLogEntry; + +static uint8_t s_registers[REG_COUNT]; +static bool s_fail; +static FakeI2CWriteHook s_write_hook; +static void *s_write_hook_context; +static WriteLogEntry s_write_log[WRITE_LOG_MAX]; +static int s_write_log_count; + +// Provide the I2C_BMP390 symbol the driver expects +static I2CSlavePort s_fake_slave; +I2CSlavePort *const I2C_BMP390 = &s_fake_slave; + +void fake_i2c_init(void) { + memset(s_registers, 0, sizeof(s_registers)); + s_fail = false; + s_write_hook = NULL; + s_write_hook_context = NULL; + s_write_log_count = 0; +} + +void fake_i2c_set_register(uint8_t reg, uint8_t value) { + s_registers[reg] = value; +} + +void fake_i2c_set_register_block(uint8_t reg, const uint8_t *data, uint32_t len) { + for (uint32_t i = 0; i < len && (reg + i) < REG_COUNT; i++) { + s_registers[reg + i] = data[i]; + } +} + +uint8_t fake_i2c_get_register(uint8_t reg) { + return s_registers[reg]; +} + +void fake_i2c_set_fail(bool fail) { + s_fail = fail; +} + +void fake_i2c_set_write_hook(FakeI2CWriteHook hook, void *context) { + s_write_hook = hook; + s_write_hook_context = context; +} + +bool fake_i2c_was_written(uint8_t reg, uint8_t value) { + for (int i = 0; i < s_write_log_count; i++) { + if (s_write_log[i].reg == reg && s_write_log[i].value == value) { + return true; + } + } + return false; +} + +uint8_t fake_i2c_last_written(uint8_t reg) { + for (int i = s_write_log_count - 1; i >= 0; i--) { + if (s_write_log[i].reg == reg) { + return s_write_log[i].value; + } + } + return 0; +} + +// I2C API stubs + +void i2c_use(I2CSlavePort *slave) { + (void)slave; +} + +void i2c_release(I2CSlavePort *slave) { + (void)slave; +} + +bool i2c_read_register(I2CSlavePort *slave, uint8_t register_address, uint8_t *result) { + (void)slave; + if (s_fail) return false; + *result = s_registers[register_address]; + return true; +} + +bool i2c_read_register_block(I2CSlavePort *slave, uint8_t register_address_start, + uint32_t read_size, uint8_t *result_buffer) { + (void)slave; + if (s_fail) return false; + for (uint32_t i = 0; i < read_size && (register_address_start + i) < REG_COUNT; i++) { + result_buffer[i] = s_registers[register_address_start + i]; + } + return true; +} + +bool i2c_write_register(I2CSlavePort *slave, uint8_t register_address, uint8_t value) { + (void)slave; + if (s_fail) return false; + + s_registers[register_address] = value; + + if (s_write_log_count < WRITE_LOG_MAX) { + s_write_log[s_write_log_count].reg = register_address; + s_write_log[s_write_log_count].value = value; + s_write_log_count++; + } + + if (s_write_hook) { + s_write_hook(register_address, value, s_write_hook_context); + } + + return true; +} diff --git a/tests/fakes/fake_i2c.h b/tests/fakes/fake_i2c.h new file mode 100644 index 000000000..150303090 --- /dev/null +++ b/tests/fakes/fake_i2c.h @@ -0,0 +1,25 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include +#include + +typedef void (*FakeI2CWriteHook)(uint8_t reg, uint8_t value, void *context); + +void fake_i2c_init(void); + +void fake_i2c_set_register(uint8_t reg, uint8_t value); + +void fake_i2c_set_register_block(uint8_t reg, const uint8_t *data, uint32_t len); + +uint8_t fake_i2c_get_register(uint8_t reg); + +void fake_i2c_set_fail(bool fail); + +void fake_i2c_set_write_hook(FakeI2CWriteHook hook, void *context); + +bool fake_i2c_was_written(uint8_t reg, uint8_t value); + +uint8_t fake_i2c_last_written(uint8_t reg); diff --git a/tests/fw/drivers/test_pressure_bmp390.c b/tests/fw/drivers/test_pressure_bmp390.c new file mode 100644 index 000000000..eb0710abe --- /dev/null +++ b/tests/fw/drivers/test_pressure_bmp390.c @@ -0,0 +1,393 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "clar.h" + +#include "stubs_logging.h" +#include "stubs_mutex.h" +#include "stubs_passert.h" +#include "stubs_pebble_tasks.h" +#include "stubs_prompt.h" +#include "stubs_syscall_internal.h" + +#include "drivers/pressure.h" +#include "fakes/fake_i2c.h" + +#include + +// Stub for delay_us — no-op in tests +void delay_us(uint32_t us) { (void)us; } +void delay_init(void) { } + +// BMP390 register addresses (must match driver) +#define REG_CHIP_ID 0x00 +#define REG_STATUS 0x03 +#define REG_DATA 0x04 +#define REG_INT_STATUS 0x11 +#define REG_PWR_CTRL 0x1B +#define REG_OSR 0x1C +#define REG_ODR 0x1D +#define REG_CONFIG 0x1F +#define REG_CALIB_DATA 0x31 +#define REG_CMD 0x7E + +#define CHIP_ID_VALUE 0x60 +#define CALIB_DATA_LEN 21 + +// PWR_CTRL bit definitions +#define PRESS_EN (1 << 0) +#define TEMP_EN (1 << 1) +#define MODE_FORCED (1 << 4) +#define MODE_NORMAL (3 << 4) + +// IIR filter coefficients (CONFIG register bits [3:1]) +#define IIR_COEFF_0 (0x00 << 1) +#define IIR_COEFF_7 (0x03 << 1) +#define IIR_COEFF_15 (0x04 << 1) +#define IIR_COEFF_31 (0x05 << 1) + +// INT_STATUS bits +#define DRDY_BIT (1 << 3) + +// Known calibration data from a real BMP390 — produces verifiable output +static const uint8_t s_test_calib_data[CALIB_DATA_LEN] = { + // par_t1 (uint16): 27356 = 0x6ACC + 0xCC, 0x6A, + // par_t2 (uint16): 18789 = 0x4965 + 0x65, 0x49, + // par_t3 (int8): -7 + 0xF9, + // par_p1 (int16): 217 = 0x00D9 + 0xD9, 0x00, + // par_p2 (int16): -163 = 0xFF5D + 0x5D, 0xFF, + // par_p3 (int8): 14 + 0x0E, + // par_p4 (int8): 0 + 0x00, + // par_p5 (uint16): 25352 = 0x6308 + 0x08, 0x63, + // par_p6 (uint16): 29536 = 0x7360 + 0x60, 0x73, + // par_p7 (int8): -1 + 0xFF, + // par_p8 (int8): -7 + 0xF9, + // par_p9 (int16): 13000 = 0x32C8 + 0xC8, 0x32, + // par_p10 (int8): 29 + 0x1D, + // par_p11 (int8): 0 + 0x00, +}; + +// Hook for simulating BMP390 behavior on register writes +static void prv_bmp390_write_hook(uint8_t reg, uint8_t value, void *context) { + if (reg == REG_CMD && value == 0xB6) { + // Soft reset: set status.cmd_rdy (bit 4) so driver sees device ready + fake_i2c_set_register(REG_STATUS, 0x10); + } + + if (reg == REG_PWR_CTRL && (value & MODE_FORCED)) { + // Forced mode: set DRDY after "measurement" + fake_i2c_set_register(REG_INT_STATUS, DRDY_BIT); + } +} + +// Hook that clears normal mode bits (simulates the hardware bug) +static void prv_normal_mode_bug_hook(uint8_t reg, uint8_t value, void *context) { + prv_bmp390_write_hook(reg, value, context); + + if (reg == REG_PWR_CTRL) { + uint8_t cleared = value & ~MODE_NORMAL; + fake_i2c_set_register(REG_PWR_CTRL, cleared); + } +} + +static void prv_setup_bmp390_registers(void) { + fake_i2c_init(); + fake_i2c_set_register(REG_CHIP_ID, CHIP_ID_VALUE); + fake_i2c_set_register(REG_STATUS, 0x10); // cmd_rdy + fake_i2c_set_register_block(REG_CALIB_DATA, s_test_calib_data, CALIB_DATA_LEN); + fake_i2c_set_write_hook(prv_bmp390_write_hook, NULL); +} + +// Set up raw sensor data registers (pressure + temperature, 6 bytes at 0x04) +static void prv_set_sensor_data(uint32_t raw_pressure, uint32_t raw_temperature) { + uint8_t data[6]; + data[0] = (raw_pressure >> 0) & 0xFF; + data[1] = (raw_pressure >> 8) & 0xFF; + data[2] = (raw_pressure >> 16) & 0xFF; + data[3] = (raw_temperature >> 0) & 0xFF; + data[4] = (raw_temperature >> 8) & 0xFF; + data[5] = (raw_temperature >> 16) & 0xFF; + fake_i2c_set_register_block(REG_DATA, data, 6); +} + +// ---- Setup / Teardown ---- + +void test_pressure_bmp390__initialize(void) { + prv_setup_bmp390_registers(); +} + +void test_pressure_bmp390__cleanup(void) { + fake_i2c_set_register(REG_CHIP_ID, 0x00); + fake_i2c_set_fail(false); + pressure_init(); +} + +// ---- Init Tests ---- + +void test_pressure_bmp390__init_success(void) { + pressure_init(); + // After init, driver should have written OSR register + cl_assert(fake_i2c_was_written(REG_OSR, 0x03)); // 8x pressure, 1x temp + // IIR defaults to off (FILTER_NONE) + cl_assert(fake_i2c_was_written(REG_CONFIG, IIR_COEFF_0)); + // PWR_CTRL should enable sensors without mode bits (sleep) + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, PRESS_EN | TEMP_EN)); +} + +void test_pressure_bmp390__init_bad_chip_id(void) { + fake_i2c_set_register(REG_CHIP_ID, 0xAA); + pressure_init(); + cl_assert(!fake_i2c_was_written(REG_OSR, 0x03)); +} + +void test_pressure_bmp390__init_i2c_failure(void) { + fake_i2c_set_fail(true); + pressure_init(); + int32_t pressure, temp; + cl_assert(!pressure_read(&pressure, &temp)); +} + +// ---- Forced Mode Read Tests ---- + +void test_pressure_bmp390__read_triggers_forced_mode(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + int32_t pressure_pa, temp_centideg; + cl_assert(pressure_read(&pressure_pa, &temp_centideg)); + + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, + PRESS_EN | TEMP_EN | MODE_FORCED)); + + cl_assert(pressure_pa > 80000); + cl_assert(pressure_pa < 120000); + cl_assert(temp_centideg > -4000); + cl_assert(temp_centideg < 8500); +} + +void test_pressure_bmp390__read_not_initialized(void) { + int32_t pressure, temp; + cl_assert(!pressure_read(&pressure, &temp)); +} + +void test_pressure_bmp390__read_null_params(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + cl_assert(pressure_read(NULL, NULL)); + + int32_t pressure; + cl_assert(pressure_read(&pressure, NULL)); + cl_assert(pressure > 80000); + + int32_t temp; + cl_assert(pressure_read(NULL, &temp)); +} + +// ---- ODR Configuration Tests ---- + +void test_pressure_bmp390__set_odr_25hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x03); // BMP390_ODR_25HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x01); // OSR_P_2X | OSR_T_1X + // Default filter mode is NONE — IIR should be off + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_50hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_50HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x02); // BMP390_ODR_50HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x00); // OSR_P_1X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_1hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_1HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x07); // BMP390_ODR_1P5625HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x03); // OSR_P_8X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_5hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_5HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x05); // BMP390_ODR_6P25HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x03); // OSR_P_8X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_10hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_10HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x04); // BMP390_ODR_12P5HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x02); // OSR_P_4X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_enables_normal_mode(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + uint8_t last_pwr = fake_i2c_last_written(REG_PWR_CTRL); + cl_assert_equal_i(last_pwr, PRESS_EN | TEMP_EN | MODE_NORMAL); +} + +void test_pressure_bmp390__set_odr_not_initialized(void) { + cl_assert(!pressure_set_odr(PRESSURE_ODR_25HZ)); +} + +// ---- Filter Mode Tests ---- + +void test_pressure_bmp390__filter_mode_default_none(void) { + pressure_init(); + // Init should write IIR off + cl_assert(fake_i2c_was_written(REG_CONFIG, IIR_COEFF_0)); +} + +void test_pressure_bmp390__filter_mode_smooth_applies_iir(void) { + pressure_init(); + cl_assert(pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH)); + + // Should immediately write IIR coeff to CONFIG + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_7); +} + +void test_pressure_bmp390__filter_mode_none_clears_iir(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_filter_mode(PRESSURE_FILTER_NONE)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__filter_smooth_odr_25hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_15); +} + +void test_pressure_bmp390__filter_smooth_odr_50hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_50HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_31); +} + +void test_pressure_bmp390__filter_smooth_odr_1hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_1HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_7); +} + +void test_pressure_bmp390__filter_none_odr_25hz(void) { + // Verify that FILTER_NONE overrides ODR-specific IIR + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_NONE); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__filter_mode_not_initialized(void) { + cl_assert(!pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH)); +} + +// ---- Normal Mode Bug Simulation ---- + +void test_pressure_bmp390__normal_mode_bits_written(void) { + fake_i2c_set_write_hook(prv_normal_mode_bug_hook, NULL); + pressure_init(); + + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, + PRESS_EN | TEMP_EN | MODE_NORMAL)); + + uint8_t readback = fake_i2c_get_register(REG_PWR_CTRL); + cl_assert_equal_i(readback & MODE_NORMAL, 0); +} + +// ---- Altitude Calculation Tests ---- + +void test_pressure_bmp390__altitude_cm_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(101325, 101325); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_cm_above_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(100500, 101325); + cl_assert(alt > 6000); + cl_assert(alt < 8000); +} + +void test_pressure_bmp390__altitude_cm_below_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(102000, 101325); + cl_assert(alt < 0); +} + +void test_pressure_bmp390__altitude_cm_zero_reference(void) { + int32_t alt = pressure_get_altitude_cm(101325, 0); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_full_sea_level(void) { + int32_t alt = pressure_get_altitude_full_cm(101325, 101325); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_full_skydive_exit(void) { + // ~4000m (~13000ft): pressure approximately 61640 Pa + int32_t alt = pressure_get_altitude_full_cm(61640, 101325); + cl_assert(alt > 380000); + cl_assert(alt < 420000); +} + +void test_pressure_bmp390__altitude_full_negative(void) { + int32_t alt = pressure_get_altitude_full_cm(103000, 101325); + cl_assert(alt < 0); +} + +void test_pressure_bmp390__altitude_full_zero_input(void) { + cl_assert_equal_i(pressure_get_altitude_full_cm(0, 101325), 0); + cl_assert_equal_i(pressure_get_altitude_full_cm(101325, 0), 0); +} + +// ---- Compensation Math Consistency ---- + +void test_pressure_bmp390__read_consistency(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + int32_t p1, t1, p2, t2; + cl_assert(pressure_read(&p1, &t1)); + cl_assert(pressure_read(&p2, &t2)); + cl_assert_equal_i(p1, p2); + cl_assert_equal_i(t1, t2); +} diff --git a/tests/fw/drivers/wscript b/tests/fw/drivers/wscript index e1b947098..e486bf634 100644 --- a/tests/fw/drivers/wscript +++ b/tests/fw/drivers/wscript @@ -24,4 +24,10 @@ def build(ctx): override_includes=['dummy_board'], platforms=['silk']) + clar(ctx, + sources_ant_glob='src/fw/drivers/pressure/bmp390.c' + ' tests/fakes/fake_i2c.c', + test_sources_ant_glob='test_pressure_bmp390.c', + override_includes=['dummy_board']) + # vim:filetype=python diff --git a/tools/generate_native_sdk/exported_symbols.json b/tools/generate_native_sdk/exported_symbols.json index 27af1f3ad..56d3a6c9a 100644 --- a/tools/generate_native_sdk/exported_symbols.json +++ b/tools/generate_native_sdk/exported_symbols.json @@ -4,7 +4,7 @@ "You should also make sure you are obeying our API design guidelines:", "https://pebbletechnology.atlassian.net/wiki/display/DEV/SDK+API+Design+Guidelines" ], - "revision" : "92", + "revision" : "93", "version" : "2.0", "files": [ "fw/drivers/ambient_light.h", @@ -51,6 +51,8 @@ "fw/applib/app_launch_reason.h", "fw/applib/cpu_cache.h", "fw/applib/health_service.h", + "fw/drivers/pressure.h", + "fw/applib/pressure_service.h", "fw/applib/moddable/moddable.h", "fw/applib/platform.h", "fw/applib/tick_timer_service.h", @@ -689,7 +691,65 @@ "name": "health_service_get_measurement_system_for_display", "addedRevision": "80" } - ]}] + ]}, { + "type": "group", + "name": "PressureService", + "exports": [ + { + "type": "type", + "name": "PressureODR", + "addedRevision": "93" + }, { + "type": "type", + "name": "PressureData", + "addedRevision": "93" + }, { + "type": "type", + "name": "PressureDataHandler", + "addedRevision": "93" + }, { + "type": "type", + "name": "PressureFilterMode", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_subscribe", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_unsubscribe", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_data_rate", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_reference", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_reference_pressure", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_use_full_formula", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_peek", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_get_altitude_cm", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_filter_mode", + "addedRevision": "93" + } + ] + }] }, { "type": "group", "name": "DataLogging", diff --git a/waftools/openocd_swd_cmsisdap.cfg b/waftools/openocd_swd_cmsisdap.cfg index 84c4e7899..d3c18976a 100644 --- a/waftools/openocd_swd_cmsisdap.cfg +++ b/waftools/openocd_swd_cmsisdap.cfg @@ -1,2 +1,3 @@ source [find interface/cmsis-dap.cfg] +cmsis_dap_vid_pid 0x2e8a 0x000c transport select swd