Moved the user interface into a separate task. Added a real seconds timer to this task.

Tue, 05 Nov 2019 11:31:59 +0100

author
Michiel Broek <mbroek@mbse.eu>
date
Tue, 05 Nov 2019 11:31:59 +0100
changeset 21
043ae27633f8
parent 20
7c1dacafed03
child 22
cceb36fd3a2a

Moved the user interface into a separate task. Added a real seconds timer to this task.

main/CMakeLists.txt file | annotate | diff | comparison | revisions
main/co2meter.c file | annotate | diff | comparison | revisions
main/config.c file | annotate | diff | comparison | revisions
main/config.h file | annotate | diff | comparison | revisions
main/task_user.c file | annotate | diff | comparison | revisions
main/task_user.h file | annotate | diff | comparison | revisions
--- a/main/CMakeLists.txt	Mon Nov 04 19:35:05 2019 +0100
+++ b/main/CMakeLists.txt	Tue Nov 05 11:31:59 2019 +0100
@@ -1,3 +1,3 @@
-idf_component_register(SRCS config.c task_mqtt.c task_ds18b20.c updates.c task_adc.c xutil.c co2meter.c u8g2_esp32_hal.c task_wifi.c
+idf_component_register(SRCS config.c task_user.c task_mqtt.c task_ds18b20.c updates.c task_adc.c xutil.c co2meter.c u8g2_esp32_hal.c task_wifi.c
 	INCLUDE_DIRS ".")
 
--- a/main/co2meter.c	Mon Nov 04 19:35:05 2019 +0100
+++ b/main/co2meter.c	Tue Nov 05 11:31:59 2019 +0100
@@ -7,21 +7,9 @@
 
 static const char *TAG = "co2meter";
 
-#define PIN_SDA 		(CONFIG_I2C_MASTER_SDA)
-#define PIN_SCL			(CONFIG_I2C_MASTER_SCL)
-#define ROT_ENC_A_GPIO		(CONFIG_ROT_ENC_A_GPIO)
-#define ROT_ENC_B_GPIO		(CONFIG_ROT_ENC_B_GPIO)
-#define ROT_ENC_SW_GPIO		(CONFIG_ROT_ENC_SW_GPIO)
-#define INACTIVITY		480					///< Time in 250 mSec units.
 
-#define	EDIT_TYPE_TEXT		0					///< Editor type is text
-#define	EDIT_TYPE_INT		1					///< Editor type is integer
-#define	EDIT_TYPE_FLOAT		2					///< Editor type is float
-
-
+const esp_app_desc_t                    *app_desc = NULL;               ///< Application description
 int					Main_Loop1 = ML1_INIT;		///< Loop 1 init
-int					Main_Loop2 = -1;		///< Loop 2 invalid
-int					New_Loop2 = ML2_DONE;		///< Loop 2 new state
 bool                            	System_TimeOk = false;          ///< System time status
 time_t                          	now;                            ///< Current time
 struct tm                       	timeinfo;                       ///< Current time structure
@@ -31,12 +19,7 @@
 static TaskHandle_t			xTaskADC = NULL;
 static TaskHandle_t			xTaskWifi = NULL;
 static TaskHandle_t			xTaskMQTT = NULL;
-const esp_app_desc_t			*app_desc = NULL;		///< Application description
-u8g2_t					u8g2;				///< A structure which will contain all the data for one display
-rotary_encoder_info_t			rinfo = { 0 };			///< Rotary encoder record
-rotary_encoder_event_t			event = { 0 };
-QueueHandle_t				event_queue;
-static int				PushDuration = 0;		///< Duration of the pushed button
+static TaskHandle_t			xTaskUser = NULL;
 
 extern unit_t				units[3];			///< Pressure test units
 extern SemaphoreHandle_t		xSemaphoreUnits;		///< Units lock semaphore
@@ -45,457 +28,9 @@
 extern ADC_State                	*adc_state;             	///< ADC state
 extern SemaphoreHandle_t        	xSemaphoreADC;          	///< ADC lock semaphore
 extern WIFI_State			*wifi_state;			///< WiFi state
+extern EventGroupHandle_t		xEventGroupUser;
 extern int				count_pub;			///< Published MQTT messages in transit
-static xQueueHandle			gpio_evt_queue = NULL;		///< Rotary pushbutton queue
-static int				usertimer = 0;			///< User inactive timeout
-
-
-
-/**
- * @brief Get a keyboard character from the rotary encoder.
- * @param curkey The referenced value if the key being edited. NOTE, start at 0 for a new char??
- * @param type The edittype, all values, integer or float.
- * @param x The x position on the screen.
- * @param y The y position on the screen.
- * @return 1 if short keypress, meaning enter key. 2 if long press, enter key and editing is ready.
- */
-int getkey(int *curkey, int type, int x, int y)
-{
-    int	key = *curkey;
-    int	rc = 0;
-
-    u8g2_DrawHLine(&u8g2, x, y+3, 12);
-    u8g2_SendBuffer(&u8g2);
-
-    for (;;) {
-    	if (xQueueReceive(event_queue, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) {
-	    usertimer = INACTIVITY;
-	    if (event.state.position != 0) {
-
-		u8g2_SetDrawColor(&u8g2, 0);
-		u8g2_DrawGlyph(&u8g2, x, y, key);
-		u8g2_SetDrawColor(&u8g2, 1);
-		u8g2_SendBuffer(&u8g2);
-
-	    	if (event.state.position > 0) {
-		    if (key == 126)
-		    	key = 171;
-		    else if (key < 126)
-		    	key++;
-	    	} else if (event.state.position < 0) {
-		    if (key == 171)
-		    	key = 126;
-		    else if (key > 32)
-		    	key--;
-		}
-
-		ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
-	    	u8g2_DrawGlyph(&u8g2, x, y, key);
-	    	u8g2_SendBuffer(&u8g2);
-	    }
-    	} else {
-	    if (PushDuration) {
-		if (PushDuration > 500)
-		    rc = 2;
-		else
-		    rc = 1;
-	    	PushDuration = 0;
-		break;
-	    }
-    	}
-    }
-    u8g2_SetDrawColor(&u8g2, 0);
-    u8g2_DrawHLine(&u8g2, x, y+3, 12);
-    u8g2_SetDrawColor(&u8g2, 1);
-    u8g2_SendBuffer(&u8g2);
-
-    *curkey = key;
-    return rc;
-}
-
-
-
-/**
- * @brief Editor using the rotary switch.
- * @param label The label of the edit field.
- * @param txt The string to edit.
- * @param errmsg The error message if needed.
- * @param len The maximum length for the string.
- * @param type The edit type.
- */
-void rotary_editer(char *label, char *txt, char *errmsg, int len, int type)
-{
-    char        buf[65];
-    int		key, x, y, rc;
-
-    u8g2_ClearBuffer(&u8g2);
-    u8g2_DrawHLine(&u8g2, 0, 14, 128);
-    u8g2_DrawHLine(&u8g2, 0, 49, 128);
-    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tf);
-    sprintf(buf, "Edit %s", label);
-    u8g2_DrawStr(&u8g2,0,12,buf);
-
-    if (strlen(errmsg)) {
-	u8g2_SetFont(&u8g2, u8g2_font_t0_12b_tf);
-	u8g2_DrawStr(&u8g2, 0, 61, errmsg);
-    }
-    u8g2_SetFont(&u8g2, u8g2_font_t0_12_tf);
-    y = 36;
-    u8g2_DrawStr(&u8g2, 0, y, txt);
-    u8g2_SendBuffer(&u8g2);
-
-    for (;;) {
-	x = u8g2_GetUTF8Width(&u8g2, txt);
-	key = 'a';
-	rc = getkey(&key, type, x, y);
-	if (rc == 1) {
-	    if (key >= 32 && key <= 126 && strlen(txt) < len) {
-		txt[strlen(txt) + 1] = '\0';
-                txt[strlen(txt)] = key;
-	    } else if (key == 171 && strlen(txt)) {
-		// delete key
-		txt[strlen(txt) - 1] = '\0';
-	    }
-printf("strlen %d  x %d  key %d\n", strlen(txt), x, key);
-	} else if (rc == 2) {
-	    break;
-	}
-    }
-}
-
-
-
-/**
- * @brief Write a menu line on the display.
- * @param bright Display the line with a bold or normal font.
- * @param x The horizontal start position of the line.
- * @param y The vertical bottom of the line position.
- * @param format The formatted data to display.
- */
-void menu_line(int bright, int x, int y, const char *format, ...)
-{
-    char        buf[65];
-    va_list     va_ptr;
-
-    if (bright)
-        u8g2_SetFont(&u8g2, u8g2_font_t0_12b_tr);
-    else
-        u8g2_SetFont(&u8g2, u8g2_font_t0_12_tr);
-
-    va_start(va_ptr, format);
-    vsnprintf(buf, 65, format, va_ptr);
-    va_end(va_ptr);
-
-    u8g2_DrawStr(&u8g2, x, y, buf);
-}
-
-
-
-/**
- * @brief Clear the display and prepare the top of the display.
- * @format The formatted data to display at the top.
- */
-void screen_top(const char *format, ...)
-{
-    char        buf[65];
-    va_list     va_ptr;
-
-    va_start(va_ptr, format);
-    vsnprintf(buf, 65, format, va_ptr);
-    va_end(va_ptr);
-
-    u8g2_ClearBuffer(&u8g2);
-    u8g2_DrawHLine(&u8g2, 0, 14, 128);
-
-    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tr);
-    u8g2_uint_t w = u8g2_GetStrWidth(&u8g2, buf);
-    u8g2_DrawStr(&u8g2, (128 - w) / 2,12, buf);
-}
-
-
-
-/**
- * @brief The splash screen shown during cold boot or user wakeup.
- */
-void screen_splash()
-{
-    screen_top("CO2 meter %s", app_desc->version);
-
-    u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
-    u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, "START");
-    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,50, "START");
-
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0); // wake up display
-}
-
-
-
-/**
- * @brief The main overview screen.
- */
-void screen_main()
-{
-    char	buf[65];
-    int		i;
-
-    screen_top("CO2 meter %s", app_desc->version);
-
-    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
-
-    	u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
-    	sprintf(buf, "%.1f °C", units[0].temperature / 1000.0);
-    	u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
-    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);
-    	u8g2_SetFont(&u8g2, u8g2_font_t0_18b_tf);
-
-	for (i = 0; i < 3; i++) {
-    	    sprintf(buf, "%.1f", units[i].pressure / 1000.0);
-    	    w = u8g2_GetUTF8Width(&u8g2, buf);
-    	    u8g2_DrawUTF8(&u8g2, ((42 - w) / 2) + i * 43,63, buf);
-	}
-	xSemaphoreGive(xSemaphoreUnits);
-    }
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0); // wake up display
-}
-
-
-
-/**
- * @brief The unit display screen.
- * @param no The unit index number.
- */
-void screen_unit(int no)
-{
-    char        buf[65];
 
-    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
-
-	screen_top("Unit %d %s", no + 1, units[no].mode ? "On":"Off");
-
-    	u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
-    	sprintf(buf, "%.1f °C", units[no].temperature / 1000.0);
-    	u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
-    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);
-
-    	sprintf(buf, "%.2f bar", units[no].pressure / 1000.0);
-    	w = u8g2_GetUTF8Width(&u8g2, buf);
-    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,63, buf);
-
-	xSemaphoreGive(xSemaphoreUnits);
-    }
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0); // wake up display
-}
-
-
-
-/**
- * @brief The unit zero setup screen.
- * @param no The unit index number.
- * @param sub The submenu index number.
- */
-void screen_unit_zero(int no, int sub)
-{
-    screen_top("Unit %d zero mV", no + 1);
-    menu_line(       0, 2, 25, "Current   %d", units[no].pressure_zero);
-    menu_line(sub == 0, 2, 37, "New value %d", units[no].pressure_voltage / (adc_state->Batt_voltage / 1000));
-    menu_line(sub == 1, 2, 49, "Return");
-printf("current %d  p_voltage %d  batt %d\n", units[no].pressure_zero, units[no].pressure_voltage, adc_state->Batt_voltage);
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-/**
- * @brief The unit setup screen.
- * @param no The unit index number.
- * @param sub The submenu index number.
- */
-void screen_unit_setup(int no, int sub)
-{
-    screen_top("Unit %d setup", no + 1);
-    menu_line(sub == 0, 2, 25, "Mode    %s", units[no].mode ? "ON":"OFF");
-    menu_line(sub == 1, 2, 37, "Zero mV %d", units[no].pressure_zero);
-    menu_line(sub == 2, 2, 49, "DS18B20 %s", units[no].temperature_rom_code);
-    menu_line(sub == 3, 2, 61, "Return");
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-void screen_wifi()
-{
-    char	buf[65];
-
-    screen_top("WiFi Status");
-    snprintf(buf, 65, "SSID %s", wifi_state->STA_ssid);
-    u8g2_DrawStr(&u8g2, 1, 28, buf);
-    snprintf(buf, 65, "Online %s", wifi_state->STA_online ? "Yes":"No");
-    u8g2_DrawStr(&u8g2, 1, 43, buf);
-    snprintf(buf, 65, "RSSI %d", wifi_state->STA_rssi);
-    u8g2_DrawStr(&u8g2, 1, 59, buf);
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-void screen_wifi_setup(int sub)
-{
-    screen_top("WiFi Setup");
-    menu_line(sub == 0, 2, 25, "Connect");
-    menu_line(sub == 1, 2, 37, "New");
-    menu_line(sub == 2, 2, 49, "Delete");
-    menu_line(sub == 3, 2, 61, "Return");
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-void screen_network()
-{
-    screen_top("Network Status");
-    menu_line(0, 1, 25, "IP   %s", wifi_state->STA_ip);
-    menu_line(0, 1, 37, "Mask %s", wifi_state->STA_nm);
-    menu_line(0, 1, 49, "GW   %s", wifi_state->STA_gw);
-    menu_line(0, 1, 61, "Name %s", config.hostname);
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-void screen_mqtt()
-{
-    screen_top("MQTT Status");
-    menu_line(0, 1, 25, "serv %s", config.mqtt_server);
-    menu_line(0, 1, 37, "port %d", config.mqtt_port);
-    menu_line(0, 1, 49, "user %s", config.mqtt_user);
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-void screen_update()
-{
-    screen_top("Update firmware");
-    menu_line(0, 1, 43, "Push to update");
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-/**
- * @brief Fatal messages on the screen.
- * @param e1 The first line.
- * @param e2 The second line.
- */
-void screen_fatal(char *e1, char *e2)
-{
-    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tr);
-    u8g2_DrawStr(&u8g2,2,12,e1);
-    u8g2_DrawStr(&u8g2,2,24,e2);
-    u8g2_SendBuffer(&u8g2);
-    u8g2_SetPowerSave(&u8g2, 0);
-}
-
-
-
-/**
- * @brief Interrupt service routine for the rotary pushbutton.
- */
-static void IRAM_ATTR gpio_isr_handler(void* arg)
-{
-    uint32_t gpio_num = (uint32_t) arg;
-    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
-}
-
-
-
-/**
- * @brief GPIO queue task. See if there is a rotary pushbutton event on the queue.
- */
-static void gpio_task(void* arg)
-{
-    uint32_t		io_num;
-    static int64_t	pushed = 0;
-
-    for(;;) {
-        if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
-	    if (io_num == ROT_ENC_SW_GPIO) {
-		if (gpio_get_level(io_num) == 0) {
-		    pushed = esp_timer_get_time();
-		    PushDuration = 0;
-		} else if (gpio_get_level(io_num) == 1) {
-		    PushDuration = (esp_timer_get_time() - pushed) / 1000;
-		    ESP_LOGI(TAG, "GPIO rotary button intr, val: %d time: %d", gpio_get_level(io_num), PushDuration);
-		}
-	    } else {
-            	ESP_LOGE(TAG, "GPIO[%d] unknown intr, val: %d", io_num, gpio_get_level(io_num));
-	    }
-	    usertimer = INACTIVITY;
-        }
-    }
-}
-
-
-
-/**
- * @brief Select new menu number on a postitive or negative rotary position.
- * @param pos The new position, positive, negative or zero.
- * @param next_menu The selected menu if rotated clockwise.
- * @param prev_menu The selected menu if rotated counter-clockwise.
- */
-static void rotate_to_menu(rotary_encoder_position_t pos, int next_menu, int prev_menu)
-{
-    if (pos > 0)
-	New_Loop2 = next_menu;
-    else if (pos < 0)
-	New_Loop2 = prev_menu;
-    ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
-}
-
-
-
-/**
- * @brief Rotate subscreens numbers.
- * @param pos The new position, positive, negative or zero.
- * @param min The lowest number. If already at the lowest, select the highest.
- * @param max The highest number. If already at the highest, select the lowest.
- * @param cursub The subscreen number by reference. This is updated with the new number.
- * @return Returns true if a new number is selected, false if nothing changed.
- */
-bool rotate_to_sub(rotary_encoder_position_t pos, int min, int max, int *cursub)
-{
-   int	sub = *cursub;
-   bool	rc = false;
-
-   if (pos > 0) {
-	if (sub < max)
-	    sub++;
-	else
-	    sub = min;
-	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
-	rc = true;
-    } else if (pos < 0) {
-	if (sub > min)
-	    sub--;
-	else
-	    sub = max;
-	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
-	rc = true;
-    }
-
-    *cursub = sub;
-    return rc;
-}
 
 
 
@@ -505,31 +40,16 @@
     gettimeofday(&now, NULL);
     int			sleep_time_ms = (now.tv_sec - sleep_enter_time.tv_sec) * 1000 + (now.tv_usec - sleep_enter_time.tv_usec) / 1000;
     esp_err_t           ret;
-    char		txt[65];
 
     Main_Loop1 = ML1_INIT;
-    Main_Loop2 = -1;
-
-    /*
-     * Setup the OLED display.
-     * See: https://github.com/nkolban/esp32-snippets/blob/master/hardware/displays/U8G2/
-     */
-    u8g2_esp32_hal_t u8g2_esp32_hal = U8G2_ESP32_HAL_DEFAULT;
-    u8g2_esp32_hal.sda = PIN_SDA;
-    u8g2_esp32_hal.scl = PIN_SCL;
-    u8g2_esp32_hal_init(u8g2_esp32_hal);
-
-    u8g2_Setup_sh1106_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);  // init u8g2 structure
-    u8x8_SetI2CAddress(&u8g2.u8x8, 0x78);
-    u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
-
     app_desc = esp_ota_get_app_description();
+    /* event handler and event group for the user interface */
+    xEventGroupUser = xEventGroupCreate();
 
     switch (esp_sleep_get_wakeup_cause()) {
         case ESP_SLEEP_WAKEUP_EXT1: {
 	    ESP_LOGI(TAG, "Starting from deep sleep, Rotary switch pressed");
-	    New_Loop2 = ML2_INIT;
-	    screen_splash();
+	    user_wakeup();
             break;
         }
         case ESP_SLEEP_WAKEUP_TIMER: {
@@ -539,7 +59,7 @@
         case ESP_SLEEP_WAKEUP_UNDEFINED:
         default:
             ESP_LOGI(TAG, "Starting from hard reset");
-	    screen_splash();
+	    user_cold();
     }
 
     const int wakeup_time_sec = 55;
@@ -590,7 +110,6 @@
         } else {
             ESP_LOGE(TAG, "Failed to initialize SPIFFS (%d)", ret);
         }
-	screen_fatal("SPIFFS:", "init error");
         return; // Stop application.
     }
 
@@ -598,7 +117,6 @@
     ret = esp_spiffs_info(NULL, &total, &used);
     if (ret != ESP_OK) {
         ESP_LOGE(TAG, "Failed to get SPIFFS partition information");
-	screen_fatal("SPIFFS:", "partition error");
         return; // Stop application.
     } else {
         ESP_LOGI(TAG, "Partition size: %d, used: %d - %d%%", total, used, (used * 100) / total);
@@ -637,6 +155,7 @@
     xSemaphoreADC = xSemaphoreCreateMutex();
     xSemaphoreUnits = xSemaphoreCreateMutex();
 
+    xTaskCreate(&task_user,    "task_user",     2048, NULL,10, &xTaskUser);
     xTaskCreate(&task_ds18b20, "task_ds18b20",  2560, NULL, 8, &xTaskDS18B20);
     xTaskCreate(&task_adc,     "task_adc",      2560, NULL, 8, &xTaskADC);
     esp_log_level_set("wifi", ESP_LOG_ERROR);
@@ -646,41 +165,13 @@
     xTaskCreate(&task_mqtt,    "task_mqtt",     4096, NULL, 5, &xTaskMQTT);
 
     /*
-     * Setup the Rotary Encoder.
-     * esp32-rotary-encoder requires that the GPIO ISR service is 
-     * installed before calling rotary_encoder_register()
-     */
-    ESP_ERROR_CHECK(gpio_install_isr_service(0));
-    ESP_ERROR_CHECK(rotary_encoder_init(&rinfo, ROT_ENC_A_GPIO, ROT_ENC_B_GPIO));
-    ESP_ERROR_CHECK(rotary_encoder_enable_half_steps(&rinfo, false));
-
-    gpio_config_t io_conf;
-    io_conf.intr_type = GPIO_PIN_INTR_ANYEDGE;
-    io_conf.pin_bit_mask = (1ULL << ROT_ENC_SW_GPIO);
-    io_conf.mode = GPIO_MODE_INPUT;
-    gpio_config(&io_conf);
-
-    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
-    xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);
-
-    gpio_isr_handler_add(ROT_ENC_SW_GPIO, gpio_isr_handler, (void*) ROT_ENC_SW_GPIO);
-
-    // Create a queue for events from the rotary encoder driver.
-    // Tasks can read from this queue to receive up to date position information.
-    event_queue = rotary_encoder_create_queue();
-    ESP_ERROR_CHECK(rotary_encoder_set_queue(&rinfo, event_queue));
-
-    /*
      * Main application loop.
      */
     while (1) {
 
 	ESP_LOGI(TAG, "Entered app loop");
-	//event = { 0 };
-	int sub = 0;
-	u8g2_SetPowerSave(&u8g2, 1);
 
-	/* Measure process or user input via rotary switch */
+	/* Measure process */
 	while (1) {
 	    switch (Main_Loop1) {
 		case ML1_INIT:
@@ -695,8 +186,8 @@
 		case ML1_CONNECT:
                     if (ready_WiFi()) {
                         Main_Loop1 = ML1_MQTT_CONNECT;
-			if (Main_Loop2 == ML2_WIFI)
-			    screen_wifi();
+//			if (Main_Loop2 == ML2_WIFI)
+//			    screen_wifi();
 		    }
                     break;
 
@@ -747,12 +238,13 @@
 			    }
 			    write_units();
 			    xSemaphoreGive(xSemaphoreUnits);
-			    switch (Main_Loop2) {
-				case ML2_USER:	screen_main(); break;
-				case ML2_UNIT1:	screen_unit(0); break;
-				case ML2_UNIT2: screen_unit(1); break;
-				case ML2_UNIT3: screen_unit(2); break;
-			    }
+			    user_refresh();
+//			    switch (Main_Loop2) {
+//				case ML2_USER:	screen_main(); break;
+//				case ML2_UNIT1:	screen_unit(0); break;
+//				case ML2_UNIT2: screen_unit(1); break;
+//				case ML2_UNIT3: screen_unit(2); break;
+//			    }
 			}
 		    }
 		    break;
@@ -791,10 +283,10 @@
 
 		case ML1_WIFI_OFF:
 		    if (! ready_WiFi()) {
-			ESP_LOGI(TAG, "Loop timer: Done");
+			ESP_LOGI(TAG, "Loop timer: Done %s", user_busy() ? "true":"false");
 			Main_Loop1 = ML1_DONE;
-			if (Main_Loop2 == ML2_WIFI)
-			    screen_wifi();
+//			if (Main_Loop2 == ML2_WIFI)
+//			    screen_wifi();
 		    }
 		    break;
 
@@ -802,222 +294,7 @@
 		    break;
 	    }
 
-	    /*
-	     * One time actions
-	     */
-	    if (New_Loop2 != Main_Loop2) {
-
-		Main_Loop2 = New_Loop2;
-
-		switch (Main_Loop2) {
-		    case ML2_INIT:
-			ESP_LOGI(TAG, "Loop user: Init");
-			New_Loop2 = ML2_USER;
-			usertimer = INACTIVITY;
-			break;
-
-		    case ML2_USER:
-			ESP_LOGI(TAG, "Loop user: User mainmenu");
-			screen_main();
-			break;
-
-		    case ML2_UNIT1:
-		    case ML2_UNIT2:
-		    case ML2_UNIT3:
-			ESP_LOGI(TAG, "Loop user: Unit %d", Main_Loop2 - ML2_UNIT1);
-			screen_unit(Main_Loop2 - ML2_UNIT1);
-			break;
-
-		    case ML2_WIFI:
-			ESP_LOGI(TAG, "Loop user: WiFi");
-			screen_wifi();
-			break;
-
-		    case ML2_NETWORK:
-			ESP_LOGI(TAG, "Loop user: Network");
-			screen_network();
-			break;
-
-		    case ML2_MQTT:
-			ESP_LOGI(TAG, "Loop user: MQTT");
-			screen_mqtt();
-			break;
-
-		    case ML2_SETUP_MQTT:
-			ESP_LOGI(TAG, "Loop user: MQTT setup");
-			sprintf(txt, "EDtXt");
-			rotary_editer("MQTT demo", txt, "", 16, EDIT_TYPE_TEXT);
-			New_Loop2 = ML2_MQTT;
-			break;
-
-		    case ML2_UPDATE:
-			ESP_LOGI(TAG, "Loop user: Update");
-			screen_update();
-			break;
-
-		    case ML2_SETUP_UNIT1:
-		    case ML2_SETUP_UNIT2:
-		    case ML2_SETUP_UNIT3:
-			ESP_LOGI(TAG, "Loop user: Setup Unit %d", Main_Loop2 - ML2_SETUP_UNIT1);
-			sub = 0;
-			screen_unit_setup(Main_Loop2 - ML2_SETUP_UNIT1, sub);
-			break;
-
-		    case ML2_ZERO_UNIT1:
-		    case ML2_ZERO_UNIT2:
-		    case ML2_ZERO_UNIT3:
-			ESP_LOGI(TAG, "Loop user: Zero Unit %d", Main_Loop2 - ML2_ZERO_UNIT1);
-                        sub = 0;
-                        screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, sub);
-                        break;
-
-		    case ML2_INACTIVE:
-			ESP_LOGI(TAG, "Loop user: Inactive");
-			u8g2_SetPowerSave(&u8g2, 1); // powersave display
-			New_Loop2 = ML2_DONE;
-			break;
-
-		    default:
-			break;
-		}
-	    }
-
-	    /*
-	     * Handle rotationg the rotary encoder.
-	     */
-	    if (Main_Loop2 < ML2_INACTIVE) {
-		// If not configured, start configure
-		// If configured select first unit
-		// Handle screen (first is show measured values)
-
-		// Display per unit. Temp + Pressure + state. Press is setup this sensor.
-		// Setup menu: 	Sensors
-		//		WiFi
-		//		MQTT
-		//		System (timers etc)
-		//		Update OTA
-		//		Return
-
-		// Sensors menu:	Assignments, turn to choose one.
-		// Sensors setup menu:	DS18B20 addr	Press is assign
-		//			DS18B20 addr
-
-		if (xQueueReceive(event_queue, &event, 250 / portTICK_PERIOD_MS) == pdTRUE) {
-		    usertimer = INACTIVITY;
-		    switch (Main_Loop2) {
-		    	case ML2_USER:		rotate_to_menu(event.state.position, ML2_UNIT1, ML2_USER); break;
-		    	case ML2_UNIT1:		rotate_to_menu(event.state.position, ML2_UNIT2, ML2_USER); break;
-		    	case ML2_UNIT2:		rotate_to_menu(event.state.position, ML2_UNIT3, ML2_UNIT1); break;
-		    	case ML2_UNIT3:		rotate_to_menu(event.state.position, ML2_WIFI, ML2_UNIT2); break;
-		    	case ML2_WIFI:		rotate_to_menu(event.state.position, ML2_NETWORK, ML2_UNIT3); break;
-		    	case ML2_NETWORK:	rotate_to_menu(event.state.position, ML2_MQTT, ML2_WIFI); break;
-		    	case ML2_MQTT:		rotate_to_menu(event.state.position, ML2_UPDATE, ML2_NETWORK); break;
-		    	case ML2_UPDATE:	rotate_to_menu(event.state.position, ML2_UPDATE, ML2_MQTT); break;
-			case ML2_SETUP_UNIT1:
-			case ML2_SETUP_UNIT2:
-			case ML2_SETUP_UNIT3:	if (rotate_to_sub(event.state.position, 0, 3, &sub))
-						    screen_unit_setup(Main_Loop2 - ML2_SETUP_UNIT1, sub);
-						break;
-			case ML2_ZERO_UNIT1:
-			case ML2_ZERO_UNIT2:
-			case ML2_ZERO_UNIT3:	if (rotate_to_sub(event.state.position, 0, 1, &sub))
-						    screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, sub);
-						break;
-			default:
-		    				ESP_LOGI(TAG, "Event: position %d, direction %s", event.state.position,
-                                		event.state.direction ? (event.state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? "CW":"CCW"):"NOT_SET");
-		    }
-                } else {
-                    // Poll current position and direction
-//                    rotary_encoder_state_t state = { 0 };
-//                    ESP_ERROR_CHECK(rotary_encoder_get_state(&rinfo, &state));
-
-//                    ESP_LOGI(TAG, "Poll: position %d, direction %s timer %d", state.position,
-//                    state.direction ? (state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? "CW" : "CCW") : "NOT_SET", usertimer);
-		    if (usertimer) {
-			usertimer--;
-			if ((usertimer % 240) == 0) { // Each minute
-			    ESP_LOGI(TAG, "usertimer %d", usertimer);
-			    if (Main_Loop1 == ML1_DONE)
-				Main_Loop1 = ML1_INIT;
-			}
-		    } else
-			New_Loop2 = ML2_INACTIVE;
-                }
-	    }
-
-	    /*
-	     * Handle pressed rotary button.
-	     */
-	    if (PushDuration) {
-		int idx = 0;
-	    	switch (Main_Loop2) {
-		    case ML2_UNIT1:
-		    case ML2_UNIT2:
-		    case ML2_UNIT3:
-			New_Loop2 = ML2_SETUP_UNIT1 + (Main_Loop2 - ML2_UNIT1);
-			break;
-
-		    case ML2_SETUP_UNIT1:
-		    case ML2_SETUP_UNIT2:
-		    case ML2_SETUP_UNIT3:
-			idx = Main_Loop2 - ML2_SETUP_UNIT1;
-			if (sub == 0) {
-			    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
-				if (units[idx].mode)
-				    units[idx].mode = 0;
-				else
-				    units[idx].mode = 1;
-				write_units();
-                            	xSemaphoreGive(xSemaphoreUnits);
-			    }
-			    screen_unit_setup(idx, sub);
-			    if (Main_Loop1 == ML1_DONE)
-				Main_Loop1 = ML1_INIT;
-			}
-			if (sub == 1)
-			    New_Loop2 = ML2_ZERO_UNIT1 + idx;
-			if (sub == 3)
-			    New_Loop2 = ML2_UNIT1 + idx;
-			printf("unit setup sub %d  new %d  idx %d\n", sub, New_Loop2, idx);
-			break;
-
-		    case ML2_ZERO_UNIT1:
-		    case ML2_ZERO_UNIT2:
-		    case ML2_ZERO_UNIT3:
-			idx = Main_Loop2 - ML2_ZERO_UNIT1;
-                        if (sub == 0) {
-			    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE &&
-			      xSemaphoreTake(xSemaphoreADC, 10) == pdTRUE &&
-			      adc_state->Pressure[idx].voltage > 165 &&
-			      adc_state->Pressure[idx].voltage < 660) {
-				units[idx].pressure_zero = adc_state->Pressure[idx].voltage / (adc_state->Batt_voltage / 1000);
-				write_units();
-				xSemaphoreGive(xSemaphoreADC);
-                                xSemaphoreGive(xSemaphoreUnits);
-				screen_unit_zero(idx, sub);
-				if (Main_Loop1 == ML1_DONE)
-                            	    Main_Loop1 = ML1_INIT;
-			    }
-			}
-			if (sub == 1) {
-			    New_Loop2 = ML2_SETUP_UNIT1 + idx;
-			    sub = 1;
-			}
-			printf("unit zero sub %d  new %d  idx %d\n", sub, New_Loop2, idx);
-			break;
-
-		    case ML2_MQTT:
-			New_Loop2 = ML2_SETUP_MQTT;
-			break;
-
-		    default:
-			break;
-	    	}
-	    	PushDuration = 0;
-	    }
-
-	    if (Main_Loop1 == ML1_DONE && Main_Loop2 == ML2_DONE)
+	    if (Main_Loop1 == ML1_DONE && ! user_busy())
 	    	break;
 
 	    vTaskDelay(10 / portTICK_PERIOD_MS);
@@ -1027,9 +304,10 @@
     	gettimeofday(&sleep_enter_time, NULL);
 	esp_deep_sleep_start();
 
+//	ESP_LOGI(TAG, "Do nothing loop");
+//	vTaskDelay(55000 / portTICK_PERIOD_MS);
+
 	Main_Loop1 = ML1_INIT;
-	New_Loop2 = ML2_INIT;
     }
-
 }
 
--- a/main/config.c	Mon Nov 04 19:35:05 2019 +0100
+++ b/main/config.c	Tue Nov 05 11:31:59 2019 +0100
@@ -98,6 +98,8 @@
         size_t bytes = fread(dst, 1, sizeof(units), f);
         fclose(f);
         ESP_LOGI(TAG, "/spiffs/etc/units.conf read %d bytes", bytes);
+	for (int i = 0; i < 3; i++)
+	   ESP_LOGI(TAG, "%d %s %d %4d %3d", i, units[i].alias, units[i].pressure_channel, units[i].pressure_voltage, units[i].pressure_zero);
     }
 }
 
--- a/main/config.h	Mon Nov 04 19:35:05 2019 +0100
+++ b/main/config.h	Tue Nov 05 11:31:59 2019 +0100
@@ -58,6 +58,7 @@
 #include "task_adc.h"
 #include "task_wifi.h"
 #include "task_mqtt.h"
+#include "task_user.h"
 #include "xutil.h"
 
 
@@ -180,7 +181,7 @@
     int			pressure_state;			///< Reading status
     uint8_t		pressure_channel;		///< ADC channel
     uint32_t		pressure_voltage;		///< Measured voltage in mV.
-    uint32_t		pressure_zero;			///< Zero offset in mV.
+    uint32_t		pressure_zero;			///< Zero offset promille of the full scale.
     uint32_t		pressure;			///< Pressure in bar * 1000;
     int			mode;				///< Unit mode
     uint32_t		alarm;				///< Alarm bits
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_user.c	Tue Nov 05 11:31:59 2019 +0100
@@ -0,0 +1,868 @@
+/**
+ * @file task_user.c
+ * @brief co2meter project.
+ */
+
+#include "config.h"
+
+static const char *TAG = "task_user";
+
+
+EventGroupHandle_t			xEventGroupUser;		///< Events User task
+esp_timer_handle_t			timerHandle;			///< Seconds timer
+uint32_t				SecsCount = 0;			///< Seconds counter
+uint32_t				UserTimer = 0;                  ///< User inactive timeout
+int					Main_Loop2 = -1;		///< Effective menu
+int					New_Loop2 = ML2_INIT;		///< New menu
+int					SubMenu = 0;			///< Submenu number
+u8g2_t					u8g2;				///< A structure which will contain all the data for one display
+rotary_encoder_info_t			rinfo = { 0 };			///< Rotary encoder record
+rotary_encoder_event_t			event = { 0 };
+QueueHandle_t				event_queue;
+static int				PushDuration = 0;		///< Duration of the pushed button
+
+extern const esp_app_desc_t		*app_desc;
+extern unit_t				units[3];			///< Pressure test units
+extern SemaphoreHandle_t		xSemaphoreUnits;		///< Units lock semaphore
+extern DS18B20_State            	*ds18b20_state;         	///< DS18B20 state
+extern SemaphoreHandle_t        	xSemaphoreDS18B20;      	///< DS18B20 lock semaphore
+extern ADC_State                	*adc_state;             	///< ADC state
+extern SemaphoreHandle_t        	xSemaphoreADC;          	///< ADC lock semaphore
+extern WIFI_State			*wifi_state;			///< WiFi state
+extern int				count_pub;			///< Published MQTT messages in transit
+static xQueueHandle			gpio_evt_queue = NULL;		///< Rotary pushbutton queue
+extern int				Main_Loop1;			///< Main measure loop
+
+
+
+const int TASK_USER_COLD = BIT0;					///< System cold start
+const int TASK_USER_WAKEUP = BIT1;					///< System wakeup from deepsleep
+const int TASK_USER_BUSY = BIT2;					///< User interface is busy doing something.
+const int TASK_USER_REFRESH = BIT3;					///< Refresh measurement results
+
+
+
+/**
+ * @brief Seconds timer callback.
+ */
+void TimerCallback(void *arg);
+
+
+/***************************************************************************/
+
+
+
+void TimerCallback(void *arg)
+{
+    SecsCount++;
+    if ((SecsCount % 60) == 0) {
+	if (Main_Loop1 == ML1_DONE)
+	    Main_Loop1 = ML1_INIT;
+    }
+
+    if (UserTimer == 1) {
+	ESP_LOGI(TAG, "User inactivity timeout");
+	xEventGroupClearBits(xEventGroupUser, TASK_USER_BUSY);
+        u8g2_SetPowerSave(&u8g2, 1);
+    }
+    if (UserTimer) {
+	UserTimer--;
+    }
+}
+
+
+
+void user_cold()
+{
+    xEventGroupSetBits(xEventGroupUser, TASK_USER_COLD);
+}
+
+
+
+void user_wakeup()
+{
+    xEventGroupSetBits(xEventGroupUser, TASK_USER_WAKEUP);
+}
+
+
+
+void user_refresh()
+{
+    xEventGroupSetBits(xEventGroupUser, TASK_USER_REFRESH);
+}
+
+
+
+bool user_busy(void)
+{
+    if (xEventGroupGetBits(xEventGroupUser) & TASK_USER_BUSY)
+	return true;
+    return false;
+}
+
+
+
+
+/**
+ * @brief Get a keyboard character from the rotary encoder.
+ * @param curkey The referenced value if the key being edited. NOTE, start at 0 for a new char??
+ * @param type The edittype, all values, integer or float.
+ * @param x The x position on the screen.
+ * @param y The y position on the screen.
+ * @return 1 if short keypress, meaning enter key. 2 if long press, enter key and editing is ready.
+ */
+int getkey(int *curkey, int type, int x, int y)
+{
+    int	key = *curkey;
+    int	rc = 0;
+
+    u8g2_DrawHLine(&u8g2, x, y+3, 12);
+    u8g2_SendBuffer(&u8g2);
+
+    for (;;) {
+    	if (xQueueReceive(event_queue, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) {
+	    UserTimer = INACTIVITY;
+	    if (event.state.position != 0) {
+
+		u8g2_SetDrawColor(&u8g2, 0);
+		u8g2_DrawGlyph(&u8g2, x, y, key);
+		u8g2_SetDrawColor(&u8g2, 1);
+		u8g2_SendBuffer(&u8g2);
+
+	    	if (event.state.position > 0) {
+		    if (key == 126)
+		    	key = 171;
+		    else if (key < 126)
+		    	key++;
+	    	} else if (event.state.position < 0) {
+		    if (key == 171)
+		    	key = 126;
+		    else if (key > 32)
+		    	key--;
+		}
+
+		ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
+	    	u8g2_DrawGlyph(&u8g2, x, y, key);
+	    	u8g2_SendBuffer(&u8g2);
+	    }
+    	} else {
+	    if (PushDuration) {
+		if (PushDuration > 500)
+		    rc = 2;
+		else
+		    rc = 1;
+	    	PushDuration = 0;
+		break;
+	    }
+    	}
+    }
+    u8g2_SetDrawColor(&u8g2, 0);
+    u8g2_DrawHLine(&u8g2, x, y+3, 12);
+    u8g2_SetDrawColor(&u8g2, 1);
+    u8g2_SendBuffer(&u8g2);
+
+    *curkey = key;
+    return rc;
+}
+
+
+
+/**
+ * @brief Editor using the rotary switch.
+ * @param label The label of the edit field.
+ * @param txt The string to edit.
+ * @param errmsg The error message if needed.
+ * @param len The maximum length for the string.
+ * @param type The edit type.
+ */
+void rotary_editer(char *label, char *txt, char *errmsg, int len, int type)
+{
+    char        buf[65];
+    int		key, x, y, rc;
+
+    u8g2_ClearBuffer(&u8g2);
+    u8g2_DrawHLine(&u8g2, 0, 14, 128);
+    u8g2_DrawHLine(&u8g2, 0, 49, 128);
+    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tf);
+    sprintf(buf, "Edit %s", label);
+    u8g2_DrawStr(&u8g2,0,12,buf);
+
+    if (strlen(errmsg)) {
+	u8g2_SetFont(&u8g2, u8g2_font_t0_12b_tf);
+	u8g2_DrawStr(&u8g2, 0, 61, errmsg);
+    }
+    u8g2_SetFont(&u8g2, u8g2_font_t0_12_tf);
+    y = 36;
+    u8g2_DrawStr(&u8g2, 0, y, txt);
+    u8g2_SendBuffer(&u8g2);
+
+    for (;;) {
+	x = u8g2_GetUTF8Width(&u8g2, txt);
+	key = 'a';
+	rc = getkey(&key, type, x, y);
+	if (rc == 1) {
+	    if (key >= 32 && key <= 126 && strlen(txt) < len) {
+		txt[strlen(txt) + 1] = '\0';
+                txt[strlen(txt)] = key;
+	    } else if (key == 171 && strlen(txt)) {
+		// delete key
+		txt[strlen(txt) - 1] = '\0';
+	    }
+printf("strlen %d  x %d  key %d\n", strlen(txt), x, key);
+	} else if (rc == 2) {
+	    break;
+	}
+    }
+}
+
+
+
+/**
+ * @brief Write a menu line on the display.
+ * @param bright Display the line with a bold or normal font.
+ * @param x The horizontal start position of the line.
+ * @param y The vertical bottom of the line position.
+ * @param format The formatted data to display.
+ */
+void menu_line(int bright, int x, int y, const char *format, ...)
+{
+    char        buf[65];
+    va_list     va_ptr;
+
+    if (bright)
+        u8g2_SetFont(&u8g2, u8g2_font_t0_12b_tr);
+    else
+        u8g2_SetFont(&u8g2, u8g2_font_t0_12_tr);
+
+    va_start(va_ptr, format);
+    vsnprintf(buf, 65, format, va_ptr);
+    va_end(va_ptr);
+
+    u8g2_DrawStr(&u8g2, x, y, buf);
+}
+
+
+
+/**
+ * @brief Clear the display and prepare the top of the display.
+ * @format The formatted data to display at the top.
+ */
+void screen_top(const char *format, ...)
+{
+    char        buf[65];
+    va_list     va_ptr;
+
+    va_start(va_ptr, format);
+    vsnprintf(buf, 65, format, va_ptr);
+    va_end(va_ptr);
+
+    u8g2_ClearBuffer(&u8g2);
+    u8g2_DrawHLine(&u8g2, 0, 14, 128);
+
+    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tr);
+    u8g2_uint_t w = u8g2_GetStrWidth(&u8g2, buf);
+    u8g2_DrawStr(&u8g2, (128 - w) / 2,12, buf);
+}
+
+
+
+/**
+ * @brief The splash screen shown during cold boot or user wakeup.
+ */
+void screen_splash()
+{
+    screen_top("CO2 meter %s", app_desc->version);
+
+    u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
+    u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, "START");
+    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,50, "START");
+
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0); // wake up display
+}
+
+
+
+/**
+ * @brief The main overview screen.
+ */
+void screen_main()
+{
+    char	buf[65];
+    int		i;
+
+    screen_top("CO2 meter %s", app_desc->version);
+
+    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
+
+    	u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
+    	sprintf(buf, "%.1f °C", units[0].temperature / 1000.0);
+    	u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
+    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);
+    	u8g2_SetFont(&u8g2, u8g2_font_t0_18b_tf);
+
+	for (i = 0; i < 3; i++) {
+    	    sprintf(buf, "%.1f", units[i].pressure / 1000.0);
+    	    w = u8g2_GetUTF8Width(&u8g2, buf);
+    	    u8g2_DrawUTF8(&u8g2, ((42 - w) / 2) + i * 43,63, buf);
+	}
+	xSemaphoreGive(xSemaphoreUnits);
+    }
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0); // wake up display
+}
+
+
+
+/**
+ * @brief The unit display screen.
+ * @param no The unit index number.
+ */
+void screen_unit(int no)
+{
+    char        buf[65];
+
+    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
+
+	screen_top("Unit %d %s", no + 1, units[no].mode ? "On":"Off");
+
+    	u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
+    	sprintf(buf, "%.1f °C", units[no].temperature / 1000.0);
+    	u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
+    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);
+
+    	sprintf(buf, "%.2f bar", units[no].pressure / 1000.0);
+    	w = u8g2_GetUTF8Width(&u8g2, buf);
+    	u8g2_DrawUTF8(&u8g2, (128 - w) / 2,63, buf);
+
+	xSemaphoreGive(xSemaphoreUnits);
+    }
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0); // wake up display
+}
+
+
+
+/**
+ * @brief The unit zero setup screen.
+ * @param no The unit index number.
+ * @param sub The submenu index number.
+ */
+void screen_unit_zero(int no, int sub)
+{
+    screen_top("Unit %d zero mV", no + 1);
+    menu_line(       0, 2, 25, "Current   %d", units[no].pressure_zero);
+    menu_line(sub == 0, 2, 37, "New value %d", units[no].pressure_voltage / (adc_state->Batt_voltage / 1000));
+    menu_line(sub == 1, 2, 49, "Return");
+printf("current %d  p_voltage %d  batt %d\n", units[no].pressure_zero, units[no].pressure_voltage, adc_state->Batt_voltage);
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+/**
+ * @brief The unit setup screen.
+ * @param no The unit index number.
+ * @param sub The submenu index number.
+ */
+void screen_unit_setup(int no, int sub)
+{
+    screen_top("Unit %d setup", no + 1);
+    menu_line(sub == 0, 2, 25, "Mode    %s", units[no].mode ? "ON":"OFF");
+    menu_line(sub == 1, 2, 37, "Zero mV %d", units[no].pressure_zero);
+    menu_line(sub == 2, 2, 49, "DS18B20 %s", units[no].temperature_rom_code);
+    menu_line(sub == 3, 2, 61, "Return");
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+void screen_wifi()
+{
+    char	buf[65];
+
+    screen_top("WiFi Status");
+    snprintf(buf, 65, "SSID %s", wifi_state->STA_ssid);
+    u8g2_DrawStr(&u8g2, 1, 28, buf);
+    snprintf(buf, 65, "Online %s", wifi_state->STA_online ? "Yes":"No");
+    u8g2_DrawStr(&u8g2, 1, 43, buf);
+    snprintf(buf, 65, "RSSI %d", wifi_state->STA_rssi);
+    u8g2_DrawStr(&u8g2, 1, 59, buf);
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+void screen_wifi_setup(int sub)
+{
+    screen_top("WiFi Setup");
+    menu_line(sub == 0, 2, 25, "Connect");
+    menu_line(sub == 1, 2, 37, "New");
+    menu_line(sub == 2, 2, 49, "Delete");
+    menu_line(sub == 3, 2, 61, "Return");
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+void screen_network()
+{
+    screen_top("Network Status");
+    menu_line(0, 1, 25, "IP   %s", wifi_state->STA_ip);
+    menu_line(0, 1, 37, "Mask %s", wifi_state->STA_nm);
+    menu_line(0, 1, 49, "GW   %s", wifi_state->STA_gw);
+    menu_line(0, 1, 61, "Name %s", config.hostname);
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+void screen_mqtt()
+{
+    screen_top("MQTT Status");
+    menu_line(0, 1, 25, "serv %s", config.mqtt_server);
+    menu_line(0, 1, 37, "port %d", config.mqtt_port);
+    menu_line(0, 1, 49, "user %s", config.mqtt_user);
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+void screen_update()
+{
+    screen_top("Update firmware");
+    menu_line(0, 1, 43, "Push to update");
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+/**
+ * @brief Fatal messages on the screen.
+ * @param e1 The first line.
+ * @param e2 The second line.
+ */
+void screen_fatal(char *e1, char *e2)
+{
+    u8g2_SetFont(&u8g2, u8g2_font_t0_15_tr);
+    u8g2_DrawStr(&u8g2,2,12,e1);
+    u8g2_DrawStr(&u8g2,2,24,e2);
+    u8g2_SendBuffer(&u8g2);
+    u8g2_SetPowerSave(&u8g2, 0);
+}
+
+
+
+/**
+ * @brief Interrupt service routine for the rotary pushbutton.
+ */
+static void IRAM_ATTR gpio_isr_handler(void* arg)
+{
+    uint32_t gpio_num = (uint32_t) arg;
+    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
+}
+
+
+
+/**
+ * @brief GPIO queue task. See if there is a rotary pushbutton event on the queue.
+ */
+static void gpio_task(void* arg)
+{
+    uint32_t		io_num;
+    static int64_t	pushed = 0;
+
+    for(;;) {
+        if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
+	    if (io_num == ROT_ENC_SW_GPIO) {
+		if (gpio_get_level(io_num) == 0) {
+		    pushed = esp_timer_get_time();
+		    PushDuration = 0;
+		} else if (gpio_get_level(io_num) == 1) {
+		    PushDuration = (esp_timer_get_time() - pushed) / 1000;
+		    ESP_LOGI(TAG, "GPIO rotary button intr, val: %d time: %d", gpio_get_level(io_num), PushDuration);
+		}
+	    } else {
+            	ESP_LOGE(TAG, "GPIO[%d] unknown intr, val: %d", io_num, gpio_get_level(io_num));
+	    }
+	    UserTimer = INACTIVITY;
+        }
+    }
+}
+
+
+
+/**
+ * @brief Select new menu number on a postitive or negative rotary position.
+ * @param pos The new position, positive, negative or zero.
+ * @param next_menu The selected menu if rotated clockwise.
+ * @param prev_menu The selected menu if rotated counter-clockwise.
+ */
+static void rotate_to_menu(rotary_encoder_position_t pos, int next_menu, int prev_menu)
+{
+    if (pos > 0)
+	New_Loop2 = next_menu;
+    else if (pos < 0)
+	New_Loop2 = prev_menu;
+    ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
+}
+
+
+
+/**
+ * @brief Rotate subscreens numbers.
+ * @param pos The new position, positive, negative or zero.
+ * @param min The lowest number. If already at the lowest, select the highest.
+ * @param max The highest number. If already at the highest, select the lowest.
+ * @param cursub The subscreen number by reference. This is updated with the new number.
+ * @return Returns true if a new number is selected, false if nothing changed.
+ */
+bool rotate_to_sub(rotary_encoder_position_t pos, int min, int max, int *cursub)
+{
+   int	sub = *cursub;
+   bool	rc = false;
+
+   if (pos > 0) {
+	if (sub < max)
+	    sub++;
+	else
+	    sub = min;
+	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
+	rc = true;
+    } else if (pos < 0) {
+	if (sub > min)
+	    sub--;
+	else
+	    sub = max;
+	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
+	rc = true;
+    }
+
+    *cursub = sub;
+    return rc;
+}
+
+
+
+void menu_change(void)
+{
+    char	txt[65];
+
+    if (New_Loop2 != Main_Loop2) {
+
+	Main_Loop2 = New_Loop2;
+
+        switch (Main_Loop2) {
+              case ML2_INIT:
+                      ESP_LOGI(TAG, "Loop user: Init");
+                      New_Loop2 = ML2_USER;
+                      break;
+
+              case ML2_USER:
+                      ESP_LOGI(TAG, "Loop user: User mainmenu");
+                      screen_main();
+                      break;
+
+              case ML2_UNIT1:
+              case ML2_UNIT2:
+              case ML2_UNIT3:
+                      ESP_LOGI(TAG, "Loop user: Unit %d", Main_Loop2 - ML2_UNIT1);
+                      screen_unit(Main_Loop2 - ML2_UNIT1);
+                      break;
+
+              case ML2_WIFI:
+                      ESP_LOGI(TAG, "Loop user: WiFi");
+                      screen_wifi();
+                      break;
+
+              case ML2_NETWORK:
+                      ESP_LOGI(TAG, "Loop user: Network");
+                      screen_network();
+                      break;
+
+              case ML2_MQTT:
+                      ESP_LOGI(TAG, "Loop user: MQTT");
+                      screen_mqtt();
+                      break;
+
+              case ML2_SETUP_MQTT:
+                      ESP_LOGI(TAG, "Loop user: MQTT setup");
+                      sprintf(txt, "EDtXt");
+                      rotary_editer("MQTT demo", txt, "", 16, EDIT_TYPE_TEXT);
+                      New_Loop2 = ML2_MQTT;
+                      break;
+
+              case ML2_UPDATE:
+                      ESP_LOGI(TAG, "Loop user: Update");
+                      screen_update();
+                      break;
+
+              case ML2_SETUP_UNIT1:
+              case ML2_SETUP_UNIT2:
+              case ML2_SETUP_UNIT3:
+                      ESP_LOGI(TAG, "Loop user: Setup Unit %d", Main_Loop2 - ML2_SETUP_UNIT1);
+                      SubMenu = 0;
+                      screen_unit_setup(Main_Loop2 - ML2_SETUP_UNIT1, SubMenu);
+                      break;
+
+              case ML2_ZERO_UNIT1:
+              case ML2_ZERO_UNIT2:
+              case ML2_ZERO_UNIT3:
+                      ESP_LOGI(TAG, "Loop user: Zero Unit %d", Main_Loop2 - ML2_ZERO_UNIT1);
+                        SubMenu = 0;
+                        screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, SubMenu);
+                        break;
+
+              case ML2_INACTIVE:
+                      ESP_LOGI(TAG, "Loop user: Inactive");
+                      u8g2_SetPowerSave(&u8g2, 1); // powersave display
+                      New_Loop2 = ML2_DONE;
+                      break;
+
+              default:
+                      break;
+	}
+    }
+}
+
+
+
+void menu_rotary(void)
+{
+    switch (Main_Loop2) {
+	case ML2_USER:          rotate_to_menu(event.state.position, ML2_UNIT1, ML2_USER); break;
+	case ML2_UNIT1:         rotate_to_menu(event.state.position, ML2_UNIT2, ML2_USER); break;
+	case ML2_UNIT2:         rotate_to_menu(event.state.position, ML2_UNIT3, ML2_UNIT1); break;
+	case ML2_UNIT3:         rotate_to_menu(event.state.position, ML2_WIFI, ML2_UNIT2); break;
+	case ML2_WIFI:          rotate_to_menu(event.state.position, ML2_NETWORK, ML2_UNIT3); break;
+	case ML2_NETWORK:       rotate_to_menu(event.state.position, ML2_MQTT, ML2_WIFI); break;
+	case ML2_MQTT:          rotate_to_menu(event.state.position, ML2_UPDATE, ML2_NETWORK); break;
+	case ML2_UPDATE:        rotate_to_menu(event.state.position, ML2_UPDATE, ML2_MQTT); break;
+	case ML2_SETUP_UNIT1:
+	case ML2_SETUP_UNIT2:
+	case ML2_SETUP_UNIT3:   if (rotate_to_sub(event.state.position, 0, 3, &SubMenu))
+				    screen_unit_setup(Main_Loop2 - ML2_SETUP_UNIT1, SubMenu);
+				break;
+	case ML2_ZERO_UNIT1:
+	case ML2_ZERO_UNIT2:
+	case ML2_ZERO_UNIT3:    if (rotate_to_sub(event.state.position, 0, 1, &SubMenu))
+				    screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, SubMenu);
+				break;
+	default:
+				ESP_LOGI(TAG, "Event: position %d, direction %s", event.state.position,
+				    event.state.direction ? (event.state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? "CW":"CCW"):"NOT_SET");
+    }
+}
+
+
+
+void menu_loop(void)
+{
+    int idx = 0;
+
+    switch (Main_Loop2) {
+	case ML2_UNIT1:
+	case ML2_UNIT2:
+	case ML2_UNIT3:
+		New_Loop2 = ML2_SETUP_UNIT1 + (Main_Loop2 - ML2_UNIT1);
+		break;
+
+	case ML2_SETUP_UNIT1:
+	case ML2_SETUP_UNIT2:
+	case ML2_SETUP_UNIT3:
+		idx = Main_Loop2 - ML2_SETUP_UNIT1;
+		if (SubMenu == 0) {
+		    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
+			if (units[idx].mode)
+			    units[idx].mode = 0;
+			else
+			    units[idx].mode = 1;
+			write_units();
+			xSemaphoreGive(xSemaphoreUnits);
+		    }
+		    screen_unit_setup(idx, SubMenu);
+		    if (Main_Loop1 == ML1_DONE)
+			Main_Loop1 = ML1_INIT;
+		}
+		if (SubMenu == 1)
+		    New_Loop2 = ML2_ZERO_UNIT1 + idx;
+		if (SubMenu == 3)
+		    New_Loop2 = ML2_UNIT1 + idx;
+		printf("unit setup sub %d  new %d  idx %d\n", SubMenu, New_Loop2, idx);
+		break;
+
+	case ML2_ZERO_UNIT1:
+	case ML2_ZERO_UNIT2:
+	case ML2_ZERO_UNIT3:
+		idx = Main_Loop2 - ML2_ZERO_UNIT1;
+		if (SubMenu == 0) {
+		    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE &&
+			xSemaphoreTake(xSemaphoreADC, 10) == pdTRUE &&
+			adc_state->Pressure[idx].voltage > 165 &&
+			adc_state->Pressure[idx].voltage < 660) {
+			units[idx].pressure_zero = adc_state->Pressure[idx].voltage / (adc_state->Batt_voltage / 1000);
+			write_units();
+			xSemaphoreGive(xSemaphoreADC);
+			xSemaphoreGive(xSemaphoreUnits);
+			screen_unit_zero(idx, SubMenu);
+			if (Main_Loop1 == ML1_DONE)
+			    Main_Loop1 = ML1_INIT;
+		    }
+		}
+		if (SubMenu == 1) {
+		    New_Loop2 = ML2_SETUP_UNIT1 + idx;
+		    SubMenu = 1;
+		}
+		printf("unit zero sub %d  new %d  idx %d\n", SubMenu, New_Loop2, idx);
+		break;
+
+	case ML2_MQTT:
+		New_Loop2 = ML2_SETUP_MQTT;
+		break;
+
+	default:
+		break;
+    }
+}
+
+
+
+void task_user(void *pvParameter)
+{
+    esp_err_t           ret;
+
+    ESP_LOGI(TAG, "Starting User task");
+    Main_Loop2 = -1;
+
+    /*
+     * Setup the OLED display.
+     * See: https://github.com/nkolban/esp32-snippets/blob/master/hardware/displays/U8G2/
+     */
+    u8g2_esp32_hal_t u8g2_esp32_hal = U8G2_ESP32_HAL_DEFAULT;
+    u8g2_esp32_hal.sda = PIN_SDA;
+    u8g2_esp32_hal.scl = PIN_SCL;
+    u8g2_esp32_hal_init(u8g2_esp32_hal);
+
+    u8g2_Setup_sh1106_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);  // init u8g2 structure
+    u8x8_SetI2CAddress(&u8g2.u8x8, 0x78);
+    u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
+
+    /*
+     * Setup the Rotary Encoder.
+     * esp32-rotary-encoder requires that the GPIO ISR service is 
+     * installed before calling rotary_encoder_register()
+     */
+    ESP_ERROR_CHECK(gpio_install_isr_service(0));
+    ESP_ERROR_CHECK(rotary_encoder_init(&rinfo, ROT_ENC_A_GPIO, ROT_ENC_B_GPIO));
+    ESP_ERROR_CHECK(rotary_encoder_enable_half_steps(&rinfo, false));
+
+    gpio_config_t io_conf;
+    io_conf.intr_type = GPIO_PIN_INTR_ANYEDGE;
+    io_conf.pin_bit_mask = (1ULL << ROT_ENC_SW_GPIO);
+    io_conf.mode = GPIO_MODE_INPUT;
+    gpio_config(&io_conf);
+
+    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
+    xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);
+
+    gpio_isr_handler_add(ROT_ENC_SW_GPIO, gpio_isr_handler, (void*) ROT_ENC_SW_GPIO);
+
+    // Create a queue for events from the rotary encoder driver.
+    // Tasks can read from this queue to receive up to date position information.
+    event_queue = rotary_encoder_create_queue();
+    ESP_ERROR_CHECK(rotary_encoder_set_queue(&rinfo, event_queue));
+
+    esp_timer_create_args_t timerSecond = {
+        .callback = &TimerCallback,
+        .name = "SecondsTimer"
+    };
+
+    /*
+     * Create a one second periodic timer.
+     */
+    ESP_ERROR_CHECK(esp_timer_create(&timerSecond, &timerHandle));
+    ret = esp_timer_start_periodic(timerHandle, 1000000);
+    assert(ret == ESP_OK);
+
+    EventBits_t uxBits;
+    ESP_LOGI(TAG, "User task loop enter");
+
+    /*
+     * Task loop forever.
+     */
+    while (1) {
+
+	uxBits = xEventGroupWaitBits(xEventGroupUser, TASK_USER_COLD | TASK_USER_WAKEUP, pdFALSE, pdFALSE, portMAX_DELAY );
+
+	if (uxBits & TASK_USER_COLD) {
+	    ESP_LOGI(TAG, "User task cold start");
+	    screen_splash();
+	    xEventGroupClearBits(xEventGroupUser, TASK_USER_COLD);
+	    UserTimer = 10;
+	}
+
+	if (uxBits & TASK_USER_WAKEUP) {
+	    ESP_LOGI(TAG, "User task wakeup");
+	    xEventGroupSetBits(xEventGroupUser, TASK_USER_BUSY);
+	    xEventGroupClearBits(xEventGroupUser, TASK_USER_REFRESH);
+	    screen_main();
+	    UserTimer = INACTIVITY;
+	    New_Loop2 = ML2_INIT;
+	    Main_Loop2 = -1;
+	    SubMenu = 0;
+
+	    while (UserTimer) {
+
+		menu_change();
+		if (xQueueReceive(event_queue, &event, 250 / portTICK_PERIOD_MS) == pdTRUE) {
+		    UserTimer = INACTIVITY;
+		    menu_rotary();
+		}
+
+		if (PushDuration) {
+		    PushDuration = 0;
+		    menu_loop();
+		}
+
+		if (xEventGroupGetBits(xEventGroupUser) & TASK_USER_REFRESH) {
+		    ESP_LOGI(TAG, "User task refresh");
+		    switch (Main_Loop2) {
+			case ML2_USER:  screen_main(); break;
+			case ML2_UNIT1: screen_unit(0); break;
+			case ML2_UNIT2: screen_unit(1); break;
+			case ML2_UNIT3: screen_unit(2); break;
+		    }
+		    xEventGroupClearBits(xEventGroupUser, TASK_USER_REFRESH);
+		}
+
+		vTaskDelay(10 / portTICK_PERIOD_MS);
+	    }
+
+	    xEventGroupClearBits(xEventGroupUser, TASK_USER_WAKEUP);
+	}
+	vTaskDelay(10 / portTICK_PERIOD_MS);
+    }
+
+// If not configured, start configure
+// If configured select first unit
+// Handle screen (first is show measured values)
+// Display per unit. Temp + Pressure + state. Press is setup this sensor.
+// Setup menu: 	Sensors
+//		WiFi
+//		MQTT
+//		System (timers etc)
+//		Update OTA
+//		Return
+
+// Sensors menu:	Assignments, turn to choose one.
+// Sensors setup menu:	DS18B20 addr	Press is assign
+//			DS18B20 addr
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_user.h	Tue Nov 05 11:31:59 2019 +0100
@@ -0,0 +1,36 @@
+/**
+ * @file task_user.h
+ * @brief The FreeRTOS task to maintain MQTT connections.
+ */
+
+#ifndef	_TASK_USER_H
+#define	_TASK_USER_H
+
+
+#define PIN_SDA                 (CONFIG_I2C_MASTER_SDA)
+#define PIN_SCL                 (CONFIG_I2C_MASTER_SCL)
+#define ROT_ENC_A_GPIO          (CONFIG_ROT_ENC_A_GPIO)
+#define ROT_ENC_B_GPIO          (CONFIG_ROT_ENC_B_GPIO)
+#define ROT_ENC_SW_GPIO         (CONFIG_ROT_ENC_SW_GPIO)
+#define INACTIVITY              120                                     ///< User inactivity time
+
+#define EDIT_TYPE_TEXT          0                                       ///< Editor type is text
+#define EDIT_TYPE_INT           1                                       ///< Editor type is integer
+#define EDIT_TYPE_FLOAT         2                                       ///< Editor type is float
+
+
+
+void user_cold(void);
+void user_wakeup(void);
+void user_refresh(void);
+bool user_busy(void);
+
+
+/**
+ * @brief The FreeRTOS task to run the user interface.
+ */
+void task_user(void *);
+
+
+#endif
+

mercurial