Skip to content

CODE

COMPONENT ARCHITECTURE

- driver
    - node_acc_adxl367
        - include
            - node_acc_adxl367.h
            - node_acc_adxl367_test.h
        - node_acc_adxl367.c
        - node_acc_adxl367_test.c
        - CMakeLists.txt
        - readme.txt

driver/node_acc_adxl355/CMakeLists.txt

set(src_dirs
    .
)

set(include_dirs
    include
)

set(requires
    node_i2c
    freertos
    log
    esp_driver_gpio    
)

idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})

node_acc_adxl367.h

/**
 * @file node_acc_adxl367.h
 * @brief ADXL367 (I2C): init, range/ODR, modes, XYZ/temp, FIFO, INT map, activity/inactivity.
 *
 * I2C via driver/node_i2c. Registers: driver/ref-adxl367. Tests/integration: node_acc_adxl367_test.h.
 */
#pragma once

#include <stdbool.h>
#include <stdint.h>

#include "driver/i2c_master.h"
#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

/** Default 7-bit I2C address when ASEL is high (see datasheet). */
#define NODE_ACC_ADXL367_I2C_ADDR_0X53 0x53u
/** Alternate 7-bit I2C address when ASEL is low. */
#define NODE_ACC_ADXL367_I2C_ADDR_0X1D 0x1Du

/** Measurement range (FILTER_CTL[7:6]). */
typedef enum {
    NODE_ACC_ADXL367_RANGE_2G = 0u,
    NODE_ACC_ADXL367_RANGE_4G = 1u,
    NODE_ACC_ADXL367_RANGE_8G = 2u,
} node_acc_adxl367_range_t;

/** Output data rate (FILTER_CTL[2:0]). Matches ADXL367 ODR encoding. */
typedef enum {
    NODE_ACC_ADXL367_ODR_12_5_HZ = 0u,
    NODE_ACC_ADXL367_ODR_25_HZ = 1u,
    NODE_ACC_ADXL367_ODR_50_HZ = 2u,
    NODE_ACC_ADXL367_ODR_100_HZ = 3u,
    NODE_ACC_ADXL367_ODR_200_HZ = 4u,
    NODE_ACC_ADXL367_ODR_400_HZ = 5u,
} node_acc_adxl367_odr_t;

/** Power/measurement mode (POWER_CTL[1:0]). */
typedef enum {
    NODE_ACC_ADXL367_MODE_STANDBY = 0u,
    NODE_ACC_ADXL367_MODE_MEASURE = 2u,
} node_acc_adxl367_mode_t;

/**
 * Temperature / ADC channel selection (TEMP_CTL / ADC_CTL).
 * adxl367_adc_read_en() in ref clears TEMP when toggling ADC; we only mask ADC_CTL for ADC_EN
 * so TEMP_ONLY does not use that helper verbatim.
 */
typedef enum {
    /** Accel only. */
    NODE_ACC_ADXL367_MEAS_ACCEL_ONLY = 0u,
    /** Temp only (API). */
    NODE_ACC_ADXL367_MEAS_TEMP_ONLY,
    /** Accel + temp. */
    NODE_ACC_ADXL367_MEAS_ACCEL_AND_TEMP,
} node_acc_adxl367_meas_mode_t;

/** FIFO mode (FIFO_CONTROL[1:0]). See adxl367_set_fifo_mode. */
typedef enum {
    NODE_ACC_ADXL367_FIFO_DISABLED = 0u,
    NODE_ACC_ADXL367_FIFO_OLDEST_SAVED = 1u,
    NODE_ACC_ADXL367_FIFO_STREAM = 2u,
    NODE_ACC_ADXL367_FIFO_TRIGGERED = 3u,
} node_acc_adxl367_fifo_mode_t;

/** FIFO channel / format (FIFO_CONTROL[6:3]). See adxl367_set_fifo_format. */
typedef enum {
    NODE_ACC_ADXL367_FIFO_FMT_XYZ = 0u,
    NODE_ACC_ADXL367_FIFO_FMT_X = 1u,
    NODE_ACC_ADXL367_FIFO_FMT_Y = 2u,
    NODE_ACC_ADXL367_FIFO_FMT_Z = 3u,
    NODE_ACC_ADXL367_FIFO_FMT_XYZT = 4u,
} node_acc_adxl367_fifo_format_t;

/**
 * Device instance. @c i2c from @c i2c_add_device() after @c i2c_bus_init().
 */
typedef struct node_acc_adxl367_dev {
    i2c_master_dev_handle_t i2c;
    node_acc_adxl367_range_t range;
    node_acc_adxl367_odr_t odr;
    node_acc_adxl367_mode_t mode;
    node_acc_adxl367_meas_mode_t meas_mode;
    node_acc_adxl367_fifo_mode_t fifo_mode;
    node_acc_adxl367_fifo_format_t fifo_format;
} node_acc_adxl367_dev_t;

/** INTMAP bits; layout matches ref adxl367_int_map. */
typedef struct node_acc_adxl367_int_map {
    uint8_t err_fuse;
    uint8_t err_user_regs;
    uint8_t kpalv_timer;
    uint8_t temp_adc_hi;
    uint8_t temp_adc_low;
    uint8_t tap_two;
    uint8_t tap_one;
    uint8_t int_low;
    uint8_t awake;
    uint8_t inact;
    uint8_t act;
    uint8_t fifo_overrun;
    uint8_t fifo_watermark;
    uint8_t fifo_ready;
    uint8_t data_ready;
} node_acc_adxl367_int_map_t;

/** ACT_INACT_CTL ref mode: ABS fixed vs REL referenced baseline (ref adxl367_setup_activity_detection). */
typedef enum {
    NODE_ACC_ADXL367_AI_REF_ABS = 0u,
    NODE_ACC_ADXL367_AI_REF_REL = 1u,
} node_acc_adxl367_ai_ref_t;

/** STATUS (0x0B). */
#define NODE_ACC_ADXL367_STATUS_DATA_RDY (1u << 0)
#define NODE_ACC_ADXL367_STATUS_FIFO_RDY (1u << 1)
#define NODE_ACC_ADXL367_STATUS_FIFO_WATERMARK (1u << 2)
#define NODE_ACC_ADXL367_STATUS_FIFO_OVERRUN (1u << 3)
#define NODE_ACC_ADXL367_STATUS_ACT (1u << 4)
#define NODE_ACC_ADXL367_STATUS_INACT (1u << 5)
#define NODE_ACC_ADXL367_STATUS_AWAKE (1u << 6)

/** g to THRESH_ACT LSB at +/-2g scale intent (~4000 LSB/g, ref path). */
#define NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G(g) ((uint16_t)((float)(g) * 4000.0f))

/**
 * @brief Soft-reset, probe, default 2g / 100 Hz, measure mode, then @c NODE_ACC_ADXL367_MEAS_ACCEL_ONLY.
 */
esp_err_t node_acc_adxl367_init(node_acc_adxl367_dev_t *dev);

esp_err_t node_acc_adxl367_soft_reset(node_acc_adxl367_dev_t *dev);
esp_err_t node_acc_adxl367_probe(node_acc_adxl367_dev_t *dev);

esp_err_t node_acc_adxl367_set_range(node_acc_adxl367_dev_t *dev, node_acc_adxl367_range_t range);
esp_err_t node_acc_adxl367_set_odr(node_acc_adxl367_dev_t *dev, node_acc_adxl367_odr_t odr);
esp_err_t node_acc_adxl367_set_mode(node_acc_adxl367_dev_t *dev, node_acc_adxl367_mode_t mode);

/**
 * @brief Configure TEMP_CTL / ADC_CTL per mode. Stays in current power mode (typically measure).
 */
esp_err_t node_acc_adxl367_set_measurement_mode(node_acc_adxl367_dev_t *dev,
                                                 node_acc_adxl367_meas_mode_t mode);

/** Raw XYZ; ESP_ERR_INVALID_STATE in MEAS_TEMP_ONLY. Same as ref adxl367_get_raw_xyz (DATA_RDY, XDATA_H). */
esp_err_t node_acc_adxl367_read_xyz_raw(node_acc_adxl367_dev_t *dev, int16_t *x, int16_t *y, int16_t *z);

/**
 * @brief Raw temperature (14-bit sign-extended). Requires TEMP_EN; not allowed in @c NODE_ACC_ADXL367_MEAS_ACCEL_ONLY.
 */
esp_err_t node_acc_adxl367_read_temp_raw(node_acc_adxl367_dev_t *dev, int16_t *raw);

/** Celsius using ref-adxl367 adxl367_temp_conv coefficients. */
esp_err_t node_acc_adxl367_read_temp_celsius(node_acc_adxl367_dev_t *dev, float *celsius);

/** Read STATUS register (0x0B). */
esp_err_t node_acc_adxl367_read_status(node_acc_adxl367_dev_t *dev, uint8_t *status);

/**
 * @brief Route interrupt sources to INT1 or INT2 (INTMAP1 / INTMAP2). See @c adxl367_int_map.
 * @param int_pin 1 = INT1, 2 = INT2.
 */
esp_err_t node_acc_adxl367_int_map(node_acc_adxl367_dev_t *dev, uint8_t int_pin,
                                   const node_acc_adxl367_int_map_t *map);

/** Activity: ref adxl367_setup_activity_detection. threshold 13-bit; time_act_samples at ODR. */
esp_err_t node_acc_adxl367_setup_activity_detection(node_acc_adxl367_dev_t *dev,
                                                    node_acc_adxl367_ai_ref_t ref_mode,
                                                    uint16_t threshold,
                                                    uint8_t time_act_samples);

/** Inactivity: ref adxl367_setup_inactivity_detection. */
esp_err_t node_acc_adxl367_setup_inactivity_detection(node_acc_adxl367_dev_t *dev,
                                                      node_acc_adxl367_ai_ref_t ref_mode,
                                                      uint16_t threshold,
                                                      uint16_t time_inact_samples);

/** Clear ACT/INACT enables (ACT_INACT_CTL[3:0]). */
esp_err_t node_acc_adxl367_act_inact_disable(node_acc_adxl367_dev_t *dev);

/** AXIS_MASK bits [2:0]: set bit disables axis. */
esp_err_t node_acc_adxl367_set_activity_axes_enabled(node_acc_adxl367_dev_t *dev, bool enable_x,
                                                   bool enable_y, bool enable_z);

/** ACT_INACT_CTL LINKLOOP 0-3 (datasheet). */
esp_err_t node_acc_adxl367_set_act_inact_linkloop(node_acc_adxl367_dev_t *dev, uint8_t linkloop_mode_0_to_3);

/**
 * @brief FIFO setup: mode, format, watermark sample-sets count, 14-bit+CH ID read mode (adxl367_fifo_setup).
 * @param sample_sets_nb Watermark in sample sets (e.g. 3 entries per set for XYZ).
 */
esp_err_t node_acc_adxl367_fifo_setup(node_acc_adxl367_dev_t *dev,
                                      node_acc_adxl367_fifo_mode_t mode,
                                      node_acc_adxl367_fifo_format_t format,
                                      uint16_t sample_sets_nb);

esp_err_t node_acc_adxl367_fifo_disable(node_acc_adxl367_dev_t *dev);

/** FIFO entry count (10-bit), ref adxl367_get_nb_of_fifo_entries. */
esp_err_t node_acc_adxl367_fifo_get_entry_count(node_acc_adxl367_dev_t *dev, uint16_t *count);

/**
 * @brief Read and parse FIFO for @c NODE_ACC_ADXL367_FIFO_FMT_XYZ (14b+CH ID).
 * Fills up to @p max_triplets complete XYZ samples; @p out_triplets may be less if FIFO is partial.
 */
esp_err_t node_acc_adxl367_fifo_drain_xyz(node_acc_adxl367_dev_t *dev, int16_t *x, int16_t *y, int16_t *z,
                                          size_t max_triplets, size_t *out_triplets);

#ifdef __cplusplus
}
#endif

node_acc_adxl367.c

/**
 * @file node_acc_adxl367.c
 * @brief ADXL367 I2C driver (ref-adxl367/adxl367.c). Tests: node_acc_adxl367_test.h.
 */
#include "node_acc_adxl367.h"

#include <stddef.h>

#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "node_i2c.h"

static const char *TAG = "node_acc_adxl367";

/* Register map (subset) */
#define REG_DEVID_AD 0x00u
#define REG_DEVID_MST 0x01u
#define REG_PARTID 0x02u
#define REG_REVID 0x03u
#define REG_STATUS 0x0Bu
#define REG_XDATA_H 0x0Eu
#define REG_SOFT_RESET 0x1Fu
#define REG_FILTER_CTL 0x2Cu
#define REG_POWER_CTL 0x2Du
#define REG_TEMP_H 0x14u
#define REG_ADC_CTL 0x3Cu
#define REG_TEMP_CTL 0x3Du
#define REG_FIFO_ENTRIES_L 0x0Cu
#define REG_FIFO_ENTRIES_H 0x0Du
#define REG_FIFO_CONTROL 0x28u
#define REG_FIFO_SAMPLES 0x29u
#define REG_I2C_FIFO_DATA 0x18u
#define REG_INTMAP1_LWR 0x2Au
#define REG_INTMAP2_LWR 0x2Bu
#define REG_INTMAP1_UPPER 0x3Au
#define REG_INTMAP2_UPPER 0x3Bu

#define REG_THRESH_ACT_H 0x20u
#define REG_THRESH_ACT_L 0x21u
#define REG_TIME_ACT 0x22u
#define REG_THRESH_INACT_H 0x23u
#define REG_THRESH_INACT_L 0x24u
#define REG_TIME_INACT_H 0x25u
#define REG_TIME_INACT_L 0x26u
#define REG_ACT_INACT_CTL 0x27u
#define REG_AXIS_MASK 0x43u

#define INTMAP_UPPER_MASK 0xDFu

#define THRESH_H_MASK 0x7Fu
#define THRESH_L_MASK 0xFCu
#define ACT_EN_MSK 0x03u
#define INACT_EN_MSK 0x0Cu
#define LINKLOOP_MSK (0x3u << 4)
#define AXIS_MASK_ACT_BLOCK_MASK 0x07u

#define DEVID_AD_VAL 0xADu
#define DEVID_MST_VAL 0x1Du
#define PARTID_VAL 0xF7u
#define REVID_ADXL367 0x03u

#define RESET_KEY 0x52u

#define STATUS_DATA_RDY (1u << 0)

#define FILTER_CTL_RANGE_MASK 0xC0u
#define FILTER_CTL_ODR_MASK 0x07u

#define POWER_CTL_MEASURE_MASK 0x03u

#define ADC_EN_MASK (1u << 0)
#define TEMP_EN_MASK (1u << 0)

#define FIFO_CTL_MODE_MASK 0x03u
#define FIFO_CTL_CH_MASK 0x78u
#define FIFO_CTL_SAMPLES_BIT (1u << 2)

#define ADC_CTL_FIFO_RD_MODE_MASK 0xC0u
#define FIFO_RD_14B_CHID 3u

#define STATUS_FIFO_RDY (1u << 1)
#define STATUS_FIFO_WATERMARK (1u << 2)
#define STATUS_FIFO_OVERRUN (1u << 3)

#define FIFO_MAX_READ_BYTES 512u

/** adxl367_temp_conv in ref-adxl367/adxl367.c */
#define TEMP_OFFSET 1185
#define TEMP_SCALE_NUM 18518518LL
#define TEMP_SCALE_DEN 1000000000LL

#define I2C_TIMEOUT_MS 1000

/* Low-level helpers: node_i2c only (see node_i2c.h). */

static esp_err_t reg_write(node_acc_adxl367_dev_t *dev, uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = {reg, val};
    return i2c_write_data(dev->i2c, buf, sizeof(buf), I2C_TIMEOUT_MS);
}

static esp_err_t reg_read(node_acc_adxl367_dev_t *dev, uint8_t reg, uint8_t *data, size_t len)
{
    if (len == 0 || data == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    return i2c_write_read_data(dev->i2c, &reg, 1, data, len, I2C_TIMEOUT_MS);
}

static esp_err_t reg_update_mask(node_acc_adxl367_dev_t *dev, uint8_t reg, uint8_t mask, uint8_t set_bits)
{
    uint8_t v = 0;
    esp_err_t err = reg_read(dev, reg, &v, 1);
    if (err != ESP_OK) {
        return err;
    }
    v = (uint8_t)((v & (uint8_t)~mask) | (set_bits & mask));
    return reg_write(dev, reg, v);
}

/** 14-bit pack: ref adxl367_get_raw_xyz. */
static int16_t pack_14bit_xy(uint8_t hi, uint8_t lo)
{
    int16_t v = (int16_t)(((int16_t)hi << 6) | (int16_t)(lo >> 2));
    if (v & (int16_t)(1 << 13)) {
        v |= (int16_t)(3 << 14);
    }
    return v;
}

static esp_err_t adc_en_set_only(node_acc_adxl367_dev_t *dev, bool enable)
{
    return reg_update_mask(dev, REG_ADC_CTL, ADC_EN_MASK, enable ? ADC_EN_MASK : 0u);
}

static esp_err_t temp_en_set(node_acc_adxl367_dev_t *dev, bool enable)
{
    return reg_update_mask(dev, REG_TEMP_CTL, TEMP_EN_MASK, enable ? TEMP_EN_MASK : 0u);
}

/** Poll STATUS until DATA_RDY (ref @c adxl367_get_raw_xyz uses an unbounded while; we cap for RTOS). */
static esp_err_t wait_data_rdy(node_acc_adxl367_dev_t *dev)
{
    const int max_poll = 200;
    for (int i = 0; i < max_poll; i++) {
        uint8_t st = 0;
        esp_err_t err = reg_read(dev, REG_STATUS, &st, 1);
        if (err != ESP_OK) {
            return err;
        }
        if (st & STATUS_DATA_RDY) {
            return ESP_OK;
        }
        vTaskDelay(pdMS_TO_TICKS(1));
    }
    return ESP_ERR_TIMEOUT;
}

esp_err_t node_acc_adxl367_set_measurement_mode(node_acc_adxl367_dev_t *dev,
                                                node_acc_adxl367_meas_mode_t mode)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (mode > NODE_ACC_ADXL367_MEAS_ACCEL_AND_TEMP) {
        return ESP_ERR_INVALID_ARG;
    }

    esp_err_t err = adc_en_set_only(dev, false);
    if (err != ESP_OK) {
        return err;
    }

    switch (mode) {
    case NODE_ACC_ADXL367_MEAS_ACCEL_ONLY:
        err = temp_en_set(dev, false);
        break;
    case NODE_ACC_ADXL367_MEAS_TEMP_ONLY:
        err = temp_en_set(dev, true);
        break;
    case NODE_ACC_ADXL367_MEAS_ACCEL_AND_TEMP:
        err = temp_en_set(dev, true);
        break;
    default:
        return ESP_ERR_INVALID_ARG;
    }
    if (err != ESP_OK) {
        return err;
    }
    dev->meas_mode = mode;
    return ESP_OK;
}

esp_err_t node_acc_adxl367_read_temp_raw(node_acc_adxl367_dev_t *dev, int16_t *raw)
{
    if (dev == NULL || raw == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (dev->meas_mode == NODE_ACC_ADXL367_MEAS_ACCEL_ONLY) {
        return ESP_ERR_INVALID_STATE;
    }

    esp_err_t err = wait_data_rdy(dev);
    if (err != ESP_OK) {
        return err;
    }

    uint8_t t[2];
    err = reg_read(dev, REG_TEMP_H, t, sizeof(t));
    if (err != ESP_OK) {
        return err;
    }
    *raw = pack_14bit_xy(t[0], t[1]);
    return ESP_OK;
}

esp_err_t node_acc_adxl367_read_temp_celsius(node_acc_adxl367_dev_t *dev, float *celsius)
{
    if (dev == NULL || celsius == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    int16_t raw = 0;
    esp_err_t err = node_acc_adxl367_read_temp_raw(dev, &raw);
    if (err != ESP_OK) {
        return err;
    }
    int64_t v = (int64_t)raw + (int64_t)TEMP_OFFSET;
    v = v * TEMP_SCALE_NUM;
    *celsius = (float)v / (float)TEMP_SCALE_DEN;
    return ESP_OK;
}

esp_err_t node_acc_adxl367_soft_reset(node_acc_adxl367_dev_t *dev)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t err = reg_write(dev, REG_SOFT_RESET, RESET_KEY);
    if (err != ESP_OK) {
        return err;
    }
    vTaskDelay(pdMS_TO_TICKS(20));
    dev->range = NODE_ACC_ADXL367_RANGE_2G;
    dev->odr = NODE_ACC_ADXL367_ODR_100_HZ;
    dev->mode = NODE_ACC_ADXL367_MODE_STANDBY;
    dev->meas_mode = NODE_ACC_ADXL367_MEAS_ACCEL_ONLY;
    dev->fifo_mode = NODE_ACC_ADXL367_FIFO_DISABLED;
    dev->fifo_format = NODE_ACC_ADXL367_FIFO_FMT_XYZ;
    return ESP_OK;
}

static int16_t pack_fifo_14b_chid(uint8_t hi, uint8_t lo)
{
    int16_t v = (int16_t)(((int16_t)(hi & 0x3Fu) << 8) | lo);
    if (v & (int16_t)(1 << 13)) {
        v |= (int16_t)(3 << 14);
    }
    return v;
}

static esp_err_t fifo_set_sample_sets_nb(node_acc_adxl367_dev_t *dev, uint16_t sets_nb)
{
    esp_err_t err = reg_update_mask(dev, REG_FIFO_CONTROL, FIFO_CTL_SAMPLES_BIT,
                                    (sets_nb & (1u << 9)) ? FIFO_CTL_SAMPLES_BIT : 0u);
    if (err != ESP_OK) {
        return err;
    }
    return reg_write(dev, REG_FIFO_SAMPLES, (uint8_t)(sets_nb & 0xFFu));
}

static esp_err_t fifo_read_payload(node_acc_adxl367_dev_t *dev, uint8_t *data, size_t len)
{
    uint8_t reg = REG_I2C_FIFO_DATA;
    return i2c_write_read_data(dev->i2c, &reg, 1, data, len, I2C_TIMEOUT_MS);
}

esp_err_t node_acc_adxl367_read_status(node_acc_adxl367_dev_t *dev, uint8_t *status)
{
    if (dev == NULL || status == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    return reg_read(dev, REG_STATUS, status, 1);
}

static uint8_t intmap_bit(uint8_t v)
{
    return (uint8_t)(v & 1u);
}

esp_err_t node_acc_adxl367_int_map(node_acc_adxl367_dev_t *dev, uint8_t int_pin,
                                   const node_acc_adxl367_int_map_t *map)
{
    if (dev == NULL || map == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (int_pin != 1u && int_pin != 2u) {
        return ESP_ERR_INVALID_ARG;
    }

    uint8_t upper = (uint8_t)((intmap_bit(map->err_fuse) << 7) | (intmap_bit(map->err_user_regs) << 6) |
                              (intmap_bit(map->kpalv_timer) << 4) | (intmap_bit(map->temp_adc_hi) << 3) |
                              (intmap_bit(map->temp_adc_low) << 2) | (intmap_bit(map->tap_two) << 1) |
                              intmap_bit(map->tap_one));

    uint8_t lower = (uint8_t)((intmap_bit(map->int_low) << 7) | (intmap_bit(map->awake) << 6) |
                             (intmap_bit(map->inact) << 5) | (intmap_bit(map->act) << 4) |
                             (intmap_bit(map->fifo_overrun) << 3) | (intmap_bit(map->fifo_watermark) << 2) |
                             (intmap_bit(map->fifo_ready) << 1) | intmap_bit(map->data_ready));

    uint8_t reg_upper = (int_pin == 1u) ? REG_INTMAP1_UPPER : REG_INTMAP2_UPPER;
    uint8_t reg_lower = (int_pin == 1u) ? REG_INTMAP1_LWR : REG_INTMAP2_LWR;

    esp_err_t err = reg_update_mask(dev, reg_upper, INTMAP_UPPER_MASK, upper);
    if (err != ESP_OK) {
        return err;
    }
    return reg_write(dev, reg_lower, lower);
}

esp_err_t node_acc_adxl367_act_inact_disable(node_acc_adxl367_dev_t *dev)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    return reg_update_mask(dev, REG_ACT_INACT_CTL, ACT_EN_MSK | INACT_EN_MSK, 0u);
}

esp_err_t node_acc_adxl367_set_activity_axes_enabled(node_acc_adxl367_dev_t *dev, bool enable_x,
                                                     bool enable_y, bool enable_z)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (!enable_x && !enable_y && !enable_z) {
        return ESP_ERR_INVALID_ARG;
    }
    uint8_t block = 0u;
    if (!enable_x) {
        block |= (1u << 0);
    }
    if (!enable_y) {
        block |= (1u << 1);
    }
    if (!enable_z) {
        block |= (1u << 2);
    }
    return reg_update_mask(dev, REG_AXIS_MASK, AXIS_MASK_ACT_BLOCK_MASK, block);
}

esp_err_t node_acc_adxl367_set_act_inact_linkloop(node_acc_adxl367_dev_t *dev, uint8_t linkloop_mode_0_to_3)
{
    if (dev == NULL || linkloop_mode_0_to_3 > 3u) {
        return ESP_ERR_INVALID_ARG;
    }
    return reg_update_mask(dev, REG_ACT_INACT_CTL, LINKLOOP_MSK, (uint8_t)(linkloop_mode_0_to_3 << 4));
}

/* Register writes follow driver/ref-adxl367/adxl367.c adxl367_setup_activity_detection(). */
esp_err_t node_acc_adxl367_setup_activity_detection(node_acc_adxl367_dev_t *dev,
                                                    node_acc_adxl367_ai_ref_t ref_mode,
                                                    uint16_t threshold,
                                                    uint8_t time_act_samples)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (threshold > 0x1FFFu || ref_mode > NODE_ACC_ADXL367_AI_REF_REL) {
        return ESP_ERR_INVALID_ARG;
    }

    uint8_t act_val = (ref_mode == NODE_ACC_ADXL367_AI_REF_ABS) ? 1u : 3u;
    esp_err_t err = reg_update_mask(dev, REG_ACT_INACT_CTL, ACT_EN_MSK, act_val);
    if (err != ESP_OK) {
        return err;
    }
    err = reg_update_mask(dev, REG_THRESH_ACT_H, THRESH_H_MASK, (uint8_t)(threshold >> 6));
    if (err != ESP_OK) {
        return err;
    }
    err = reg_update_mask(dev, REG_THRESH_ACT_L, THRESH_L_MASK, (uint8_t)((threshold & 0x3Fu) << 2));
    if (err != ESP_OK) {
        return err;
    }
    return reg_write(dev, REG_TIME_ACT, time_act_samples);
}

/* Register writes follow driver/ref-adxl367/adxl367.c adxl367_setup_inactivity_detection(). */
esp_err_t node_acc_adxl367_setup_inactivity_detection(node_acc_adxl367_dev_t *dev,
                                                      node_acc_adxl367_ai_ref_t ref_mode,
                                                      uint16_t threshold,
                                                      uint16_t time_inact_samples)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (threshold > 0x1FFFu || ref_mode > NODE_ACC_ADXL367_AI_REF_REL) {
        return ESP_ERR_INVALID_ARG;
    }

    uint8_t inact_val = (ref_mode == NODE_ACC_ADXL367_AI_REF_ABS) ? 1u : 3u;
    esp_err_t err = reg_update_mask(dev, REG_ACT_INACT_CTL, INACT_EN_MSK, (uint8_t)(inact_val << 2));
    if (err != ESP_OK) {
        return err;
    }
    err = reg_update_mask(dev, REG_THRESH_INACT_H, THRESH_H_MASK, (uint8_t)(threshold >> 6));
    if (err != ESP_OK) {
        return err;
    }
    err = reg_update_mask(dev, REG_THRESH_INACT_L, THRESH_L_MASK, (uint8_t)((threshold & 0x3Fu) << 2));
    if (err != ESP_OK) {
        return err;
    }
    err = reg_write(dev, REG_TIME_INACT_H, (uint8_t)(time_inact_samples >> 8));
    if (err != ESP_OK) {
        return err;
    }
    return reg_write(dev, REG_TIME_INACT_L, (uint8_t)(time_inact_samples & 0xFFu));
}

esp_err_t node_acc_adxl367_fifo_setup(node_acc_adxl367_dev_t *dev,
                                      node_acc_adxl367_fifo_mode_t mode,
                                      node_acc_adxl367_fifo_format_t format,
                                      uint16_t sample_sets_nb)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (mode > NODE_ACC_ADXL367_FIFO_TRIGGERED || format > NODE_ACC_ADXL367_FIFO_FMT_XYZT) {
        return ESP_ERR_INVALID_ARG;
    }

    esp_err_t err = reg_update_mask(dev, REG_FIFO_CONTROL, FIFO_CTL_MODE_MASK, (uint8_t)mode);
    if (err != ESP_OK) {
        return err;
    }
    err = reg_update_mask(dev, REG_FIFO_CONTROL, FIFO_CTL_CH_MASK, (uint8_t)((uint8_t)format << 3));
    if (err != ESP_OK) {
        return err;
    }
    err = fifo_set_sample_sets_nb(dev, sample_sets_nb);
    if (err != ESP_OK) {
        return err;
    }
    /* adxl367_set_fifo_read_mode: ADXL367_14B_CHID -> ADC_CTL[7:6] = 3 */
    err = reg_update_mask(dev, REG_ADC_CTL, ADC_CTL_FIFO_RD_MODE_MASK, FIFO_RD_14B_CHID << 6);
    if (err != ESP_OK) {
        return err;
    }

    dev->fifo_mode = mode;
    dev->fifo_format = format;
    return ESP_OK;
}

esp_err_t node_acc_adxl367_fifo_disable(node_acc_adxl367_dev_t *dev)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t err = reg_update_mask(dev, REG_FIFO_CONTROL, FIFO_CTL_MODE_MASK, 0u);
    if (err == ESP_OK) {
        dev->fifo_mode = NODE_ACC_ADXL367_FIFO_DISABLED;
    }
    return err;
}

esp_err_t node_acc_adxl367_fifo_get_entry_count(node_acc_adxl367_dev_t *dev, uint16_t *count)
{
    if (dev == NULL || count == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    uint8_t v[2];
    esp_err_t err = reg_read(dev, REG_FIFO_ENTRIES_L, v, sizeof(v));
    if (err != ESP_OK) {
        return err;
    }
    *count = (uint16_t)(((uint16_t)(v[1] & 0x03u) << 8) | v[0]);
    return ESP_OK;
}

esp_err_t node_acc_adxl367_fifo_drain_xyz(node_acc_adxl367_dev_t *dev, int16_t *x, int16_t *y, int16_t *z,
                                          size_t max_triplets, size_t *out_triplets)
{
    if (dev == NULL || x == NULL || y == NULL || z == NULL || out_triplets == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (dev->fifo_format != NODE_ACC_ADXL367_FIFO_FMT_XYZ) {
        return ESP_ERR_INVALID_STATE;
    }

    uint16_t n_ent = 0;
    esp_err_t err = node_acc_adxl367_fifo_get_entry_count(dev, &n_ent);
    if (err != ESP_OK) {
        return err;
    }
    if (n_ent == 0) {
        *out_triplets = 0;
        return ESP_OK;
    }

    size_t n_bytes = (size_t)n_ent * 2u;
    if (n_bytes > FIFO_MAX_READ_BYTES) {
        n_bytes = FIFO_MAX_READ_BYTES;
    }

    uint8_t buf[FIFO_MAX_READ_BYTES];
    err = fifo_read_payload(dev, buf, n_bytes);
    if (err != ESP_OK) {
        return err;
    }

    int16_t *px = x;
    int16_t *py = y;
    int16_t *pz = z;
    size_t nt = 0;

    for (size_t i = 0; i + 1 < n_bytes && nt < max_triplets; i += 2) {
        uint8_t ch = (uint8_t)(buf[i] >> 6);
        int16_t val = pack_fifo_14b_chid(buf[i], buf[i + 1]);
        switch (ch) {
        case 0u:
            if ((size_t)(px - x) >= max_triplets) {
                return ESP_ERR_NO_MEM;
            }
            *px++ = val;
            break;
        case 1u:
            if ((size_t)(py - y) >= max_triplets) {
                return ESP_ERR_NO_MEM;
            }
            *py++ = val;
            break;
        case 2u:
            if ((size_t)(pz - z) >= max_triplets) {
                return ESP_ERR_NO_MEM;
            }
            *pz++ = val;
            nt++;
            break;
        default:
            ESP_LOGE(TAG, "FIFO bad channel id %u", (unsigned)ch);
            return ESP_ERR_INVALID_RESPONSE;
        }
    }

    *out_triplets = nt;
    return ESP_OK;
}

esp_err_t node_acc_adxl367_probe(node_acc_adxl367_dev_t *dev)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    uint8_t v = 0;
    esp_err_t err = reg_read(dev, REG_DEVID_AD, &v, 1);
    if (err != ESP_OK) {
        return err;
    }
    if (v != DEVID_AD_VAL) {
        ESP_LOGE(TAG, "DEVID_AD mismatch: got 0x%02X expect 0x%02X", v, DEVID_AD_VAL);
        return ESP_ERR_NOT_FOUND;
    }
    err = reg_read(dev, REG_DEVID_MST, &v, 1);
    if (err != ESP_OK) {
        return err;
    }
    if (v != DEVID_MST_VAL) {
        ESP_LOGE(TAG, "DEVID_MST mismatch: got 0x%02X expect 0x%02X", v, DEVID_MST_VAL);
        return ESP_ERR_NOT_FOUND;
    }
    err = reg_read(dev, REG_PARTID, &v, 1);
    if (err != ESP_OK) {
        return err;
    }
    if (v != PARTID_VAL) {
        ESP_LOGE(TAG, "PARTID mismatch: got 0x%02X expect 0x%02X", v, PARTID_VAL);
        return ESP_ERR_NOT_FOUND;
    }
    err = reg_read(dev, REG_REVID, &v, 1);
    if (err != ESP_OK) {
        return err;
    }
    if (v != REVID_ADXL367) {
        ESP_LOGE(TAG, "REVID mismatch: got 0x%02X expect 0x%02X (ADXL367)", v, REVID_ADXL367);
        return ESP_ERR_NOT_FOUND;
    }
    return ESP_OK;
}

esp_err_t node_acc_adxl367_set_range(node_acc_adxl367_dev_t *dev, node_acc_adxl367_range_t range)
{
    if (dev == NULL || range > NODE_ACC_ADXL367_RANGE_8G) {
        return ESP_ERR_INVALID_ARG;
    }
    uint8_t shifted = (uint8_t)((uint8_t)range << 6);
    esp_err_t err = reg_update_mask(dev, REG_FILTER_CTL, FILTER_CTL_RANGE_MASK, shifted);
    if (err == ESP_OK) {
        dev->range = range;
    }
    return err;
}

esp_err_t node_acc_adxl367_set_odr(node_acc_adxl367_dev_t *dev, node_acc_adxl367_odr_t odr)
{
    if (dev == NULL || odr > NODE_ACC_ADXL367_ODR_400_HZ) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t err = reg_update_mask(dev, REG_FILTER_CTL, FILTER_CTL_ODR_MASK, (uint8_t)odr);
    if (err == ESP_OK) {
        dev->odr = odr;
    }
    return err;
}

esp_err_t node_acc_adxl367_set_mode(node_acc_adxl367_dev_t *dev, node_acc_adxl367_mode_t mode)
{
    if (dev == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (mode != NODE_ACC_ADXL367_MODE_STANDBY && mode != NODE_ACC_ADXL367_MODE_MEASURE) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t err =
        reg_update_mask(dev, REG_POWER_CTL, POWER_CTL_MEASURE_MASK, (uint8_t)mode);
    if (err != ESP_OK) {
        return err;
    }
    if (mode == NODE_ACC_ADXL367_MODE_MEASURE) {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    dev->mode = mode;
    return ESP_OK;
}

esp_err_t node_acc_adxl367_read_xyz_raw(node_acc_adxl367_dev_t *dev, int16_t *x, int16_t *y, int16_t *z)
{
    if (dev == NULL || x == NULL || y == NULL || z == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    if (dev->meas_mode == NODE_ACC_ADXL367_MEAS_TEMP_ONLY) {
        return ESP_ERR_INVALID_STATE;
    }

    /* Same flow as Analog @c adxl367_get_raw_xyz: wait DATA_RDY, then read 6 bytes from XDATA_H (burst). */
    esp_err_t err = wait_data_rdy(dev);
    if (err != ESP_OK) {
        if (err == ESP_ERR_TIMEOUT) {
            ESP_LOGW(TAG, "DATA_RDY timeout");
        }
        return err;
    }

    uint8_t b[6];
    err = reg_read(dev, REG_XDATA_H, b, sizeof(b));
    if (err != ESP_OK) {
        return err;
    }

    *x = pack_14bit_xy(b[0], b[1]);
    *y = pack_14bit_xy(b[2], b[3]);
    *z = pack_14bit_xy(b[4], b[5]);
    return ESP_OK;
}

esp_err_t node_acc_adxl367_init(node_acc_adxl367_dev_t *dev)
{
    if (dev == NULL || dev->i2c == NULL) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t err = node_acc_adxl367_soft_reset(dev);
    if (err != ESP_OK) {
        return err;
    }

    err = node_acc_adxl367_probe(dev);
    if (err != ESP_OK) {
        return err;
    }

    err = node_acc_adxl367_set_range(dev, NODE_ACC_ADXL367_RANGE_2G);
    if (err != ESP_OK) {
        return err;
    }

    err = node_acc_adxl367_set_odr(dev, NODE_ACC_ADXL367_ODR_100_HZ);
    if (err != ESP_OK) {
        return err;
    }

    err = node_acc_adxl367_set_mode(dev, NODE_ACC_ADXL367_MODE_MEASURE);
    if (err != ESP_OK) {
        return err;
    }

    err = node_acc_adxl367_set_measurement_mode(dev, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (err != ESP_OK) {
        return err;
    }

    ESP_LOGI(TAG, "Init OK (2g, 100 Hz, measure, accel-only)");
    return ESP_OK;
}

node_acc_adxl367_test.h

/**
 * @file node_acc_adxl367_test.h
 * @brief Phases 1-5 tests; Phase 6: run_phase6_default_app. Driver: node_acc_adxl367.h.
 */
#pragma once

#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

esp_err_t node_acc_adxl367_run_phase1_test(void);
esp_err_t node_acc_adxl367_run_phase2_test(void);
esp_err_t node_acc_adxl367_run_phase3_test(void);
esp_err_t node_acc_adxl367_run_phase4_test(void);
esp_err_t node_acc_adxl367_run_phase5_test(void);

/** Phase 6 default: does not return. Calls transition monitor (UART motion/still lines). */
void node_acc_adxl367_run_phase6_default_app(void);

esp_err_t node_acc_adxl367_run_xyz_stats_capture(void);

/** Software motion/still from max sample delta; does not return. */
void node_acc_adxl367_run_transition_monitor_forever(void);

#ifdef __cplusplus
}
#endif

node_acc_adxl367_test.c

/**
 * @file node_acc_adxl367_test.c
 * @brief Phases 1-5 tests; Phase 6: run_phase6_default_app.
 */
#include "node_acc_adxl367_test.h"

#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"

#include "node_acc_adxl367.h"
#include "node_i2c.h"

#include <inttypes.h>
#include <stdint.h>

static const char *TAG = "adxl367_test";

/** ~g from raw accel code (same scale intent as @c NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G). */
#define ADXL367_STATS_CODE_TO_G(c) ((double)(c) / 4000.0)

/** ADXL367 INT1 on this board (DOC: MORE-PERCEPTION/ADXL367). Override if wiring differs. */
#ifndef NODE_ACC_ADXL367_TEST_INT1_GPIO
#define NODE_ACC_ADXL367_TEST_INT1_GPIO 10
#endif

/** ADXL367 INT2 (DOC: MORE-PERCEPTION/ADXL367). */
#ifndef NODE_ACC_ADXL367_TEST_INT2_GPIO
#define NODE_ACC_ADXL367_TEST_INT2_GPIO 18
#endif

/** Phase 5: monitor window (ms) for linked ACT/INACT demo. */
#ifndef NODE_ACC_ADXL367_TEST_P5_WINDOW_MS
#define NODE_ACC_ADXL367_TEST_P5_WINDOW_MS 60000
#endif
#ifndef NODE_ACC_ADXL367_TEST_P5_POLL_MS
#define NODE_ACC_ADXL367_TEST_P5_POLL_MS 50
#endif
/** Delay before gpio_intr_enable(INT2) after config (baseline settle). */
#ifndef NODE_ACC_ADXL367_TEST_P5_ARM_MS
#define NODE_ACC_ADXL367_TEST_P5_ARM_MS 3000
#endif
/** Phase 5 THRESH_ACT scale (g, +/-2g intent). */
#ifndef NODE_ACC_ADXL367_TEST_P5_THRESH_G
#define NODE_ACC_ADXL367_TEST_P5_THRESH_G 0.22f
#endif
/** TIME_ACT sample count at ODR. */
#ifndef NODE_ACC_ADXL367_TEST_P5_TIME_ACT_SAMPLES
#define NODE_ACC_ADXL367_TEST_P5_TIME_ACT_SAMPLES 32u
#endif
/** THRESH_INACT (g) for referenced inactivity. */
#ifndef NODE_ACC_ADXL367_TEST_P5_THRESH_INACT_G
#define NODE_ACC_ADXL367_TEST_P5_THRESH_INACT_G 0.10f
#endif
/** TIME_INACT sample count. */
#ifndef NODE_ACC_ADXL367_TEST_P5_TIME_INACT_SAMPLES
#define NODE_ACC_ADXL367_TEST_P5_TIME_INACT_SAMPLES 250u
#endif
/** ACT_INACT_CTL[5:4]: 3 = linked + internal STATUS ack (see datasheet). */
#ifndef NODE_ACC_ADXL367_TEST_P5_LINKLOOP_MODE
#define NODE_ACC_ADXL367_TEST_P5_LINKLOOP_MODE 3u
#endif

/** Stats capture duration (ms), one sample per DATA_RDY. */
#ifndef NODE_ACC_ADXL367_TEST_STATS_MS
#define NODE_ACC_ADXL367_TEST_STATS_MS 15000
#endif

/** max(|dx|,|dy|,|dz|) between consecutive samples; above: still_to_motion. */
#ifndef NODE_ACC_ADXL367_MONITOR_DELTA_HIGH_LSB
#define NODE_ACC_ADXL367_MONITOR_DELTA_HIGH_LSB 350
#endif
/** Below this for MONITOR_STILL_COUNT samples: motion_to_still. */
#ifndef NODE_ACC_ADXL367_MONITOR_DELTA_LOW_LSB
#define NODE_ACC_ADXL367_MONITOR_DELTA_LOW_LSB 120
#endif
#ifndef NODE_ACC_ADXL367_MONITOR_STILL_COUNT
#define NODE_ACC_ADXL367_MONITOR_STILL_COUNT 45u
#endif
/** Ignore saturated / invalid samples (|code| above this on any axis). */
#ifndef NODE_ACC_ADXL367_MONITOR_ABS_MAX
#define NODE_ACC_ADXL367_MONITOR_ABS_MAX 7800
#endif

static uint32_t adxl367_uabs_diff(int16_t a, int16_t b)
{
    int32_t d = (int32_t)a - (int32_t)b;
    if (d < 0) {
        d = -d;
    }
    return (uint32_t)d;
}

static uint32_t adxl367_max_u32(uint32_t a, uint32_t b, uint32_t c)
{
    uint32_t m = a;
    if (b > m) {
        m = b;
    }
    if (c > m) {
        m = c;
    }
    return m;
}

static void IRAM_ATTR p4_fifo_int_isr(void *arg)
{
    SemaphoreHandle_t sem = (SemaphoreHandle_t)arg;
    if (sem != NULL) {
        (void)xSemaphoreGiveFromISR(sem, NULL);
    }
}

static void IRAM_ATTR p5_act_int_isr(void *arg)
{
    SemaphoreHandle_t sem = (SemaphoreHandle_t)arg;
    if (sem != NULL) {
        (void)xSemaphoreGiveFromISR(sem, NULL);
    }
}

static esp_err_t probe_i2c_and_init(node_acc_adxl367_dev_t *acc, i2c_master_dev_handle_t *out_dev)
{
    static const uint8_t k_addrs[] = {
        NODE_ACC_ADXL367_I2C_ADDR_0X1D,
        NODE_ACC_ADXL367_I2C_ADDR_0X53,
    };

    esp_err_t ret = ESP_ERR_NOT_FOUND;
    *out_dev = NULL;

    for (size_t a = 0; a < sizeof(k_addrs) / sizeof(k_addrs[0]); a++) {
        uint8_t addr = k_addrs[a];
        ret = i2c_add_device(addr, I2C_MASTER_FREQ_HZ, out_dev);
        if (ret != ESP_OK) {
            continue;
        }
        acc->i2c = *out_dev;
        ret = node_acc_adxl367_init(acc);
        if (ret == ESP_OK) {
            ESP_LOGI(TAG, "ADXL367 at I2C 0x%02X", (unsigned)addr);
            return ESP_OK;
        }
        ESP_LOGD(TAG, "no chip at 0x%02X: %s", (unsigned)addr, esp_err_to_name(ret));
        (void)i2c_remove_device(*out_dev);
        *out_dev = NULL;
    }

    if (*out_dev == NULL) {
        ESP_LOGE(TAG, "No ADXL367 on I2C (tried 0x1D and 0x53).");
    }
    return ret;
}

esp_err_t node_acc_adxl367_run_xyz_stats_capture(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "stats: set_measurement_mode failed: %s", esp_err_to_name(ret));
        goto out;
    }

    int64_t sx = 0, sy = 0, sz = 0;
    int16_t min_x = 0, max_x = 0, min_y = 0, max_y = 0, min_z = 0, max_z = 0;
    uint32_t n = 0;
    bool first = true;

    ESP_LOGI(TAG, "======== XYZ stats capture: %d ms (keep board still for noise floor) ========",
             (int)NODE_ACC_ADXL367_TEST_STATS_MS);
    const TickType_t t_end = xTaskGetTickCount() + pdMS_TO_TICKS(NODE_ACC_ADXL367_TEST_STATS_MS);

    while (xTaskGetTickCount() < t_end) {
        int16_t x = 0, y = 0, z = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "stats: read_xyz_raw failed: %s", esp_err_to_name(ret));
            break;
        }
        sx += x;
        sy += y;
        sz += z;
        if (first) {
            min_x = max_x = x;
            min_y = max_y = y;
            min_z = max_z = z;
            first = false;
        } else {
            if (x < min_x) {
                min_x = x;
            }
            if (x > max_x) {
                max_x = x;
            }
            if (y < min_y) {
                min_y = y;
            }
            if (y > max_y) {
                max_y = y;
            }
            if (z < min_z) {
                min_z = z;
            }
            if (z > max_z) {
                max_z = z;
            }
        }
        n++;
    }

    if (ret == ESP_OK && n > 0) {
        const int32_t mx = (int32_t)(sx / (int64_t)n);
        const int32_t my = (int32_t)(sy / (int64_t)n);
        const int32_t mz = (int32_t)(sz / (int64_t)n);
        const int32_t span_x = (int32_t)max_x - (int32_t)min_x;
        const int32_t span_y = (int32_t)max_y - (int32_t)min_y;
        const int32_t span_z = (int32_t)max_z - (int32_t)min_z;

        ESP_LOGI(TAG, "stats: samples=%u (~%.0f Hz if steady DATA_RDY)", (unsigned)n,
                 (double)n * 1000.0 / (double)NODE_ACC_ADXL367_TEST_STATS_MS);
        ESP_LOGI(TAG, "stats: X mean=%" PRId32 " (~%.4f g)  min=%d max=%d  span=%" PRId32 " (~%.4f g)", mx,
                 ADXL367_STATS_CODE_TO_G(mx), (int)min_x, (int)max_x, (int32_t)span_x,
                 ADXL367_STATS_CODE_TO_G(span_x));
        ESP_LOGI(TAG, "stats: Y mean=%" PRId32 " (~%.4f g)  min=%d max=%d  span=%" PRId32 " (~%.4f g)", my,
                 ADXL367_STATS_CODE_TO_G(my), (int)min_y, (int)max_y, (int32_t)span_y,
                 ADXL367_STATS_CODE_TO_G(span_y));
        ESP_LOGI(TAG, "stats: Z mean=%" PRId32 " (~%.4f g)  min=%d max=%d  span=%" PRId32 " (~%.4f g)", mz,
                 ADXL367_STATS_CODE_TO_G(mz), (int)min_z, (int)max_z, (int32_t)span_z,
                 ADXL367_STATS_CODE_TO_G(span_z));
        ESP_LOGI(TAG,
                 "stats: THRESH_ACT LSB ~ g*4000 (+/-2g intent)");
    } else if (n == 0) {
        ret = ESP_ERR_INVALID_STATE;
    }

out:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    return ret;
}

esp_err_t node_acc_adxl367_run_phase1_test(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    for (int i = 0; i < 8; i++) {
        int16_t x = 0, y = 0, z = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "read_xyz_raw failed: %s", esp_err_to_name(ret));
            break;
        }
        ESP_LOGI(TAG, "p1 sample %d: x=%d y=%d z=%d", i, (int)x, (int)y, (int)z);
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    (void)i2c_remove_device(i2c_dev);
    return ret;
}

esp_err_t node_acc_adxl367_run_phase2_test(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    /* ACCEL_ONLY: one XYZ sample */
    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        goto out;
    }
    {
        int16_t x = 0, y = 0, z = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "p2 accel-only xyz failed: %s", esp_err_to_name(ret));
            goto out;
        }
        ESP_LOGI(TAG, "p2 accel-only: x=%d y=%d z=%d", (int)x, (int)y, (int)z);
    }

    /* ACCEL_AND_TEMP */
    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_AND_TEMP);
    if (ret != ESP_OK) {
        goto out;
    }
    {
        int16_t x = 0, y = 0, z = 0;
        float tc = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "p2 accel+temp xyz failed: %s", esp_err_to_name(ret));
            goto out;
        }
        ret = node_acc_adxl367_read_temp_celsius(&acc, &tc);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "p2 accel+temp temp C failed: %s", esp_err_to_name(ret));
            goto out;
        }
        ESP_LOGI(TAG, "p2 accel+temp: x=%d y=%d z=%d temp=%.2f C", (int)x, (int)y, (int)z, (double)tc);
    }

    /* TEMP_ONLY */
    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_TEMP_ONLY);
    if (ret != ESP_OK) {
        goto out;
    }
    {
        float tc = 0;
        ret = node_acc_adxl367_read_temp_celsius(&acc, &tc);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "p2 temp-only temp failed: %s", esp_err_to_name(ret));
            goto out;
        }
        ESP_LOGI(TAG, "p2 temp-only: temp=%.2f C", (double)tc);

        int16_t x = 0, y = 0, z = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_ERR_INVALID_STATE) {
            ESP_LOGE(TAG, "p2 temp-only: expected INVALID_STATE for xyz");
            ret = ESP_FAIL;
            goto out;
        }
        ESP_LOGI(TAG, "p2 temp-only: xyz correctly rejected (INVALID_STATE)");
        ret = ESP_OK;
    }

out:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    return ret;
}

/** STATUS FIFO watermark bit. */
#define P3_STATUS_FIFO_WATERMARK (1u << 2)

esp_err_t node_acc_adxl367_run_phase3_test(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        goto out;
    }

    /* Stream mode, XYZ, watermark at 8 sample sets (XYZ => 8*3 FIFO entries when full). */
    ret = node_acc_adxl367_fifo_setup(&acc, NODE_ACC_ADXL367_FIFO_STREAM, NODE_ACC_ADXL367_FIFO_FMT_XYZ, 8);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p3 fifo_setup failed: %s", esp_err_to_name(ret));
        goto out;
    }

    for (int w = 0; w < 50; w++) {
        uint8_t st = 0;
        ret = node_acc_adxl367_read_status(&acc, &st);
        if (ret != ESP_OK) {
            goto out;
        }
        if ((st & P3_STATUS_FIFO_WATERMARK) != 0) {
            ESP_LOGI(TAG, "p3 STATUS 0x%02X (FIFO watermark)", (unsigned)st);
            break;
        }
        vTaskDelay(pdMS_TO_TICKS(20));
    }

    uint16_t n_ent = 0;
    ret = node_acc_adxl367_fifo_get_entry_count(&acc, &n_ent);
    if (ret != ESP_OK) {
        goto out;
    }
    ESP_LOGI(TAG, "p3 FIFO entries before drain: %u", (unsigned)n_ent);

    int16_t x[32], y[32], z[32];
    size_t nt = 0;
    ret = node_acc_adxl367_fifo_drain_xyz(&acc, x, y, z, sizeof(x) / sizeof(x[0]), &nt);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p3 fifo_drain_xyz failed: %s", esp_err_to_name(ret));
        goto out;
    }
    ESP_LOGI(TAG, "p3 drained complete XYZ triplets: %u", (unsigned)nt);
    for (size_t i = 0; i < nt && i < 4u; i++) {
        ESP_LOGI(TAG, "p3 triplet %u: x=%d y=%d z=%d", (unsigned)i, (int)x[i], (int)y[i], (int)z[i]);
    }

    ret = node_acc_adxl367_fifo_disable(&acc);
    if (ret != ESP_OK) {
        goto out;
    }
    ESP_LOGI(TAG, "p3 FIFO disabled OK");

out:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    return ret;
}

esp_err_t node_acc_adxl367_run_phase4_test(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};
    SemaphoreHandle_t sem = NULL;
    bool handler_added = false;

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    sem = xSemaphoreCreateBinary();
    if (sem == NULL) {
        ret = ESP_ERR_NO_MEM;
        goto out;
    }

    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        goto out_sem;
    }

    node_acc_adxl367_int_map_t imap = {0};
    imap.fifo_watermark = 1u;
    imap.int_low = 1u;
    ret = node_acc_adxl367_int_map(&acc, 1u, &imap);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p4 int_map failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    ret = node_acc_adxl367_fifo_setup(&acc, NODE_ACC_ADXL367_FIFO_STREAM, NODE_ACC_ADXL367_FIFO_FMT_XYZ, 8);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p4 fifo_setup failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    ret = gpio_install_isr_service(0);
    if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
        ESP_LOGE(TAG, "p4 gpio_install_isr_service failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    gpio_config_t io = {
        .pin_bit_mask = 1ull << NODE_ACC_ADXL367_TEST_INT1_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    ret = gpio_config(&io);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p4 gpio_config failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    ret = gpio_isr_handler_add((gpio_num_t)NODE_ACC_ADXL367_TEST_INT1_GPIO, p4_fifo_int_isr, sem);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p4 gpio_isr_handler_add failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    handler_added = true;
    gpio_intr_enable((gpio_num_t)NODE_ACC_ADXL367_TEST_INT1_GPIO);

    if (xSemaphoreTake(sem, pdMS_TO_TICKS(3000)) != pdTRUE) {
        ESP_LOGE(TAG, "p4 timeout waiting for INT1 (FIFO watermark)");
        ret = ESP_ERR_TIMEOUT;
        goto out_gpio;
    }

    uint8_t st = 0;
    ret = node_acc_adxl367_read_status(&acc, &st);
    if (ret != ESP_OK) {
        goto out_gpio;
    }
    ESP_LOGI(TAG, "p4 STATUS 0x%02X (after INT)", (unsigned)st);

    uint16_t n_ent = 0;
    ret = node_acc_adxl367_fifo_get_entry_count(&acc, &n_ent);
    if (ret != ESP_OK) {
        goto out_gpio;
    }
    ESP_LOGI(TAG, "p4 FIFO entries: %u", (unsigned)n_ent);

    int16_t x[32], y[32], z[32];
    size_t nt = 0;
    ret = node_acc_adxl367_fifo_drain_xyz(&acc, x, y, z, sizeof(x) / sizeof(x[0]), &nt);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p4 fifo_drain_xyz failed: %s", esp_err_to_name(ret));
        goto out_gpio;
    }
    ESP_LOGI(TAG, "p4 drained XYZ triplets: %u", (unsigned)nt);
    if (nt > 0) {
        ESP_LOGI(TAG, "p4 first triplet: x=%d y=%d z=%d", (int)x[0], (int)y[0], (int)z[0]);
    }

    ret = node_acc_adxl367_fifo_disable(&acc);
    if (ret != ESP_OK) {
        goto out_gpio;
    }
    ESP_LOGI(TAG, "p4 FIFO disabled OK");

out_gpio:
    if (handler_added) {
        gpio_intr_disable((gpio_num_t)NODE_ACC_ADXL367_TEST_INT1_GPIO);
        gpio_isr_handler_remove((gpio_num_t)NODE_ACC_ADXL367_TEST_INT1_GPIO);
    }
    gpio_reset_pin((gpio_num_t)NODE_ACC_ADXL367_TEST_INT1_GPIO);

out_sem:
    vSemaphoreDelete(sem);

out:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    return ret;
}

esp_err_t node_acc_adxl367_run_phase5_test(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};
    SemaphoreHandle_t sem = NULL;
    bool handler_added = false;

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        return ret;
    }

    sem = xSemaphoreCreateBinary();
    if (sem == NULL) {
        ret = ESP_ERR_NO_MEM;
        goto out;
    }

    ret = node_acc_adxl367_fifo_disable(&acc);
    if (ret != ESP_OK) {
        goto out_sem;
    }

    ret = node_acc_adxl367_act_inact_disable(&acc);
    if (ret != ESP_OK) {
        goto out_sem;
    }

    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        goto out_sem;
    }

    /* REF activity/inact, LINKLOOP=3, X axis only. */
    ret = node_acc_adxl367_setup_activity_detection(
        &acc, NODE_ACC_ADXL367_AI_REF_REL, NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G(NODE_ACC_ADXL367_TEST_P5_THRESH_G),
        (uint8_t)NODE_ACC_ADXL367_TEST_P5_TIME_ACT_SAMPLES);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 setup_activity failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    ret = node_acc_adxl367_setup_inactivity_detection(
        &acc, NODE_ACC_ADXL367_AI_REF_REL,
        NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G(NODE_ACC_ADXL367_TEST_P5_THRESH_INACT_G),
        (uint16_t)NODE_ACC_ADXL367_TEST_P5_TIME_INACT_SAMPLES);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 setup_inactivity failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    ret = node_acc_adxl367_set_act_inact_linkloop(&acc, (uint8_t)NODE_ACC_ADXL367_TEST_P5_LINKLOOP_MODE);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 linkloop failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    /* AXIS_MASK: block Y/Z so ACT/INACT use X only (same mask applies to both). */
    ret = node_acc_adxl367_set_activity_axes_enabled(&acc, true, false, false);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 set_activity_axes failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    vTaskDelay(pdMS_TO_TICKS(200));

    node_acc_adxl367_int_map_t imap = {0};
    imap.act = 1u;
    imap.inact = 1u;
    imap.int_low = 1u;
    ret = node_acc_adxl367_int_map(&acc, 2u, &imap);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 int_map failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    ret = gpio_install_isr_service(0);
    if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
        ESP_LOGE(TAG, "p5 gpio_install_isr_service failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    gpio_config_t io = {
        .pin_bit_mask = 1ull << NODE_ACC_ADXL367_TEST_INT2_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    ret = gpio_config(&io);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 gpio_config failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }

    ret = gpio_isr_handler_add((gpio_num_t)NODE_ACC_ADXL367_TEST_INT2_GPIO, p5_act_int_isr, sem);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "p5 gpio_isr_handler_add failed: %s", esp_err_to_name(ret));
        goto out_sem;
    }
    handler_added = true;
    /* INT2 off until arm delay. */

    ESP_LOGI(TAG, "p5 linked ACT/INACT, window %d s", (int)(NODE_ACC_ADXL367_TEST_P5_WINDOW_MS / 1000));
    ESP_LOGI(TAG, "THRESH_ACT ~%.2f g (%u LSB), TIME_ACT=%u; THRESH_INACT ~%.2f g (%u LSB), TIME_INACT=%u",
             (double)NODE_ACC_ADXL367_TEST_P5_THRESH_G,
             (unsigned)NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G(NODE_ACC_ADXL367_TEST_P5_THRESH_G),
             (unsigned)NODE_ACC_ADXL367_TEST_P5_TIME_ACT_SAMPLES, (double)NODE_ACC_ADXL367_TEST_P5_THRESH_INACT_G,
             (unsigned)NODE_ACC_ADXL367_THRESH_ACT_LSB_FROM_G(NODE_ACC_ADXL367_TEST_P5_THRESH_INACT_G),
             (unsigned)NODE_ACC_ADXL367_TEST_P5_TIME_INACT_SAMPLES);
    ESP_LOGI(TAG, "LINKLOOP=%u, axes: X only (Y/Z masked)", (unsigned)NODE_ACC_ADXL367_TEST_P5_LINKLOOP_MODE);
    ESP_LOGI(TAG, "Waiting %d ms with INT2 disabled (reference settle; avoids false start)",
             (int)NODE_ACC_ADXL367_TEST_P5_ARM_MS);

    vTaskDelay(pdMS_TO_TICKS(NODE_ACC_ADXL367_TEST_P5_ARM_MS));

    {
        uint8_t st_clr = 0;
        for (int i = 0; i < 3; i++) {
            (void)node_acc_adxl367_read_status(&acc, &st_clr);
        }
        while (xSemaphoreTake(sem, 0) == pdTRUE) {
        }

        gpio_intr_enable((gpio_num_t)NODE_ACC_ADXL367_TEST_INT2_GPIO);
        vTaskDelay(pdMS_TO_TICKS(50));
        while (xSemaphoreTake(sem, 0) == pdTRUE) {
        }

        ESP_LOGI(TAG, "INT2 on; poll STATUS");

        const TickType_t t_end = xTaskGetTickCount() + pdMS_TO_TICKS(NODE_ACC_ADXL367_TEST_P5_WINDOW_MS);

        ret = ESP_OK;
        while (xTaskGetTickCount() < t_end) {
            while (xSemaphoreTake(sem, 0) == pdTRUE) {
                uint8_t st = 0;
                (void)node_acc_adxl367_read_status(&acc, &st);
                if ((st & NODE_ACC_ADXL367_STATUS_ACT) != 0) {
                    ESP_LOGI(TAG, "[activity] motion (STATUS 0x%02X)", (unsigned)st);
                } else if ((st & NODE_ACC_ADXL367_STATUS_INACT) != 0) {
                    ESP_LOGI(TAG, "[inactive] still / below inact threshold (STATUS 0x%02X)", (unsigned)st);
                } else {
                    ESP_LOGD(TAG, "INT2 edge, STATUS 0x%02X (no ACT/INACT latched)", (unsigned)st);
                }
            }

            vTaskDelay(pdMS_TO_TICKS(NODE_ACC_ADXL367_TEST_P5_POLL_MS));
        }

        ESP_LOGI(TAG, "======== Phase 5 end ========");
    }

    if (handler_added) {
        gpio_intr_disable((gpio_num_t)NODE_ACC_ADXL367_TEST_INT2_GPIO);
        gpio_isr_handler_remove((gpio_num_t)NODE_ACC_ADXL367_TEST_INT2_GPIO);
    }
    gpio_reset_pin((gpio_num_t)NODE_ACC_ADXL367_TEST_INT2_GPIO);

    (void)node_acc_adxl367_act_inact_disable(&acc);
    node_acc_adxl367_int_map_t zmap = {0};
    (void)node_acc_adxl367_int_map(&acc, 2u, &zmap);

out_sem:
    vSemaphoreDelete(sem);

out:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    return ret;
}

void node_acc_adxl367_run_transition_monitor_forever(void)
{
    i2c_master_dev_handle_t i2c_dev = NULL;
    node_acc_adxl367_dev_t acc = {0};

    esp_err_t ret = probe_i2c_and_init(&acc, &i2c_dev);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "transition monitor: init failed");
        while (1) {
            vTaskDelay(pdMS_TO_TICKS(2000));
        }
    }

    ret = node_acc_adxl367_set_measurement_mode(&acc, NODE_ACC_ADXL367_MEAS_ACCEL_ONLY);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "transition monitor: accel-only failed: %s", esp_err_to_name(ret));
        goto hang;
    }
    (void)node_acc_adxl367_fifo_disable(&acc);
    (void)node_acc_adxl367_act_inact_disable(&acc);

    vTaskDelay(pdMS_TO_TICKS(200));

    ESP_LOGI(TAG, "transition monitor: max sample delta, high>%d low<%d still_cnt=%u",
             (int)NODE_ACC_ADXL367_MONITOR_DELTA_HIGH_LSB, (int)NODE_ACC_ADXL367_MONITOR_DELTA_LOW_LSB,
             (unsigned)NODE_ACC_ADXL367_MONITOR_STILL_COUNT);

    bool have_prev = false;
    int16_t px = 0, py = 0, pz = 0;
    bool moving = false;
    uint32_t still_cnt = 0;

    for (;;) {
        int16_t x = 0, y = 0, z = 0;
        ret = node_acc_adxl367_read_xyz_raw(&acc, &x, &y, &z);
        if (ret != ESP_OK) {
            vTaskDelay(pdMS_TO_TICKS(10));
            continue;
        }

        if (x > NODE_ACC_ADXL367_MONITOR_ABS_MAX || x < -NODE_ACC_ADXL367_MONITOR_ABS_MAX ||
            y > NODE_ACC_ADXL367_MONITOR_ABS_MAX || y < -NODE_ACC_ADXL367_MONITOR_ABS_MAX ||
            z > NODE_ACC_ADXL367_MONITOR_ABS_MAX || z < -NODE_ACC_ADXL367_MONITOR_ABS_MAX) {
            have_prev = false;
            vTaskDelay(pdMS_TO_TICKS(5));
            continue;
        }

        if (!have_prev) {
            px = x;
            py = y;
            pz = z;
            have_prev = true;
            continue;
        }

        const uint32_t dx = adxl367_uabs_diff(x, px);
        const uint32_t dy = adxl367_uabs_diff(y, py);
        const uint32_t dz = adxl367_uabs_diff(z, pz);
        const uint32_t dmax = adxl367_max_u32(dx, dy, dz);

        px = x;
        py = y;
        pz = z;

        if (dmax > (uint32_t)NODE_ACC_ADXL367_MONITOR_DELTA_HIGH_LSB) {
            still_cnt = 0;
            if (!moving) {
                ESP_LOGI(TAG, "[still_to_motion] max_delta=%u (~%.4f g)", (unsigned)dmax, (double)dmax / 4000.0);
                moving = true;
            }
        } else if (dmax < (uint32_t)NODE_ACC_ADXL367_MONITOR_DELTA_LOW_LSB) {
            still_cnt++;
            if (moving && still_cnt >= NODE_ACC_ADXL367_MONITOR_STILL_COUNT) {
                ESP_LOGI(TAG, "[motion_to_still] max_delta=%u (~%.4f g)", (unsigned)dmax, (double)dmax / 4000.0);
                moving = false;
                still_cnt = 0;
            }
        } else {
            still_cnt = 0;
        }
    }

hang:
    if (i2c_dev != NULL) {
        (void)i2c_remove_device(i2c_dev);
    }
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void node_acc_adxl367_run_phase6_default_app(void)
{
    ESP_LOGI(TAG, "phase6 default app");
    node_acc_adxl367_run_transition_monitor_forever();
}

main.cpp

/**
 * @file AIoTNode.cpp
 */
#include "nvs_flash.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "node_i2c.h"
#include "node_acc_adxl367_test.h"

#ifdef __cplusplus
extern "C" {
#endif

static const char *TAG = "AIoTNode";

#ifndef AIOTNODE_BOOT_RUN_ADXL367_STATS
#define AIOTNODE_BOOT_RUN_ADXL367_STATS 0
#endif

void app_main(void)
{
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_ERROR_CHECK(i2c_bus_init());
#if AIOTNODE_BOOT_RUN_ADXL367_STATS
    ret = node_acc_adxl367_run_xyz_stats_capture();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "XYZ stats failed: %s", esp_err_to_name(ret));
    }
#endif
    node_acc_adxl367_run_phase6_default_app();
}

#ifdef __cplusplus
}
#endif