main/task_user.c

Sun, 14 Jun 2020 14:53:06 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sun, 14 Jun 2020 14:53:06 +0200
changeset 57
232f318a6b51
parent 56
8c88a3d8ecf2
child 60
07a1a07fdc8c
permissions
-rw-r--r--

Code cleanup, doxygen comments added.

/**
 * @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
static xQueueHandle                     gpio_evt_queue = NULL;          ///< Rotary pushbutton queue
static int				PushDuration = 0;		///< Duration of the pushed button
struct strStations			APs[10];			///< List of APs we know
int					edit_ssid = 0;			///< SSID being edited
int					num_ssids = 0;			///< Number of SSIDs we know
struct strStations			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

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;
    struct strStations	ap;
    uint8_t		*dst = (uint8_t *)&ap;
    FILE		*f;
    size_t		bytes;

    num_ssids = 0;
    memset(dst, 0, sizeof(ap));
    f = fopen("/spiffs/etc/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", config.hostname);
    u8g2_SendBuffer(&u8g2);
}



/**
 * @brief The network setup menu.
 * @param sub The menu entry to hilite.
 */
void screen_network_setup(int sub)
{
    screen_top("Netwerk setup");
    menu_line(sub == 0, 1, 25, "Naam %s", config.hostname);
    menu_line(sub == 1, 1, 37, "NTP  %s", config.ntp_server);
    menu_line(sub == 2, 1, 49, "Terug");
    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 MQTT setup menu.
 * @param sub The submenu entry to hilite.
 */
void screen_mqtt_setup(int sub)
{
    screen_top("MQTT Setup");
    menu_line(sub == 0, 1, 25, "serv %s", config.mqtt_server);
    menu_line(sub == 1, 1, 37, "port %d", config.mqtt_port);
    menu_line(sub == 2, 1, 49, "user %s", config.mqtt_user);
    menu_line(sub == 3, 1, 61, "Terug");
    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  %4d", err_connect);
    u8g2_DrawStr(&u8g2, 1, 28, buf);
    snprintf(buf, 64, "Watchdog %4d", err_alarm);
    u8g2_DrawStr(&u8g2, 1, 43, buf);
    snprintf(buf, 64, "DS18B20  %4d", 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_LOGI(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[%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 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_SETUP_WIFI:
		ESP_LOGI(TAG, "Loop user: WiFi setup");
		screen_wifi_setup(SubMenu);
		break;

	    case ML2_LIST_APS:
		ESP_LOGI(TAG, "Loop user: WiFi list APs");
		SubMenu = SubOffset = 0;
		screen_list_aps(SubMenu, SubOffset);
		break;

	    case ML2_EDIT_AP:
		ESP_LOGI(TAG, "Loop user: WiFi edit AP");
		screen_edit_ap(0);
		break;

            case ML2_NETWORK:
                screen_network();
                break;

	    case ML2_SETUP_NETWORK:
		ESP_LOGI(TAG, "Loop user: Network setup");
		screen_network_setup(SubMenu);
		break;

            case ML2_MQTT:
                screen_mqtt();
                break;

            case ML2_SETUP_MQTT:
                ESP_LOGI(TAG, "Loop user: MQTT setup");
		screen_mqtt_setup(SubMenu);
		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:
                ESP_LOGI(TAG, "Loop user: Setup Unit %d", Main_Loop2 - ML2_SETUP_UNIT1);
                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:
                ESP_LOGI(TAG, "Loop user: Zero Unit %d", Main_Loop2 - ML2_ZERO_UNIT1);
                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:
		ESP_LOGI(TAG, "Loop user: Select sensor %d", Main_Loop2 - ML2_SEL_SENSOR1);
		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;
	case ML2_SETUP_WIFI:	if (rotate_to_sub(event.state.position, 2, &SubMenu, &SubOffset))
				    screen_wifi_setup(SubMenu);
				break;
	case ML2_LIST_APS:	if (rotate_to_sub(event.state.position, num_ssids, &SubMenu, &SubOffset))
				    screen_list_aps(SubMenu, SubOffset);
				break;
	case ML2_EDIT_AP:	if (edit_ssid < 0) {
				    if (rotate_to_sub(event.state.position, 2, &SubMenu, &SubOffset))
                                    	screen_edit_ap(SubMenu);
				} else {
				    if (rotate_to_sub(event.state.position, 3, &SubMenu, &SubOffset))
				    	screen_edit_ap(SubMenu);
				}
				break;
	case ML2_SETUP_MQTT:	if (rotate_to_sub(event.state.position, 3, &SubMenu, &SubOffset))
				    screen_mqtt_setup(SubMenu);
				break;
	case ML2_SETUP_NETWORK:	if (rotate_to_sub(event.state.position, 2, &SubMenu, &SubOffset))
				    screen_network_setup(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");
    }
}



/*
 * 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;
    char	txt[32];

    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, %d mV set %d", 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:
		New_Loop2 = ML2_SETUP_WIFI;
		break;

	case ML2_SETUP_WIFI:
		if (SubMenu == 0) {
		    New_Loop2 = ML2_LIST_APS;
		} else if (SubMenu == 1) {
		    edit_ssid = -1;
		    editAP.SSID[0] = '\0';
		    editAP.Password[0] = '\0';
		    SubMenu = SubOffset = 0;
                    New_Loop2 = ML2_EDIT_AP;
		} else if (SubMenu == 2) {
		    New_Loop2 = ML2_WIFI;
		}
		break;

	case ML2_LIST_APS:
		if ((SubMenu + SubOffset) < num_ssids) {
		    edit_ssid = SubMenu + SubOffset;
		    memcpy(editAP.SSID, APs[edit_ssid].SSID, 32);
                    memcpy(editAP.Password, APs[edit_ssid].Password, 64);
		    SubMenu = SubOffset = 0;
		    New_Loop2 = ML2_EDIT_AP;
		} else {
		    New_Loop2 = ML2_SETUP_WIFI;
		    SubMenu = SubOffset = 0;
		}
		break;

	case ML2_EDIT_AP:
		if (SubMenu == 0) {
		    rotary_editer("SSID", editAP.SSID, "", 16, EDIT_TYPE_TEXT);
		    screen_edit_ap(SubMenu);
		} else if (SubMenu == 1) {
		    rotary_editer("PSK", editAP.Password, "", 16, EDIT_TYPE_TEXT);
                    screen_edit_ap(SubMenu);
		} else if (SubMenu == 2 || SubMenu == 3) {
		    update_running = 1; // Block measurements
		    int timeout = 600;
		    if (edit_ssid < 0)
                        New_Loop2 = ML2_SETUP_WIFI;
                    else
                        New_Loop2 = ML2_LIST_APS;

    		    for (;;) {
        		vTaskDelay(100 / portTICK_PERIOD_MS);
        		if (Main_Loop1 == ML1_DONE)
        		    break;
        		if (timeout)
        		    timeout--;
        		else {
        		    ESP_LOGE(TAG, "Timout request stop");
        		    goto saveerr;
        		}
    		    }
		    if (edit_ssid >= 0) {
			ESP_LOGI(TAG, "Remove %s", APs[edit_ssid].SSID);
                    	remove_station((uint8_t *)APs[edit_ssid].SSID);
		    }
		    if (SubMenu == 2) {
		    	ESP_LOGI(TAG, "Add %s %s", editAP.SSID, editAP.Password);
		    	add_station((uint8_t *)editAP.SSID, (uint8_t *)editAP.Password);
		    }
		}
saveerr:
		update_running = 0;
		break;

	case ML2_NETWORK:
		New_Loop2 = ML2_SETUP_NETWORK;
		break;

	case ML2_SETUP_NETWORK:
		if (SubMenu == 0) {
                    rotary_editer("Hostnaam", config.hostname, "", 16, EDIT_TYPE_TEXT);
                    screen_network_setup(SubMenu);
                } else if (SubMenu == 1) {
                    rotary_editer("NTP server", config.ntp_server, "", 16, EDIT_TYPE_TEXT);
                    screen_network_setup(SubMenu);
                } else if (SubMenu == 2) {
                    ESP_LOGI(TAG, "Hostname `%s`", config.hostname);
                    ESP_LOGI(TAG, "NTP server `%s`", config.ntp_server);
                    write_config();
                    New_Loop2 = ML2_NETWORK;
                }
                break;

	case ML2_MQTT:
		New_Loop2 = ML2_SETUP_MQTT;
		break;

	case ML2_SETUP_MQTT:
		if (SubMenu == 0) {
		    rotary_editer("MQTT server", config.mqtt_server, "", 16, EDIT_TYPE_TEXT);
		    screen_mqtt_setup(SubMenu);
		} else if (SubMenu == 1) {
		    sprintf(txt, "%d", config.mqtt_port);
		    rotary_editer("MQTT server poort", txt, "", 6, EDIT_TYPE_INT);
		    config.mqtt_port = atoi(txt);
		    screen_mqtt_setup(SubMenu);
		} else if (SubMenu == 2) {
		    rotary_editer("MQTT user", config.mqtt_user, "", 16, EDIT_TYPE_TEXT);
                    rotary_editer("MQTT password", config.mqtt_pwd, "", 16, EDIT_TYPE_TEXT);
		    screen_mqtt_setup(SubMenu);
		} else if (SubMenu == 3) {
		    ESP_LOGI(TAG, "mqtt_server %s:%d", config.mqtt_server, config.mqtt_port);
		    ESP_LOGI(TAG, "mqtt_user/pass `%s/%s`", config.mqtt_user, config.mqtt_pwd);
		    write_config();
                    New_Loop2 = 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_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));

    /*
     * 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