| /* |
| * Copyright (c) 2019 Centaur Analytics, Inc |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT ti_tmp11x |
| |
| #include <zephyr/device.h> |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/sensor.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/drivers/sensor/tmp11x.h> |
| #include <zephyr/dt-bindings/sensor/tmp11x.h> |
| #include <zephyr/sys/util.h> |
| #include <zephyr/sys/byteorder.h> |
| #include <zephyr/sys/__assert.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/kernel.h> |
| |
| #include "tmp11x.h" |
| |
| #define EEPROM_SIZE_REG sizeof(uint16_t) |
| #define EEPROM_TMP117_RESERVED (2 * sizeof(uint16_t)) |
| #define EEPROM_MIN_BUSY_MS 7 |
| #define RESET_MIN_BUSY_MS 2 |
| |
| LOG_MODULE_REGISTER(TMP11X, CONFIG_SENSOR_LOG_LEVEL); |
| |
| int tmp11x_reg_read(const struct device *dev, uint8_t reg, uint16_t *val) |
| { |
| const struct tmp11x_dev_config *cfg = dev->config; |
| |
| if (i2c_burst_read_dt(&cfg->bus, reg, (uint8_t *)val, 2) < 0) { |
| return -EIO; |
| } |
| |
| *val = sys_be16_to_cpu(*val); |
| |
| return 0; |
| } |
| |
| int tmp11x_reg_write(const struct device *dev, uint8_t reg, uint16_t val) |
| { |
| const struct tmp11x_dev_config *cfg = dev->config; |
| uint8_t tx_buf[3] = {reg, val >> 8, val & 0xFF}; |
| |
| return i2c_write_dt(&cfg->bus, tx_buf, sizeof(tx_buf)); |
| } |
| |
| int tmp11x_write_config(const struct device *dev, uint16_t mask, uint16_t conf) |
| { |
| uint16_t config = 0; |
| int result; |
| |
| result = tmp11x_reg_read(dev, TMP11X_REG_CFGR, &config); |
| |
| if (result < 0) { |
| return result; |
| } |
| |
| config &= ~mask; |
| config |= conf; |
| |
| return tmp11x_reg_write(dev, TMP11X_REG_CFGR, config); |
| } |
| |
| static inline bool tmp11x_is_offset_supported(const struct tmp11x_data *drv_data) |
| { |
| return drv_data->id == TMP117_DEVICE_ID || drv_data->id == TMP119_DEVICE_ID; |
| } |
| |
| /** |
| * @brief Convert sensor_value temperature to TMP11X register format |
| * |
| * This function converts a temperature from sensor_value format (val1 in degrees C, |
| * val2 in micro-degrees C) to the TMP11X register format. It uses 64-bit arithmetic |
| * to prevent overflow and clamps the result to the valid int16_t range. |
| * |
| * @param val Pointer to sensor_value containing temperature |
| * @return Temperature value in TMP11X register format (int16_t) |
| */ |
| static inline int16_t tmp11x_sensor_value_to_reg_format(const struct sensor_value *val) |
| { |
| int64_t temp_micro = ((int64_t)val->val1 * 1000000) + val->val2; |
| int64_t temp_scaled = (temp_micro * 10) / TMP11X_RESOLUTION; |
| |
| /* Clamp to int16_t range */ |
| if (temp_scaled > INT16_MAX) { |
| return INT16_MAX; |
| } else if (temp_scaled < INT16_MIN) { |
| return INT16_MIN; |
| } else { |
| return (int16_t)temp_scaled; |
| } |
| } |
| |
| static bool check_eeprom_bounds(const struct device *dev, off_t offset, size_t len) |
| { |
| struct tmp11x_data *drv_data = dev->data; |
| |
| if ((offset + len) > EEPROM_TMP11X_SIZE || offset % EEPROM_SIZE_REG != 0 || |
| len % EEPROM_SIZE_REG != 0) { |
| return false; |
| } |
| |
| /* TMP117 and TMP119 uses EEPROM[2] as temperature offset register */ |
| if ((drv_data->id == TMP117_DEVICE_ID || drv_data->id == TMP119_DEVICE_ID) && |
| offset <= EEPROM_TMP117_RESERVED && (offset + len) > EEPROM_TMP117_RESERVED) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| int tmp11x_eeprom_await(const struct device *dev) |
| { |
| int res; |
| uint16_t val; |
| |
| k_sleep(K_MSEC(EEPROM_MIN_BUSY_MS)); |
| |
| WAIT_FOR((res = tmp11x_reg_read(dev, TMP11X_REG_EEPROM_UL, &val)) != 0 || |
| val & TMP11X_EEPROM_UL_BUSY, |
| 100, k_msleep(1)); |
| |
| return res; |
| } |
| |
| int tmp11x_eeprom_write(const struct device *dev, off_t offset, const void *data, size_t len) |
| { |
| uint8_t reg; |
| const uint16_t *src = data; |
| int res; |
| |
| if (!check_eeprom_bounds(dev, offset, len)) { |
| return -EINVAL; |
| } |
| |
| res = tmp11x_reg_write(dev, TMP11X_REG_EEPROM_UL, TMP11X_EEPROM_UL_UNLOCK); |
| if (res) { |
| return res; |
| } |
| |
| for (reg = (offset / 2); reg < offset / 2 + len / 2; reg++) { |
| uint16_t val = *src; |
| |
| res = tmp11x_reg_write(dev, reg + TMP11X_REG_EEPROM1, val); |
| if (res != 0) { |
| break; |
| } |
| |
| res = tmp11x_eeprom_await(dev); |
| src++; |
| |
| if (res != 0) { |
| break; |
| } |
| } |
| |
| res = tmp11x_reg_write(dev, TMP11X_REG_EEPROM_UL, 0); |
| |
| return res; |
| } |
| |
| int tmp11x_eeprom_read(const struct device *dev, off_t offset, void *data, size_t len) |
| { |
| uint8_t reg; |
| uint16_t *dst = data; |
| int res = 0; |
| |
| if (!check_eeprom_bounds(dev, offset, len)) { |
| return -EINVAL; |
| } |
| |
| for (reg = (offset / 2); reg < offset / 2 + len / 2; reg++) { |
| res = tmp11x_reg_read(dev, reg + TMP11X_REG_EEPROM1, dst); |
| if (res != 0) { |
| break; |
| } |
| dst++; |
| } |
| |
| return res; |
| } |
| |
| /** |
| * @brief Check the Device ID |
| * |
| * @param[in] dev Pointer to the device structure |
| * @param[in] id Pointer to the variable for storing the device id |
| * |
| * @retval 0 on success |
| * @retval -EIO Otherwise |
| */ |
| static inline int tmp11x_device_id_check(const struct device *dev, uint16_t *id) |
| { |
| if (tmp11x_reg_read(dev, TMP11X_REG_DEVICE_ID, id) != 0) { |
| LOG_ERR("%s: Failed to get Device ID register!", dev->name); |
| return -EIO; |
| } |
| |
| if ((*id != TMP116_DEVICE_ID) && (*id != TMP117_DEVICE_ID) && (*id != TMP119_DEVICE_ID)) { |
| LOG_ERR("%s: Failed to match the device IDs!", dev->name); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int tmp11x_sample_fetch(const struct device *dev, enum sensor_channel chan) |
| { |
| struct tmp11x_data *drv_data = dev->data; |
| uint16_t value; |
| uint16_t cfg_reg = 0; |
| int rc; |
| |
| __ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_AMBIENT_TEMP); |
| |
| /* clear sensor values */ |
| drv_data->sample = 0U; |
| |
| /* Make sure that a data is available */ |
| rc = tmp11x_reg_read(dev, TMP11X_REG_CFGR, &cfg_reg); |
| if (rc < 0) { |
| LOG_ERR("%s, Failed to read from CFGR register", dev->name); |
| return rc; |
| } |
| |
| if ((cfg_reg & TMP11X_CFGR_DATA_READY) == 0) { |
| LOG_DBG("%s: no data ready", dev->name); |
| return -EBUSY; |
| } |
| |
| /* Get the most recent temperature measurement */ |
| rc = tmp11x_reg_read(dev, TMP11X_REG_TEMP, &value); |
| if (rc < 0) { |
| LOG_ERR("%s: Failed to read from TEMP register!", dev->name); |
| return rc; |
| } |
| |
| /* store measurements to the driver */ |
| drv_data->sample = (int16_t)value; |
| |
| return 0; |
| } |
| |
| /* |
| * See datasheet "Temperature Results and Limits" section for more |
| * details on processing sample data. |
| */ |
| static void tmp11x_temperature_to_sensor_value(int16_t temperature, struct sensor_value *val) |
| { |
| int32_t tmp; |
| |
| tmp = (temperature * (int32_t)TMP11X_RESOLUTION) / 10; |
| val->val1 = tmp / 1000000; /* uCelsius */ |
| val->val2 = tmp % 1000000; |
| } |
| |
| static int tmp11x_channel_get(const struct device *dev, enum sensor_channel chan, |
| struct sensor_value *val) |
| { |
| struct tmp11x_data *drv_data = dev->data; |
| |
| if (chan != SENSOR_CHAN_AMBIENT_TEMP) { |
| return -ENOTSUP; |
| } |
| |
| tmp11x_temperature_to_sensor_value(drv_data->sample, val); |
| |
| return 0; |
| } |
| |
| static int16_t tmp11x_conv_value(const struct sensor_value *val) |
| { |
| uint32_t freq_micro = sensor_value_to_micro(val); |
| |
| switch (freq_micro) { |
| case 64000000: /* 1 / 15.5 ms has been rounded down */ |
| return TMP11X_DT_ODR_15_5_MS; |
| case 8000000: |
| return TMP11X_DT_ODR_125_MS; |
| case 4000000: |
| return TMP11X_DT_ODR_250_MS; |
| case 2000000: |
| return TMP11X_DT_ODR_500_MS; |
| case 1000000: |
| return TMP11X_DT_ODR_1000_MS; |
| case 250000: |
| return TMP11X_DT_ODR_4000_MS; |
| case 125000: |
| return TMP11X_DT_ODR_8000_MS; |
| case 62500: |
| return TMP11X_DT_ODR_16000_MS; |
| default: |
| LOG_ERR("%" PRIu32 " uHz not supported", freq_micro); |
| return -EINVAL; |
| } |
| } |
| |
| static bool tmp11x_is_attr_store_supported(enum sensor_attribute attr) |
| { |
| switch ((int)attr) { |
| case SENSOR_ATTR_SAMPLING_FREQUENCY: |
| case SENSOR_ATTR_LOWER_THRESH: |
| case SENSOR_ATTR_UPPER_THRESH: |
| case SENSOR_ATTR_OFFSET: |
| case SENSOR_ATTR_OVERSAMPLING: |
| case SENSOR_ATTR_TMP11X_SHUTDOWN_MODE: |
| case SENSOR_ATTR_TMP11X_CONTINUOUS_CONVERSION_MODE: |
| case SENSOR_ATTR_TMP11X_ALERT_PIN_POLARITY: |
| case SENSOR_ATTR_TMP11X_ALERT_MODE: |
| return true; |
| case SENSOR_ATTR_TMP11X_ONE_SHOT_MODE: |
| return false; |
| } |
| |
| return false; |
| } |
| |
| static int tmp11x_attr_store_reload(const struct device *dev) |
| { |
| int await_res = tmp11x_eeprom_await(dev); |
| int reset_res = tmp11x_reg_write(dev, TMP11X_REG_CFGR, TMP11X_CFGR_RESET); |
| |
| k_sleep(K_MSEC(RESET_MIN_BUSY_MS)); |
| |
| return await_res != 0 ? await_res : reset_res; |
| } |
| |
| static int tmp11x_attr_set(const struct device *dev, enum sensor_channel chan, |
| enum sensor_attribute attr, const struct sensor_value *val) |
| { |
| const struct tmp11x_dev_config *cfg = dev->config; |
| struct tmp11x_data *drv_data = dev->data; |
| int16_t value; |
| int res = 0; |
| bool store; |
| int store_res = 0; |
| |
| if (chan != SENSOR_CHAN_AMBIENT_TEMP) { |
| return -ENOTSUP; |
| } |
| |
| store = cfg->store_attr_values && tmp11x_is_attr_store_supported(attr); |
| if (store) { |
| store_res = tmp11x_reg_write(dev, TMP11X_REG_EEPROM_UL, TMP11X_EEPROM_UL_UNLOCK); |
| if (store_res != 0) { |
| return store_res; |
| } |
| } |
| |
| switch ((int)attr) { |
| case SENSOR_ATTR_SAMPLING_FREQUENCY: |
| value = tmp11x_conv_value(val); |
| if (value < 0) { |
| return value; |
| } |
| |
| res = tmp11x_write_config(dev, TMP11X_CFGR_CONV, value); |
| break; |
| |
| case SENSOR_ATTR_OFFSET: |
| if (!tmp11x_is_offset_supported(drv_data)) { |
| LOG_ERR("%s: Offset is not supported", dev->name); |
| return -EINVAL; |
| } |
| /* |
| * The offset is encoded into the temperature register format. |
| */ |
| value = tmp11x_sensor_value_to_reg_format(val); |
| |
| res = tmp11x_reg_write(dev, TMP117_REG_TEMP_OFFSET, value); |
| break; |
| |
| case SENSOR_ATTR_OVERSAMPLING: |
| /* sensor supports averaging 1, 8, 32 and 64 samples */ |
| switch (val->val1) { |
| case 1: |
| value = TMP11X_AVG_1_SAMPLE; |
| break; |
| |
| case 8: |
| value = TMP11X_AVG_8_SAMPLES; |
| break; |
| |
| case 32: |
| value = TMP11X_AVG_32_SAMPLES; |
| break; |
| |
| case 64: |
| value = TMP11X_AVG_64_SAMPLES; |
| break; |
| |
| default: |
| res = -EINVAL; |
| break; |
| } |
| |
| if (res == 0) { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_AVG, value); |
| } |
| |
| break; |
| |
| case SENSOR_ATTR_TMP11X_SHUTDOWN_MODE: |
| res = tmp11x_write_config(dev, TMP11X_CFGR_MODE, TMP11X_MODE_SHUTDOWN); |
| break; |
| |
| case SENSOR_ATTR_TMP11X_CONTINUOUS_CONVERSION_MODE: |
| res = tmp11x_write_config(dev, TMP11X_CFGR_MODE, TMP11X_MODE_CONTINUOUS); |
| break; |
| |
| case SENSOR_ATTR_TMP11X_ONE_SHOT_MODE: |
| res = tmp11x_write_config(dev, TMP11X_CFGR_MODE, TMP11X_MODE_ONE_SHOT); |
| break; |
| |
| #ifdef CONFIG_TMP11X_TRIGGER |
| case SENSOR_ATTR_TMP11X_ALERT_PIN_POLARITY: |
| if (val->val1 == TMP11X_ALERT_PIN_ACTIVE_HIGH) { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_PIN_POL, |
| TMP11X_CFGR_ALERT_PIN_POL); |
| } else { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_PIN_POL, 0); |
| } |
| break; |
| |
| case SENSOR_ATTR_TMP11X_ALERT_MODE: |
| if (val->val1 == TMP11X_ALERT_THERM_MODE) { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_MODE, |
| TMP11X_CFGR_ALERT_MODE); |
| } else { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_MODE, 0); |
| } |
| break; |
| |
| case SENSOR_ATTR_UPPER_THRESH: |
| /* Convert temperature to register format */ |
| value = tmp11x_sensor_value_to_reg_format(val); |
| res = tmp11x_reg_write(dev, TMP11X_REG_HIGH_LIM, value); |
| break; |
| |
| case SENSOR_ATTR_LOWER_THRESH: |
| /* Convert temperature to register format */ |
| value = tmp11x_sensor_value_to_reg_format(val); |
| res = tmp11x_reg_write(dev, TMP11X_REG_LOW_LIM, value); |
| break; |
| |
| case SENSOR_ATTR_TMP11X_ALERT_PIN_SELECT: |
| if (val->val1 == TMP11X_ALERT_PIN_ALERT_SEL) { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_DR_SEL, 0); |
| } else { |
| res = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_DR_SEL, |
| TMP11X_CFGR_ALERT_DR_SEL); |
| } |
| break; |
| #endif /* CONFIG_TMP11X_TRIGGER */ |
| |
| default: |
| res = -ENOTSUP; |
| break; |
| } |
| |
| if (store) { |
| store_res = tmp11x_attr_store_reload(dev); |
| } |
| |
| return res != 0 ? res : store_res; |
| } |
| |
| static int tmp11x_attr_get(const struct device *dev, enum sensor_channel chan, |
| enum sensor_attribute attr, struct sensor_value *val) |
| { |
| uint16_t data; |
| int rc; |
| |
| if (chan != SENSOR_CHAN_AMBIENT_TEMP) { |
| return -ENOTSUP; |
| } |
| |
| switch (attr) { |
| case SENSOR_ATTR_CONFIGURATION: |
| rc = tmp11x_reg_read(dev, TMP11X_REG_CFGR, &data); |
| |
| if (rc == 0) { |
| val->val1 = data; |
| val->val2 = 0; |
| } |
| |
| return rc; |
| |
| case SENSOR_ATTR_OFFSET: |
| if (!tmp11x_is_offset_supported(dev->data)) { |
| LOG_ERR("%s: Offset is not supported", dev->name); |
| return -EINVAL; |
| } |
| |
| rc = tmp11x_reg_read(dev, TMP117_REG_TEMP_OFFSET, &data); |
| if (rc == 0) { |
| tmp11x_temperature_to_sensor_value(data, val); |
| } |
| |
| return rc; |
| |
| #ifdef CONFIG_TMP11X_TRIGGER |
| case SENSOR_ATTR_UPPER_THRESH: |
| rc = tmp11x_reg_read(dev, TMP11X_REG_HIGH_LIM, &data); |
| if (rc == 0) { |
| tmp11x_temperature_to_sensor_value(data, val); |
| } |
| |
| return rc; |
| |
| case SENSOR_ATTR_LOWER_THRESH: |
| rc = tmp11x_reg_read(dev, TMP11X_REG_LOW_LIM, &data); |
| if (rc == 0) { |
| tmp11x_temperature_to_sensor_value(data, val); |
| } |
| |
| return rc; |
| #endif /* CONFIG_TMP11X_TRIGGER */ |
| |
| default: |
| return -ENOTSUP; |
| } |
| } |
| |
| static DEVICE_API(sensor, tmp11x_driver_api) = { |
| .attr_set = tmp11x_attr_set, |
| .attr_get = tmp11x_attr_get, |
| .sample_fetch = tmp11x_sample_fetch, |
| .channel_get = tmp11x_channel_get, |
| #ifdef CONFIG_TMP11X_TRIGGER |
| .trigger_set = tmp11x_trigger_set, |
| #endif |
| }; |
| |
| static int tmp11x_init(const struct device *dev) |
| { |
| struct tmp11x_data *drv_data = dev->data; |
| const struct tmp11x_dev_config *cfg = dev->config; |
| int rc; |
| uint16_t id; |
| |
| if (!device_is_ready(cfg->bus.bus)) { |
| LOG_ERR("I2C dev %s not ready", cfg->bus.bus->name); |
| return -EINVAL; |
| } |
| |
| /* Check the Device ID */ |
| rc = tmp11x_device_id_check(dev, &id); |
| if (rc < 0) { |
| return rc; |
| } |
| LOG_DBG("Got device ID: %x", id); |
| drv_data->id = id; |
| |
| rc = tmp11x_write_config(dev, TMP11X_CFGR_CONV, cfg->odr); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| rc = tmp11x_write_config(dev, TMP11X_CFGR_AVG, cfg->oversampling); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| int8_t value = cfg->alert_pin_polarity ? TMP11X_CFGR_ALERT_PIN_POL : 0; |
| |
| rc = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_PIN_POL, value); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| value = cfg->alert_mode ? TMP11X_CFGR_ALERT_MODE : 0; |
| rc = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_MODE, value); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| value = cfg->alert_dr_sel ? TMP11X_CFGR_ALERT_DR_SEL : 0; |
| rc = tmp11x_write_config(dev, TMP11X_CFGR_ALERT_DR_SEL, value); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| #ifdef CONFIG_TMP11X_TRIGGER |
| drv_data->dev = dev; |
| rc = tmp11x_init_interrupt(dev); |
| if (rc < 0) { |
| LOG_ERR("%s: Failed to initialize alert pin", dev->name); |
| return rc; |
| } |
| #endif /* CONFIG_TMP11X_TRIGGER */ |
| |
| return rc; |
| } |
| |
| #ifdef CONFIG_PM_DEVICE |
| BUILD_ASSERT(!DT_INST_NODE_HAS_PROP(_num, power_domains), "Driver does not support power domain"); |
| static int tmp11x_pm_control(const struct device *dev, enum pm_device_action action) |
| { |
| int ret = 0; |
| |
| switch (action) { |
| case PM_DEVICE_ACTION_RESUME: { |
| const struct tmp11x_dev_config *cfg = dev->config; |
| |
| ret = tmp11x_write_config(dev, TMP11X_CFGR_CONV, cfg->odr); |
| if (ret < 0) { |
| LOG_ERR("Failed to resume TMP11X"); |
| } |
| break; |
| } |
| case PM_DEVICE_ACTION_SUSPEND: { |
| ret = tmp11x_write_config(dev, TMP11X_CFGR_MODE, TMP11X_MODE_SHUTDOWN); |
| if (ret < 0) { |
| LOG_ERR("Failed to suspend TMP11X"); |
| } |
| break; |
| } |
| default: |
| ret = -ENOTSUP; |
| } |
| |
| return ret; |
| } |
| #endif /* CONFIG_PM_DEVICE */ |
| |
| #ifdef CONFIG_TMP11X_TRIGGER |
| #define DEFINE_TMP11X_TRIGGER(_num) .alert_gpio = GPIO_DT_SPEC_INST_GET_OR(_num, alert_gpios, {}), |
| #else |
| #define DEFINE_TMP11X_TRIGGER(_num) |
| #endif |
| |
| #define DEFINE_TMP11X(_num) \ |
| static struct tmp11x_data tmp11x_data_##_num; \ |
| static const struct tmp11x_dev_config tmp11x_config_##_num = { \ |
| .bus = I2C_DT_SPEC_INST_GET(_num), \ |
| .odr = DT_INST_PROP(_num, odr), \ |
| .oversampling = DT_INST_PROP(_num, oversampling), \ |
| .alert_pin_polarity = DT_INST_PROP(_num, alert_polarity), \ |
| .alert_mode = DT_INST_PROP(_num, alert_mode), \ |
| .alert_dr_sel = DT_INST_PROP(_num, alert_dr_sel), \ |
| .store_attr_values = DT_INST_PROP(_num, store_attr_values), \ |
| DEFINE_TMP11X_TRIGGER(_num)}; \ |
| \ |
| PM_DEVICE_DT_INST_DEFINE(_num, tmp11x_pm_control); \ |
| \ |
| SENSOR_DEVICE_DT_INST_DEFINE(_num, tmp11x_init, PM_DEVICE_DT_INST_GET(_num), \ |
| &tmp11x_data_##_num, &tmp11x_config_##_num, POST_KERNEL, \ |
| CONFIG_SENSOR_INIT_PRIORITY, &tmp11x_driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(DEFINE_TMP11X) |