/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * File Name          : app_freertos.c
  * Description        : Code for freertos applications
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "BME280_STM32.h"
#include "pid.h"
#include "icons.h"
#include "ssd1306.h"
#include "string.h"
#include "stdio.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

typedef struct {
  char text[6];
  uint8_t drytemp;
  uint32_t drytime;
} MenuItem;

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
float aimTemperature, Temperature, Pressure, aimHumidity, Humidity;	//BME280 results and aim values
uint8_t powerFan = 0, powerHeater = 0;																	//Set fan/heater power 0 - 100

//PID settings
PIDController pid = {
		.Kp  = 5.0f,
		.Ki  = 2.0f,
		.Kd  = 1.0f,
		.T   = 0.25f,			//4 times per second
		.tau = 0.2f,			//Low-pass filter (0 - no filter)
		.limMin = 0.0f,
		.limMax = 100.0f
};

MenuItem menuProg[] = {
//  text    temp  time in seconds
  {"PLA",   45,   4*3600},
  {"ABS",   60,   2*3600},
  {"PETG",  65,   2*3600},
  {"TPU",   50,   4*3600},
  {"NYLON", 70,   8*3600},
  {"PVA",   45,   4*3600},
  {"ASA",   60,   4*3600},
  {"PP",    55,   6*3600},
  {"SILIC", 65,   3*3600},
  {"TEST",  27,   30}
};

uint8_t menuMax = 9;			// Max menu index
uint8_t prog = 0;					// Selected program
uint8_t menuFrame = 0;		// Menu list frame index
uint8_t mode = 0;					// Current mode: 0 - Idle menu; 1 - Drying; 2 - Drying stop question; 3 - Keeping;
													// 4 - Keeping stop question; 98 - Thermal runout; 99 - sensor error;
uint8_t qConfirm = 0;    	// Question menu (Yes/No) selected answer
uint32_t time_cnt = 0;		// Time counter for Drying / Keeping action

/* USER CODE END Variables */
/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
  .name = "defaultTask",
  .priority = (osPriority_t) osPriorityNormal,
  .stack_size = 128 * 4
};
/* Definitions for svcDisplay */
osThreadId_t svcDisplayHandle;
const osThreadAttr_t svcDisplay_attributes = {
  .name = "svcDisplay",
  .priority = (osPriority_t) osPriorityLow,
  .stack_size = 128 * 4
};
/* Definitions for svcSensors */
osThreadId_t svcSensorsHandle;
const osThreadAttr_t svcSensors_attributes = {
  .name = "svcSensors",
  .priority = (osPriority_t) osPriorityLow,
  .stack_size = 128 * 4
};
/* Definitions for svcKeys */
osThreadId_t svcKeysHandle;
const osThreadAttr_t svcKeys_attributes = {
  .name = "svcKeys",
  .priority = (osPriority_t) osPriorityLow,
  .stack_size = 128 * 4
};
/* Definitions for qKeysPressed */
osMessageQueueId_t qKeysPressedHandle;
const osMessageQueueAttr_t qKeysPressed_attributes = {
  .name = "qKeysPressed"
};
/* Definitions for qDisplay */
osMessageQueueId_t qDisplayHandle;
const osMessageQueueAttr_t qDisplay_attributes = {
  .name = "qDisplay"
};
/* Definitions for timeCounter */
osTimerId_t timeCounterHandle;
const osTimerAttr_t timeCounter_attributes = {
  .name = "timeCounter"
};

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void update_ui(void);					//update display main function
void draw_info_frame(void);		//draw frame and pics for info frame
void draw_info(void);					//update drying/keeping info
void draw_time_info(void);		//update drying/keeping timers
void draw_header(void);				//update display header lines
void draw_menu(void);					//Draw program select menu
void draw_question(void);			//Draw STOP? question
void draw_error(void);				//Draw sensor error message

void setPWMs(void);						//set Timer values according to requested settings
/* USER CODE END FunctionPrototypes */

void StartDefaultTask(void *argument);
void svc_display(void *argument);
void svc_sensors(void *argument);
void svc_keys(void *argument);
void clock_tick(void *argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* USER CODE BEGIN RTOS_MUTEX */
  /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  /* USER CODE END RTOS_SEMAPHORES */

  /* Create the timer(s) */
  /* creation of timeCounter */
  timeCounterHandle = osTimerNew(clock_tick, osTimerPeriodic, NULL, &timeCounter_attributes);

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* Create the queue(s) */
  /* creation of qKeysPressed */
  qKeysPressedHandle = osMessageQueueNew (16, sizeof(uint8_t), &qKeysPressed_attributes);

  /* creation of qDisplay */
  qDisplayHandle = osMessageQueueNew (16, sizeof(uint8_t), &qDisplay_attributes);

  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues, ... */
  /* USER CODE END RTOS_QUEUES */

  /* Create the thread(s) */
  /* creation of defaultTask */
  defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

  /* creation of svcDisplay */
  svcDisplayHandle = osThreadNew(svc_display, NULL, &svcDisplay_attributes);

  /* creation of svcSensors */
  svcSensorsHandle = osThreadNew(svc_sensors, NULL, &svcSensors_attributes);

  /* creation of svcKeys */
  svcKeysHandle = osThreadNew(svc_keys, NULL, &svcKeys_attributes);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

  /* USER CODE BEGIN RTOS_EVENTS */
  /* add events, ... */
  /* USER CODE END RTOS_EVENTS */

}

/* USER CODE BEGIN Header_StartDefaultTask */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */

	osTimerStart(timeCounterHandle, 1000);

  /* Infinite loop */
	uint8_t key;			//pressed key id
  for(;;)
  {
  	//Check if there is a key pressed
  	if (osMessageQueueGetCount(qKeysPressedHandle) > 0) {
  		osMessageQueueGet(qKeysPressedHandle, &key, NULL, 50);

  		switch (key) {
				case 0:				// Down button
					switch (mode) {
						case 0:
							if (prog < menuMax) prog++;
							if (prog > menuFrame + 2) menuFrame = prog - 2;
							break;
						case 2:
						case 4:
							qConfirm = 0;
							break;
					}
					break;

				case 1:				// OK button
					switch (mode) {
						case 0:																			//Program has been chosen, lets start heating
							time_cnt = menuProg[prog].drytime;
							aimTemperature = menuProg[prog].drytemp;
							pid.integrator = 0;
							powerFan = 100;
							mode = 1;
							break;
						case 1:																			//Ask for confirmation to stop drying
							mode = 2;
							qConfirm = 0;
							break;
						case 3:																			//Ask for confirmation to stop keeping
							mode = 4;
							qConfirm = 0;
							break;
						case 2:																			//resume heating or stop and go to idle mode
							if (qConfirm) {
								aimTemperature = 0;
								mode = 0;
							} else {
								mode = 1;
							}
							break;
						case 4:																			//resume keeping or stop and go to idle mode
							if (qConfirm) {
								aimTemperature = 0;
								aimHumidity = 0;
								mode = 0;
							} else {
								mode = 3;
							}
							break;
					}
					break;

				case 2:				// Up button
					switch (mode) {
						case 0:
							if (prog > 0) prog--;
							if (prog < menuFrame) menuFrame = prog;
							break;
						case 2:
						case 4:
							qConfirm = 1;
							break;
					}
					break;
			} //switch key
  		update_ui();
  	} //key queue check

    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

/* USER CODE BEGIN Header_svc_display */
/**
* @brief Function implementing the svcDisplay thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_svc_display */
void svc_display(void *argument)
{
  /* USER CODE BEGIN svc_display */

  /* Infinite loop */
  for(;;)
  {

		if (mode == 1 || mode == 3) {
			draw_info();
		}

    osDelay(500);
  }
  /* USER CODE END svc_display */
}

/* USER CODE BEGIN Header_svc_sensors */
/**
* @brief Function implementing the svcSensors thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_svc_sensors */
void svc_sensors(void *argument)
{
  /* USER CODE BEGIN svc_sensors */

	uint16_t tro = 0;			//counter for thermal runout protection

	aimTemperature = Temperature = Pressure = aimHumidity = Humidity = 0.0f;
	PID_Init(&pid);

  /* Infinite loop */
  for(;;)
  {
  	if (mode < 98) {
			BME280_Measure();
			if (Temperature == 0.0f && Humidity == 0.0f) {
				//Some sensor malfunction
				mode = 99;
				aimTemperature = 0;
				aimHumidity = 0;
				powerFan = 100;
				powerHeater = 0;
				update_ui();
			} else {
				switch (mode) { 			//update fan/heater settings here.
					case 0:
						if (powerHeater > 0) powerHeater = 0;
						if (Temperature >= 35) powerFan = 100;
							else powerFan = 0;
						break;
					case 1:
					case 2:
						powerHeater = PID_Update(&pid, aimTemperature, Temperature);
						if (powerHeater > 0) powerFan = 100;
							else powerFan = 85;
						break;
					case 3:
					case 4:
						if (Humidity > 12) powerHeater = PID_Update(&pid, aimTemperature, Temperature);
							else powerHeater = 0;
						break;
				} //switch (mode)
			} //else

			//Simple thermal runout protection
			if (Temperature > aimTemperature && Temperature > 45) tro++;
				else if (tro != 0) tro = 0;
			if (tro > 480) {					//480 - 2 minutes (120 sec * 4 as this task runs 4 times per second)
				mode = 98;
				aimTemperature = 0;
				aimHumidity = 0;
				powerFan = 100;
				powerHeater = 0;
				update_ui();
			}
  	} //if (mode < 98)

  	setPWMs();

    osDelay(250);
  }
  /* USER CODE END svc_sensors */
}

/* USER CODE BEGIN Header_svc_keys */
/**
* @brief Function implementing the svcKeys thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_svc_keys */
void svc_keys(void *argument)
{
  /* USER CODE BEGIN svc_keys */
	uint8_t bBuff[3] = {0xFF}; // buttons are grounded when pressed
	uint8_t bState[3] = {0xFF};	 //last reported state. We need this to prevent repeated reports
	uint8_t read, i;

  /* Infinite loop */
  for(;;)
  {
  	read = KEY_DN_GPIO_Port->IDR & 0x00000007;	//Read 3 bits of buttons port (PB0, PB1, PB2)
  	// check every button and shift its values to the array
  	for (i=0; i<3; i++) {
  		bBuff[i] = (bBuff[i]<<1) | ((read >> i) & 1);								//Shift button buffer left and assign a button bit from the read data
  		if ( ((bBuff[i] & 0x0F) == 0b00000000) && (bState[i] == 1) ) {	//if we read 0 last 4 times and last report was 1 then the button has been pressed
  			bState[i] = 0;
  			osMessageQueuePut(qKeysPressedHandle, &i, 0, 50);					//Put the pressed button id to the key queue
  		}
  		if ( ((bBuff[i] & 0x0F) == 0b00001111) && (bState[i] == 0) ) {	//if we read 1 last 4 times and last report was 0 then the button has been released
  			bState[i] = 1;
  		}
  	}

    osDelay(4);
  }
  /* USER CODE END svc_keys */
}

/* clock_tick function */
void clock_tick(void *argument)
{
  /* USER CODE BEGIN clock_tick */

	//time counters here. Happens every second
	switch (mode) {
		case 1:
		case 2:
			time_cnt--;
			if (time_cnt == 0) {
				aimTemperature = 35;
				pid.integrator = 0;
				powerFan = 85;
				mode = 3;
				update_ui();
			}
			break;
		case 3:
		case 4:
			time_cnt++;
			break;
	}
	draw_time_info();


  /* USER CODE END clock_tick */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */

//update display main function
void update_ui(void) {
	ssd1306_Fill(Black);
	draw_header();
	switch (mode) {
		case 0:
			draw_menu();
			break;
		case 1:
		case 3:
			draw_info_frame();
			break;
		case 2:
		case 4:
			draw_question();
			break;
		case 98:
		case 99:
			draw_error();
			break;
	} //switch (mode)
	ssd1306_UpdateScreen();
}

//draw frame and pics for info frame
void draw_info_frame(void) {
	ssd1306_DrawBitmap(4, 16, humidity16, 16, 16, White);
	ssd1306_DrawBitmap(0, 32, temperature24, 24, 24, White);
	ssd1306_DrawBitmap(26, 54, heat_icon, 18, 8, White);
	ssd1306_DrawBitmap(80, 54, fan_icon, 16, 8, White);
}

//update drying/keeping info
void draw_info(void) {
	char num[8];

	//Humidity
	ssd1306_DrawRectangle(26, 16, 61, 26, Black, 1);
	sprintf(num, "%.1f%%", Humidity);
	ssd1306_SetCursor(26, 16);
	ssd1306_WriteString(num, Font_7x10, White);

	//Temperature
	ssd1306_DrawRectangle(26, 32, 122, 58, Black, 1);
	sprintf(num, "%.1f*C", Temperature);
	ssd1306_SetCursor(26, 32);
	ssd1306_WriteString(num, Font_16x26, White);

	//PWM Heater
	ssd1306_DrawRectangle(47, 55, 65, 63, Black, 1);
	sprintf(num, "%u", powerHeater);
	ssd1306_SetCursor(47, 55);
	ssd1306_WriteString(num, Font_6x8, White);
	//PWM Fan
	ssd1306_DrawRectangle(98, 55, 116, 63, Black, 1);
	sprintf(num, "%u", powerFan);
	ssd1306_SetCursor(98, 55);
	ssd1306_WriteString(num, Font_6x8, White);

	ssd1306_UpdateScreen();
}

//update drying/keeping timers
void draw_time_info(void) {
	if (mode < 98) {
		ssd1306_SetCursor(78, 8);
		if (mode > 0) {
			uint8_t hrs, mins, secs;
			uint32_t rem;
			hrs = time_cnt / 3600;
			rem = time_cnt % 3600;
			mins = rem / 60;
			secs = rem % 60;
			char ts[9];
			sprintf(ts, "%02d:%02d:%02d", hrs, mins, secs);
			ssd1306_DrawRectangle(78, 8, 128, 16, Black, 1);
			ssd1306_WriteString(ts, Font_6x8, White);
		} else {
			ssd1306_WriteString("00:00:00", Font_6x8, White);
		}
		ssd1306_UpdateScreen();
	}// if mode < 98
}

//update display header lines
void draw_header(void) {
	char header[6], action[8];

	switch (mode) {
		case 0:
			strcpy(header, "MENU");
			strcpy(action, "Idle");
			break;
		case 1:
			strcpy(header, menuProg[prog].text);
			strcpy(action, "Drying");
			break;
		case 2:
			strcpy(header, "STOP?");
			strcpy(action, "Drying");
			break;
		case 3:
			strcpy(header, "DONE!");
			strcpy(action, "Keeping");
			break;
		case 4:
			strcpy(header, "STOP?");
			strcpy(action, "Keeping");
			break;
		case 98:
		case 99:
			strcpy(header, "ERROR");
			break;
	} //Switch (mode)
	ssd1306_SetCursor(0, 0);
	ssd1306_WriteString(header, Font_7x10, White);

	if (mode < 98) {
		ssd1306_SetCursor(78, 0);
		ssd1306_WriteString(action, Font_6x8, White);
	}// if mode < 98

}

//Draw program select menu
void draw_menu(void) {
	char str[7];
	for (uint8_t i=0; i<3; i++) {
		if (prog == i + menuFrame) {
			ssd1306_Line(0, i*15 + 16, 127, i*15 + 16, White);
			ssd1306_Line(0, i*15 + 31, 127, i*15 + 31, White);
		}
		ssd1306_SetCursor(0, i*15 + 17);
		ssd1306_WriteString(menuProg[i + menuFrame].text, Font_7x10, White);
		ssd1306_SetCursor(78, i*15 + 16);
		sprintf(str, "%u *c", menuProg[i + menuFrame].drytemp);
		ssd1306_WriteString(str, Font_6x8, White);
		ssd1306_SetCursor(78, i*15 + 24);
		sprintf(str, "%luh%lum", menuProg[i + menuFrame].drytime/3600, menuProg[i + menuFrame].drytime%3600/60);
		ssd1306_WriteString(str, Font_6x8, White);
	} //for
}

//Draw STOP? question
void draw_question(void) {
	ssd1306_DrawRoundRectangle(4, 20, 123, 60, White);
	ssd1306_SetCursor(20, 33);
	ssd1306_WriteString("YES  NO", Font_7x10, White);
	if (qConfirm) {
		ssd1306_DrawRoundRectangle(17, 30, 56, 49, White);
	} else {
		ssd1306_DrawRoundRectangle(77, 30, 104, 49, White);
	}
}

//Draw sensor error message
void draw_error(void) {
	char str1[8], str2[7];

	if (mode == 99) {
		strcpy(str1, "SENSOR");
		strcpy(str2, "FAIL");
	}
	if (mode == 98) {
		strcpy(str1, "THERMAL");
		strcpy(str2, "RUNOUT");
	}
	ssd1306_DrawBitmap(111, 0, error_icon, 14, 16, White);
	ssd1306_SetCursor(26, 24);
	ssd1306_WriteString(str1, Font_7x10, White);
	ssd1306_SetCursor(38, 40);
	ssd1306_WriteString(str2, Font_7x10, White);
}

//set Timer values according to requested settings
void setPWMs(void) {
	TIM3->CCR1 = powerHeater;
	TIM3->CCR2 = powerFan;
	if (powerHeater < powerFan) {
		TIM3->ARR = powerHeater - 10;
	} else {
		TIM3->ARR = powerFan - 10;
	}

}

/* USER CODE END Application */

