diff --git a/nodes/CMakeLists.txt b/nodes/CMakeLists.txt index 3c3210e..503d3fe 100644 --- a/nodes/CMakeLists.txt +++ b/nodes/CMakeLists.txt @@ -4,4 +4,5 @@ cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(plein_de_eeeeeeeeeeee) -target_sources(app PRIVATE src/main.c src/window_status.c src/window_status.h src/thermometer.c src/thermometer.h src/hygrometer.c src/hygrometer.h src/co2_level.c src/co2_level.h src/supervisor.c src/supervisor.h src/ble_advertiser.c src/ble_advertiser.h) +target_sources(app PRIVATE src/main.c src/window_status.c src/window_status.h src/thermometer.c src/thermometer.h src/hygrometer.c src/hygrometer.h src/co2_level.c src/co2_level.h src/supervisor.c src/supervisor.h src/ble_advertiser.c src/ble_advertiser.h src/battery_percent.h src/battery_percent.c) +target_sources(app PRIVATE src/modules/battery.h src/modules/battery.c) diff --git a/nodes/prj.conf b/nodes/prj.conf index d2fac32..2bf9438 100644 --- a/nodes/prj.conf +++ b/nodes/prj.conf @@ -1,3 +1,5 @@ +CONFIG_ADC=y CONFIG_I2C=y CONFIG_SENSOR=y CONFIG_BT=y +CONFIG_GPIO=y diff --git a/nodes/src/battery_percent.c b/nodes/src/battery_percent.c new file mode 100644 index 0000000..d722a92 --- /dev/null +++ b/nodes/src/battery_percent.c @@ -0,0 +1,42 @@ +#include "battery_percent.h" + +/** +* Taken from sample/board/nordic/battery/src/main.c +* A discharge curve specific to the power source. +*/ +static const struct battery_level_point levels[] = { + /* "Curve" here eyeballed from captured data for the [Adafruit + * 3.7v 2000 mAh](https://www.adafruit.com/product/2011) LIPO + * under full load that started with a charge of 3.96 V and + * dropped about linearly to 3.58 V over 15 hours. It then + * dropped rapidly to 3.10 V over one hour, at which point it + * stopped transmitting. + * + * Based on eyeball comparisons we'll say that 15/16 of life + * goes between 3.95 and 3.55 V, and 1/16 goes between 3.55 V + * and 3.1 V. + */ + + { 10000, 3950 }, + { 625, 3550 }, + { 0, 3100 }, +}; + +enum error_code battery_init(){ + enum error_code ret = init_failed; + if(0 == battery_measure_enable(true)){ + ret = success; + }else{} + return ret; +} + +enum error_code battery_get_value(int* holder){ + enum error_code ret = read_failed; + // battery_voltage in [mV] + int battery_voltage = battery_sample(); + if(battery_voltage >= 0){ + *holder = battery_level_pptt(battery_voltage, levels); + ret = success; + }else{} + return ret; +} diff --git a/nodes/src/battery_percent.h b/nodes/src/battery_percent.h new file mode 100644 index 0000000..ba2ab66 --- /dev/null +++ b/nodes/src/battery_percent.h @@ -0,0 +1,27 @@ +#ifndef BATTERY_PERCENT_H +#define BATTERY_PERCENT_H + +#include + +#include "modules/battery.h" + +#include "error_code.h" + +/** +* @brief init the battery module +* @return init_failed upon any error during init +* @return success otherwise +*/ +enum error_code battery_init(); + +/** +* @brief Retrieve the battery charge percentage and stores it in the given parameter +* @param[out] holder : pointer where the measurement will be stored +* @return read_failed upon any error occuring during measurement +* @return success otherwise +* @note the content of holder should not be trusted upon returning something else than success +*/ +enum error_code battery_get_value(int* holder); + +#endif //BATTERY_PERCENT_H + diff --git a/nodes/src/ble_advertiser.c b/nodes/src/ble_advertiser.c index 7d07e30..ba29760 100644 --- a/nodes/src/ble_advertiser.c +++ b/nodes/src/ble_advertiser.c @@ -8,6 +8,7 @@ static const char BT_KEY_WINDOW = 0x01; static const char BT_KEY_HUMIDITY = 0x02; static const char BT_KEY_TEMP = 0x03; static const char BT_KEY_CO2_LVL = 0x04; +static const char BT_KEY_BATT = 0x05; // value size [B] static const char BT_PREAMBLE_SIZE = 2; @@ -15,17 +16,19 @@ static const char BT_VALUE_SIZE_WINDOW = 1; static const char BT_VALUE_SIZE_HUMIDITY = 1; static const char BT_VALUE_SIZE_TEMP = 2; static const char BT_VALUE_SIZE_CO2_LVL = 4; +static const char BT_VALUE_SIZE_BATT = 1; // value start index in the frame - equal to {prior field index + prior value size + key size} static const int BT_AD_DATA_INDEX_WINDOW = BT_PREAMBLE_SIZE + BT_KEY_SIZE; static const int BT_AD_DATA_INDEX_HUMIDITY = BT_AD_DATA_INDEX_WINDOW + BT_VALUE_SIZE_WINDOW + BT_KEY_SIZE; static const int BT_AD_DATA_INDEX_TEMP = BT_AD_DATA_INDEX_HUMIDITY + BT_VALUE_SIZE_HUMIDITY + BT_KEY_SIZE; static const int BT_AD_DATA_INDEX_CO2_LVL = BT_AD_DATA_INDEX_TEMP + BT_VALUE_SIZE_TEMP + BT_KEY_SIZE; +static const int BT_AD_DATA_INDEX_BATT = BT_AD_DATA_INDEX_CO2_LVL + BT_VALUE_SIZE_CO2_LVL + BT_KEY_SIZE; // sum of all value size + size for all keys + size preamble static const char BT_AD_TOTAL_SIZE = - BT_PREAMBLE_SIZE + (4 * BT_KEY_SIZE) + - BT_VALUE_SIZE_WINDOW + BT_VALUE_SIZE_HUMIDITY + BT_VALUE_SIZE_TEMP + BT_VALUE_SIZE_CO2_LVL; + BT_PREAMBLE_SIZE + (5 * BT_KEY_SIZE) + + BT_VALUE_SIZE_WINDOW + BT_VALUE_SIZE_HUMIDITY + BT_VALUE_SIZE_TEMP + BT_VALUE_SIZE_CO2_LVL + BT_VALUE_SIZE_BATT; // data field in the broadcasted frame // all values are set to 0 and the "company id" field is set to 0xffff @@ -34,7 +37,8 @@ static uint8_t ad_data[] = { BT_KEY_WINDOW, 0x00, BT_KEY_HUMIDITY, 0x00, BT_KEY_TEMP, 0x00, 0x00, - BT_KEY_CO2_LVL, 0x00, 0x00, 0x00, 0x00 + BT_KEY_CO2_LVL, 0x00, 0x00, 0x00, 0x00, + BT_KEY_BATT, 0x00 }; // frame that will be broadcasted static const struct bt_data ad[] = { @@ -49,12 +53,14 @@ enum error_code ble_advertise( enum window_status window_value, int hygro_value, int thermo_value, - int co2_lvl_value + int co2_lvl_value, + int batt_value ){ enum error_code ret = write_failed; int shift = 0; ad_data[BT_AD_DATA_INDEX_WINDOW] = (uint8_t)(window_value == open ? 0x1 : 0x0); ad_data[BT_AD_DATA_INDEX_HUMIDITY] = (uint8_t)(hygro_value & 0xff); + ad_data[BT_AD_DATA_INDEX_BATT] = (uint8_t)(batt_value & 0xff); for(int i=0;i>(shift)) & 0xff); diff --git a/nodes/src/ble_advertiser.h b/nodes/src/ble_advertiser.h index 7025baf..f129837 100644 --- a/nodes/src/ble_advertiser.h +++ b/nodes/src/ble_advertiser.h @@ -17,10 +17,11 @@ enum error_code ble_init(); /** * @brief Sets the given values as data and broadcast the BLE frame -* @param window_value : window opening status in {open/closed} -* @param hygro_value : humidity percentage [%] -* @param thermo_value : temperature in [d°C] -* @param co2_lvl_value : co2 level in [ppm] +* @param window_value : window opening status in {open/closed} +* @param hygro_value : humidity percentage [%] +* @param thermo_value : temperature in [d°C] +* @param co2_lvl_value : co2 level in [ppm] +* @param battery_percent_value : battery current level of charge [%] * @return write_failed if any problem occur during the BLE broadcast * @return success otherwise * @note The broadcast is stopped at the return time @@ -29,7 +30,8 @@ enum error_code ble_advertise( enum window_status window_value, int hygro_value, int thermo_value, - int co2_lvl_value + int co2_lvl_value, + int batt_value ); #endif //BLE_ADVERTISER_H diff --git a/nodes/src/modules/battery.c b/nodes/src/modules/battery.c new file mode 100644 index 0000000..3a31a91 --- /dev/null +++ b/nodes/src/modules/battery.c @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC + * Copyright (c) 2019-2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "battery.h" + +LOG_MODULE_REGISTER(BATTERY, CONFIG_ADC_LOG_LEVEL); + +#define VBATT DT_PATH(vbatt) +#define ZEPHYR_USER DT_PATH(zephyr_user) + +#ifdef CONFIG_BOARD_THINGY52_NRF52832 +/* This board uses a divider that reduces max voltage to + * reference voltage (600 mV). + */ +#define BATTERY_ADC_GAIN ADC_GAIN_1 +#else +/* Other boards may use dividers that only reduce battery voltage to + * the maximum supported by the hardware (3.6 V) + */ +#define BATTERY_ADC_GAIN ADC_GAIN_1_6 +#endif + +struct io_channel_config { + uint8_t channel; +}; + +struct divider_config { + struct io_channel_config io_channel; + struct gpio_dt_spec power_gpios; + /* output_ohm is used as a flag value: if it is nonzero then + * the battery is measured through a voltage divider; + * otherwise it is assumed to be directly connected to Vdd. + */ + uint32_t output_ohm; + uint32_t full_ohm; +}; + +static const struct divider_config divider_config = { +#if DT_NODE_HAS_STATUS_OKAY(VBATT) + .io_channel = { + DT_IO_CHANNELS_INPUT(VBATT), + }, + .power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}), + .output_ohm = DT_PROP(VBATT, output_ohms), + .full_ohm = DT_PROP(VBATT, full_ohms), +#else /* /vbatt exists */ + .io_channel = { + DT_IO_CHANNELS_INPUT(ZEPHYR_USER), + }, +#endif /* /vbatt exists */ +}; + +struct divider_data { + const struct device *adc; + struct adc_channel_cfg adc_cfg; + struct adc_sequence adc_seq; + int16_t raw; +}; +static struct divider_data divider_data = { +#if DT_NODE_HAS_STATUS_OKAY(VBATT) + .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)), +#else + .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)), +#endif +}; + +static int divider_setup(void) +{ + const struct divider_config *cfg = ÷r_config; + const struct io_channel_config *iocp = &cfg->io_channel; + const struct gpio_dt_spec *gcp = &cfg->power_gpios; + struct divider_data *ddp = ÷r_data; + struct adc_sequence *asp = &ddp->adc_seq; + struct adc_channel_cfg *accp = &ddp->adc_cfg; + int rc; + + if (!device_is_ready(ddp->adc)) { + LOG_ERR("ADC device is not ready %s", ddp->adc->name); + return -ENOENT; + } + + if (gcp->port) { + if (!device_is_ready(gcp->port)) { + LOG_ERR("%s: device not ready", gcp->port->name); + return -ENOENT; + } + rc = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE); + if (rc != 0) { + LOG_ERR("Failed to control feed %s.%u: %d", + gcp->port->name, gcp->pin, rc); + return rc; + } + } + + *asp = (struct adc_sequence){ + .channels = BIT(0), + .buffer = &ddp->raw, + .buffer_size = sizeof(ddp->raw), + .oversampling = 4, + .calibrate = true, + }; + +#ifdef CONFIG_ADC_NRFX_SAADC + *accp = (struct adc_channel_cfg){ + .gain = BATTERY_ADC_GAIN, + .reference = ADC_REF_INTERNAL, + .acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40), + }; + + if (cfg->output_ohm != 0) { + accp->input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0 + + iocp->channel; + } else { + accp->input_positive = SAADC_CH_PSELP_PSELP_VDD; + } + + asp->resolution = 14; +#else /* CONFIG_ADC_var */ +#error Unsupported ADC +#endif /* CONFIG_ADC_var */ + + rc = adc_channel_setup(ddp->adc, accp); + LOG_INF("Setup AIN%u got %d", iocp->channel, rc); + + return rc; +} + +static bool battery_ok; + +static int battery_setup(void) +{ + int rc = divider_setup(); + + battery_ok = (rc == 0); + LOG_INF("Battery setup: %d %d", rc, battery_ok); + return rc; +} + +SYS_INIT(battery_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + +int battery_measure_enable(bool enable) +{ + int rc = -ENOENT; + + if (battery_ok) { + const struct gpio_dt_spec *gcp = ÷r_config.power_gpios; + + rc = 0; + if (gcp->port) { + rc = gpio_pin_set_dt(gcp, enable); + } + } + return rc; +} + +int battery_sample(void) +{ + int rc = -ENOENT; + + if (battery_ok) { + struct divider_data *ddp = ÷r_data; + const struct divider_config *dcp = ÷r_config; + struct adc_sequence *sp = &ddp->adc_seq; + + rc = adc_read(ddp->adc, sp); + sp->calibrate = false; + if (rc == 0) { + int32_t val = ddp->raw; + + adc_raw_to_millivolts(adc_ref_internal(ddp->adc), + ddp->adc_cfg.gain, + sp->resolution, + &val); + + if (dcp->output_ohm != 0) { + rc = val * (uint64_t)dcp->full_ohm + / dcp->output_ohm; + LOG_INF("raw %u ~ %u mV => %d mV\n", + ddp->raw, val, rc); + } else { + rc = val; + LOG_INF("raw %u ~ %u mV\n", ddp->raw, val); + } + } + } + + return rc; +} + +unsigned int battery_level_pptt(unsigned int batt_mV, + const struct battery_level_point *curve) +{ + const struct battery_level_point *pb = curve; + + if (batt_mV >= pb->lvl_mV) { + /* Measured voltage above highest point, cap at maximum. */ + return pb->lvl_pptt; + } + /* Go down to the last point at or below the measured voltage. */ + while ((pb->lvl_pptt > 0) + && (batt_mV < pb->lvl_mV)) { + ++pb; + } + if (batt_mV < pb->lvl_mV) { + /* Below lowest point, cap at minimum */ + return pb->lvl_pptt; + } + + /* Linear interpolation between below and above points. */ + const struct battery_level_point *pa = pb - 1; + + return pb->lvl_pptt + + ((pa->lvl_pptt - pb->lvl_pptt) + * (batt_mV - pb->lvl_mV) + / (pa->lvl_mV - pb->lvl_mV)); +} diff --git a/nodes/src/modules/battery.h b/nodes/src/modules/battery.h new file mode 100644 index 0000000..9e7fd36 --- /dev/null +++ b/nodes/src/modules/battery.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef APPLICATION_BATTERY_H_ +#define APPLICATION_BATTERY_H_ + +/** Enable or disable measurement of the battery voltage. + * + * @param enable true to enable, false to disable + * + * @return zero on success, or a negative error code. + */ +int battery_measure_enable(bool enable); + +/** Measure the battery voltage. + * + * @return the battery voltage in millivolts, or a negative error + * code. + */ +int battery_sample(void); + +/** A point in a battery discharge curve sequence. + * + * A discharge curve is defined as a sequence of these points, where + * the first point has #lvl_pptt set to 10000 and the last point has + * #lvl_pptt set to zero. Both #lvl_pptt and #lvl_mV should be + * monotonic decreasing within the sequence. + */ +struct battery_level_point { + /** Remaining life at #lvl_mV. */ + uint16_t lvl_pptt; + + /** Battery voltage at #lvl_pptt remaining life. */ + uint16_t lvl_mV; +}; + +/** Calculate the estimated battery level based on a measured voltage. + * + * @param batt_mV a measured battery voltage level. + * + * @param curve the discharge curve for the type of battery installed + * on the system. + * + * @return the estimated remaining capacity in parts per ten + * thousand. + */ +unsigned int battery_level_pptt(unsigned int batt_mV, + const struct battery_level_point *curve); + +#endif /* APPLICATION_BATTERY_H_ */ diff --git a/nodes/src/supervisor.c b/nodes/src/supervisor.c index d1fd00a..525fcee 100644 --- a/nodes/src/supervisor.c +++ b/nodes/src/supervisor.c @@ -22,17 +22,18 @@ enum error_code supervisor_run(){ int co2_lvl_value = -1; int hygro_value = -1; int thermo_value = -1; + int batt_value = -1; enum window_status window_value = unknown; - enum error_code co2_lvl_status, hygro_status, thermo_status, window_status; + enum error_code co2_lvl_status, hygro_status, thermo_status, window_status, batt_status; int current_sleep_time = SLEEP_MIN_DURATION; while(1){ co2_lvl_status = co2_lvl_get_value(&co2_lvl_value); hygro_status = hygro_get_value(&hygro_value); thermo_status = thermo_get_value(&thermo_value); window_status = window_get_value(&window_value); - // todo : manage non-success error codes - if(success != co2_lvl_status){ - co2_lvl_value = -1; + batt_status = battery_get_value(&batt_value); + if(success != window_status){ + window_value = unknown; }else{} if(success != hygro_status){ hygro_value = -1; @@ -40,10 +41,13 @@ enum error_code supervisor_run(){ if(success != thermo_status){ thermo_value = -1; }else{} - if(success != window_status){ - window_value = unknown; + if(success != co2_lvl_status){ + co2_lvl_value = -1; }else{} - ble_advertise(window_value, hygro_value, thermo_value, co2_lvl_value); + if(success != batt_status){ + batt_value = -1; + }else{} + ble_advertise(window_value, hygro_value, thermo_value, co2_lvl_value, batt_value); if((co2_lvl_value > CO2_LEVEL_EMPTY_ROOM) || (window_value == open)){ // there are people in the room, or someone forgot to close the window current_sleep_time = SLEEP_MIN_DURATION; @@ -54,7 +58,7 @@ enum error_code supervisor_run(){ current_sleep_time = SLEEP_MAX_DURATION; }else{} } - k_sleep(K_MINUTES(current_sleep_time)); + //k_sleep(K_MINUTES(current_sleep_time)); } return error_unknown; // should never return } diff --git a/nodes/src/supervisor.h b/nodes/src/supervisor.h index 31a0658..ff71e2b 100644 --- a/nodes/src/supervisor.h +++ b/nodes/src/supervisor.h @@ -10,6 +10,7 @@ #include "thermometer.h" #include "hygrometer.h" #include "co2_level.h" +#include "battery_percent.h" extern const int SLEEP_GRANULARITY; // [min] extern const int SLEEP_MIN_DURATION; // [min]