main/task_user.c

Tue, 03 Oct 2023 17:24:06 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Tue, 03 Oct 2023 17:24:06 +0200
changeset 77
15dc572a7fcb
parent 71
995557380e5f
child 78
e03d729aecb8
permissions
-rw-r--r--

Version 0.3.0. Backported network code from experimental roaming project. Will now connect after reset to the strongest AP. Id the signal level drops below -67, extra scans are done to see for a better AP. Nothing is done yet. Removed config.conf file, all info is taken from the project menu and live tests. Better log the board type and send it via json mqtt. Send bssid and current channel too.

/**
 * @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
uint32_t				AlarmTimer = 0;			///< Alarm timer
int					Main_Loop2 = -1;		///< Effective menu
int					New_Loop2 = ML2_INIT;		///< New menu
int					SubMenu = 0;			///< Submenu number
int					SubOffset = 0;			///< Submenu offset
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 };			///< Rotary encoder events
QueueHandle_t				event_queue;			///< Events queue
QueueHandle_t				gpio_evt_queue = NULL;		///< Rotary pushbutton queue
static int				PushDuration = 0;		///< Duration of the pushed button
wifiStation_t				APs[10];			///< List of APs we know
int					edit_ssid = 0;			///< SSID being edited
int					num_ssids = 0;			///< Number of SSIDs we know
wifiStation_t				editAP;				///< Data of station to edit
char					sensors[DS18B20_MAX][17];	///< Sensors to select
uint32_t				err_connect = 0;		///< Connect error count
uint32_t				err_alarm = 0;			///< Alarm watchdog error count
uint32_t				err_temp = 0;			///< DS18B20 read errors

extern const esp_app_desc_t		*app_desc;			///< App description
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 SemaphoreHandle_t		xSemaphoreWiFi;			///< WiFi lock semaphore
extern int				count_pub;			///< Published MQTT messages in transit
extern int				Main_Loop1;			///< Main measure loop
extern int				update_running;			///< If update is running
extern int				num_sensors;			///< Detected DS18B20 sensors
extern wifiStation_t			wifiStation;
extern char				hostname[];


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 requested


/**
 * @brief Seconds timer callback.
 * @param arg Arguments for the timer.
 */
void TimerCallback(void *arg);


/***************************************************************************/



void TimerCallback(void *arg)
{
    SecsCount++;
    if ((SecsCount % MAINLOOP_TIMER) == 0) {
	if (Main_Loop1 == ML1_DONE && update_running == 0)
	    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--;
    }

    if (AlarmTimer == 1) {
	ESP_LOGI(TAG, "Alarm Timer timeout");
	Main_Loop1 = ML1_INIT;
	err_alarm++;
    }

    if (AlarmTimer) {
	AlarmTimer--;
    }
}



void user_cold()
{
    xEventGroupSetBits(xEventGroupUser, TASK_USER_COLD);
}



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.
 * @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;
    int8_t	ascent = u8g2_GetAscent(&u8g2);
    int8_t	descent = u8g2_GetDescent(&u8g2);
    int8_t	charheight = u8g2_GetMaxCharHeight(&u8g2);
    int8_t	charwidth = u8g2_GetMaxCharWidth(&u8g2);

    u8g2_DrawGlyph(&u8g2, x, y, key);
    u8g2_DrawHLine(&u8g2, x, y+3, 12);
    u8g2_UpdateDisplay(&u8g2);

    ESP_LOGI(TAG, "getkey(%c, %d, %d, %d) a %d  d %d  h %d  w %d", key, type, x, y, ascent, descent, charheight, charwidth);

    for (;;) {
    	if (xQueueReceive(event_queue, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) {
	    UserTimer = INACTIVITY;
	    if (event.state.position != 0) {

		u8g2_SetDrawColor(&u8g2, 0);
		u8g2_DrawBox(&u8g2, x, y - 12, charwidth, charheight);
		u8g2_SetDrawColor(&u8g2, 1);
		u8g2_DrawHLine(&u8g2, x, y+3, charwidth);

		if (event.state.position > 0) {
		    /*
		     * If turned fast, the encoder registers multiple steps.
		     * So follow these steps.
		     */
		    for (int i = 0; i < event.state.position; i++) {
			if (type == EDIT_TYPE_CAPS) {
			    if (key < 127)
                            	key++;
                            if (key > 96 && key < 123)
                            	key = 123;
			} else if (type == EDIT_TYPE_INT) {
			    if (key < 127)
                            	key++;
                            if (key < 45)
                            	key = 45;
                            if (key == 46 || key == 47)
                            	key = 48;
                            if (key > 57)
                            	key = 127;
			} else if (type == EDIT_TYPE_FLOAT) {

			} else { // EDIT_TYPE_TEXT
			    if (key < 127)
                        	key++;
			}
		    }
		} else if (event.state.position < 0) {
		    for (int i = 0; i > event.state.position; i--) {
			if (type == EDIT_TYPE_CAPS) {
			    if (key > 32)
                            	key--;
                            if (key > 96 && key < 123)
                            	key = 96;
			} else if (type == EDIT_TYPE_INT) {
			    if (key > 45)
                                key--;
                            if (key > 57)
                            	key = 57;
                            if (key == 46 || key == 47)
                          	key = 45;
			} else if (type == EDIT_TYPE_FLOAT) {

			} else { // EDIT_TYPE_TEXT
			    if (key > 32)
				key--;
			}
		    }
		}

		ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
	    	u8g2_DrawGlyph(&u8g2, x, y, key);
	    	u8g2_SendBuffer(&u8g2);
		u8g2_UpdateDisplay(&u8g2);
	    }
    	} else {
	    if (PushDuration) {
		UserTimer = INACTIVITY;
		if (PushDuration > 500)
		    rc = 2;
		else
		    rc = 1;
	    	PushDuration = 0;
		break;
	    }
    	}
    }
    u8g2_SetDrawColor(&u8g2, 0);
    if (key == 127)
	u8g2_DrawBox(&u8g2, x, y - 12, charwidth, charheight); // Erase DEL character
    u8g2_DrawHLine(&u8g2, x, y+3, charwidth);
    u8g2_SetDrawColor(&u8g2, 1);
    u8g2_UpdateDisplay(&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_unifont_t_symbols);
    y = 36;
    u8g2_DrawStr(&u8g2, 0, y, txt);
    u8g2_SendBuffer(&u8g2);
    ESP_LOGI(TAG, "rotary_editer(%s, %s, %s, %d, %d)", label, txt, errmsg, len, type);

    /*
     * Choose initial edit key
     */
    if (type == EDIT_TYPE_CAPS)
	key = 'A';
    else if (type == EDIT_TYPE_INT || type == EDIT_TYPE_FLOAT)
	key = '0';
    else
	key = 'a';

    for (;;) {
	x = u8g2_GetUTF8Width(&u8g2, txt);
	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 == 127 && strlen(txt)) {
		// delete key
		txt[strlen(txt) - 1] = '\0';
		if (strlen(txt))
		    key = txt[strlen(txt) - 1];
	    }
	    ESP_LOGI(TAG, " strlen %d  x %d  key %d  `%s`", strlen(txt), x, key, txt);
	} else if (rc == 2) {
	    break;
	}
    }
    ESP_LOGI(TAG, "rotary_editer() `%s`", txt);
}



/**
 * @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.
 * @param 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, "Welkom");
    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,50, "Welkom");
    u8g2_SendBuffer(&u8g2);
    u8g2_SetPowerSave(&u8g2, 0); // wake up display
}



/**
 * @brief The main overview screen.
 */
void screen_main()
{
    char	buf[65];
    int		i, mode[3];
    uint32_t	temperature = 0, pressure[3];

    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
	temperature = units[0].temperature;
	for (i = 0; i < 3; i++) {
	    pressure[i] = units[i].pressure;
	    mode[i] = units[i].mode;
	}
	xSemaphoreGive(xSemaphoreUnits);
    } else {
	ESP_LOGE(TAG, "screen_main() lock error");
    }

    screen_top("CO2 meter %s", app_desc->version);
    u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
    sprintf(buf, "%.1f °C", temperature / 1000.0);
    u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);

    for (i = 0; i < 3; i++) {
	if (mode[i])
	    u8g2_SetFont(&u8g2, u8g2_font_t0_18b_tf);
	else
	    u8g2_SetFont(&u8g2, u8g2_font_t0_18_tf);
	sprintf(buf, "%.1f", pressure[i] / 1000.0);
	w = u8g2_GetUTF8Width(&u8g2, buf);
	u8g2_DrawUTF8(&u8g2, ((42 - w) / 2) + i * 43,63, buf);
    }

    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];
    int		mode = 0;
    uint32_t	temperature = 0, pressure = 0;

    if (xSemaphoreTake(xSemaphoreUnits, 35) == pdTRUE) {
	mode = units[no].mode;
	temperature = units[no].temperature;
	pressure = units[no].pressure;
	xSemaphoreGive(xSemaphoreUnits);
    } else {
	ESP_LOGE(TAG, "screen_unit(%d) lock error", no);
    }

    screen_top("Meter %d %s", no + 1, mode ? "Aan":"Uit");

    u8g2_SetFont(&u8g2, u8g2_font_t0_22b_tf);
    sprintf(buf, "%.1f °C", temperature / 1000.0);
    u8g2_uint_t w = u8g2_GetUTF8Width(&u8g2, buf);
    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,40, buf);

    sprintf(buf, "%.2f bar", pressure / 1000.0);
    w = u8g2_GetUTF8Width(&u8g2, buf);
    u8g2_DrawUTF8(&u8g2, (128 - w) / 2,63, buf);
    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("Meter %d nulpunt", no + 1);
    menu_line(       0, 1, 25, "Huidig %d", units[no].pressure_zero);
    menu_line(sub == 0, 1, 37, "Nieuw  %d", units[no].pressure_voltage / (adc_state->Batt_voltage / 1000));
    menu_line(sub == 1, 1, 49, "Terug");
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The list of detected DS18B20 sensors.
 * @param no The unit index number.
 * @param sub The submenu index number.
 * @param offset The offset in the list.
 */

void screen_list_sensors(int no, int sub, int offset)
{
    int		i;

    if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) {
	for (i = 0; i < ds18b20_state->num_sensors; i++) {
	    strncpy(sensors[i], ds18b20_state->sensor[i].rom_code, 17);
	    sensors[i][16] = '\0';
	}
	xSemaphoreGive(xSemaphoreDS18B20);
    } else {
	ESP_LOGE(TAG, "screen_list_sensors() DS18B20 lock error");
    }

    screen_top("DS18B20 lijst");
    if (num_sensors == 0) {
        menu_line(0, 1, 25, "Geen sensors");
    } else {
        for (i = 0; i < num_sensors; i++) {
            menu_line(sub == i, 1, 25 + (i * 12), sensors[i + offset]);
            if (i == 3)
                break;
        }
        if ((i + offset) == num_sensors)
            menu_line(sub == i, 1, 25 + (i * 12), "Terug");
    }
    u8g2_SendBuffer(&u8g2);
}



/**
 * @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("Meter %d setup", no + 1);
    menu_line(sub == 0, 1, 25, "Werking %s", units[no].mode ? "Aan":"Uit");
    menu_line(sub == 1, 1, 37, "Nulpunt %d", units[no].pressure_zero);
    menu_line(sub == 2, 1, 49, "DS18B20 %s", units[no].temperature_rom_code);
    menu_line(sub == 3, 1, 61, "Terug");
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The WiFi overview screen.
 */
void screen_wifi()
{
    char	buf[65];
    int8_t	rssi = 0;
    bool	online = false;

    if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
    	snprintf(buf, 65, "SSID %s", wifi_state->STA_ssid);
	rssi = wifi_state->STA_rssi;
	online = wifi_state->STA_online;
	xSemaphoreGive(xSemaphoreWiFi);
    } else {
	ESP_LOGE(TAG, "screen_wifi() lock error");
    }

    screen_top("WiFi Status");
    u8g2_DrawStr(&u8g2, 1, 28, buf);
    snprintf(buf, 65, "RSSI %d", rssi);
    u8g2_DrawStr(&u8g2, 1, 43, buf);
    snprintf(buf, 65, "Verbonden %s", online ? "Ja":"Nee");
    u8g2_DrawStr(&u8g2, 1, 59, buf);
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The WiFi setup menu.
 * @param sub The submenu entry to hilite.
 */
/*void screen_wifi_setup(int sub)
{
    screen_top("WiFi AP setup");
    menu_line(sub == 0, 1, 25, "Lijst AP");
    menu_line(sub == 1, 1, 37, "Nieuw AP");
    menu_line(sub == 2, 1, 49, "Terug");
    u8g2_SendBuffer(&u8g2);
}*/



/**
 * @brief Show the list if WiFi Access Points.
 * @param sub The sub entry line.
 * @param offset The offset in the list.
 */
/*void screen_list_aps(int sub, int offset)
{
    int			i;
    wifiStation_t	ap;
    uint8_t		*dst = (uint8_t *)&ap;
    FILE		*f;
    size_t		bytes;

    num_ssids = 0;
    memset(dst, 0, sizeof(ap));
    f = fopen("/spiffs/stations.conf", "r");
    if (f) {
	while (1) {
	    bytes = fread(dst, 1, sizeof(ap), f);
	    if (bytes < sizeof(ap)) {
		fclose(f);
		break;
	    }
	    memcpy(APs[num_ssids].SSID, ap.SSID, 32);
	    memcpy(APs[num_ssids].Password, ap.Password, 64);
	    num_ssids++;
	}
    }

    screen_top("WiFi AP lijst");
    if (num_ssids == 0) {
	menu_line(0, 1, 25, "Geen AP's");
    } else {
	for (i = 0; i < num_ssids; i++) {
	    menu_line(sub == i, 1, 25 + (i * 12), APs[i + offset].SSID);
	    if (i == 3)
		break;
	}
	if ((i + offset) == num_ssids)
	    menu_line(sub == i, 1, 25 + (i * 12), "Terug");
    }
    u8g2_SendBuffer(&u8g2);
}*/



/**
 * @brief Edit WiFi AP menu.
 * @param sub The menu entry to hilite.
 */
/*void screen_edit_ap(int sub)
{
    screen_top("WiFi wijzig AP");
    menu_line(sub == 0, 1, 25, "SSID %s", editAP.SSID);
    menu_line(sub == 1, 1, 37, "PSK  %s", editAP.Password);
    menu_line(sub == 2, 1, 49, "Opslaan");
    if (edit_ssid >= 0) {
    	menu_line(sub == 3, 1, 61, "Verwijder");
    }
    u8g2_SendBuffer(&u8g2);
}*/



/**
 * @brief The network status screen.
 */
void screen_network()
{
    char	ip[17], nm[17], gw[17];

    if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
	strcpy(ip, wifi_state->STA_ip);
	strcpy(nm, wifi_state->STA_nm);
	strcpy(gw, wifi_state->STA_gw);
        xSemaphoreGive(xSemaphoreWiFi);
    } else {
        ESP_LOGE(TAG, "screen_network() lock error");
    }

    screen_top("Netwerk Status");
    menu_line(0, 1, 25, "IP   %s", ip);
    menu_line(0, 1, 37, "Mask %s", nm);
    menu_line(0, 1, 49, "GW   %s", gw);
    menu_line(0, 1, 61, "Naam %s", hostname);
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief MQTT status
 */
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);
}



/**
 * @brief The OTA update menu.
 */
void screen_update()
{
    screen_top("Update software");
    menu_line(0, 1, 43, "Druk voor update");
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The update status screen.
 * @param m1 The first message line or NULL.
 * @param m2 The second message line or NULL.
 */
void screen_updating(char *m1, char *m2)
{
    screen_top("Bijwerken ...");
    u8g2_SetFont(&u8g2, u8g2_font_unifont_t_symbols);
    if (m1) {
	u8g2_DrawUTF8(&u8g2,2,30, m1);
    }
    if (m2) {
	u8g2_DrawUTF8(&u8g2,2,55, m2);
    }
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The counters display screen.
 */
void screen_counters()
{
    char        buf[65];

    screen_top("Software fouten");
    snprintf(buf, 64, "Network  %4lu", err_connect);
    u8g2_DrawStr(&u8g2, 1, 28, buf);
    snprintf(buf, 64, "Watchdog %4lu", err_alarm);
    u8g2_DrawStr(&u8g2, 1, 43, buf);
    snprintf(buf, 64, "DS18B20  %4lu", err_temp);
    u8g2_DrawStr(&u8g2, 1, 59, buf);
    u8g2_SendBuffer(&u8g2);
}



/**
 * @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_LOGD(TAG, "GPIO rotary button intr, val: %d time: %d", gpio_get_level(io_num), PushDuration);
		    if (! user_busy()) {
			xEventGroupSetBits(xEventGroupUser, TASK_USER_WAKEUP);
		    }
		}
	    } else {
            	ESP_LOGE(TAG, "GPIO[%lu] 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 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.
 * @param curoffset The offset in the list. 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 max, int *cursub, int *curoffset)
{
    int		sub = *cursub;
    int		offset = *curoffset;
    bool	rc = false;

   if (pos > 0) {
	if ((sub + offset) < max) {
	    if (sub < 3) {
	    	sub++;
	    } else {
		offset++;
	    }
	} else {
	    sub = offset = 0;
	}
	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
	rc = true;
    } else if (pos < 0) {
	if ((sub + offset) > 0) {
	    if (offset > 0) {
		offset--;
	    } else {
		sub--;
	    }
	} else {
	    if (max > 3) {
		offset = max - 3;
		sub = 3;
	    } else {
	    	sub = max;
		offset = 0;
	    }
	}
	ESP_ERROR_CHECK(rotary_encoder_reset(&rinfo));
	rc = true;
    }

    *cursub = sub;
    *curoffset = offset;
    return rc;
}



/**
 * @brief Handle menu changes.
 */
void menu_change(void)
{
    if (New_Loop2 != Main_Loop2) {

	Main_Loop2 = New_Loop2;
        switch (Main_Loop2) {
	    case ML2_INIT:
                ESP_LOGI(TAG, "Loop user: Init");
		New_Loop2 = -1;
                if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
		    for (int i = 0; i < 3; i++) {
                        if (units[i].mode) {
			    if (New_Loop2 == -1) { // Not selected yet
				New_Loop2 = i;
			    } else if (New_Loop2 >= 0) { // One selected
				New_Loop2 = -2;
				break; // Multiple units are active
			    }
			}
		    }
                    xSemaphoreGive(xSemaphoreUnits);
                } else {
                    ESP_LOGE(TAG, "menu_change() ML2_INIT lock");
                }
		if (New_Loop2 < 0)
                    New_Loop2 = ML2_USER;
		else
		    New_Loop2 += ML2_UNIT1;
                break;

            case ML2_USER:
                screen_main();
                break;

            case ML2_UNIT1:
            case ML2_UNIT2:
            case ML2_UNIT3:
                screen_unit(Main_Loop2 - ML2_UNIT1);
                break;

            case ML2_WIFI:
                screen_wifi();
                break;

            case ML2_NETWORK:
                screen_network();
                break;

            case ML2_MQTT:
                screen_mqtt();
                break;

            case ML2_UPDATE:
                screen_update();
                break;

	    case ML2_COUNTERS:
		screen_counters();
		break;

            case ML2_SETUP_UNIT1:
            case ML2_SETUP_UNIT2:
            case ML2_SETUP_UNIT3:
                SubMenu = SubOffset = 0;
                screen_unit_setup(Main_Loop2 - ML2_SETUP_UNIT1, SubMenu);
                break;

            case ML2_ZERO_UNIT1:
            case ML2_ZERO_UNIT2:
            case ML2_ZERO_UNIT3:
                SubMenu = SubOffset = 0;
                screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, SubMenu);
                break;

	    case ML2_SEL_SENSOR1:
	    case ML2_SEL_SENSOR2:
	    case ML2_SEL_SENSOR3:
		SubMenu = SubOffset = 0;
		screen_list_sensors(Main_Loop2 - ML2_SEL_SENSOR1, SubMenu, SubOffset);
		break;

            case ML2_INACTIVE:
		ESP_LOGI(TAG, "Loop user: Inactive");
		u8g2_SetPowerSave(&u8g2, 1); // powersave display
		New_Loop2 = ML2_DONE;
		break;

            default:
		break;
	}
    }
}



/**
 * @brief Handle rotary switch for menu navigation.
 */
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_COUNTERS, ML2_MQTT); break;
	case ML2_COUNTERS:	rotate_to_menu(event.state.position, ML2_COUNTERS, ML2_UPDATE); break;
	case ML2_SETUP_UNIT1:
	case ML2_SETUP_UNIT2:
	case ML2_SETUP_UNIT3:   if (rotate_to_sub(event.state.position, 3, &SubMenu, &SubOffset))
				    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, 1, &SubMenu, &SubOffset))
				    screen_unit_zero(Main_Loop2 - ML2_ZERO_UNIT1, SubMenu);
				break;
	case ML2_SEL_SENSOR1:
	case ML2_SEL_SENSOR2:
	case ML2_SEL_SENSOR3:	if (rotate_to_sub(event.state.position, num_sensors, &SubMenu, &SubOffset))
                                    screen_list_sensors(Main_Loop2 - ML2_SEL_SENSOR1, SubMenu, SubOffset);
				break;
	default:
				ESP_LOGI(TAG, "Event: position %ld, direction %s", event.state.position,
				    event.state.direction ? (event.state.direction == ROTARY_ENCODER_DIRECTION_CLOCKWISE ? "CW":"CCW"):"NOT_SET");
    }
}



/*
 * Refresh screens that are in focus. Called by the main measurement loop.
 */
void user_refresh(void)
{
    if (UserTimer)
	xEventGroupSetBits(xEventGroupUser, TASK_USER_REFRESH);
}



/**
 * @brief Pressed keys actions
 */
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);
		    } else {
			ESP_LOGE(TAG, "menu_loop() ML2_SETUP_UNIT%d units lock", idx + 1);
		    }
		    screen_unit_setup(idx, SubMenu);
		    if (Main_Loop1 == ML1_DONE)
			Main_Loop1 = ML1_INIT;
		} else if (SubMenu == 1) {
		    New_Loop2 = ML2_ZERO_UNIT1 + idx;
		} else if (SubMenu == 2) {
		    New_Loop2 = ML2_SEL_SENSOR1 + idx;
		} else if (SubMenu == 3) {
		    New_Loop2 = ML2_UNIT1 + 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) {
			if (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();
			    ESP_LOGI(TAG, "Zero set unit %d, %lu mV set %lu", idx, adc_state->Pressure[idx].voltage, units[idx].pressure_zero);
		    	}
			xSemaphoreGive(xSemaphoreADC);
			xSemaphoreGive(xSemaphoreUnits);
			screen_unit_zero(idx, SubMenu);
			if (Main_Loop1 == ML1_DONE)
			    Main_Loop1 = ML1_INIT;
		    }
		} else if (SubMenu == 1) {
		    New_Loop2 = ML2_SETUP_UNIT1 + idx;
		    SubMenu = 1;
		}
		break;

	case ML2_SEL_SENSOR1:
	case ML2_SEL_SENSOR2:
	case ML2_SEL_SENSOR3:
		idx = Main_Loop2 - ML2_SEL_SENSOR1;
		if ((SubMenu + SubOffset) < num_sensors) {
		    ESP_LOGI(TAG, "Select sensor %d %s for unit %d", SubMenu + SubOffset, sensors[SubMenu + SubOffset], idx + 1);
		    if (xSemaphoreTake(xSemaphoreUnits, 25) == pdTRUE) {
			strcpy(units[idx].temperature_rom_code, sensors[SubMenu + SubOffset]);
                        write_units();
                        xSemaphoreGive(xSemaphoreUnits);
			New_Loop2 = ML2_SETUP_UNIT1 + idx;
                    	SubMenu = 2;
                    	SubOffset = 0;
			if (Main_Loop1 == ML1_DONE)
                            Main_Loop1 = ML1_INIT;
                    } else {
			ESP_LOGE(TAG, "Failed units lock for new romcode");
		    }
		} else {
		    New_Loop2 = ML2_SETUP_UNIT1 + idx;
		    SubMenu = 2;
		    SubOffset = 0;
		}
                break;

	case ML2_WIFI:
		break;

	case ML2_NETWORK:
		break;

	case ML2_MQTT:
		break;

	case ML2_UPDATE:
		bin_update();
		screen_update();
		break;

	default:
		break;
    }
}



void task_user(void *pvParameter)
{
    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_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));

    /*
     * Create a one second periodic timer.
     */
    esp_timer_create_args_t timerSecond = {
        .callback = &TimerCallback,
        .name = "SecondsTimer"
    };
    ESP_ERROR_CHECK(esp_timer_create(&timerSecond, &timerHandle));
    ESP_ERROR_CHECK(esp_timer_start_periodic(timerHandle, 1000000));

    EventBits_t uxBits;

    /*
     * 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);
	    UserTimer = INACTIVITY;
	    New_Loop2 = ML2_INIT;
	    Main_Loop2 = -1;
	    SubMenu = 0;

	    while (UserTimer) {

		menu_change();

		if (xEventGroupGetBits(xEventGroupUser) & TASK_USER_REFRESH) {
		    xEventGroupClearBits(xEventGroupUser, TASK_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;
        		case ML2_WIFI:  	screen_wifi();     break;
			case ML2_COUNTERS:	screen_counters(); break;
    		    }
		}

		if (xQueueReceive(event_queue, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) {
		    UserTimer = INACTIVITY;
		    menu_rotary();
		}

		if (PushDuration) {
		    PushDuration = 0;
		    menu_loop();
		}
	    }

	    xEventGroupClearBits(xEventGroupUser, TASK_USER_WAKEUP);
	}
    }
}

mercurial