main/iotbalkon.c

Sun, 16 Apr 2023 12:27:12 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sun, 16 Apr 2023 12:27:12 +0200
changeset 30
7448b8dd4288
parent 29
551a53b31373
child 31
ec5c7794dcd6
permissions
-rw-r--r--

Preparations for BLE GATT. Added extra time after INA219 is powered on before measurement. Reduced LEDC frequency to 60 Hz, that makes the LED lights less nervous. Hardware mod on output 4, now needs external pulldown resistor.

/**
 * @file iotbalkon.c
 * @brief iotbalkon project.
 */

#include "config.h"

static const char *TAG = "iotbalkon";

/*
 * Main State table.
 */
typedef enum {
    State_Init = 0,
    State_Connect_Wifi,
    State_Connect_MQTT,
    State_Working,
    State_WorkDone,
    State_Disconnect_MQTT,
    State_Disconnect_Wifi,
    State_Wait,
    State_Measure,
    State_GoSleep
} Main_State;


static TaskHandle_t			xTaskBMP280 = NULL;
static TaskHandle_t			xTaskINA219 = NULL;
static TaskHandle_t			xTaskAPDS9930 = NULL;
static TaskHandle_t			xTaskMQTT = NULL;
static TaskHandle_t			xTaskWifi = NULL;
#ifdef CONFIG_ENABLE_BLE_GATT
static TaskHandle_t			xTaskBLE = NULL;
#endif
static TaskHandle_t			xTaskOUT = NULL;

#define	DS_TIME				60
#define DS_CURRENT			6.6
#define	MAX_LOOPS			32
#define SUB_TIME			1000

float					temperature;
float					pressure;
float					solarVolts, solarCurrent, solarPower;
float					batteryVolts, batteryCurrent, batteryPower;
int					batteryState;
float					s_Volts[MAX_LOOPS + 1];
float					s_Current[MAX_LOOPS + 1];
float					b_Volts[MAX_LOOPS + 1];
float					b_Current[MAX_LOOPS + 1];
bool					m_Valid[MAX_LOOPS + 1];
uint64_t				m_Time[MAX_LOOPS + 1];
uint64_t				gLastTime;
uint64_t				gTimeNext;

uint8_t					loopno = 0;
uint8_t					loops = 0;
uint8_t					ST_LOOPS = 6;

int					DisCounter = 0;

extern BMP280_State			*bmp280_state;			///< I2C state
extern SemaphoreHandle_t		xSemaphoreBMP280;		///< I2C lock semaphore
extern bmp280_params_t			bmp280_params;
extern bmp280_t				bmp280_dev;
extern SemaphoreHandle_t		xSemaphoreINA219;
extern ina219_t				ina219_b_dev;
extern ina219_t				ina219_s_dev;
extern INA219_State			*ina219_state;
extern SemaphoreHandle_t		xSemaphoreAPDS9930;
extern apds9930_t			apds9930_dev;
extern APDS9930_State			*apds9930_state;
extern SemaphoreHandle_t		xSemaphoreWiFi;
extern WIFI_State			*wifi_state;			///< WiFi state

uint32_t				Alarm = 0;

/*
 * Alarm bits
 */
#define AL_ACCULOW			0x01
#define AL_NOWIFI			0x02


#define ST_INTERVAL			10000
#define MAX_LOOPS			32

/*
 * Variables in NVS namespace "balkon"
 */
uint8_t					Relay1;
uint8_t					Relay2;
uint8_t					Dimmer3;
uint8_t					Dimmer4;
uint8_t					DS_Active;
uint32_t				DS_Time;


//                              0%    10%    20%    30%    40%    50%    60%    70%    80%    90%   100%
float Charge_C_5[11]     = { 12.40, 12.60, 12.75, 12.95, 13.20, 13.35, 13.55, 13.67, 14.00, 15.25, 15.94 };
float Charge_C_10[11]    = { 12.20, 12.38, 12.60, 12.80, 13.05, 13.20, 13.28, 13.39, 13.60, 14.20, 15.25 };
float Charge_C_20[11]    = { 12.00, 12.07, 12.42, 12.70, 12.85, 13.02, 13.11, 13.15, 13.25, 13.60, 14.15 };
float Charge_C_40[11]    = { 11.55, 11.80, 12.25, 12.57, 12.70, 12.80, 12.90, 12.95, 13.00, 13.20, 13.50 };
float Rest[11]           = { 11.50, 11.70, 11.88, 12.10, 12.22, 12.30, 12.40, 12.50, 12.57, 12.64, 12.72 };
float Load_C_20[11]      = { 11.45, 11.70, 11.80, 12.05, 12.20, 12.28, 12.37, 12.48, 12.55, 12.57, 12.60 };
float Load_C_10[11]      = { 10.98, 11.27, 11.50, 11.65, 11.85, 12.00, 12.10, 12.20, 12.30, 12.40, 12.50 };
float Load_C_5[11]       = { 10.20, 10.65, 10.90, 11.15, 11.35, 11.55, 11.63, 11.75, 11.90, 12.00, 12.08 };


/*
 * Calculate the load state of the battery.
 */
void BatteryState(float Voltage, float Current) {
  int i;

  batteryState = 0;
  ESP_LOGI(TAG, "Batt %.4fV %.4fmA", Voltage, Current);

  if (Current < -750) {
    // Load current > C/5, 1A
    for (i = 0; i < 10; i++) {
      if (Load_C_5[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Load C/5   %d%%", batteryState);

  } else if (Current < -375) {
    // Load current > C/10, 500mA
    for (i = 0; i < 10; i++) {
      if (Load_C_10[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Load C/10  %d%%", batteryState);

  } else if (Current < -125) {
    // Load current > C/20, 250mA
    for (i = 0; i < 10; i++) {
      if (Load_C_20[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Load C/20  %d%%", batteryState);

  } else if (Current > 750) {
    // Charge > C/5, 1A
    for (i = 0; i < 10; i++) {
      if (Charge_C_5[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Charge C/5  %d%%", batteryState);

  } else if (Current > 375) {
    // Charge > C/10, 500mA
    for (i = 0; i < 10; i++) {
      if (Charge_C_10[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Charge C/10 %d%%", batteryState);

  } else if (Current > 187.5) {
    // Charge > C/20, 250 mA
    for (i = 0; i < 10; i++) {
      if (Charge_C_20[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Charge C/20 %d%%", batteryState);

  } else if (Current > 62.5) {
    // Charge > C/40, 125 mA
    for (i = 0; i < 10; i++) {
      if (Charge_C_40[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Charge C/40 %d%%", batteryState);

  } else {
    // Rest
    for (i = 0; i < 10; i++) {
      if (Rest[i + 1] >= Voltage) {
        break;
      }
    }
    batteryState = i * 10;
    ESP_LOGI(TAG, "Rest   %d%%", batteryState);
  }
}



uint64_t millis(void) {
    return esp_timer_get_time() / 1000;
}



/*
 * Read the temperature and pressure from the BMP280.
 */
void getTempBaro() {
    temperature = 20;
    pressure = 1000;

    request_bmp280();

    while (! ready_bmp280()) {
	vTaskDelay(10 / portTICK_PERIOD_MS);
    }

    if (xSemaphoreTake(xSemaphoreBMP280, 25) == pdTRUE) {
	temperature = bmp280_state->temperature;
	pressure = bmp280_state->pressure / 100.0;
	ESP_LOGI(TAG, "Temperature: %.2f Pressure: %.1f hPa", temperature, pressure);
	xSemaphoreGive(xSemaphoreBMP280);
    } else {
	ESP_LOGE(TAG, "Can't lock xSemaphoreBMP280");
    }
}



/*
 * Read voltage and current from both INA219 boards. The results
 * are stored in an array so that we later can average the results.
 */
void getVoltsCurrent() {

    request_ina219();

    while (! ready_ina219()) {
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }

    if (xSemaphoreTake(xSemaphoreINA219, 25) == pdTRUE) {

	s_Volts[loopno] = ina219_state->Solar.volts + ina219_state->Solar.shunt;
	s_Current[loopno] = ina219_state->Solar.current;
	b_Volts[loopno] = ina219_state->Battery.volts + ina219_state->Battery.shunt;
        b_Current[loopno] = ina219_state->Battery.current;
	m_Valid[loopno] = ina219_state->valid;
	xSemaphoreGive(xSemaphoreINA219);

	/*
	 * Check for crazy values
	 */
	if ((b_Volts[loopno] < 10) || (b_Volts[loopno] > 15.0)) {
	    m_Valid[loopno] = false;
	}
	if ((b_Current[loopno] < 0) || (b_Current[loopno] > 2000)) {
	    m_Valid[loopno] = false;
	}
	if (s_Volts[loopno] < 0) {
	    s_Volts[loopno] = 0.0;
	}
	if (s_Volts[loopno] > 24) {
	    m_Valid[loopno] = false;
	}
	if (s_Current[loopno] < 0) {
	    s_Current[loopno] = 0.0;
	}
	if (s_Current[loopno] > 2000) {
	    m_Valid[loopno] = false;
	}
    }

    uint64_t  ms = millis();
    m_Time[loopno] = ms - gLastTime;
    gLastTime = ms;

    ESP_LOGI(TAG, "Measure: %d  Valid: %s  Battery: %.3fV %.1fmA  Solar: %.3fV %.1fmA  time %llu",
		  loopno, (m_Valid[loopno]) ? "true":"false", b_Volts[loopno], b_Current[loopno], s_Volts[loopno], s_Current[loopno], m_Time[loopno]);

    if (loopno < (MAX_LOOPS - 1))
	loopno++;
}



void getLightValues() {

    request_apds9930();

    while (! ready_apds9930()) {
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}


void app_main(void)
{
    uint64_t	totalTime, gTimeInMillis;

#ifdef CONFIG_CODE_PRODUCTION
    ESP_LOGI(TAG, "Starting production");
#endif
#ifdef CONFIG_CODE_TESTING
    ESP_LOGI(TAG, "Starting testing");
#endif

    /*
     * Initialize NVS
     */
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    nvsio_init();
    Relay1 = nvsio_read_u8((char *)"out1");
    Relay2 = nvsio_read_u8((char *)"out2");
    Dimmer3 = nvsio_read_u8((char *)"out3");
    Dimmer4 = nvsio_read_u8((char *)"out4");
    DS_Active = nvsio_read_u8((char *)"ds_active");
    DS_Time = nvsio_read_u32((char *)"ds_time");
    if (DS_Time == 0) {
	DS_Time = DS_TIME;
	nvsio_write_u32((char *)"ds_time", DS_Time);
    }

    gLastTime = millis();
    ESP_ERROR_CHECK(i2cdev_init());

    bmp280_init_default_params(&bmp280_params);
    /*
     * Override some defaults to make the BMP280 use less power
     * and still provide enough resolution.
     */
    bmp280_params.oversampling_pressure = BMP280_LOW_POWER;
    bmp280_params.oversampling_temperature = BMP280_LOW_POWER;
    bmp280_params.standby = BMP280_STANDBY_1000;

    memset(&bmp280_dev, 0, sizeof(bmp280_t));
    memset(&ina219_b_dev, 0, sizeof(ina219_t));
    memset(&ina219_s_dev, 0, sizeof(ina219_t));
    memset(&apds9930_dev, 0, sizeof(apds9930_t));

    i2c_dev_t dev = { 0 };
    dev.cfg.sda_io_num = CONFIG_I2C_MASTER_SDA;
    dev.cfg.scl_io_num = CONFIG_I2C_MASTER_SCL;
    dev.cfg.master.clk_speed = 100000;

    dev.addr = 0x39;
    if (i2c_dev_probe(&dev, I2C_DEV_WRITE) == 0) {
	ESP_ERROR_CHECK(apds9930_init_desc(&apds9930_dev, 0x39, 0, CONFIG_I2C_MASTER_SDA, CONFIG_I2C_MASTER_SCL));
	ESP_ERROR_CHECK(apds9930_init(&apds9930_dev));
	ESP_LOGI(TAG, "Found APDS-9930 id: 0x%02x", apds9930_dev.id);
    } else {
	ESP_LOGW(TAG, "No APDS-9930 found");
    }
    dev.addr = 0x40;
    if (i2c_dev_probe(&dev, I2C_DEV_WRITE) == 0) {
	ESP_ERROR_CHECK(ina219_init_desc(&ina219_b_dev, 0x40, 0, CONFIG_I2C_MASTER_SDA, CONFIG_I2C_MASTER_SCL));
	ESP_ERROR_CHECK(ina219_init(&ina219_b_dev));
	ESP_LOGI(TAG, "Found INA219 @0x40 Battery");
    } else {
	ESP_LOGW(TAG, "No INA219 @0x40 found");
    }
    dev.addr = 0x41;
    if (i2c_dev_probe(&dev, I2C_DEV_WRITE) == 0) {
	ESP_ERROR_CHECK(ina219_init_desc(&ina219_s_dev, 0x41, 0, CONFIG_I2C_MASTER_SDA, CONFIG_I2C_MASTER_SCL));
	ESP_ERROR_CHECK(ina219_init(&ina219_s_dev));
        ESP_LOGI(TAG, "Found INA219 @0x41 Solar");
    } else {
	ESP_LOGW(TAG, "No INA219 @0x41 found");
    }
    dev.addr = 0x76;
    if (i2c_dev_probe(&dev, I2C_DEV_WRITE) == 0) {
	ESP_ERROR_CHECK(bmp280_init_desc(&bmp280_dev, BMP280_I2C_ADDRESS_0, 0, CONFIG_I2C_MASTER_SDA, CONFIG_I2C_MASTER_SCL));
	ESP_ERROR_CHECK(bmp280_init(&bmp280_dev, &bmp280_params));
	ESP_LOGI(TAG, "Found BMP280 @ 0x76 id: 0x%02x", bmp280_dev.id);
    } else {
    	dev.addr = 0x77;
    	if (i2c_dev_probe(&dev, I2C_DEV_WRITE) == 0) {
	    ESP_ERROR_CHECK(bmp280_init_desc(&bmp280_dev, BMP280_I2C_ADDRESS_1, 0, CONFIG_I2C_MASTER_SDA, CONFIG_I2C_MASTER_SCL));
	    ESP_ERROR_CHECK(bmp280_init(&bmp280_dev, &bmp280_params));
            ESP_LOGI(TAG, "Found BMP280 @ 0x77 id: 0x%02x", bmp280_dev.id);
    	} else {
	    ESP_LOGW(TAG, "No BMP280 found");
	}
    }

    /*
     * Create FreeRTOS tasks
     */
    xSemaphoreBMP280 = xSemaphoreCreateMutex();
    xSemaphoreINA219 = xSemaphoreCreateMutex();
    xSemaphoreAPDS9930 = xSemaphoreCreateMutex();


    xTaskCreate(&task_bmp280,   "task_bmp280",      2560, NULL, 8, &xTaskBMP280);
    xTaskCreate(&task_ina219,   "task_ina219",      2560, NULL, 8, &xTaskINA219);
    xTaskCreate(&task_apds9930, "task_apds9930",    2560, NULL, 8, &xTaskAPDS9930);
    xTaskCreate(&task_out,      "task_out",         2560, NULL, 9, &xTaskOUT);
    xTaskCreate(&task_mqtt,     "task_mqtt",        4096, NULL, 5, &xTaskMQTT);
    xTaskCreate(&task_wifi,     "task_wifi",        4096, NULL, 3, &xTaskWifi);
#ifdef CONFIG_ENABLE_BLE_GATT
    xTaskCreate(&task_ble,      "task_ble",         4096, NULL, 3, &xTaskBLE);
#endif

    vTaskDelay(10 / portTICK_PERIOD_MS);

    /*
     * Main application loop.
     */
    int		State = State_Init;
    int		OldState = State_Init + 1;
    uint8_t	ds_time = DS_Time;


    while (0) {
	request_bmp280();
	request_ina219();
	request_apds9930();
	vTaskDelay(5000 / portTICK_PERIOD_MS);
    }

    while (1) {
	if (OldState != State) {
	    ESP_LOGD(TAG, "Switch to state %d", State);
	    OldState = State;
	}

	gTimeInMillis = millis();

	switch (State) {
	    case State_Init:		getTempBaro();
					getLightValues();
					getVoltsCurrent();
					State = State_Connect_Wifi;
					DisCounter = 0;
					request_WiFi(true);
		    			break;

	    case State_Connect_Wifi:	if (ready_WiFi()) {
					    State = State_Connect_MQTT;
					    /*
					     * Give the network stack a bit more time to setup
					     */
					    vTaskDelay(250 / portTICK_PERIOD_MS);
					    request_mqtt(true);
					    Alarm &= ~AL_NOWIFI;
					    ESP_LOGD(TAG, "Connected counter WiFi %d", DisCounter);
					    DisCounter = 0;
					} else {
					    /*
					     * 15 seconds connection try.
					     */
					    DisCounter++;
					    vTaskDelay(500 / portTICK_PERIOD_MS);
					    ESP_LOGI(TAG, "* Counter WiFi %d", DisCounter);
					    if (DisCounter > 30) {
						Alarm |= AL_NOWIFI;
						request_WiFi(false);
						State = State_Init;
						vTaskDelay(4000 / portTICK_PERIOD_MS);
					    }
					}
					break;

	    case State_Connect_MQTT:	if (ready_mqtt()) {
					    State = State_Working;
					    ESP_LOGI(TAG, "Connected counter MQTT %d", DisCounter);
                                            DisCounter = 0;
					} else {
					    DisCounter++;
					    if (DisCounter > 60) {
						request_mqtt(false);
						request_WiFi(false);
						State = State_Init;
						vTaskDelay(2000 / portTICK_PERIOD_MS);
					    }
					    vTaskDelay(1000 / portTICK_PERIOD_MS);
					}
					break;

	    case State_Working:		/*
					 * The final measure power usage.
					 * This one should include the WiFi usage current.
					 */
					getVoltsCurrent();
					solarVolts = solarCurrent = solarPower = batteryVolts = batteryCurrent = batteryPower = 0;
					loops = 0;
      					totalTime = 0;
					for (int i = 0; i < loopno; i++) {
	      				    /*
    					     * If there are only 2 loops, and the flag that we came from deep-sleep is set
    					     * then assume the following:
   					     *   1. No current (or very low for the dc converter) during DS_TIME seconds.
    					     *   2. Low power during 0.5 second (no WiFi yet).
    					     *   3. 5 seconds power usage for the last measurement.
    					     * Calculate the average battery current from this.
    					     *
    					     * If there are more loops and we came from continues running do the following:
    					     *   1. Take the current use from all exept the last one, weight loops * 10 seconds.
    					     *   2. Take the last one, and weight for 5 seconds.
    					     * Calculate the average battery current from this.
    					     */
					    if (m_Valid[i]) {
						solarVolts += s_Volts[i];
						solarCurrent += s_Current[i];
						batteryVolts += b_Volts[i];
						if (i == (loopno - 1)) {
						    // Add the extra time
						    m_Time[i] += SUB_TIME;
						}
						batteryCurrent += b_Current[i] * m_Time[i];
						totalTime += m_Time[i];
						loops++;
					    }
					}

      					if (DS_Active) {
        				    totalTime += DS_Time * 1000;
        				    batteryCurrent += DS_CURRENT * DS_Time * 1000;
					    ESP_LOGI(TAG, "Added %ld totalTime %lld", DS_Time * 1000, totalTime);
      					}

      					// If valid measurements
      					if (loops) {
					    solarVolts = solarVolts / loops;
					    solarCurrent = solarCurrent / loops;
					    solarPower = solarVolts * solarCurrent;
					    batteryVolts = batteryVolts / loops;
					    batteryCurrent = batteryCurrent / totalTime;
					    batteryPower = batteryVolts * batteryCurrent;
					    BatteryState(batteryVolts, (0 - batteryCurrent) + solarCurrent);

					    ESP_LOGI(TAG, "  Solar Volts: %.4fV Current %.4fmA Power %.4fmW", solarVolts, solarCurrent, solarPower);
					    ESP_LOGI(TAG, "Battery Volts: %.4fV Current %.4fmA Power %.4fmW", batteryVolts, batteryCurrent, batteryPower);

#ifdef CONFIG_CODE_PRODUCTION
					    /*   Check alarm conditions */
					    if (batteryState <= 10) {
					    	Alarm |= AL_ACCULOW;
					    } else {
					    	Alarm &= ~AL_ACCULOW;
					    }
#endif
#ifdef CONFIG_CODE_TESTING
					    Alarm &= ~AL_ACCULOW;
#endif
      					}
					getTempBaro();
					vTaskDelay(2000 / portTICK_PERIOD_MS);
      					publish();
      					State = State_WorkDone;
					/*
					 * Wait an extra second so the publish message will reach
					 * the broker and the subscriptions are really here.
					 */
      					gTimeNext = millis() + 1000;
					break;

	    case State_WorkDone:	if (gTimeInMillis >= gTimeNext) {
					    State = State_Disconnect_MQTT;
					    request_mqtt(false);
					}
					vTaskDelay(50 / portTICK_PERIOD_MS);
					break;

	    case State_Disconnect_MQTT:	wait_mqtt(10000);
					State = State_Disconnect_Wifi;
					break;

	    case State_Disconnect_Wifi:	request_WiFi(false);
      					// // Reset values for average current measurement.
					loopno = 0;
					gLastTime = millis();

					/*
      					 *  If any output is on, use 6 10 seconds loops.
      					 *  If nothing on, do a deep sleep.
      					 */
					if (Relay1 || Relay2 || Dimmer3 || Dimmer4) {
					    DS_Active = 0;
					    nvsio_write_u8((char *)"ds_active", 0);

        				    // Active mode, 60 seconds loop
					    ST_LOOPS = 6;
					    gTimeNext = millis() + ST_INTERVAL;
					    ESP_LOGD(TAG, "Start sleeploops");
					    State = State_Wait;
      					} else {
					    ds_time = DS_TIME;
        				    if (solarVolts < 6) {
						// At night, increase the deep-sleep time.
          					ds_time *= 4;
        				    }
					    nvsio_write_u8((char *)"ds_active", 1);
					    DS_Active = 1;
					    nvsio_write_u32((char *)"ds_time", ds_time);
					    DS_Time = ds_time;
        				    State = State_GoSleep;
      					}
					break;

	    case State_Wait:		if (gTimeInMillis >= gTimeNext) {
					    State = State_Measure;
					}
					vTaskDelay(50 / portTICK_PERIOD_MS);
					break;

	    case State_Measure:		gTimeNext = millis() + ST_INTERVAL;
					getVoltsCurrent();
					if (loopno >= ST_LOOPS) {
					    ESP_LOGD(TAG, "Enough loops, do connect");
        				    getLightValues();
					    State = State_Connect_Wifi;
					    DisCounter = 0;
					    request_WiFi(true);
					} else {
					    State = State_Wait;
					}
					break;

	    case State_GoSleep:		ESP_LOGI(TAG, "Going to deep-sleep for %ld seconds", DS_Time);
					ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(DS_Time * 1e6));
					esp_deep_sleep_start();
					break;
	}
    }
    // Not reached.
}

mercurial