Initial checkin brewboard

Sat, 20 Oct 2018 13:23:15 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 20 Oct 2018 13:23:15 +0200
changeset 0
b74b0e4902c3
child 1
ad2c8b13eb88

Initial checkin brewboard

.hgignore file | annotate | diff | comparison | revisions
Makefile file | annotate | diff | comparison | revisions
README.md file | annotate | diff | comparison | revisions
components/PID/PID_v1.c file | annotate | diff | comparison | revisions
components/PID/PID_v1.h file | annotate | diff | comparison | revisions
components/PID/README.md file | annotate | diff | comparison | revisions
components/PID/component.mk file | annotate | diff | comparison | revisions
components/esp32-ds18b20/LICENSE file | annotate | diff | comparison | revisions
components/esp32-ds18b20/README.md file | annotate | diff | comparison | revisions
components/esp32-ds18b20/component.mk file | annotate | diff | comparison | revisions
components/esp32-ds18b20/doc/.gitignore file | annotate | diff | comparison | revisions
components/esp32-ds18b20/doc/Doxyfile file | annotate | diff | comparison | revisions
components/esp32-ds18b20/ds18b20.c file | annotate | diff | comparison | revisions
components/esp32-ds18b20/include/ds18b20.h file | annotate | diff | comparison | revisions
components/esp32-owb/LICENSE file | annotate | diff | comparison | revisions
components/esp32-owb/README.md file | annotate | diff | comparison | revisions
components/esp32-owb/component.mk file | annotate | diff | comparison | revisions
components/esp32-owb/doc/.gitignore file | annotate | diff | comparison | revisions
components/esp32-owb/doc/Doxyfile file | annotate | diff | comparison | revisions
components/esp32-owb/include/owb.h file | annotate | diff | comparison | revisions
components/esp32-owb/include/owb_gpio.h file | annotate | diff | comparison | revisions
components/esp32-owb/include/owb_rmt.h file | annotate | diff | comparison | revisions
components/esp32-owb/owb.c file | annotate | diff | comparison | revisions
components/esp32-owb/owb_gpio.c file | annotate | diff | comparison | revisions
components/esp32-owb/owb_rmt.c file | annotate | diff | comparison | revisions
components/spidriver/component.mk file | annotate | diff | comparison | revisions
components/spidriver/spi_master_lobo.c file | annotate | diff | comparison | revisions
components/spidriver/spi_master_lobo.h file | annotate | diff | comparison | revisions
components/tft/DefaultFont.c file | annotate | diff | comparison | revisions
components/tft/DejaVuSans18.c file | annotate | diff | comparison | revisions
components/tft/DejaVuSans24.c file | annotate | diff | comparison | revisions
components/tft/SmallFont.c file | annotate | diff | comparison | revisions
components/tft/Ubuntu16.c file | annotate | diff | comparison | revisions
components/tft/comic24.c file | annotate | diff | comparison | revisions
components/tft/component.mk file | annotate | diff | comparison | revisions
components/tft/def_small.c file | annotate | diff | comparison | revisions
components/tft/minya24.c file | annotate | diff | comparison | revisions
components/tft/stmpe610.h file | annotate | diff | comparison | revisions
components/tft/tft.c file | annotate | diff | comparison | revisions
components/tft/tft.h file | annotate | diff | comparison | revisions
components/tft/tftspi.c file | annotate | diff | comparison | revisions
components/tft/tftspi.h file | annotate | diff | comparison | revisions
components/tft/tooney32.c file | annotate | diff | comparison | revisions
components/vnc_server/Kconfig file | annotate | diff | comparison | revisions
components/vnc_server/component.mk file | annotate | diff | comparison | revisions
components/vnc_server/vnc-server.c file | annotate | diff | comparison | revisions
components/vnc_server/vnc-server.h file | annotate | diff | comparison | revisions
components/websocket/Kconfig file | annotate | diff | comparison | revisions
components/websocket/README.md file | annotate | diff | comparison | revisions
components/websocket/component.mk file | annotate | diff | comparison | revisions
components/websocket/include/websocket.h file | annotate | diff | comparison | revisions
components/websocket/include/websocket_server.h file | annotate | diff | comparison | revisions
components/websocket/websocket.c file | annotate | diff | comparison | revisions
components/websocket/websocket_server.c file | annotate | diff | comparison | revisions
docs/Doxyfile file | annotate | diff | comparison | revisions
image/fonts/BigFont.fon file | annotate | diff | comparison | revisions
image/fonts/DejaVuSans12.fon file | annotate | diff | comparison | revisions
image/fonts/DejaVuSans18.fon file | annotate | diff | comparison | revisions
image/fonts/DejaVuSans24.fon file | annotate | diff | comparison | revisions
image/fonts/DotMatrix_M.fon file | annotate | diff | comparison | revisions
image/fonts/Grotesk24x48.fon file | annotate | diff | comparison | revisions
image/fonts/SmallFont.fon file | annotate | diff | comparison | revisions
image/fonts/Ubuntu.fon file | annotate | diff | comparison | revisions
image/fonts/arial_bold.fon file | annotate | diff | comparison | revisions
image/fonts/ocrfont.c file | annotate | diff | comparison | revisions
image/fonts/swiss721_outline.fon file | annotate | diff | comparison | revisions
image/log/br1806301750.json file | annotate | diff | comparison | revisions
image/log/br1806302023.json file | annotate | diff | comparison | revisions
image/www/app/error-handler.js.gz file | annotate | diff | comparison | revisions
image/www/app/images/clipboard.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/connect.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/disconnect.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/drag.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/error.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/expander.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/fullscreen.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/handle.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/handle_bg.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/info.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/mouse_left.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/mouse_middle.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/mouse_none.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/mouse_right.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/settings.svg.gz file | annotate | diff | comparison | revisions
image/www/app/images/warning.svg.gz file | annotate | diff | comparison | revisions
image/www/app/locale/de.json file | annotate | diff | comparison | revisions
image/www/app/locale/el.json file | annotate | diff | comparison | revisions
image/www/app/locale/es.json file | annotate | diff | comparison | revisions
image/www/app/locale/nl.json file | annotate | diff | comparison | revisions
image/www/app/locale/pl.json file | annotate | diff | comparison | revisions
image/www/app/locale/sv.json file | annotate | diff | comparison | revisions
image/www/app/locale/tr.json file | annotate | diff | comparison | revisions
image/www/app/locale/zh.json file | annotate | diff | comparison | revisions
image/www/app/localization.js.gz file | annotate | diff | comparison | revisions
image/www/app/sounds/bell.mp3 file | annotate | diff | comparison | revisions
image/www/app/sounds/bell.oga file | annotate | diff | comparison | revisions
image/www/app/styles/base.css file | annotate | diff | comparison | revisions
image/www/app/styles/web.css file | annotate | diff | comparison | revisions
image/www/app/ui.js.gz file | annotate | diff | comparison | revisions
image/www/app/webutil.js.gz file | annotate | diff | comparison | revisions
image/www/chart.html file | annotate | diff | comparison | revisions
image/www/core/base64.js.gz file | annotate | diff | comparison | revisions
image/www/core/des.js.gz file | annotate | diff | comparison | revisions
image/www/core/display.js.gz file | annotate | diff | comparison | revisions
image/www/core/encodings.js.gz file | annotate | diff | comparison | revisions
image/www/core/inflator.js.gz file | annotate | diff | comparison | revisions
image/www/core/input/mouse.js.gz file | annotate | diff | comparison | revisions
image/www/core/rfb.js.gz file | annotate | diff | comparison | revisions
image/www/core/rfb.js.orig file | annotate | diff | comparison | revisions
image/www/core/util/browser.js.gz file | annotate | diff | comparison | revisions
image/www/core/util/events.js.gz file | annotate | diff | comparison | revisions
image/www/core/util/eventtarget.js.gz file | annotate | diff | comparison | revisions
image/www/core/util/logging.js.gz file | annotate | diff | comparison | revisions
image/www/core/util/polyfill.js.gz file | annotate | diff | comparison | revisions
image/www/core/util/strings.js.gz file | annotate | diff | comparison | revisions
image/www/core/websock.js.gz file | annotate | diff | comparison | revisions
image/www/css/style.css file | annotate | diff | comparison | revisions
image/www/index.html file | annotate | diff | comparison | revisions
image/www/js/Chart.min.js file | annotate | diff | comparison | revisions
image/www/js/Chart.min.js.gz file | annotate | diff | comparison | revisions
image/www/js/ch-plug-anno.min.js file | annotate | diff | comparison | revisions
image/www/js/ch-plug-anno.min.js.gz file | annotate | diff | comparison | revisions
image/www/js/jquery-1.12.4.min.js file | annotate | diff | comparison | revisions
image/www/js/jquery-1.12.4.min.js.gz file | annotate | diff | comparison | revisions
image/www/js/modl/babel.js.gz file | annotate | diff | comparison | revisions
image/www/js/modl/brloader.js.gz file | annotate | diff | comparison | revisions
image/www/js/utils/common.js.gz file | annotate | diff | comparison | revisions
image/www/js/webui.js file | annotate | diff | comparison | revisions
image/www/js/zlib/adler32.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/constants.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/crc32.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/deflate.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/gzheader.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/inffast.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/inflate.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/inftrees.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/messages.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/trees.js.gz file | annotate | diff | comparison | revisions
image/www/js/zlib/zstream.js.gz file | annotate | diff | comparison | revisions
image/www/log file | annotate | diff | comparison | revisions
image/www/logfiles.json file | annotate | diff | comparison | revisions
image/www/logs.html file | annotate | diff | comparison | revisions
image/www/vnc.html.gz file | annotate | diff | comparison | revisions
image/www/vnc.html.orig file | annotate | diff | comparison | revisions
image/www/webui.html file | annotate | diff | comparison | revisions
main/Kconfig.projbuild file | annotate | diff | comparison | revisions
main/automation.c file | annotate | diff | comparison | revisions
main/automation.h file | annotate | diff | comparison | revisions
main/brewboard.c file | annotate | diff | comparison | revisions
main/buttons.c file | annotate | diff | comparison | revisions
main/buttons.h file | annotate | diff | comparison | revisions
main/ca_cert.pem file | annotate | diff | comparison | revisions
main/calibration.c file | annotate | diff | comparison | revisions
main/calibration.h file | annotate | diff | comparison | revisions
main/component.mk file | annotate | diff | comparison | revisions
main/config.c file | annotate | diff | comparison | revisions
main/config.h file | annotate | diff | comparison | revisions
main/files.c file | annotate | diff | comparison | revisions
main/files.h file | annotate | diff | comparison | revisions
main/manual.c file | annotate | diff | comparison | revisions
main/manual.h file | annotate | diff | comparison | revisions
main/recipes.c file | annotate | diff | comparison | revisions
main/recipes.h file | annotate | diff | comparison | revisions
main/setup.c file | annotate | diff | comparison | revisions
main/setup.h file | annotate | diff | comparison | revisions
main/task_driver.c file | annotate | diff | comparison | revisions
main/task_driver.h file | annotate | diff | comparison | revisions
main/task_ds18b20.c file | annotate | diff | comparison | revisions
main/task_ds18b20.h file | annotate | diff | comparison | revisions
main/task_http.c file | annotate | diff | comparison | revisions
main/task_http.h file | annotate | diff | comparison | revisions
main/task_sdcard.c file | annotate | diff | comparison | revisions
main/task_sdcard.h file | annotate | diff | comparison | revisions
main/task_sound.c file | annotate | diff | comparison | revisions
main/task_sound.h file | annotate | diff | comparison | revisions
main/task_tft.c file | annotate | diff | comparison | revisions
main/task_tft.h file | annotate | diff | comparison | revisions
main/task_wifi.c file | annotate | diff | comparison | revisions
main/task_wifi.h file | annotate | diff | comparison | revisions
main/updates.c file | annotate | diff | comparison | revisions
main/updates.h file | annotate | diff | comparison | revisions
partitions.csv file | annotate | diff | comparison | revisions
sdkconfig file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,7 @@
+docs/doxygen-warning-log.txt
+sdkconfig.old
+
+
+syntax: glob
+build/*
+docs/html/*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,9 @@
+#
+# This is a project Makefile. It is assumed the directory this Makefile resides in is a
+# project subdirectory.
+#
+
+PROJECT_NAME := brewboard
+
+include $(IDF_PATH)/make/project.mk
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,10 @@
+# BrewBoard on ESP32 using FreeRTOS.
+
+
+TODO:
+
+      - Recepten uploaden via de webserver.
+      - Praktijktest.
+      - Nosleep js code toevoegen.
+      - On WiFi disconnect cleanup VNC websocket.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/PID/PID_v1.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,286 @@
+
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <math.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_system.h"
+#include "esp_log.h"
+
+#include "PID_v1.h"
+
+double		dispKp;			// we'll hold on to the tuning parameters in user-entered
+double		dispKi;			// format for display purposes
+double		dispKd;
+
+double		kp;			// (P)roportional Tuning Parameter
+double		ki;			// (I)ntegral Tuning Parameter
+double		kd;			// (D)erivative Tuning Parameter
+
+int		controllerDirection;
+int		pOn;
+
+double		*myInput;		// Pointers to the Input, Output, and Setpoint variables
+double		*myOutput;		// This creates a hard link between the variables and the 
+double		*mySetpoint;		// PID, freeing the user from having to constantly tell us
+	        	                // what these values are.  with pointers we'll just know.
+
+unsigned long	lastTime;
+double		outputSum, lastInput;
+
+unsigned long	SampleTime;
+double		outMin, outMax;
+bool		inAuto, pOnE;
+
+
+static const char *TAG = "pid";
+
+void PID_Initialize(void);
+
+
+
+void PID(double* Input, double* Output, double* Setpoint, double Kp, double Ki, double Kd, PID_PON POn, PID_DIRECTION Direction) {
+    myOutput = Output;
+    myInput = Input;
+    mySetpoint = Setpoint;
+    inAuto = false;
+
+    PID_SetOutputLimits(0, 255);
+
+    SampleTime = 100;
+
+    PID_SetControllerDirection(Direction);
+    PID_SetTunings(Kp, Ki, Kd, POn);
+
+    lastTime = (xTaskGetTickCount() * portTICK_PERIOD_MS ) - SampleTime;
+}
+
+
+
+/*
+ * This, as they say, is where the magic happens.  this function should be called
+ *   every time "void loop()" executes.  the function will decide for itself whether a new
+ *   pid Output needs to be computed.  returns true when the output is computed,
+ *   false when nothing has been done.
+ */
+bool PID_Compute(void)
+{
+    if (!inAuto)
+	return false;
+	      
+    unsigned long now = xTaskGetTickCount() * portTICK_PERIOD_MS;
+    unsigned long timeChange = (now - lastTime);
+    if (timeChange >= SampleTime) {
+	/*Compute all the working error variables*/
+	double input = *myInput;
+	double error = *mySetpoint - input;
+	double dInput = (input - lastInput);
+	outputSum+= (ki * error);
+
+	/*Add Proportional on Measurement, if P_ON_M is specified*/
+	if (!pOnE) 
+	    outputSum-= kp * dInput;
+
+	if (outputSum > outMax)
+	    outputSum= outMax;
+	else if (outputSum < outMin)
+	    outputSum= outMin;
+	
+	/*Add Proportional on Error, if P_ON_E is specified*/
+	double output;
+	if (pOnE)
+	    output = kp * error;
+	else
+	    output = 0;
+
+	/*Compute Rest of PID Output*/
+	output += outputSum - kd * dInput;
+
+	if (output > outMax)
+	    output = outMax;
+	else if (output < outMin)
+	    output = outMin;
+	*myOutput = output;
+
+	/*Remember some variables for next time*/
+	lastInput = input;
+	lastTime = now;
+	return true;
+    } else
+	return false;
+}
+
+
+
+/*
+ * This function allows the controller's dynamic performance to be adjusted.
+ * it's called automatically from the constructor, but tunings can also
+ * be adjusted on the fly during normal operation
+ */
+void PID_SetTunings(double Kp, double Ki, double Kd, PID_PON POn)
+{
+    if (Kp<0 || Ki<0 || Kd<0) {
+	ESP_LOGE(TAG, "SetTunings negative input");
+	return;
+    }
+
+    pOn = POn;
+    pOnE = POn == PID_P_ON_E;
+
+    dispKp = Kp; dispKi = Ki; dispKd = Kd;
+
+    ESP_LOGI(TAG, "SetTunings(%.3f, %.3f, %.3f, %s)", Kp, Ki, Kd, (POn) ? "P_ON_E":"P_ON_M"); 
+    double SampleTimeInSec = ((double)SampleTime)/1000;
+    kp = Kp;
+    ki = Ki * SampleTimeInSec;
+    kd = Kd / SampleTimeInSec;
+
+    if (controllerDirection == PID_REVERSE) {
+	kp = (0 - kp);
+	ki = (0 - ki);
+	kd = (0 - kd);
+    }
+}
+
+
+
+/*
+ * sets the period, in Milliseconds, at which the calculation is performed
+ */
+void PID_SetSampleTime(int NewSampleTime)
+{
+    ESP_LOGI(TAG, "SetSampleTime(%d)", NewSampleTime);
+
+    if (NewSampleTime > 0) {
+	double ratio  = (double)NewSampleTime / (double)SampleTime;
+	ki *= ratio;
+	kd /= ratio;
+	SampleTime = (unsigned long)NewSampleTime;
+    }
+}
+
+
+
+/*
+ * This function will be used far more often than SetInputLimits.  while
+ *  the input to the controller will generally be in the 0-1023 range (which is
+ *  the default already,)  the output will be a little different.  maybe they'll
+ *  be doing a time window and will need 0-8000 or something.  or maybe they'll
+ *  want to clamp it from 0-125.  who knows.  at any rate, that can all be done
+ *  here.
+ */
+void PID_SetOutputLimits(double Min, double Max)
+{
+    if(Min >= Max) {
+        ESP_LOGE(TAG, "SetOutputLimits Min >= Max");
+	return;
+    }
+
+    ESP_LOGI(TAG, "SetOutputLimits(%.0f, %.0f)", Min, Max);
+    outMin = Min;
+    outMax = Max;
+
+    if(inAuto) {
+	if(*myOutput > outMax)
+	    *myOutput = outMax;
+	else if(*myOutput < outMin)
+	    *myOutput = outMin;
+
+    	if(outputSum > outMax)
+	    outputSum= outMax;
+	else if(outputSum < outMin)
+	    outputSum= outMin;
+    }
+}
+
+
+
+/*
+ * Allows the controller Mode to be set to manual (0) or Automatic (non-zero)
+ * when the transition from manual to auto occurs, the controller is
+ * automatically initialized
+ */
+void PID_SetMode(PID_MODE Mode)
+{
+    bool newAuto = (Mode == PID_AUTOMATIC);
+
+    ESP_LOGI(TAG, "SetMode(%s)", (Mode) ? "AUTOMATIC":"MANUAL");
+    if(newAuto && !inAuto) {  /*we just went from manual to auto*/
+	PID_Initialize();
+    }
+    inAuto = newAuto;
+}
+
+
+
+/*
+ * does all the things that need to happen to ensure a bumpless transfer
+ *  from manual to automatic mode.
+ */
+void PID_Initialize()
+{
+    outputSum = *myOutput;
+    lastInput = *myInput;
+    if(outputSum > outMax)
+	outputSum = outMax;
+    else if(outputSum < outMin)
+	outputSum = outMin;
+}
+
+
+
+/*
+ * The PID will either be connected to a DIRECT acting process (+Output leads
+ * to +Input) or a REVERSE acting process(+Output leads to -Input.)  we need to
+ * know which one, because otherwise we may increase the output when we should
+ * be decreasing.  This is called from the constructor.
+ */
+void PID_SetControllerDirection(PID_DIRECTION Direction)
+{
+    ESP_LOGI(TAG, "SetControllerDirection(%s)", (Direction) ? "REVERSE":"DIRECT");
+
+    if(inAuto && Direction !=controllerDirection) {
+	kp = (0 - kp);
+	ki = (0 - ki);
+	kd = (0 - kd);
+    }
+    controllerDirection = Direction;
+}
+
+
+
+/*
+ * Just because you set the Kp=-1 doesn't mean it actually happened.  these
+ * functions query the internal state of the PID.  they're here for display
+ * purposes.  this are the functions the PID Front-end uses for example
+ */
+double PID_GetKp()
+{ 
+    return  dispKp;
+}
+
+double PID_GetKi()
+{
+    return  dispKi;
+}
+
+double PID_GetKd()
+{
+    return  dispKd;
+}
+
+PID_MODE PID_GetMode()
+{
+    return  inAuto ? PID_AUTOMATIC : PID_MANUAL;
+}
+
+PID_DIRECTION PID_GetDirection()
+{
+    return controllerDirection;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/PID/PID_v1.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,137 @@
+/**
+ * @file
+ * @brief Interface definitions for the PID controller.
+ *
+ * Loger description
+ */
+
+#ifndef	_PID_V1_H
+#define	_PID_V1_H
+
+/**
+ * @brief PID mode codes.
+ */
+typedef enum
+{
+    PID_MANUAL    = 0,		///< PID mode manual.
+    PID_AUTOMATIC = 1,		///< PID mode is automatic.
+} PID_MODE;
+
+/**
+ * @brief PID direction codes.
+ */
+typedef enum
+{
+    PID_DIRECT  = 0,		///< PID direction normal.
+    PID_REVERSE = 1,		///< PID direction reverse.
+} PID_DIRECTION;
+
+/**
+ * @brief PID calculation mode.
+ */
+typedef enum
+{
+    PID_P_ON_M = 0,		///< PID on Measurement.
+    PID_P_ON_E = 1,		///< PID on Errors.
+} PID_PON;
+
+
+
+/**
+ * @brief Setup the PID controller and initialize.
+ * @param[in] Input The measured value.
+ * @param[in,out] Output The computed result.
+ * @param[in] Setpoint The setpoint to regulate to.
+ * @param[in] Kp The Proportional value.
+ * @param[in] Ki The Intergral value.
+ * @param[in] Kd The Derivate value.
+ * @param[in] POn 0 - PID on Measurement, 1 - PID on Error.
+ * @param[in] ControllerDirection Direct or Reverse action.
+ */
+void PID(double* Input, double* Output, double* Setpoint, double Kp, double Ki, double Kd, PID_PON POn, PID_DIRECTION Direction);
+
+/**
+ * @brief sets PID to either Manual or Automatic.
+ * @param Mode Manual (0) or Automatic (1).
+ */
+void PID_SetMode(PID_MODE Mode);
+
+/**
+ * @brief This, as they say, is where the magic happens.  this function should be called
+ *        every time "void loop()" executes.  the function will decide for itself whether a new
+ *        pid Output needs to be computed.
+ * @return Returns true when the output is computed, false when nothing has been done.
+ */
+bool PID_Compute(void);
+
+/**
+ * @brief Clamps the output to a specific range. 0-255 by default, but it's likely the user
+ *        will want to change this depending on the application.
+ * @param[in] Min The minimal value that Output can be.
+ * @param[in] Max The maximum value that Output can be.
+ */
+void PID_SetOutputLimits(double Min, double Max);
+
+/**
+ * @brief While most users will set the tunings once in the constructor, this function gives
+ *        the user the option of changing tunings during runtime for Adaptive control.
+ * @param[in] Kp The Proportional value.
+ * @param[in] Ki The Intergral value.
+ * @param[in] Kd The Derivate value.
+ * @param[in] POn 0 - PID on Measurement, 1 - PID on Error.
+ */
+void PID_SetTunings(double Kp, double Ki, double Kd, PID_PON POn);
+
+/**
+ * @brief The PID will either be connected to a DIRECT acting process (+Output leads
+ *        to +Input) or a REVERSE acting process(+Output leads to -Input.)  we need to
+ *        know which one, because otherwise we may increase the output when we should
+ *        be decreasing.
+ * @param[in] Direction Direct or Reverse action.
+ */
+void PID_SetControllerDirection(PID_DIRECTION Direction);
+
+/**
+ * @brief Sets the frequency, in Milliseconds, with which the PID calculation is performed.
+ *        default is 100
+ * @param[in] NewSampleTime The new time in Milliseconds.
+ */
+void PID_SetSampleTime(int NewSampleTime);
+
+/**
+ * @brief This function queries the internal state of the PID.  It's here for display
+ *        purposes.  this are the functions the PID Front-end uses for example.
+ * @return The Kp setting.
+ */
+double PID_GetKp();
+
+/**
+ * @brief This function queries the internal state of the PID.  It's here for display
+ *        purposes.  this are the functions the PID Front-end uses for example.
+ * @return The Ki setting.
+ */
+double PID_GetKi();
+
+/**
+ * @brief This function queries the internal state of the PID.  It's here for display
+ *        purposes.  this are the functions the PID Front-end uses for example.
+ * @return The Kp setting.
+ */
+double PID_GetKd();
+
+/**
+ * @brief This function queries the internal state of the PID.  It's here for display
+ *        purposes.  this are the functions the PID Front-end uses for example.
+ * @return Returns PID_AUTOMATIC or PID_MANUAL.
+ */
+PID_MODE PID_GetMode();
+
+/**
+ * @brief This function queries the internal state of the PID.  It's here for display
+ *        purposes.  this are the functions the PID Front-end uses for example.
+ * @return Returns PID_DIRECT or PID_REVERSE.
+ */
+PID_DIRECTION PID_GetDirection();
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/PID/README.md	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,12 @@
+# PID
+
+## Introduction
+
+A PID controller seeks to keep some input variable close to a desired setpoint by adjusting an output. The way in which it does this can be 'tuned' by adjusting three parameters (P,I,D).
+
+This is a rewrite of the code from http://playground.arduino.cc/Code/PIDLibrary by Brett Beauregard.
+
+For an ultra-detailed explanation of why the code is the way it is, please visit: http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
+
+## Features
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/PID/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2 @@
+# Use defaults.
+COMPONENT_ADD_INCLUDEDIRS := .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/LICENSE	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 David Antliff
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/README.md	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,65 @@
+# esp32-ds18b20
+
+## Introduction
+
+This is a ESP32-compatible C component for the Maxim Integrated DS18B20 Programmable Resolution 1-Wire Digital Thermometer device.
+
+It supports multiple devices on the same 1-Wire bus.
+
+It is written and tested for v2.1 and v3.0 of the [ESP-IDF](https://github.com/espressif/esp-idf) environment, using the xtensa-esp32-elf toolchain (gcc version 5.2.0).
+
+## Dependencies
+
+Requires [esp32-owb](https://github.com/DavidAntliff/esp32-owb).
+
+## Example
+
+See [esp32-ds18b20-example](https://github.com/DavidAntliff/esp32-ds18b20-example) for an example that supports single and multiple devices on a single bus.
+
+## Features
+
+In cooperation with the underlying esp32-owb component, this component includes:
+
+ * External power supply mode (parasitic mode not yet supported).
+ * Static (stack-based) or dynamic (malloc-based) memory model.
+ * No globals - support any number of DS18B20 devices on any number of 1-Wire buses simultaneously.
+ * 1-Wire device detection and validation, including search for multiple devices on a single bus.
+ * Addressing optimisation for a single (solo) device on a bus.
+ * CRC checks on temperature data.
+ * Programmable temperature measurement resolution (9, 10, 11 or 12-bit resolution).
+ * Temperature conversion and retrieval.
+ * Separation of conversion and temperature retrieval to allow for simultaneous conversion across multiple devices.
+
+## Documentation
+
+Automatically generated API documentation (doxygen) is available [here](https://davidantliff.github.io/esp32-ds18b20/index.html).
+
+## Source Code
+
+The source is available from [GitHub](https://www.github.com/DavidAntliff/esp32-ds18b20).
+
+## License
+
+The code in this project is licensed under the MIT license - see LICENSE for details.
+
+## Links
+
+ * [DS18B20 Datasheet](http://datasheets.maximintegrated.com/en/ds/DS18B20.pdf)
+ * [1-Wire Communication Through Software](https://www.maximintegrated.com/en/app-notes/index.mvp/id/126)
+ * [1-Wire Search Algorithm](https://www.maximintegrated.com/en/app-notes/index.mvp/id/187)
+ * [Espressif IoT Development Framework for ESP32](https://github.com/espressif/esp-idf)
+
+## Acknowledgements
+
+Parts of this code are based on references provided to the public domain by Maxim Integrated.
+
+"1-Wire" is a registered trademark of Maxim Integrated.
+
+## Roadmap
+
+The following features are anticipated but not yet implemented:
+
+ * Concurrency support (multiple tasks accessing devices on the same bus).
+ * Alarm support.
+ * EEPROM support.
+ * Parasitic power support.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1 @@
+# Use defaults
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/doc/.gitignore	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,3 @@
+html/
+latex/
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/doc/Doxyfile	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2475 @@
+# Doxyfile 1.8.13
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
+# for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "esp32-ds18b20"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "ESP32-compatible C library for Maxim Integrated DS18B20 Programmable Resolution 1-Wire Digital Thermometer."
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       =
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 0.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 0
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = NO
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = ../README.md \
+                         ../include \
+                         ..
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f \
+                         *.for \
+                         *.tcl \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = ../README.md
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see http://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = YES
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sf.net) file that captures the
+# structure of the code including all documentation. Note that this feature is
+# still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/ds18b20.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,496 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * @file ds18b20.c
+ *
+ * Resolution is cached in the DS18B20_Info object to avoid querying the hardware
+ * every time a temperature conversion is required. However this can result in the
+ * cached value becoming inconsistent with the hardware value, so care must be taken.
+ *
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <math.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "driver/gpio.h"
+#include "esp_system.h"
+#include "esp_log.h"
+
+#include "ds18b20.h"
+#include "owb.h"
+
+static const char * TAG = "ds18b20";
+static const int T_CONV = 750;   // maximum conversion time at 12-bit resolution in milliseconds
+
+// Function commands
+#define DS18B20_FUNCTION_TEMP_CONVERT       0x44
+#define DS18B20_FUNCTION_SCRATCHPAD_WRITE   0x4E
+#define DS18B20_FUNCTION_SCRATCHPAD_READ    0xBE
+#define DS18B20_FUNCTION_SCRATCHPAD_COPY    0x48
+#define DS18B20_FUNCTION_EEPROM_RECALL      0xB8
+#define DS18B20_FUNCTION_POWER_SUPPLY_READ  0xB4
+
+/// @cond ignore
+typedef struct
+{
+    uint8_t temperature[2];    // [0] is LSB, [1] is MSB
+    uint8_t trigger_high;
+    uint8_t trigger_low;
+    uint8_t configuration;
+    uint8_t reserved[3];
+    uint8_t crc;
+} Scratchpad;
+/// @endcond ignore
+
+static void _init(DS18B20_Info * ds18b20_info, const OneWireBus * bus)
+{
+    if (ds18b20_info != NULL)
+    {
+        ds18b20_info->bus = bus;
+        memset(&ds18b20_info->rom_code, 0, sizeof(ds18b20_info->rom_code));
+        ds18b20_info->use_crc = false;
+        ds18b20_info->resolution = DS18B20_RESOLUTION_INVALID;
+        ds18b20_info->solo = false;   // assume multiple devices unless told otherwise
+        ds18b20_info->init = true;
+    }
+    else
+    {
+        ESP_LOGE(TAG, "ds18b20_info is NULL");
+    }
+}
+
+static bool _is_init(const DS18B20_Info * ds18b20_info)
+{
+    bool ok = false;
+    if (ds18b20_info != NULL)
+    {
+        if (ds18b20_info->init)
+        {
+            // OK
+            ok = true;
+        }
+        else
+        {
+            ESP_LOGE(TAG, "ds18b20_info is not initialised");
+        }
+    }
+    else
+    {
+        ESP_LOGE(TAG, "ds18b20_info is NULL");
+    }
+    return ok;
+}
+
+static bool _address_device(const DS18B20_Info * ds18b20_info)
+{
+    bool present = false;
+    if (_is_init(ds18b20_info))
+    {
+        owb_reset(ds18b20_info->bus, &present);
+        if (present)
+        {
+            if (ds18b20_info->solo)
+            {
+                // if there's only one device on the bus, we can skip
+                // sending the ROM code and instruct it directly
+                owb_write_byte(ds18b20_info->bus, OWB_ROM_SKIP);
+            }
+            else
+            {
+                // if there are multiple devices on the bus, a Match ROM command
+                // must be issued to address a specific slave
+                owb_write_byte(ds18b20_info->bus, OWB_ROM_MATCH);
+                owb_write_rom_code(ds18b20_info->bus, ds18b20_info->rom_code);
+            }
+        }
+        else
+        {
+            ESP_LOGE(TAG, "ds18b20 device not responding");
+        }
+    }
+    return present;
+}
+
+static bool _check_resolution(DS18B20_RESOLUTION resolution)
+{
+    return (resolution >= DS18B20_RESOLUTION_9_BIT) && (resolution <= DS18B20_RESOLUTION_12_BIT);
+}
+
+static float _wait_for_conversion(DS18B20_RESOLUTION resolution)
+{
+    float elapsed_time = 0.0f;
+    if (_check_resolution(resolution))
+    {
+        int divisor = 1 << (DS18B20_RESOLUTION_12_BIT - resolution);
+        ESP_LOGD(TAG, "divisor %d", divisor);
+        float max_conversion_time = (float)T_CONV / (float)divisor;
+        int ticks = ceil(max_conversion_time / portTICK_PERIOD_MS);
+        ESP_LOGD(TAG, "wait for conversion: %.3f ms, %d ticks", max_conversion_time, ticks);
+
+        // wait at least this maximum conversion time
+        vTaskDelay(ticks);
+
+        // TODO: measure elapsed time more accurately
+        elapsed_time = ticks * portTICK_PERIOD_MS;
+    }
+    return elapsed_time;
+}
+
+static float _decode_temp(uint8_t lsb, uint8_t msb, DS18B20_RESOLUTION resolution)
+{
+    float result = 0.0f;
+    if (_check_resolution(resolution))
+    {
+        // masks to remove undefined bits from result
+        static const uint8_t lsb_mask[4] = { ~0x03, ~0x02, ~0x01, ~0x00 };
+        uint8_t lsb_masked = lsb_mask[resolution - DS18B20_RESOLUTION_9_BIT] & lsb;
+        int16_t raw = (msb << 8) | lsb_masked;
+        result = raw / 16.0f;
+    }
+    else
+    {
+        ESP_LOGE(TAG, "Unsupported resolution %d", resolution);
+    }
+    return result;
+}
+
+static size_t _min(size_t x, size_t y)
+{
+    return x > y ? y : x;
+}
+
+static Scratchpad _read_scratchpad(const DS18B20_Info * ds18b20_info, size_t count)
+{
+    count = _min(sizeof(Scratchpad), count);   // avoid overflow
+    Scratchpad scratchpad = {0};
+    ESP_LOGD(TAG, "scratchpad read %d bytes: ", count);
+    if (_address_device(ds18b20_info))
+    {
+        owb_write_byte(ds18b20_info->bus, DS18B20_FUNCTION_SCRATCHPAD_READ);
+        owb_read_bytes(ds18b20_info->bus, (uint8_t *)&scratchpad, count);
+        //esp_log_buffer_hex(TAG, &scratchpad, count);
+    }
+    return scratchpad;
+}
+
+static bool _write_scratchpad(const DS18B20_Info * ds18b20_info, const Scratchpad * scratchpad, bool verify)
+{
+    bool result = false;
+    // Only bytes 2, 3 and 4 (trigger and configuration) can be written.
+    // All three bytes MUST be written before the next reset to avoid corruption.
+    if (_is_init(ds18b20_info))
+    {
+        if (_address_device(ds18b20_info))
+        {
+            owb_write_byte(ds18b20_info->bus, DS18B20_FUNCTION_SCRATCHPAD_WRITE);
+            owb_write_bytes(ds18b20_info->bus, (uint8_t *)&scratchpad->trigger_high, 3);
+            ESP_LOGD(TAG, "scratchpad write 3 bytes:");
+            //esp_log_buffer_hex(TAG, &scratchpad->trigger_high, 3);
+            result = true;
+
+            if (verify)
+            {
+                Scratchpad read = _read_scratchpad(ds18b20_info, offsetof(Scratchpad, configuration) + 1);
+                if (memcmp(&scratchpad->trigger_high, &read.trigger_high, 3) != 0)
+                {
+                    ESP_LOGE(TAG, "scratchpad verify failed: "
+                            "wrote {0x%02x, 0x%02x, 0x%02x}, "
+                            "read {0x%02x, 0x%02x, 0x%02x}",
+                            scratchpad->trigger_high, scratchpad->trigger_low, scratchpad->configuration,
+                            read.trigger_high, read.trigger_low, read.configuration);
+                    result = false;
+                }
+            }
+        }
+    }
+    return result;
+}
+
+
+// Public API
+
+DS18B20_Info * ds18b20_malloc(void)
+{
+    DS18B20_Info * ds18b20_info = malloc(sizeof(*ds18b20_info));
+    if (ds18b20_info != NULL)
+    {
+        memset(ds18b20_info, 0, sizeof(*ds18b20_info));
+        ESP_LOGD(TAG, "malloc %p", ds18b20_info);
+    }
+    else
+    {
+        ESP_LOGE(TAG, "malloc failed");
+    }
+
+    return ds18b20_info;
+}
+
+void ds18b20_free(DS18B20_Info ** ds18b20_info)
+{
+    if (ds18b20_info != NULL && (*ds18b20_info != NULL))
+    {
+        ESP_LOGD(TAG, "free %p", *ds18b20_info);
+        free(*ds18b20_info);
+        *ds18b20_info = NULL;
+    }
+}
+
+void ds18b20_init(DS18B20_Info * ds18b20_info, const OneWireBus * bus, OneWireBus_ROMCode rom_code)
+{
+    if (ds18b20_info != NULL)
+    {
+        _init(ds18b20_info, bus);
+        ds18b20_info->rom_code = rom_code;
+
+        // read current resolution from device as it may not be power-on or factory default
+        ds18b20_info->resolution = ds18b20_read_resolution(ds18b20_info);
+    }
+    else
+    {
+        ESP_LOGE(TAG, "ds18b20_info is NULL");
+    }
+}
+
+void ds18b20_init_solo(DS18B20_Info * ds18b20_info, const OneWireBus * bus)
+{
+    if (ds18b20_info != NULL)
+    {
+        _init(ds18b20_info, bus);
+        ds18b20_info->solo = true;
+
+        // read current resolution from device as it may not be power-on or factory default
+        ds18b20_info->resolution = ds18b20_read_resolution(ds18b20_info);
+    }
+    else
+    {
+        ESP_LOGE(TAG, "ds18b20_info is NULL");
+    }
+}
+
+void ds18b20_use_crc(DS18B20_Info * ds18b20_info, bool use_crc)
+{
+    if (_is_init(ds18b20_info))
+    {
+        ds18b20_info->use_crc = use_crc;
+        ESP_LOGD(TAG, "use_crc %d", ds18b20_info->use_crc);
+    }
+}
+
+bool ds18b20_set_resolution(DS18B20_Info * ds18b20_info, DS18B20_RESOLUTION resolution)
+{
+    bool result = false;
+    if (_is_init(ds18b20_info))
+    {
+        if (_check_resolution(ds18b20_info->resolution))
+        {
+            // read scratchpad up to and including configuration register
+            Scratchpad scratchpad = _read_scratchpad(ds18b20_info,
+                    offsetof(Scratchpad, configuration) - offsetof(Scratchpad, temperature) + 1);
+
+            // modify configuration register to set resolution
+            uint8_t value = (((resolution - 1) & 0x03) << 5) | 0x1f;
+            scratchpad.configuration = value;
+            ESP_LOGD(TAG, "configuration value 0x%02x", value);
+
+            // write bytes 2, 3 and 4 of scratchpad
+            result = _write_scratchpad(ds18b20_info, &scratchpad, /* verify */ true);
+            if (result)
+            {
+                ds18b20_info->resolution = resolution;
+                ESP_LOGD(TAG, "Resolution set to %d bits", (int)resolution);
+            }
+            else
+            {
+                // Resolution change failed - update the info resolution with the value read from configuration
+                ds18b20_info->resolution = ds18b20_read_resolution(ds18b20_info);
+                ESP_LOGW(TAG, "Resolution consistency lost - refreshed from device: %d", ds18b20_info->resolution);
+            }
+        }
+        else
+        {
+            ESP_LOGE(TAG, "Unsupported resolution %d", resolution);
+        }
+    }
+    return result;
+}
+
+DS18B20_RESOLUTION ds18b20_read_resolution(DS18B20_Info * ds18b20_info)
+{
+    DS18B20_RESOLUTION resolution = DS18B20_RESOLUTION_INVALID;
+    if (_is_init(ds18b20_info))
+    {
+        // read scratchpad up to and including configuration register
+        Scratchpad scratchpad = _read_scratchpad(ds18b20_info,
+                offsetof(Scratchpad, configuration) - offsetof(Scratchpad, temperature) + 1);
+
+        resolution = ((scratchpad.configuration >> 5) & 0x03) + DS18B20_RESOLUTION_9_BIT;
+        if (!_check_resolution(resolution))
+        {
+            ESP_LOGE(TAG, "invalid resolution read from device: 0x%02x", scratchpad.configuration);
+            resolution = DS18B20_RESOLUTION_INVALID;
+        }
+        else
+        {
+            ESP_LOGD(TAG, "Resolution read as %d", resolution);
+        }
+    }
+    return resolution;
+}
+
+bool ds18b20_convert(const DS18B20_Info * ds18b20_info)
+{
+    bool result = false;
+    if (_is_init(ds18b20_info))
+    {
+        const OneWireBus * bus = ds18b20_info->bus;
+        if (_address_device(ds18b20_info))
+        {
+            // initiate a temperature measurement
+            owb_write_byte(bus, DS18B20_FUNCTION_TEMP_CONVERT);
+            result = true;
+        }
+        else
+        {
+            ESP_LOGE(TAG, "ds18b20 device not responding");
+        }
+    }
+    return result;
+}
+
+void ds18b20_convert_all(const OneWireBus * bus)
+{
+    bool is_present = false;
+    owb_reset(bus, &is_present);
+    owb_write_byte(bus, OWB_ROM_SKIP);
+    owb_write_byte(bus, DS18B20_FUNCTION_TEMP_CONVERT);
+}
+
+float ds18b20_wait_for_conversion(const DS18B20_Info * ds18b20_info)
+{
+    float elapsed_time = 0.0f;
+    if (_is_init(ds18b20_info))
+    {
+        elapsed_time = _wait_for_conversion(ds18b20_info->resolution);
+    }
+    return elapsed_time;
+}
+
+DS18B20_ERROR ds18b20_read_temp(const DS18B20_Info * ds18b20_info, float * value)
+{
+    DS18B20_ERROR err = DS18B20_ERROR_UNKNOWN;
+    if (_is_init(ds18b20_info))
+    {
+        const OneWireBus * bus = ds18b20_info->bus;
+        if (_address_device(ds18b20_info))
+        {
+            // read measurement
+            if (owb_write_byte(bus, DS18B20_FUNCTION_SCRATCHPAD_READ) == OWB_STATUS_OK)
+            {
+                err = DS18B20_OK;
+
+                uint8_t temp_LSB = 0;
+                uint8_t temp_MSB = 0;
+                if (!ds18b20_info->use_crc)
+                {
+                    // Without CRC:
+                    owb_read_byte(bus, &temp_LSB);
+                    owb_read_byte(bus, &temp_MSB);
+                    bool is_present = false;
+                    owb_reset(bus, &is_present);  // terminate early
+                }
+                else
+                {
+                    // with CRC:
+                    uint8_t buffer[9] = {0};
+                    owb_read_bytes(bus, buffer, 9);
+
+                    temp_LSB = buffer[0];
+                    temp_MSB = buffer[1];
+
+                    if (owb_crc8_bytes(0, buffer, 9) != 0)
+                    {
+                        ESP_LOGE(TAG, "CRC failed");
+                        temp_LSB = 0x00;
+                        temp_MSB = 0x80;
+                        err = DS18B20_ERROR_CRC;
+                    }
+                }
+
+                if (err == DS18B20_OK)
+                {
+                    float temp = _decode_temp(temp_LSB, temp_MSB, ds18b20_info->resolution);
+                    ESP_LOGD(TAG, "temp_LSB 0x%02x, temp_MSB 0x%02x, temp %f", temp_LSB, temp_MSB, temp);
+
+                    if (value)
+                    {
+                        *value = temp;
+                    }
+                }
+            }
+            else
+            {
+                ESP_LOGE(TAG, "owb_write_byte failed");
+                err = DS18B20_ERROR_OWB;
+            }
+        }
+        else
+        {
+            ESP_LOGE(TAG, "ds18b20 device not responding");
+            err = DS18B20_ERROR_DEVICE;
+        }
+    }
+    return err;
+}
+
+DS18B20_ERROR ds18b20_convert_and_read_temp(const DS18B20_Info * ds18b20_info, float * value)
+{
+    DS18B20_ERROR err = DS18B20_ERROR_UNKNOWN;
+    if (_is_init(ds18b20_info))
+    {
+        if (ds18b20_convert(ds18b20_info))
+        {
+            // wait at least maximum conversion time
+            _wait_for_conversion(ds18b20_info->resolution);
+            if (value)
+            {
+                *value = 0.0f;
+                err = ds18b20_read_temp(ds18b20_info, value);
+            }
+            else
+            {
+                err = DS18B20_ERROR_NULL;
+            }
+        }
+    }
+    return err;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-ds18b20/include/ds18b20.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,196 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * @file ds18b20.h
+ * @brief Interface definitions for the Maxim Integrated DS18B20 Programmable
+ *        Resolution 1-Wire Digital Thermometer device.
+ *
+ * This component provides structures and functions that are useful for communicating
+ * with DS18B20 devices connected via a Maxim Integrated 1-Wire® bus.
+ */
+
+#ifndef DS18B20_H
+#define DS18B20_H
+
+#include "owb.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Success and error codes.
+ */
+typedef enum
+{
+    DS18B20_ERROR_UNKNOWN = -1,
+    DS18B20_OK = 0,
+    DS18B20_ERROR_DEVICE,  ///< A device error occurred
+    DS18B20_ERROR_CRC,     ///< A CRC error occurred
+    DS18B20_ERROR_OWB,     ///< A One Wire Bus error occurred
+    DS18B20_ERROR_NULL,    ///< A parameter or value is NULL
+} DS18B20_ERROR;
+
+/**
+ * @brief Symbols for the supported temperature resolution of the device.
+ */
+typedef enum
+{
+    DS18B20_RESOLUTION_INVALID = -1,  ///< Invalid resolution
+    DS18B20_RESOLUTION_9_BIT   = 9,   ///< 9-bit resolution, LSB bits 2,1,0 undefined
+    DS18B20_RESOLUTION_10_BIT  = 10,  ///< 10-bit resolution, LSB bits 1,0 undefined
+    DS18B20_RESOLUTION_11_BIT  = 11,  ///< 11-bit resolution, LSB bit 0 undefined
+    DS18B20_RESOLUTION_12_BIT  = 12,  ///< 12-bit resolution (default)
+} DS18B20_RESOLUTION;
+
+/**
+ * @brief Structure containing information related to a single DS18B20 device connected
+ * via a 1-Wire bus.
+ */
+typedef struct
+{
+    bool init;                     ///< True if struct has been initialised, otherwise false
+    bool solo;                     ///< True if device is intended to be the only one connected to the bus, otherwise false
+    bool use_crc;                  ///< True if CRC checks are to be used when retrieving information from a device on the bus
+    const OneWireBus * bus;        ///< Pointer to 1-Wire bus information relevant to this device
+    OneWireBus_ROMCode rom_code;   ///< The ROM code used to address this device on the bus
+    DS18B20_RESOLUTION resolution; ///< Temperature measurement resolution per reading
+} DS18B20_Info;
+
+/**
+ * @brief Construct a new device info instance.
+ *        New instance should be initialised before calling other functions.
+ * @return Pointer to new device info instance, or NULL if it cannot be created.
+ */
+DS18B20_Info * ds18b20_malloc(void);
+
+/**
+ * @brief Delete an existing device info instance.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @param[in,out] ds18b20_info Pointer to device info instance that will be freed and set to NULL.
+ */
+void ds18b20_free(DS18B20_Info ** ds18b20_info);
+
+/**
+ * @brief Initialise a device info instance with the specified GPIO.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @param[in] bus Pointer to initialised 1-Wire bus instance.
+ * @param[in] rom_code Device-specific ROM code to identify a device on the bus.
+ */
+void ds18b20_init(DS18B20_Info * ds18b20_info, const OneWireBus * bus, OneWireBus_ROMCode rom_code);
+
+/**
+ * @brief Initialise a device info instance with the specified GPIO as a solo device on the bus.
+ *
+ * This is subject to the requirement that this device is the ONLY device on the bus.
+ * This allows for faster commands to be used without ROM code addressing.
+ *
+ * NOTE: if additional devices are added to the bus, operation will cease to work correctly.
+ *
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @param[in] bus Pointer to initialised 1-Wire bus instance.
+ * @param[in] rom_code Device-specific ROM code to identify a device on the bus.
+ */
+void ds18b20_init_solo(DS18B20_Info * ds18b20_info, const OneWireBus * bus);
+
+/**
+ * @brief Enable or disable use of CRC checks on device communications.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @param[in] use_crc True to enable CRC checks, false to disable.
+ */
+void ds18b20_use_crc(DS18B20_Info * ds18b20_info, bool use_crc);
+
+/**
+ * @brief Set temperature measurement resolution.
+ *
+ * This programs the hardware to the specified resolution and sets the cached value to be the same.
+ * If the program fails, the value currently in hardware is used to refresh the cache.
+ *
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @param[in] resolution Selected resolution.
+ * @return True if successful, otherwise false.
+ */
+bool ds18b20_set_resolution(DS18B20_Info * ds18b20_info, DS18B20_RESOLUTION resolution);
+
+/**
+ * @brief Update and return the current temperature measurement resolution from the device.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @return The currently configured temperature measurement resolution.
+ */
+DS18B20_RESOLUTION ds18b20_read_resolution(DS18B20_Info * ds18b20_info);
+
+/**
+ * @brief Read 64-bit ROM code from device - only works when there is a single device on the bus.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ * @return The 64-bit value read from the device's ROM.
+ */
+OneWireBus_ROMCode ds18b20_read_rom(DS18B20_Info * ds18b20_info);
+
+/**
+ * @brief Start a temperature measurement conversion on a single device.
+ * @param[in] ds18b20_info Pointer to device info instance.
+ */
+bool ds18b20_convert(const DS18B20_Info * ds18b20_info);
+
+/**
+ * @brief Start temperature conversion on all connected devices.
+ *
+ * This should be followed by a sufficient delay to ensure all devices complete
+ * their conversion before the measurements are read.
+ * @param[in] bus Pointer to initialised bus instance.
+ */
+void ds18b20_convert_all(const OneWireBus * bus);
+
+/**
+ * @brief Wait for the maximum conversion time according to the current resolution of the device.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @return An estimate of the time elapsed, in milliseconds. Actual elapsed time may be greater.
+ */
+float ds18b20_wait_for_conversion(const DS18B20_Info * ds18b20_info);
+
+/**
+ * @brief Read last temperature measurement from device.
+ *
+ * This is typically called after ds18b20_start_mass_conversion(), provided enough time
+ * has elapsed to ensure that all devices have completed their conversions.
+ * @param[in] ds18b20_info Pointer to device info instance. Must be initialised first.
+ * @param[out] value Pointer to the measurement value returned by the device, in degrees Celsius.
+ * @return DS18B20_OK if read is successful, otherwise error.
+ */
+DS18B20_ERROR ds18b20_read_temp(const DS18B20_Info * ds18b20_info, float * value);
+
+/**
+ * @brief Convert, wait and read current temperature from device.
+ * @param[in] ds18b20_info Pointer to device info instance. Must be initialised first.
+ * @param[out] value Pointer to the measurement value returned by the device, in degrees Celsius.
+ * @return DS18B20_OK if read is successful, otherwise error.
+ */
+DS18B20_ERROR ds18b20_convert_and_read_temp(const DS18B20_Info * ds18b20_info, float * value);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // DS18B20_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/LICENSE	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 David Antliff
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/README.md	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,54 @@
+# esp32-owb
+
+This is a ESP32-compatible C component for the Maxim Integrated "1-Wire" protocol.
+
+It is written and tested for version 3.0 of the [ESP-IDF](https://github.com/espressif/esp-idf) environment, using the xtensa-esp32-elf toolchain (gcc version 5.2.0, crosstool-ng-1.22.0-80-g6c4433a).
+
+Support for v2.1 is available on the [ESP-IDF_v2.1](https://github.com/DavidAntliff/esp32-owb/tree/ESP-IDF_v2.1) branch.
+
+## Features
+
+This library includes:
+
+ * External power supply mode (parasitic mode not yet supported).
+ * Static (stack-based) or dynamic (malloc-based) memory model.
+ * No globals - support any number of 1-Wire buses simultaneously.
+ * 1-Wire device detection and validation, including search for multiple devices on a single bus.
+ * Addressing optimisation for a single (solo) device on a bus.
+ * 1-Wire bus operations including multi-byte read and write operations.
+ * CRC checks on ROM code.
+
+This component includes two methods of bus access - delay-driven GPIO and RMT-driven slots.
+The original implementation used CPU delays to construct the 1-Wire read/write timeslots
+however this proved to be too unreliable. A second method, using the ESP32's RMT peripheral,
+results in very accurate read/write timeslots and more reliable operation.
+
+Therefore I highly recommend that you use the RMT driver. The GPIO driver should be considered deprecated.
+
+## Documentation
+
+Automatically generated API documentation (doxygen) is available [here](https://davidantliff.github.io/esp32-owb/index.html).
+
+## Source Code
+
+The source is available from [GitHub](https://www.github.com/DavidAntliff/esp32-owb).
+
+## License
+
+The code in this project is licensed under the MIT license - see LICENSE for details.
+
+## Links
+
+ * [esp32-ds18b20](https://github.com/DavidAntliff/esp32-ds18b20) - ESP32-compatible DS18B20 Digital Thermometer component for ESP32
+ * [1-Wire Communication Through Software](https://www.maximintegrated.com/en/app-notes/index.mvp/id/126)
+ * [1-Wire Search Algorithm](https://www.maximintegrated.com/en/app-notes/index.mvp/id/187)
+ * [Espressif IoT Development Framework for ESP32](https://github.com/espressif/esp-idf)
+
+## Acknowledgements
+
+Thank you to [Chris Morgan](https://github.com/chmorgan) for his contribution of adding RMT peripheral support for more reliable operation.
+
+Parts of this code are based on references provided to the public domain by Maxim Integrated.
+
+"1-Wire" is a registered trademark of Maxim Integrated.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1 @@
+# Use defaults.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/doc/.gitignore	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,3 @@
+html/
+latex/
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/doc/Doxyfile	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2475 @@
+# Doxyfile 1.8.13
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
+# for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "esp32-owb"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "ESP32-compatible C library for Maxim Integrated 1-Wire Bus."
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       =
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 0.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 0
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = NO
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = ../README.md \
+                         ../include \
+                         ..
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f \
+                         *.for \
+                         *.tcl \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = ../README.md
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see http://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = YES
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sf.net) file that captures the
+# structure of the code including all documentation. Note that this feature is
+# still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/include/owb.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,272 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ * Copyright (c) 2017 Chris Morgan <chmorgan@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * @file
+ * @brief Interface definitions for the 1-Wire bus component.
+ *
+ * This component provides structures and functions that are useful for communicating
+ * with devices connected to a Maxim Integrated 1-Wire® bus via a single GPIO.
+ *
+ * Currently only externally powered devices are supported. Parasitic power is not supported.
+ */
+
+#ifndef ONE_WIRE_BUS_H
+#define ONE_WIRE_BUS_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+// ROM commands
+#define OWB_ROM_SEARCH        0xF0
+#define OWB_ROM_READ          0x33
+#define OWB_ROM_MATCH         0x55
+#define OWB_ROM_SKIP          0xCC
+#define OWB_ROM_SEARCH_ALARM  0xEC
+
+#define OWB_ROM_CODE_STRING_LENGTH (17)  ///< Typical length of OneWire bus ROM ID as ASCII hex string, including null terminator
+
+struct owb_driver;
+
+/**
+ * @brief Structure containing 1-Wire bus information relevant to a single instance.
+ */
+typedef struct
+{
+    const struct _OneWireBus_Timing * timing;   ///< Pointer to timing information
+    bool use_crc;                               ///< True if CRC checks are to be used when retrieving information from a device on the bus
+
+    const struct owb_driver *driver;
+} OneWireBus;
+
+/**
+ * @brief Represents a 1-Wire ROM Code. This is a sequence of eight bytes, where
+ *        the first byte is the family number, then the following 6 bytes form the
+ *        serial number. The final byte is the CRC8 check byte.
+ */
+typedef union
+{
+    /// Provides access via field names
+    struct fields
+    {
+        uint8_t family[1];         ///< family identifier (1 byte, LSB - read/write first)
+        uint8_t serial_number[6];  ///< serial number (6 bytes)
+        uint8_t crc[1];            ///< CRC check byte (1 byte, MSB - read/write last)
+    } fields;                      ///< Provides access via field names
+
+    uint8_t bytes[8];              ///< Provides raw byte access
+
+} OneWireBus_ROMCode;
+
+/**
+ * @brief Represents the state of a device search on the 1-Wire bus.
+ *
+ *        Pass a pointer to this structure to owb_search_first() and
+ *        owb_search_next() to iterate through detected devices on the bus.
+ */
+typedef struct
+{
+    OneWireBus_ROMCode rom_code;
+    int last_discrepancy;
+    int last_family_discrepancy;
+    int last_device_flag;
+} OneWireBus_SearchState;
+
+typedef enum
+{
+    OWB_STATUS_OK,
+    OWB_STATUS_NOT_INITIALIZED,
+    OWB_STATUS_PARAMETER_NULL,
+    OWB_STATUS_DEVICE_NOT_RESPONDING,
+    OWB_STATUS_CRC_FAILED,
+    OWB_STATUS_TOO_MANY_BITS,
+    OWB_STATUS_HW_ERROR
+} owb_status;
+
+/** NOTE: Driver assumes that (*init) was called prior to any other methods */
+struct owb_driver
+{
+    const char* name;
+
+    owb_status (*uninitialize)(const OneWireBus * bus);
+
+    owb_status (*reset)(const OneWireBus * bus, bool *is_present);
+
+    /** NOTE: The data is shifted out of the low bits, eg. it is written in the order of lsb to msb */
+    owb_status (*write_bits)(const OneWireBus *bus, uint8_t out, int number_of_bits_to_write);
+
+    /** NOTE: Data is read into the high bits, eg. each bit read is shifted down before the next bit is read */
+    owb_status (*read_bits)(const OneWireBus *bus, uint8_t *in, int number_of_bits_to_read);
+};
+
+#define container_of(ptr, type, member) ({                      \
+        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
+        (type *)( (char *)__mptr - offsetof(type,member) );})
+
+/**
+ * @brief call to release resources after completing use of the OneWireBus
+ */
+owb_status owb_uninitialize(OneWireBus * bus);
+
+/**
+ * @brief Enable or disable use of CRC checks on device communications.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in] use_crc True to enable CRC checks, false to disable.
+ * @return status
+ */
+owb_status owb_use_crc(OneWireBus * bus, bool use_crc);
+
+/**
+ * @brief Read ROM code from device - only works when there is a single device on the bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[out] rom_code the value read from the device's rom
+ * @return status
+ */
+owb_status owb_read_rom(const OneWireBus * bus, OneWireBus_ROMCode *rom_code);
+
+/**
+ * @brief Verify the device specified by ROM code is present.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in] rom_code ROM code to verify.
+ * @param[out] is_present set to true if a device is present, false if not
+ * @return status
+ */
+owb_status owb_verify_rom(const OneWireBus * bus, OneWireBus_ROMCode rom_code, bool* is_present);
+
+/**
+ * @brief Reset the 1-Wire bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[out] is_present set to true if at least one device is present on the bus
+ * @return status
+ */
+owb_status owb_reset(const OneWireBus * bus, bool* a_device_present);
+
+/**
+ * @brief Write a single byte to the 1-Wire bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in] data Byte value to write to bus.
+ * @return status
+ */
+owb_status owb_write_byte(const OneWireBus * bus, uint8_t data);
+
+/**
+ * @brief Read a single byte from the 1-Wire bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[out] out The byte value read from the bus.
+ * @return status
+ */
+owb_status owb_read_byte(const OneWireBus * bus, uint8_t *out);
+
+/**
+ * @brief Read a number of bytes from the 1-Wire bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in, out] buffer Pointer to buffer to receive read data.
+ * @param[in] len Number of bytes to read, must not exceed length of receive buffer.
+ * @return status.
+ */
+owb_status owb_read_bytes(const OneWireBus * bus, uint8_t * buffer, size_t len);
+
+/**
+ * @brief Write a number of bytes to the 1-Wire bus.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in] buffer Pointer to buffer to write data from.
+ * @param[in] len Number of bytes to write.
+ * @return status
+ */
+owb_status owb_write_bytes(const OneWireBus * bus, const uint8_t * buffer, size_t len);
+
+/**
+ * @brief Write a ROM code to the 1-Wire bus ensuring LSB is sent first.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in] rom_code ROM code to write to bus.
+ * @return status
+ */
+owb_status owb_write_rom_code(const OneWireBus * bus, OneWireBus_ROMCode rom_code);
+
+/**
+ * @brief 1-Wire 8-bit CRC lookup.
+ * @param[in] crc Starting CRC value. Pass in prior CRC to accumulate.
+ * @param[in] data Byte to feed into CRC.
+ * @return Resultant CRC value.
+ *         Should be zero if last byte was the CRC byte and the CRC matches.
+ */
+uint8_t owb_crc8_byte(uint8_t crc, uint8_t data);
+
+/**
+ * @brief 1-Wire 8-bit CRC lookup with accumulation over a block of bytes.
+ * @param[in] crc Starting CRC value. Pass in prior CRC to accumulate.
+ * @param[in] data Array of bytes to feed into CRC.
+ * @param[in] len Length of data array in bytes.
+ * @return Resultant CRC value.
+ *         Should be zero if last byte was the CRC byte and the CRC matches.
+ */
+uint8_t owb_crc8_bytes(uint8_t crc, const uint8_t * data, size_t len);
+
+/**
+ * @brief Locates the first device on the 1-Wire bus, if present.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in,out] state Pointer to an existing search state structure.
+ * @param[out] found_device True if a device is found, false if no devices are found.
+ *         If a device is found, the ROM Code can be obtained from the state.
+ * @return status
+ */
+owb_status owb_search_first(const OneWireBus * bus, OneWireBus_SearchState * state, bool *found_device);
+
+/**
+ * @brief Locates the next device on the 1-Wire bus, if present, starting from
+ *        the provided state. Further calls will yield additional devices, if present.
+ * @param[in] bus Pointer to initialised bus instance.
+ * @param[in,out] state Pointer to an existing search state structure.
+ * @param[out] found_device True if a device is found, false if no devices are found.
+ *         If a device is found, the ROM Code can be obtained from the state.
+ * @return status
+ */
+owb_status owb_search_next(const OneWireBus * bus, OneWireBus_SearchState * state, bool *found_device);
+
+/**
+ * @brief Create a string representation of a ROM code, most significant byte (CRC8) first.
+ * @param[in] rom_code The ROM code to convert to string representation.
+ * @param[out] buffer The destination for the string representation. It will be null terminated.
+ * @param[in] len The length of the buffer in bytes. 64-bit ROM codes require 16 characters
+ *                to represent as a string, plus a null terminator, for 17 bytes.
+ *                See OWB_ROM_CODE_STRING_LENGTH.
+ * @return pointer to the byte beyond the last byte written
+ */
+char * owb_string_from_rom_code(OneWireBus_ROMCode rom_code, char * buffer, size_t len);
+
+#include "owb_gpio.h"
+#include "owb_rmt.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // ONE_WIRE_BUS_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/include/owb_gpio.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,57 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ * Copyright (c) 2017 Chris Morgan <chmorgan@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+#ifndef OWB_GPIO_H
+#define OWB_GPIO_H
+
+#include "owb.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct
+{
+    int gpio;         ///< Value of the GPIO connected to the 1-Wire bus
+    OneWireBus bus;   ///< OneWireBus instance
+} owb_gpio_driver_info;
+
+/**
+ * @brief Initialise the GPIO driver.
+ * @return OneWireBus*, pass this into the other OneWireBus public API functions
+ */
+OneWireBus * owb_gpio_initialize(owb_gpio_driver_info *driver_info, int gpio);
+
+/**
+ * @brief Clean up after a call to owb_gpio_initialize()
+ */
+void owb_gpio_uninitialize(owb_gpio_driver_info *driver_info);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // OWB_GPIO_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/include/owb_rmt.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,59 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ * Copyright (c) 2017 Chris Morgan <chmorgan@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+#ifndef OWB_RMT_H
+#define OWB_RMT_H
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/queue.h"
+#include "freertos/ringbuf.h"
+#include "driver/rmt.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct
+{
+  int tx_channel;     ///< RMT channel to use for TX
+  int rx_channel;     ///< RMT channel to use for RX
+  RingbufHandle_t rb; ///< Ring buffer handle
+  int gpio;           ///< OneWireBus GPIO
+  OneWireBus bus;     ///< OneWireBus instance
+} owb_rmt_driver_info;
+
+/**
+ * @brief Initialise the RMT driver.
+ * @return OneWireBus*, pass this into the other OneWireBus public API functions
+ */
+OneWireBus* owb_rmt_initialize(owb_rmt_driver_info *info, uint8_t gpio_num,
+                               rmt_channel_t tx_channel, rmt_channel_t rx_channel);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // OWB_RMT_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/owb.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,570 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ * Copyright (c) 2017 Chris Morgan <chmorgan@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * @file
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_log.h"
+#include "sdkconfig.h"
+#include "driver/gpio.h"
+
+#include "owb.h"
+#include "owb_gpio.h"
+
+static const char * TAG = "owb";
+
+static bool _is_init(const OneWireBus * bus)
+{
+    bool ok = false;
+    if (bus != NULL)
+    {
+        if (bus->driver)
+        {
+            // OK
+            ok = true;
+        }
+        else
+        {
+            ESP_LOGE(TAG, "bus is not initialised");
+        }
+    }
+    else
+    {
+        ESP_LOGE(TAG, "bus is NULL");
+    }
+    return ok;
+}
+
+/**
+ * @brief 1-Wire 8-bit CRC lookup.
+ * @param[in] crc Starting CRC value. Pass in prior CRC to accumulate.
+ * @param[in] data Byte to feed into CRC.
+ * @return Resultant CRC value.
+ */
+static uint8_t _calc_crc(uint8_t crc, uint8_t data)
+{
+    // https://www.maximintegrated.com/en/app-notes/index.mvp/id/27
+    static const uint8_t table[256] = {
+            0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65,
+            157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220,
+            35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98,
+            190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255,
+            70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7,
+            219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154,
+            101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36,
+            248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185,
+            140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205,
+            17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80,
+            175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238,
+            50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115,
+            202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139,
+            87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22,
+            233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168,
+            116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53
+    };
+
+    return table[crc ^ data];
+}
+
+static uint8_t _calc_crc_block(uint8_t crc, const uint8_t * buffer, size_t len)
+{
+    do
+    {
+        crc = _calc_crc(crc, *buffer++);
+        ESP_LOGD(TAG, "crc 0x%02x, len %d", (int)crc, (int)len);
+    }
+    while (--len > 0);
+    return crc;
+}
+
+/**
+ * @param[out] is_found true if a device was found, false if not
+ * @return status
+ */
+static owb_status _search(const OneWireBus * bus, OneWireBus_SearchState * state, bool *is_found)
+{
+    // Based on https://www.maximintegrated.com/en/app-notes/index.mvp/id/187
+
+    // initialize for search
+    int id_bit_number = 1;
+    int last_zero = 0;
+    int rom_byte_number = 0;
+    uint8_t id_bit = 0;
+    uint8_t cmp_id_bit = 0;
+    uint8_t rom_byte_mask = 1;
+    uint8_t search_direction = 0;
+    bool search_result = false;
+    uint8_t crc8 = 0;
+    owb_status status;
+
+    // if the last call was not the last one
+    if (!state->last_device_flag)
+    {
+        // 1-Wire reset
+        bool is_present;
+        bus->driver->reset(bus, &is_present);
+        if (!is_present)
+        {
+            // reset the search
+            state->last_discrepancy = 0;
+            state->last_device_flag = false;
+            state->last_family_discrepancy = 0;
+            *is_found = false;
+            return OWB_STATUS_OK;
+        }
+
+        // issue the search command
+        bus->driver->write_bits(bus, OWB_ROM_SEARCH, 8);
+
+        // loop to do the search
+        do
+        {
+            id_bit = cmp_id_bit = 0;
+
+            // read a bit and its complement
+            bus->driver->read_bits(bus, &id_bit, 1);
+            bus->driver->read_bits(bus, &cmp_id_bit, 1);
+
+            // check for no devices on 1-wire (signal level is high in both bit reads)
+            if (id_bit && cmp_id_bit)
+            {
+                break;
+            } else
+            {
+                // all devices coupled have 0 or 1
+                if (id_bit != cmp_id_bit)
+                {
+                    search_direction = (id_bit) ? 1 : 0;  // bit write value for search
+                } else
+                {
+                    // if this discrepancy if before the Last Discrepancy
+                    // on a previous next then pick the same as last time
+                    if (id_bit_number < state->last_discrepancy)
+                        search_direction = ((state->rom_code.bytes[rom_byte_number] & rom_byte_mask) > 0);
+                    else
+                        // if equal to last pick 1, if not then pick 0
+                        search_direction = (id_bit_number == state->last_discrepancy);
+
+                    // if 0 was picked then record its position in LastZero
+                    if (search_direction == 0)
+                    {
+                        last_zero = id_bit_number;
+
+                        // check for Last discrepancy in family
+                        if (last_zero < 9)
+                            state->last_family_discrepancy = last_zero;
+                    }
+                }
+
+                // set or clear the bit in the ROM byte rom_byte_number
+                // with mask rom_byte_mask
+                if (search_direction == 1)
+                    state->rom_code.bytes[rom_byte_number] |= rom_byte_mask;
+                else
+                    state->rom_code.bytes[rom_byte_number] &= ~rom_byte_mask;
+
+                // serial number search direction write bit
+                bus->driver->write_bits(bus, search_direction, 1);
+
+                // increment the byte counter id_bit_number
+                // and shift the mask rom_byte_mask
+                id_bit_number++;
+                rom_byte_mask <<= 1;
+
+                // if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask
+                if (rom_byte_mask == 0)
+                {
+                    crc8 = owb_crc8_byte(crc8, state->rom_code.bytes[rom_byte_number]);  // accumulate the CRC
+                    rom_byte_number++;
+                    rom_byte_mask = 1;
+                }
+            }
+        }
+        while(rom_byte_number < 8);  // loop until through all ROM bytes 0-7
+
+        // if the search was successful then
+        if (!((id_bit_number < 65) || (crc8 != 0)))
+        {
+            // search successful so set LastDiscrepancy,LastDeviceFlag,search_result
+            state->last_discrepancy = last_zero;
+
+            // check for last device
+            if (state->last_discrepancy == 0)
+                state->last_device_flag = true;
+
+            search_result = true;
+        }
+    }
+
+    // if no device found then reset counters so next 'search' will be like a first
+    if (!search_result || !state->rom_code.bytes[0])
+    {
+        state->last_discrepancy = 0;
+        state->last_device_flag = false;
+        state->last_family_discrepancy = 0;
+        search_result = false;
+    }
+
+    status = OWB_STATUS_OK;
+
+    *is_found = search_result;
+
+    return status;
+}
+
+// Public API
+
+owb_status owb_uninitialize(OneWireBus * bus)
+{
+    owb_status status;
+
+    if(!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bus->driver->uninitialize(bus);
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_use_crc(OneWireBus * bus, bool use_crc)
+{
+    owb_status status;
+
+    if(!bus)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bus->use_crc = use_crc;
+        ESP_LOGD(TAG, "use_crc %d", bus->use_crc);
+
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_read_rom(const OneWireBus * bus, OneWireBus_ROMCode *rom_code)
+{
+    owb_status status;
+
+    memset(rom_code, 0, sizeof(OneWireBus_ROMCode));
+
+    if(!bus || !rom_code)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bool is_present;
+        bus->driver->reset(bus, &is_present);
+        if (is_present)
+        {
+            uint8_t value = OWB_ROM_READ;
+            bus->driver->write_bits(bus, value, 8);
+            owb_read_bytes(bus, rom_code->bytes, sizeof(OneWireBus_ROMCode));
+
+            if (bus->use_crc)
+            {
+                if (owb_crc8_bytes(0, rom_code->bytes, sizeof(OneWireBus_ROMCode)) != 0)
+                {
+                    ESP_LOGE(TAG, "CRC failed");
+                    memset(rom_code->bytes, 0, sizeof(OneWireBus_ROMCode));
+                    status = OWB_STATUS_CRC_FAILED;
+                } else
+                {
+                    status = OWB_STATUS_OK;
+                }
+            } else
+            {
+                status = OWB_STATUS_OK;
+            }
+            char rom_code_s[OWB_ROM_CODE_STRING_LENGTH];
+            owb_string_from_rom_code(*rom_code, rom_code_s, sizeof(rom_code_s));
+            ESP_LOGD(TAG, "rom_code %s", rom_code_s);
+        }
+        else
+        {
+            status = OWB_STATUS_DEVICE_NOT_RESPONDING;
+            ESP_LOGE(TAG, "ds18b20 device not responding");
+        }
+    }
+
+    return status;
+}
+
+owb_status owb_verify_rom(const OneWireBus * bus, OneWireBus_ROMCode rom_code, bool* is_present)
+{
+    owb_status status;
+    bool result = false;
+
+    if (!bus || !is_present)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    }
+    else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    }
+    else
+    {
+        OneWireBus_SearchState state = {
+            .last_discrepancy = 64,
+            .last_device_flag = false,
+        };
+
+        bool is_found = false;
+        while (!state.last_device_flag && !is_found)
+        {
+            _search(bus, &state, &is_found);
+            if (is_found)
+            {
+                result = true;
+                for (int i = 0; i < sizeof(state.rom_code.bytes) && result; ++i)
+                {
+                    result = rom_code.bytes[i] == state.rom_code.bytes[i];
+                    ESP_LOGD(TAG, "%02x %02x", rom_code.bytes[i], state.rom_code.bytes[i]);
+                }
+                is_found = result;
+            }
+        }
+
+        ESP_LOGD(TAG, "rom code %sfound", result ? "" : "not ");
+        *is_present = result;
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_reset(const OneWireBus * bus, bool* a_device_present)
+{
+    owb_status status;
+
+    if(!bus || !a_device_present)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if(!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bus->driver->reset(bus, a_device_present);
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_write_byte(const OneWireBus * bus, uint8_t data)
+{
+    owb_status status;
+
+    if(!bus)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bus->driver->write_bits(bus, data, 8);
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_read_byte(const OneWireBus * bus, uint8_t *out)
+{
+    owb_status status;
+
+    if(!bus || !out)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        bus->driver->read_bits(bus, out, 8);
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_read_bytes(const OneWireBus * bus, uint8_t * buffer, unsigned int len)
+{
+    owb_status status;
+
+    if(!bus || !buffer)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        for (int i = 0; i < len; ++i)
+        {
+            uint8_t out;
+            bus->driver->read_bits(bus, &out, 8);
+            buffer[i] = out;
+        }
+
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_write_bytes(const OneWireBus * bus, const uint8_t * buffer, unsigned int len)
+{
+    owb_status status;
+
+    if(!bus || !buffer)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        for (int i = 0; i < len; i++)
+        {
+            bus->driver->write_bits(bus, buffer[i], 8);
+        }
+
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+owb_status owb_write_rom_code(const OneWireBus * bus, OneWireBus_ROMCode rom_code)
+{
+    owb_status status;
+
+    if(!bus)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        owb_write_bytes(bus, (uint8_t*)&rom_code, sizeof(rom_code));
+        status = OWB_STATUS_OK;
+    }
+
+    return status;
+}
+
+uint8_t owb_crc8_byte(uint8_t crc, uint8_t data)
+{
+    return _calc_crc(crc, data);
+}
+
+uint8_t owb_crc8_bytes(uint8_t crc, const uint8_t * data, size_t len)
+{
+    return _calc_crc_block(crc, data, len);
+}
+
+owb_status owb_search_first(const OneWireBus * bus, OneWireBus_SearchState * state, bool* found_device)
+{
+    bool result;
+    owb_status status;
+
+    if(!bus || !state || !found_device)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        memset(&state->rom_code, 0, sizeof(state->rom_code));
+        state->last_discrepancy = 0;
+        state->last_family_discrepancy = 0;
+        state->last_device_flag = false;
+        _search(bus, state, &result);
+        status = OWB_STATUS_OK;
+
+        *found_device = result;
+    }
+
+    return status;
+}
+
+owb_status owb_search_next(const OneWireBus * bus, OneWireBus_SearchState * state, bool* found_device)
+{
+    owb_status status;
+    bool result = false;
+
+    if(!bus || !state || !found_device)
+    {
+        status = OWB_STATUS_PARAMETER_NULL;
+    } else if (!_is_init(bus))
+    {
+        status = OWB_STATUS_NOT_INITIALIZED;
+    } else
+    {
+        _search(bus, state, &result);
+        status = OWB_STATUS_OK;
+
+        *found_device = result;
+    }
+
+    return status;
+}
+
+char * owb_string_from_rom_code(OneWireBus_ROMCode rom_code, char * buffer, size_t len)
+{
+    for (int i = sizeof(rom_code.bytes) - 1; i >= 0; i--)
+    {
+        sprintf(buffer, "%02x", rom_code.bytes[i]);
+        buffer += 2;
+    }
+    return buffer;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/owb_gpio.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,284 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 David Antliff
+ * Copyright (c) 2017 Chris Morgan <chmorgan@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * @file
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_log.h"
+#include "sdkconfig.h"
+#include "driver/gpio.h"
+
+#include "owb.h"
+#include "owb_gpio.h"
+
+static const char * TAG = "owb_gpio";
+
+// Define PHY_DEBUG to enable GPIO output around when the bus is sampled
+// by the master (this library). This GPIO output makes it possible to
+// validate the master's sampling using an oscilloscope.
+//
+// For the debug GPIO the idle state is low and made high before the 1-wire sample
+// point and low again after the sample point
+#undef PHY_DEBUG
+
+#ifdef PHY_DEBUG
+// Update these defines to a pin that you can access
+#define PHY_DEBUG_GPIO GPIO_NUM_27
+#define PHY_DEBUG_GPIO_MASK GPIO_SEL_27
+#endif
+
+/// @cond ignore
+struct _OneWireBus_Timing
+{
+    uint32_t A, B, C, D, E, F, G, H, I, J;
+};
+//// @endcond
+
+// 1-Wire timing delays (standard) in microseconds.
+// Labels and values are from https://www.maximintegrated.com/en/app-notes/index.mvp/id/126
+static const struct _OneWireBus_Timing _StandardTiming = {
+        6,    // A - read/write "1" master pull DQ low duration
+        64,   // B - write "0" master pull DQ low duration
+        60,   // C - write "1" master pull DQ high duration
+        10,   // D - write "0" master pull DQ high duration
+        9,    // E - read master pull DQ high duration
+        55,   // F - complete read timeslot + 10ms recovery
+        0,    // G - wait before reset
+        480,  // H - master pull DQ low duration
+        70,   // I - master pull DQ high duration
+        410,  // J - complete presence timeslot + recovery
+};
+
+static void _us_delay(uint32_t time_us)
+{
+    ets_delay_us(time_us);
+}
+
+#define info_from_bus(owb) container_of(owb, owb_gpio_driver_info, bus)
+
+/**
+ * @brief Generate a 1-Wire reset (initialization).
+ * @param[in] bus Initialised bus instance.
+ * @param[out] is_present true if device is present, otherwise false.
+ * @return status
+ */
+static owb_status _reset(const OneWireBus * bus, bool * is_present)
+{
+    bool present = false;
+    portMUX_TYPE timeCriticalMutex = portMUX_INITIALIZER_UNLOCKED;
+    portENTER_CRITICAL(&timeCriticalMutex);
+
+    owb_gpio_driver_info *i = info_from_bus(bus);
+
+    gpio_set_direction(i->gpio, GPIO_MODE_OUTPUT);
+    _us_delay(bus->timing->G);
+    gpio_set_level(i->gpio, 0);  // Drive DQ low
+    _us_delay(bus->timing->H);
+    gpio_set_direction(i->gpio, GPIO_MODE_INPUT); // Release the bus
+    gpio_set_level(i->gpio, 1);  // Reset the output level for the next output
+    _us_delay(bus->timing->I);
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 1);
+#endif
+
+    int level1 = gpio_get_level(i->gpio);
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 0);
+#endif
+
+    _us_delay(bus->timing->J);   // Complete the reset sequence recovery
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 1);
+#endif
+
+    int level2 = gpio_get_level(i->gpio);
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 0);
+#endif
+
+    portEXIT_CRITICAL(&timeCriticalMutex);
+
+    present = (level1 == 0) && (level2 == 1);   // Sample for presence pulse from slave
+    ESP_LOGD(TAG, "reset: level1 0x%x, level2 0x%x, present %d", level1, level2, present);
+
+    *is_present = present;
+
+    return OWB_STATUS_OK;
+}
+
+/**
+ * @brief Send a 1-Wire write bit, with recovery time.
+ * @param[in] bus Initialised bus instance.
+ * @param[in] bit The value to send.
+ */
+static void _write_bit(const OneWireBus * bus, int bit)
+{
+    int delay1 = bit ? bus->timing->A : bus->timing->C;
+    int delay2 = bit ? bus->timing->B : bus->timing->D;
+    owb_gpio_driver_info *i = info_from_bus(bus);
+
+    portMUX_TYPE timeCriticalMutex = portMUX_INITIALIZER_UNLOCKED;
+    portENTER_CRITICAL(&timeCriticalMutex);
+
+    gpio_set_direction(i->gpio, GPIO_MODE_OUTPUT);
+    gpio_set_level(i->gpio, 0);  // Drive DQ low
+    _us_delay(delay1);
+    gpio_set_level(i->gpio, 1);  // Release the bus
+    _us_delay(delay2);
+
+    portEXIT_CRITICAL(&timeCriticalMutex);
+}
+
+/**
+ * @brief Read a bit from the 1-Wire bus and return the value, with recovery time.
+ * @param[in] bus Initialised bus instance.
+ */
+static int _read_bit(const OneWireBus * bus)
+{
+    int result = 0;
+    owb_gpio_driver_info *i = info_from_bus(bus);
+
+    portMUX_TYPE timeCriticalMutex = portMUX_INITIALIZER_UNLOCKED;
+    portENTER_CRITICAL(&timeCriticalMutex);
+
+    gpio_set_direction(i->gpio, GPIO_MODE_OUTPUT);
+    gpio_set_level(i->gpio, 0);  // Drive DQ low
+    _us_delay(bus->timing->A);
+    gpio_set_direction(i->gpio, GPIO_MODE_INPUT); // Release the bus
+    gpio_set_level(i->gpio, 1);  // Reset the output level for the next output
+    _us_delay(bus->timing->E);
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 1);
+#endif
+
+    int level = gpio_get_level(i->gpio);
+
+#ifdef PHY_DEBUG
+    gpio_set_level(PHY_DEBUG_GPIO, 0);
+#endif
+
+    _us_delay(bus->timing->F);   // Complete the timeslot and 10us recovery
+
+    portEXIT_CRITICAL(&timeCriticalMutex);
+
+    result = level & 0x01;
+
+    return result;
+}
+
+/**
+ * @brief Write 1-Wire data byte.
+ * NOTE: The data is shifted out of the low bits, eg. it is written in the order of lsb to msb
+ * @param[in] bus Initialised bus instance.
+ * @param[in] data Value to write.
+ * @param[in] number_of_bits_to_read bits to write
+ */
+static owb_status _write_bits(const OneWireBus * bus, uint8_t data, int number_of_bits_to_write)
+{
+    ESP_LOGD(TAG, "write 0x%02x", data);
+    for (int i = 0; i < number_of_bits_to_write; ++i)
+    {
+        _write_bit(bus, data & 0x01);
+        data >>= 1;
+    }
+
+    return OWB_STATUS_OK;
+}
+
+/**
+ * @brief Read 1-Wire data byte from  bus.
+ * NOTE: Data is read into the high bits, eg. each bit read is shifted down before the next bit is read
+ * @param[in] bus Initialised bus instance.
+ * @return Byte value read from bus.
+ */
+static owb_status _read_bits(const OneWireBus * bus, uint8_t *out, int number_of_bits_to_read)
+{
+    uint8_t result = 0;
+    for (int i = 0; i < number_of_bits_to_read; ++i)
+    {
+        result >>= 1;
+        if (_read_bit(bus))
+        {
+            result |= 0x80;
+        }
+    }
+    ESP_LOGD(TAG, "read 0x%02x", result);
+    *out = result;
+
+    return OWB_STATUS_OK;
+}
+
+static owb_status _uninitialize(const OneWireBus * bus)
+{
+    // Nothing to do here for this driver_info
+    return OWB_STATUS_OK;
+}
+
+static const struct owb_driver gpio_function_table =
+{
+    .name = "owb_gpio",
+    .uninitialize = _uninitialize,
+    .reset = _reset,
+    .write_bits = _write_bits,
+    .read_bits = _read_bits
+};
+
+OneWireBus* owb_gpio_initialize(owb_gpio_driver_info *driver_info, int gpio)
+{
+    ESP_LOGI(TAG, "%s(): gpio %d\n", __func__, gpio);
+
+    driver_info->gpio = gpio;
+    driver_info->bus.driver = &gpio_function_table;
+    driver_info->bus.timing = &_StandardTiming;
+
+    // platform specific:
+    gpio_pad_select_gpio(driver_info->gpio);
+
+#ifdef PHY_DEBUG
+    gpio_config_t io_conf;
+    io_conf.intr_type = GPIO_INTR_DISABLE;
+    io_conf.mode = GPIO_MODE_OUTPUT;
+    io_conf.pin_bit_mask = PHY_DEBUG_GPIO_MASK;
+    io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
+    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
+    ESP_ERROR_CHECK(gpio_config(&io_conf));
+#endif
+
+    return &(driver_info->bus);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/esp32-owb/owb_rmt.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,463 @@
+/*
+Created by Chris Morgan based on the nodemcu project driver.
+Copyright 2017 Chris Morgan <chmorgan@gmail.com>
+
+Ported to ESP32 RMT peripheral for low-level signal generation by Arnim Laeuger.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Much of the code was inspired by Derek Yerger's code, though I don't
+think much of that remains.  In any event that was..
+    (copyleft) 2006 by Derek Yerger - Free to distribute freely.
+
+The CRC code was excerpted and inspired by the Dallas Semiconductor
+sample code bearing this copyright.
+//---------------------------------------------------------------------------
+// Copyright (C) 2000 Dallas Semiconductor Corporation, All Rights Reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL DALLAS SEMICONDUCTOR BE LIABLE FOR ANY CLAIM, DAMAGES
+// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// Except as contained in this notice, the name of Dallas Semiconductor
+// shall not be used except as stated in the Dallas Semiconductor
+// Branding Policy.
+//--------------------------------------------------------------------------
+*/
+
+#include "owb.h"
+
+#include "driver/rmt.h"
+#include "driver/gpio.h"
+#include "esp_log.h"
+
+#undef OW_DEBUG
+
+
+// bus reset: duration of low phase [us]
+#define OW_DURATION_RESET 480
+// overall slot duration
+#define OW_DURATION_SLOT 75
+// write 1 slot and read slot durations [us]
+#define OW_DURATION_1_LOW    2
+#define OW_DURATION_1_HIGH (OW_DURATION_SLOT - OW_DURATION_1_LOW)
+// write 0 slot durations [us]
+#define OW_DURATION_0_LOW   65
+#define OW_DURATION_0_HIGH (OW_DURATION_SLOT - OW_DURATION_0_LOW)
+// sample time for read slot
+#define OW_DURATION_SAMPLE  (15-2)
+// RX idle threshold
+// needs to be larger than any duration occurring during write slots
+#define OW_DURATION_RX_IDLE (OW_DURATION_SLOT + 2)
+
+
+static const char * TAG = "owb_rmt";
+
+#define info_of_driver(owb) container_of(owb, owb_rmt_driver_info, bus)
+
+// flush any pending/spurious traces from the RX channel
+static void onewire_flush_rmt_rx_buf(const OneWireBus * bus)
+{
+    void *p;
+    size_t s;
+
+    owb_rmt_driver_info *i = info_of_driver(bus);
+
+    while ((p = xRingbufferReceive(i->rb, &s, 0)))
+    {
+        ESP_LOGD(TAG, "flushing entry");
+        vRingbufferReturnItem(i->rb, p);
+    }
+}
+
+static owb_status _reset(const OneWireBus *bus, bool *is_present)
+{
+    rmt_item32_t tx_items[1];
+    bool _is_present = false;
+    int res = OWB_STATUS_OK;
+
+    owb_rmt_driver_info *i = info_of_driver(bus);
+
+    tx_items[0].duration0 = OW_DURATION_RESET;
+    tx_items[0].level0 = 0;
+    tx_items[0].duration1 = 0;
+    tx_items[0].level1 = 1;
+
+    uint16_t old_rx_thresh;
+    rmt_get_rx_idle_thresh(i->rx_channel, &old_rx_thresh);
+    rmt_set_rx_idle_thresh(i->rx_channel, OW_DURATION_RESET+60);
+
+    onewire_flush_rmt_rx_buf(bus);
+    rmt_rx_start(i->rx_channel, true);
+    if (rmt_write_items(i->tx_channel, tx_items, 1, true) == ESP_OK)
+    {
+        size_t rx_size;
+        rmt_item32_t* rx_items = (rmt_item32_t *)xRingbufferReceive(i->rb, &rx_size, 100 / portTICK_PERIOD_MS);
+
+        if (rx_items)
+        {
+            if (rx_size >= (1 * sizeof(rmt_item32_t)))
+            {
+#ifdef OW_DEBUG
+                ESP_LOGI(TAG, "rx_size: %d", rx_size);
+
+                for (int i = 0; i < (rx_size / sizeof(rmt_item32_t)); i++)
+                {
+                    ESP_LOGI(TAG, "i: %d, level0: %d, duration %d", i, rx_items[i].level0, rx_items[i].duration0);
+                    ESP_LOGI(TAG, "i: %d, level1: %d, duration %d", i, rx_items[i].level1, rx_items[i].duration1);
+                }
+#endif
+
+                // parse signal and search for presence pulse
+                if ((rx_items[0].level0 == 0) && (rx_items[0].duration0 >= OW_DURATION_RESET - 2))
+                {
+                    if ((rx_items[0].level1 == 1) && (rx_items[0].duration1 > 0))
+                    {
+                        if (rx_items[1].level0 == 0)
+                        {
+                            _is_present = true;
+                        }
+                    }
+                }
+            }
+
+            vRingbufferReturnItem(i->rb, (void *)rx_items);
+        }
+        else
+        {
+            // time out occurred, this indicates an unconnected / misconfigured bus
+            ESP_LOGE(TAG, "rx_items == 0");
+            res = OWB_STATUS_HW_ERROR;
+        }
+    }
+    else
+    {
+        // error in tx channel
+        ESP_LOGE(TAG, "Error tx");
+        res = OWB_STATUS_HW_ERROR;
+    }
+
+    rmt_rx_stop(i->rx_channel);
+    rmt_set_rx_idle_thresh(i->rx_channel, old_rx_thresh);
+
+    *is_present = _is_present;
+
+    ESP_LOGD(TAG, "_is_present %d", _is_present);
+
+    return res;
+}
+
+static rmt_item32_t _encode_write_slot(uint8_t val)
+{
+    rmt_item32_t item;
+
+    item.level0 = 0;
+    item.level1 = 1;
+    if (val)
+    {
+        // write "1" slot
+        item.duration0 = OW_DURATION_1_LOW;
+        item.duration1 = OW_DURATION_1_HIGH;
+    }
+    else
+    {
+        // write "0" slot
+        item.duration0 = OW_DURATION_0_LOW;
+        item.duration1 = OW_DURATION_0_HIGH;
+    }
+
+    return item;
+}
+
+/** NOTE: The data is shifted out of the low bits, eg. it is written in the order of lsb to msb */
+static owb_status _write_bits(const OneWireBus * bus, uint8_t out, int number_of_bits_to_write)
+{
+    rmt_item32_t tx_items[number_of_bits_to_write + 1];
+    owb_rmt_driver_info *info = info_of_driver(bus);
+
+    if (number_of_bits_to_write > 8)
+    {
+        return OWB_STATUS_TOO_MANY_BITS;
+    }
+
+    // write requested bits as pattern to TX buffer
+    for (int i = 0; i < number_of_bits_to_write; i++)
+    {
+        tx_items[i] = _encode_write_slot(out & 0x01);
+        out >>= 1;
+    }
+
+    // end marker
+    tx_items[number_of_bits_to_write].level0 = 1;
+    tx_items[number_of_bits_to_write].duration0 = 0;
+
+    owb_status status;
+
+    if (rmt_write_items(info->tx_channel, tx_items, number_of_bits_to_write+1, true) == ESP_OK)
+    {
+        status = OWB_STATUS_OK;
+    }
+    else
+    {
+        status = OWB_STATUS_HW_ERROR;
+        ESP_LOGE(TAG, "rmt_write_items() failed");
+    }
+
+    return status;
+}
+
+static rmt_item32_t _encode_read_slot(void)
+{
+    rmt_item32_t item;
+
+    // construct pattern for a single read time slot
+    item.level0    = 0;
+    item.duration0 = OW_DURATION_1_LOW;   // shortly force 0
+    item.level1    = 1;
+    item.duration1 = OW_DURATION_1_HIGH;  // release high and finish slot
+    return item;
+}
+
+/** NOTE: Data is read into the high bits, eg. each bit read is shifted down before the next bit is read */
+static owb_status _read_bits(const OneWireBus * bus, uint8_t *in, int number_of_bits_to_read)
+{
+    rmt_item32_t tx_items[number_of_bits_to_read + 1];
+    uint8_t read_data = 0;
+    int res = OWB_STATUS_OK;
+
+    owb_rmt_driver_info *info = info_of_driver(bus);
+
+    if (number_of_bits_to_read > 8)
+    {
+        ESP_LOGE(TAG, "_read_bits() OWB_STATUS_TOO_MANY_BITS");
+        return OWB_STATUS_TOO_MANY_BITS;
+    }
+
+    // generate requested read slots
+    for (int i = 0; i < number_of_bits_to_read; i++)
+    {
+        tx_items[i] = _encode_read_slot();
+    }
+
+    // end marker
+    tx_items[number_of_bits_to_read].level0 = 1;
+    tx_items[number_of_bits_to_read].duration0 = 0;
+
+    onewire_flush_rmt_rx_buf(bus);
+    rmt_rx_start(info->rx_channel, true);
+    if (rmt_write_items(info->tx_channel, tx_items, number_of_bits_to_read+1, true) == ESP_OK)
+    {
+        size_t rx_size;
+        rmt_item32_t* rx_items = (rmt_item32_t *)xRingbufferReceive(info->rb, &rx_size, portMAX_DELAY);
+
+        if (rx_items)
+        {
+#ifdef OW_DEBUG
+            for (int i = 0; i < rx_size / 4; i++)
+            {
+                ESP_LOGI(TAG, "level: %d, duration %d", rx_items[i].level0, rx_items[i].duration0);
+                ESP_LOGI(TAG, "level: %d, duration %d", rx_items[i].level1, rx_items[i].duration1);
+            }
+#endif
+
+            if (rx_size >= number_of_bits_to_read * sizeof(rmt_item32_t))
+            {
+                for (int i = 0; i < number_of_bits_to_read; i++)
+                {
+                    read_data >>= 1;
+                    // parse signal and identify logical bit
+                    if (rx_items[i].level1 == 1)
+                    {
+                        if ((rx_items[i].level0 == 0) && (rx_items[i].duration0 < OW_DURATION_SAMPLE))
+                        {
+                            // rising edge occured before 15us -> bit 1
+                            read_data |= 0x80;
+                        }
+                    }
+                }
+                read_data >>= 8 - number_of_bits_to_read;
+            }
+
+            vRingbufferReturnItem(info->rb, (void *)rx_items);
+        }
+        else
+        {
+            // time out occurred, this indicates an unconnected / misconfigured bus
+            ESP_LOGE(TAG, "rx_items == 0");
+            res = OWB_STATUS_HW_ERROR;
+        }
+    }
+    else
+    {
+        // error in tx channel
+        ESP_LOGE(TAG, "Error tx");
+        res = OWB_STATUS_HW_ERROR;
+    }
+
+    rmt_rx_stop(info->rx_channel);
+
+    *in = read_data;
+    return res;
+}
+
+static owb_status _uninitialize(const OneWireBus *bus)
+{
+    owb_rmt_driver_info * info = info_of_driver(bus);
+
+    rmt_driver_uninstall(info->tx_channel);
+    rmt_driver_uninstall(info->rx_channel);
+
+    return OWB_STATUS_OK;
+}
+
+static struct owb_driver rmt_function_table =
+{
+    .name = "owb_rmt",
+    .uninitialize = _uninitialize,
+    .reset = _reset,
+    .write_bits = _write_bits,
+    .read_bits = _read_bits
+};
+
+static owb_status _init(owb_rmt_driver_info *info, uint8_t gpio_num,
+                        rmt_channel_t tx_channel, rmt_channel_t rx_channel)
+{
+    owb_status status = OWB_STATUS_HW_ERROR;
+
+    // Ensure the RMT peripheral is not already running
+    // Note: if using RMT elsewhere, don't call this here, call it at the start of your prgoram instead.
+    //periph_module_disable(PERIPH_RMT_MODULE);
+    //periph_module_enable(PERIPH_RMT_MODULE);
+
+    info->bus.driver = &rmt_function_table;
+    info->tx_channel = tx_channel;
+    info->rx_channel = rx_channel;
+    info->gpio = gpio_num;
+
+#ifdef OW_DEBUG
+    ESP_LOGI(TAG, "RMT TX channel: %d", info->tx_channel);
+    ESP_LOGI(TAG, "RMT RX channel: %d", info->rx_channel);
+#endif
+
+    rmt_config_t rmt_tx;
+    rmt_tx.channel = info->tx_channel;
+    rmt_tx.gpio_num = gpio_num;
+    rmt_tx.mem_block_num = 1;
+    rmt_tx.clk_div = 80;
+    rmt_tx.tx_config.loop_en = false;
+    rmt_tx.tx_config.carrier_en = false;
+    rmt_tx.tx_config.idle_level = 1;
+    rmt_tx.tx_config.idle_output_en = true;
+    rmt_tx.rmt_mode = RMT_MODE_TX;
+    if (rmt_config(&rmt_tx) == ESP_OK)
+    {
+        if (rmt_driver_install(rmt_tx.channel, 0, ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_SHARED) == ESP_OK)
+        {
+            rmt_config_t rmt_rx;
+            rmt_rx.channel = info->rx_channel;
+            rmt_rx.gpio_num = gpio_num;
+            rmt_rx.clk_div = 80;
+            rmt_rx.mem_block_num = 1;
+            rmt_rx.rmt_mode = RMT_MODE_RX;
+            rmt_rx.rx_config.filter_en = true;
+            rmt_rx.rx_config.filter_ticks_thresh = 30;
+            rmt_rx.rx_config.idle_threshold = OW_DURATION_RX_IDLE;
+            if (rmt_config(&rmt_rx) == ESP_OK)
+            {
+                if (rmt_driver_install(rmt_rx.channel, 512, ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_SHARED) == ESP_OK)
+                {
+                    rmt_get_ringbuf_handle(info->rx_channel, &info->rb);
+                    status = OWB_STATUS_OK;
+                }
+                else
+                {
+                    ESP_LOGE(TAG, "failed to install rx driver");
+                }
+            }
+            else
+            {
+                status = OWB_STATUS_HW_ERROR;
+                ESP_LOGE(TAG, "failed to configure rx, uninstalling rmt driver on tx channel");
+                rmt_driver_uninstall(rmt_tx.channel);
+            }
+        }
+        else
+        {
+            ESP_LOGE(TAG, "failed to install tx driver");
+        }
+    }
+    else
+    {
+        ESP_LOGE(TAG, "failed to configure tx");
+    }
+
+    // attach GPIO to previous pin
+    if (gpio_num < 32)
+    {
+        GPIO.enable_w1ts = (0x1 << gpio_num);
+    }
+    else
+    {
+        GPIO.enable1_w1ts.data = (0x1 << (gpio_num - 32));
+    }
+
+    // attach RMT channels to new gpio pin
+    // ATTENTION: set pin for rx first since gpio_output_disable() will
+    //            remove rmt output signal in matrix!
+    rmt_set_pin(info->rx_channel, RMT_MODE_RX, gpio_num);
+    rmt_set_pin(info->tx_channel, RMT_MODE_TX, gpio_num);
+
+    // force pin direction to input to enable path to RX channel
+    PIN_INPUT_ENABLE(GPIO_PIN_MUX_REG[gpio_num]);
+
+    // enable open drain
+    GPIO.pin[gpio_num].pad_driver = 1;
+
+    return status;
+}
+
+OneWireBus * owb_rmt_initialize(owb_rmt_driver_info *info, uint8_t gpio_num,
+                                rmt_channel_t tx_channel, rmt_channel_t rx_channel)
+{
+    ESP_LOGD(TAG, "%s: gpio_num: %d, tx_channel: %d, rx_channel: %d",
+             __func__, gpio_num, tx_channel, rx_channel);
+
+    owb_status status = _init(info, gpio_num, tx_channel, rx_channel);
+    if(status != OWB_STATUS_OK)
+    {
+        ESP_LOGE(TAG, "_init() failed with status %d", status);
+    }
+
+    return &(info->bus);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/spidriver/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,7 @@
+#
+# Main Makefile. This is basically the same as a component makefile.
+#
+# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
+
+COMPONENT_SRCDIRS := . 
+COMPONENT_ADD_INCLUDEDIRS := .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/spidriver/spi_master_lobo.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1153 @@
+// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/*
+----------------------------------------
+Non DMA version of the spi_master driver
+----------------------------------------
+------------------------------------------------------------------------------------
+Based on esp-idf 'spi_master', modified by LoBo (https://github.com/loboris) 03/2017
+------------------------------------------------------------------------------------
+
+* Transfers data to SPI device in direct mode, not using DMA
+* All configuration options (bus, device, transaction) are the same as in spi_master driver
+* Transfers uses the semaphore (taken in select function & given in deselect function) to protect the transfer
+* Number of the devices attached to the bus which uses hardware CS can be 3 ('NO_CS')
+* Additional devices which uses software CS can be attached to the bus, up to 'NO_DEV'
+* 'spi_bus_initialize' & 'spi_bus_remove' functions are removed, spi bus is initiated/removed in spi_lobo_bus_add_device/spi_lobo_bus_remove_device when needed
+* 'spi_lobo_bus_add_device' function has added parameter 'bus_config' and automatically initializes spi bus device if not already initialized
+* 'spi_lobo_bus_remove_device' automatically removes spi bus device if no other devices are attached to it.
+* Devices can have individual bus_configs, so different mosi, miso, sck pins can be configured for each device
+    Reconfiguring the bus is done automaticaly in 'spi_lobo_device_select' function
+* 'spi_lobo_device_select' & 'spi_lobo_device_deselect' functions handles devices configuration changes and software CS
+* Some helper functions are added ('spi_lobo_get_speed', 'spi_lobo_set_speed', ...)
+* All structures are available in header file for easy creation of user low level spi functions. See **tftfunc.c** source for examples.
+* Transimt and receive lenghts are limited only by available memory
+
+
+Main driver's function is 'spi_lobo_transfer_data()'
+
+ * TRANSMIT 8-bit data to spi device from 'trans->tx_buffer' or 'trans->tx_data' (trans->lenght/8 bytes)
+ * and RECEIVE data to 'trans->rx_buffer' or 'trans->rx_data' (trans->rx_length/8 bytes)
+ * Lengths must be 8-bit multiples!
+ * If trans->rx_buffer is NULL or trans->rx_length is 0, only transmits data
+ * If trans->tx_buffer is NULL or trans->length is 0, only receives data
+ * If the device is in duplex mode (LB_SPI_DEVICE_HALFDUPLEX flag NOT set), data are transmitted and received simultaneously.
+ * If the device is in half duplex mode (LB_SPI_DEVICE_HALFDUPLEX flag IS set), data are received after transmission
+ * 'address', 'command' and 'dummy bits' are transmitted before data phase IF set in device's configuration
+ *   and IF 'trans->length' and 'trans->rx_length' are NOT both 0
+ * If configured, devices 'pre_cb' callback is called before and 'post_cb' after the transmission
+ * If device was not previously selected, it will be selected before transmission and deselected after transmission.
+
+*/
+
+/*
+ Replace this include with
+ #include "driver/spi_master_lobo.h"
+ if the driver is located in esp-isf/components
+*/
+#include "freertos/FreeRTOS.h"
+#include <string.h>
+#include <stdio.h>
+#include "soc/gpio_sig_map.h"
+#include "soc/spi_reg.h"
+#include "soc/dport_reg.h"
+#include "soc/rtc_cntl_reg.h"
+#include "rom/ets_sys.h"
+#include "esp_types.h"
+#include "esp_attr.h"
+#include "esp_log.h"
+#include "esp_err.h"
+#include "freertos/semphr.h"
+#include "freertos/xtensa_api.h"
+#include "freertos/task.h"
+#include "freertos/ringbuf.h"
+#include "soc/soc.h"
+#include "soc/dport_reg.h"
+#include "soc/uart_struct.h"
+#include "driver/uart.h"
+#include "driver/gpio.h"
+#include "driver/periph_ctrl.h"
+#include "esp_heap_caps.h"
+#include "driver/periph_ctrl.h"
+#include "spi_master_lobo.h"
+
+
+static spi_lobo_host_t *spihost[3] = {NULL};
+
+
+static const char *SPI_TAG = "spi_lobo_master";
+#define SPI_CHECK(a, str, ret_val) \
+    if (!(a)) { \
+        ESP_LOGE(SPI_TAG,"%s(%d): %s", __FUNCTION__, __LINE__, str); \
+        return (ret_val); \
+    }
+
+/*
+ Stores a bunch of per-spi-peripheral data.
+*/
+typedef struct {
+    const uint8_t spiclk_out;       //GPIO mux output signals
+    const uint8_t spid_out;
+    const uint8_t spiq_out;
+    const uint8_t spiwp_out;
+    const uint8_t spihd_out;
+    const uint8_t spid_in;          //GPIO mux input signals
+    const uint8_t spiq_in;
+    const uint8_t spiwp_in;
+    const uint8_t spihd_in;
+    const uint8_t spics_out[3];     // /CS GPIO output mux signals
+    const uint8_t spiclk_native;    //IO pins of IO_MUX muxed signals
+    const uint8_t spid_native;
+    const uint8_t spiq_native;
+    const uint8_t spiwp_native;
+    const uint8_t spihd_native;
+    const uint8_t spics0_native;
+    const uint8_t irq;              //irq source for interrupt mux
+    const uint8_t irq_dma;          //dma irq source for interrupt mux
+    const periph_module_t module;   //peripheral module, for enabling clock etc
+    spi_dev_t *hw;                  //Pointer to the hardware registers
+} spi_signal_conn_t;
+
+/*
+ Bunch of constants for every SPI peripheral: GPIO signals, irqs, hw addr of registers etc
+*/
+static const spi_signal_conn_t io_signal[3]={
+    {
+        .spiclk_out=SPICLK_OUT_IDX,
+        .spid_out=SPID_OUT_IDX,
+        .spiq_out=SPIQ_OUT_IDX,
+        .spiwp_out=SPIWP_OUT_IDX,
+        .spihd_out=SPIHD_OUT_IDX,
+        .spid_in=SPID_IN_IDX,
+        .spiq_in=SPIQ_IN_IDX,
+        .spiwp_in=SPIWP_IN_IDX,
+        .spihd_in=SPIHD_IN_IDX,
+        .spics_out={SPICS0_OUT_IDX, SPICS1_OUT_IDX, SPICS2_OUT_IDX},
+        .spiclk_native=6,
+        .spid_native=8,
+        .spiq_native=7,
+        .spiwp_native=10,
+        .spihd_native=9,
+        .spics0_native=11,
+        .irq=ETS_SPI1_INTR_SOURCE,
+        .irq_dma=ETS_SPI1_DMA_INTR_SOURCE,
+        .module=PERIPH_SPI_MODULE,
+        .hw=&SPI1
+    }, {
+        .spiclk_out=HSPICLK_OUT_IDX,
+        .spid_out=HSPID_OUT_IDX,
+        .spiq_out=HSPIQ_OUT_IDX,
+        .spiwp_out=HSPIWP_OUT_IDX,
+        .spihd_out=HSPIHD_OUT_IDX,
+        .spid_in=HSPID_IN_IDX,
+        .spiq_in=HSPIQ_IN_IDX,
+        .spiwp_in=HSPIWP_IN_IDX,
+        .spihd_in=HSPIHD_IN_IDX,
+        .spics_out={HSPICS0_OUT_IDX, HSPICS1_OUT_IDX, HSPICS2_OUT_IDX},
+        .spiclk_native=14,
+        .spid_native=13,
+        .spiq_native=12,
+        .spiwp_native=2,
+        .spihd_native=4,
+        .spics0_native=15,
+        .irq=ETS_SPI2_INTR_SOURCE,
+        .irq_dma=ETS_SPI2_DMA_INTR_SOURCE,
+        .module=PERIPH_HSPI_MODULE,
+        .hw=&SPI2
+    }, {
+        .spiclk_out=VSPICLK_OUT_IDX,
+        .spid_out=VSPID_OUT_IDX,
+        .spiq_out=VSPIQ_OUT_IDX,
+        .spiwp_out=VSPIWP_OUT_IDX,
+        .spihd_out=VSPIHD_OUT_IDX,
+        .spid_in=VSPID_IN_IDX,
+        .spiq_in=VSPIQ_IN_IDX,
+        .spiwp_in=VSPIWP_IN_IDX,
+        .spihd_in=VSPIHD_IN_IDX,
+        .spics_out={VSPICS0_OUT_IDX, VSPICS1_OUT_IDX, VSPICS2_OUT_IDX},
+        .spiclk_native=18,
+        .spid_native=23,
+        .spiq_native=19,
+        .spiwp_native=22,
+        .spihd_native=21,
+        .spics0_native=5,
+        .irq=ETS_SPI3_INTR_SOURCE,
+        .irq_dma=ETS_SPI3_DMA_INTR_SOURCE,
+        .module=PERIPH_VSPI_MODULE,
+        .hw=&SPI3
+    }
+};
+
+
+//======================================================================================================
+
+#define DMA_CHANNEL_ENABLED(dma_chan)    (BIT(dma_chan-1))
+
+typedef void(*dmaworkaround_cb_t)(void *arg);
+
+//Set up a list of dma descriptors. dmadesc is an array of descriptors. Data is the buffer to point to.
+//--------------------------------------------------------------------------------------------
+void spi_lobo_setup_dma_desc_links(lldesc_t *dmadesc, int len, const uint8_t *data, bool isrx)
+{
+    int n = 0;
+    while (len) {
+        int dmachunklen = len;
+        if (dmachunklen > SPI_MAX_DMA_LEN) dmachunklen = SPI_MAX_DMA_LEN;
+        if (isrx) {
+            //Receive needs DMA length rounded to next 32-bit boundary
+            dmadesc[n].size = (dmachunklen + 3) & (~3);
+            dmadesc[n].length = (dmachunklen + 3) & (~3);
+        } else {
+            dmadesc[n].size = dmachunklen;
+            dmadesc[n].length = dmachunklen;
+        }
+        dmadesc[n].buf = (uint8_t *)data;
+        dmadesc[n].eof = 0;
+        dmadesc[n].sosf = 0;
+        dmadesc[n].owner = 1;
+        dmadesc[n].qe.stqe_next = &dmadesc[n + 1];
+        len -= dmachunklen;
+        data += dmachunklen;
+        n++;
+    }
+    dmadesc[n - 1].eof = 1; //Mark last DMA desc as end of stream.
+    dmadesc[n - 1].qe.stqe_next = NULL;
+}
+
+
+/*
+Code for workaround for DMA issue in ESP32 v0/v1 silicon
+*/
+
+
+static volatile int dmaworkaround_channels_busy[2] = {0, 0};
+static dmaworkaround_cb_t dmaworkaround_cb;
+static void *dmaworkaround_cb_arg;
+static portMUX_TYPE dmaworkaround_mux = portMUX_INITIALIZER_UNLOCKED;
+static int dmaworkaround_waiting_for_chan = 0;
+static bool spi_periph_claimed[3] = {true, false, false};
+static uint8_t spi_dma_chan_enabled = 0;
+static portMUX_TYPE spi_dma_spinlock = portMUX_INITIALIZER_UNLOCKED;
+
+//--------------------------------------------------------------------------------------------
+bool IRAM_ATTR spi_lobo_dmaworkaround_req_reset(int dmachan, dmaworkaround_cb_t cb, void *arg)
+{
+    int otherchan = (dmachan == 1) ? 2 : 1;
+    bool ret;
+    portENTER_CRITICAL(&dmaworkaround_mux);
+    if (dmaworkaround_channels_busy[otherchan-1]) {
+        //Other channel is busy. Call back when it's done.
+        dmaworkaround_cb = cb;
+        dmaworkaround_cb_arg = arg;
+        dmaworkaround_waiting_for_chan = otherchan;
+        ret = false;
+    } else {
+        //Reset DMA
+        DPORT_SET_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI_DMA_RST);
+        DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI_DMA_RST);
+        ret = true;
+    }
+    portEXIT_CRITICAL(&dmaworkaround_mux);
+    return ret;
+}
+
+//-------------------------------------------------------
+bool IRAM_ATTR spi_lobo_dmaworkaround_reset_in_progress()
+{
+    return (dmaworkaround_waiting_for_chan != 0);
+}
+
+//-----------------------------------------------------
+void IRAM_ATTR spi_lobo_dmaworkaround_idle(int dmachan)
+{
+    portENTER_CRITICAL(&dmaworkaround_mux);
+    dmaworkaround_channels_busy[dmachan-1] = 0;
+    if (dmaworkaround_waiting_for_chan == dmachan) {
+        //Reset DMA
+        DPORT_SET_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI_DMA_RST);
+        DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI_DMA_RST);
+        dmaworkaround_waiting_for_chan = 0;
+        //Call callback
+        dmaworkaround_cb(dmaworkaround_cb_arg);
+
+    }
+    portEXIT_CRITICAL(&dmaworkaround_mux);
+}
+
+//----------------------------------------------------------------
+void IRAM_ATTR spi_lobo_dmaworkaround_transfer_active(int dmachan)
+{
+    portENTER_CRITICAL(&dmaworkaround_mux);
+    dmaworkaround_channels_busy[dmachan-1] = 1;
+    portEXIT_CRITICAL(&dmaworkaround_mux);
+}
+
+//Returns true if this peripheral is successfully claimed, false if otherwise.
+//-----------------------------------------------------
+bool spi_lobo_periph_claim(spi_lobo_host_device_t host)
+{
+    bool ret = __sync_bool_compare_and_swap(&spi_periph_claimed[host], false, true);
+    if (ret) periph_module_enable(io_signal[host].module);
+    return ret;
+}
+
+//Returns true if this peripheral is successfully freed, false if otherwise.
+//-----------------------------------------------
+bool spi_lobo_periph_free(spi_lobo_host_device_t host)
+{
+    bool ret = __sync_bool_compare_and_swap(&spi_periph_claimed[host], true, false);
+    if (ret) periph_module_disable(io_signal[host].module);
+    return ret;
+}
+
+//-----------------------------------------
+bool spi_lobo_dma_chan_claim (int dma_chan)
+{
+    bool ret = false;
+    assert( dma_chan == 1 || dma_chan == 2 );
+
+    portENTER_CRITICAL(&spi_dma_spinlock);
+    if ( !(spi_dma_chan_enabled & DMA_CHANNEL_ENABLED(dma_chan)) ) {
+        // get the channel only when it's not claimed yet.
+        spi_dma_chan_enabled |= DMA_CHANNEL_ENABLED(dma_chan);
+        ret = true;
+    }
+    periph_module_enable( PERIPH_SPI_DMA_MODULE );
+    portEXIT_CRITICAL(&spi_dma_spinlock);
+
+    return ret;
+}
+
+//---------------------------------------
+bool spi_lobo_dma_chan_free(int dma_chan)
+{
+    assert( dma_chan == 1 || dma_chan == 2 );
+    assert( spi_dma_chan_enabled & DMA_CHANNEL_ENABLED(dma_chan) );
+
+    portENTER_CRITICAL(&spi_dma_spinlock);
+    spi_dma_chan_enabled &= ~DMA_CHANNEL_ENABLED(dma_chan);
+    if ( spi_dma_chan_enabled == 0 ) {
+        //disable the DMA only when all the channels are freed.
+        periph_module_disable( PERIPH_SPI_DMA_MODULE );
+    }
+    portEXIT_CRITICAL(&spi_dma_spinlock);
+
+    return true;
+}
+
+
+//======================================================================================================
+
+
+//----------------------------------------------------------------------------------------------------------------
+static esp_err_t spi_lobo_bus_initialize(spi_lobo_host_device_t host, spi_lobo_bus_config_t *bus_config, int init)
+{
+    bool native=true, spi_chan_claimed, dma_chan_claimed;
+
+    if (init > 0) {
+        /* ToDo: remove this when we have flash operations cooperating with this */
+        SPI_CHECK(host!=TFT_SPI_HOST, "SPI1 is not supported", ESP_ERR_NOT_SUPPORTED);
+
+        SPI_CHECK(host>=TFT_SPI_HOST && host<=TFT_VSPI_HOST, "invalid host", ESP_ERR_INVALID_ARG);
+        SPI_CHECK(spihost[host]==NULL, "host already in use", ESP_ERR_INVALID_STATE);
+    }
+    else {
+        SPI_CHECK(spihost[host]!=NULL, "host not in use", ESP_ERR_INVALID_STATE);
+    }
+    
+    SPI_CHECK(bus_config->mosi_io_num<0 || GPIO_IS_VALID_OUTPUT_GPIO(bus_config->mosi_io_num), "spid pin invalid", ESP_ERR_INVALID_ARG);
+    SPI_CHECK(bus_config->sclk_io_num<0 || GPIO_IS_VALID_OUTPUT_GPIO(bus_config->sclk_io_num), "spiclk pin invalid", ESP_ERR_INVALID_ARG);
+    SPI_CHECK(bus_config->miso_io_num<0 || GPIO_IS_VALID_GPIO(bus_config->miso_io_num), "spiq pin invalid", ESP_ERR_INVALID_ARG);
+    SPI_CHECK(bus_config->quadwp_io_num<0 || GPIO_IS_VALID_OUTPUT_GPIO(bus_config->quadwp_io_num), "spiwp pin invalid", ESP_ERR_INVALID_ARG);
+    SPI_CHECK(bus_config->quadhd_io_num<0 || GPIO_IS_VALID_OUTPUT_GPIO(bus_config->quadhd_io_num), "spihd pin invalid", ESP_ERR_INVALID_ARG);
+
+    if (init > 0) {
+        spi_chan_claimed=spi_lobo_periph_claim(host);
+        SPI_CHECK(spi_chan_claimed, "host already in use", ESP_ERR_INVALID_STATE);
+
+        //spihost[host]=malloc(sizeof(spi_lobo_host_t));
+		spihost[host]=heap_caps_malloc(sizeof(spi_lobo_host_t), MALLOC_CAP_DMA);
+		if (spihost[host]==NULL) return ESP_ERR_NO_MEM;
+		memset(spihost[host], 0, sizeof(spi_lobo_host_t));
+		// Create semaphore
+		spihost[host]->spi_lobo_bus_mutex = xSemaphoreCreateMutex();
+		if (!spihost[host]->spi_lobo_bus_mutex) return ESP_ERR_NO_MEM;
+    }
+
+    spihost[host]->cur_device = -1;
+    memcpy(&spihost[host]->cur_bus_config, bus_config, sizeof(spi_lobo_bus_config_t));
+
+    //Check if the selected pins correspond to the native pins of the peripheral
+    if (bus_config->mosi_io_num >= 0 && bus_config->mosi_io_num!=io_signal[host].spid_native) native=false;
+    if (bus_config->miso_io_num >= 0 && bus_config->miso_io_num!=io_signal[host].spiq_native) native=false;
+    if (bus_config->sclk_io_num >= 0 && bus_config->sclk_io_num!=io_signal[host].spiclk_native) native=false;
+    if (bus_config->quadwp_io_num >= 0 && bus_config->quadwp_io_num!=io_signal[host].spiwp_native) native=false;
+    if (bus_config->quadhd_io_num >= 0 && bus_config->quadhd_io_num!=io_signal[host].spihd_native) native=false;
+    
+    spihost[host]->no_gpio_matrix=native;
+    if (native) {
+        //All SPI native pin selections resolve to 1, so we put that here instead of trying to figure
+        //out which FUNC_GPIOx_xSPIxx to grab; they all are defined to 1 anyway.
+        if (bus_config->mosi_io_num > 0) PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->mosi_io_num], 1);
+        if (bus_config->miso_io_num > 0) PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->miso_io_num], 1);
+        if (bus_config->quadwp_io_num > 0) PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->quadwp_io_num], 1);
+        if (bus_config->quadhd_io_num > 0) PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->quadhd_io_num], 1);
+        if (bus_config->sclk_io_num > 0) PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->sclk_io_num], 1);
+    } else {
+        //Use GPIO 
+        if (bus_config->mosi_io_num>0) {
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->mosi_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(bus_config->mosi_io_num, GPIO_MODE_OUTPUT);
+            gpio_matrix_out(bus_config->mosi_io_num, io_signal[host].spid_out, false, false);
+            gpio_matrix_in(bus_config->mosi_io_num, io_signal[host].spid_in, false);
+        }
+        if (bus_config->miso_io_num>0) {
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->miso_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(bus_config->miso_io_num, GPIO_MODE_INPUT);
+            gpio_matrix_out(bus_config->miso_io_num, io_signal[host].spiq_out, false, false);
+            gpio_matrix_in(bus_config->miso_io_num, io_signal[host].spiq_in, false);
+        }
+        if (bus_config->quadwp_io_num>0) {
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->quadwp_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(bus_config->quadwp_io_num, GPIO_MODE_OUTPUT);
+            gpio_matrix_out(bus_config->quadwp_io_num, io_signal[host].spiwp_out, false, false);
+            gpio_matrix_in(bus_config->quadwp_io_num, io_signal[host].spiwp_in, false);
+        }
+        if (bus_config->quadhd_io_num>0) {
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->quadhd_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(bus_config->quadhd_io_num, GPIO_MODE_OUTPUT);
+            gpio_matrix_out(bus_config->quadhd_io_num, io_signal[host].spihd_out, false, false);
+            gpio_matrix_in(bus_config->quadhd_io_num, io_signal[host].spihd_in, false);
+        }
+        if (bus_config->sclk_io_num>0) {
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[bus_config->sclk_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(bus_config->sclk_io_num, GPIO_MODE_OUTPUT);
+            gpio_matrix_out(bus_config->sclk_io_num, io_signal[host].spiclk_out, false, false);
+        }
+    }
+	periph_module_enable(io_signal[host].module);
+	spihost[host]->hw=io_signal[host].hw;
+
+	if (init > 0) {
+        dma_chan_claimed=spi_lobo_dma_chan_claim(init);
+        if ( !dma_chan_claimed ) {
+        	spi_lobo_periph_free( host );
+            SPI_CHECK(dma_chan_claimed, "dma channel already in use", ESP_ERR_INVALID_STATE);
+        }
+	    spihost[host]->dma_chan = init;
+        //See how many dma descriptors we need and allocate them
+        int dma_desc_ct=(bus_config->max_transfer_sz+SPI_MAX_DMA_LEN-1)/SPI_MAX_DMA_LEN;
+        if (dma_desc_ct==0) dma_desc_ct=1; //default to 4k when max is not given
+        spihost[host]->max_transfer_sz = dma_desc_ct*SPI_MAX_DMA_LEN;
+
+        spihost[host]->dmadesc_tx=heap_caps_malloc(sizeof(lldesc_t)*dma_desc_ct, MALLOC_CAP_DMA);
+        spihost[host]->dmadesc_rx=heap_caps_malloc(sizeof(lldesc_t)*dma_desc_ct, MALLOC_CAP_DMA);
+        if (!spihost[host]->dmadesc_tx || !spihost[host]->dmadesc_rx) goto nomem;
+
+        //Tell common code DMA workaround that our DMA channel is idle. If needed, the code will do a DMA reset.
+        spi_lobo_dmaworkaround_idle(spihost[host]->dma_chan);
+
+        // Reset DMA
+        spihost[host]->hw->dma_conf.val |= SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST;
+        spihost[host]->hw->dma_out_link.start=0;
+        spihost[host]->hw->dma_in_link.start=0;
+        spihost[host]->hw->dma_conf.val &= ~(SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST);
+        spihost[host]->hw->dma_conf.out_data_burst_en=1;
+
+        //Reset timing
+        spihost[host]->hw->ctrl2.val=0;
+
+        //Disable unneeded ints
+        spihost[host]->hw->slave.rd_buf_done=0;
+        spihost[host]->hw->slave.wr_buf_done=0;
+        spihost[host]->hw->slave.rd_sta_done=0;
+        spihost[host]->hw->slave.wr_sta_done=0;
+        spihost[host]->hw->slave.rd_buf_inten=0;
+        spihost[host]->hw->slave.wr_buf_inten=0;
+        spihost[host]->hw->slave.rd_sta_inten=0;
+        spihost[host]->hw->slave.wr_sta_inten=0;
+
+        //Force a transaction done interrupt. This interrupt won't fire yet because we initialized the SPI interrupt as
+        //disabled.  This way, we can just enable the SPI interrupt and the interrupt handler will kick in, handling
+        //any transactions that are queued.
+        spihost[host]->hw->slave.trans_inten=1;
+        spihost[host]->hw->slave.trans_done=1;
+
+		//Select DMA channel.
+		DPORT_SET_PERI_REG_BITS(DPORT_SPI_DMA_CHAN_SEL_REG, 3, init, (host * 2));
+    }
+    return ESP_OK;
+
+nomem:
+	if (spihost[host]) {
+		free(spihost[host]->dmadesc_tx);
+		free(spihost[host]->dmadesc_rx);
+	}
+	free(spihost[host]);
+    spi_lobo_periph_free(host);
+	return ESP_ERR_NO_MEM;
+}
+
+//---------------------------------------------------------------------------
+static esp_err_t spi_lobo_bus_free(spi_lobo_host_device_t host, int dofree)
+{
+	if ((host == TFT_SPI_HOST) || (host > TFT_VSPI_HOST)) return ESP_ERR_NOT_SUPPORTED;  // invalid host
+
+	if (spihost[host] == NULL) return ESP_ERR_INVALID_STATE;  // host not in use
+
+    if (dofree) {
+		for (int x=0; x<NO_DEV; x++) {
+			if (spihost[host]->device[x] != NULL) return ESP_ERR_INVALID_STATE;  // not all devices freed
+		}
+    }
+    if ( spihost[host]->dma_chan > 0 ) {
+        spi_lobo_dma_chan_free ( spihost[host]->dma_chan );
+    }
+    spihost[host]->hw->slave.trans_inten=0;
+    spihost[host]->hw->slave.trans_done=0;
+    spi_lobo_periph_free(host);
+
+    if (dofree) {
+		vSemaphoreDelete(spihost[host]->spi_lobo_bus_mutex);
+	    free(spihost[host]->dmadesc_tx);
+	    free(spihost[host]->dmadesc_rx);
+		free(spihost[host]);
+		spihost[host] = NULL;
+    }
+    return ESP_OK;
+}
+
+//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+esp_err_t spi_lobo_bus_add_device(spi_lobo_host_device_t host, spi_lobo_bus_config_t *bus_config, spi_lobo_device_interface_config_t *dev_config, spi_lobo_device_handle_t *handle)
+{
+	if ((host == TFT_SPI_HOST) || (host > TFT_VSPI_HOST)) return ESP_ERR_NOT_SUPPORTED;  // invalid host
+	
+	if (spihost[host] == NULL) {
+		esp_err_t ret = spi_lobo_bus_initialize(host, bus_config, 1);
+		if (ret) return ret;
+	}
+	
+	int freecs, maxdev;
+    int apbclk=APB_CLK_FREQ;
+
+	if (spihost[host] == NULL) return ESP_ERR_INVALID_STATE;
+
+    if (dev_config->spics_io_num >= 0) {
+		if (!GPIO_IS_VALID_OUTPUT_GPIO(dev_config->spics_io_num)) return  ESP_ERR_INVALID_ARG;
+		if (dev_config->spics_ext_io_num > 0) dev_config->spics_ext_io_num = -1;
+	}
+	else {
+		//if ((dev_config->spics_ext_io_num <= 0) || (!GPIO_IS_VALID_OUTPUT_GPIO(dev_config->spics_ext_io_num))) return ESP_ERR_INVALID_ARG;
+	}
+
+    //ToDo: Check if some other device uses the same 'spics_ext_io_num'
+
+    if (dev_config->clock_speed_hz == 0) return ESP_ERR_INVALID_ARG;
+	if (dev_config->spics_io_num > 0) maxdev = NO_CS;
+	else maxdev = NO_DEV;
+
+    for (freecs=0; freecs<maxdev; freecs++) {
+        //See if this slot is free; reserve if it is by putting a dummy pointer in the slot. We use an atomic compare&swap to make this thread-safe.
+        if (__sync_bool_compare_and_swap(&spihost[host]->device[freecs], NULL, (spi_lobo_device_t *)1)) break;
+    }
+    if (freecs == maxdev) return ESP_ERR_NOT_FOUND;
+
+    // The hardware looks like it would support this, but actually setting cs_ena_pretrans when transferring in full
+    // duplex mode does absolutely nothing on the ESP32.
+    if ((dev_config->cs_ena_pretrans != 0) && (dev_config->flags & LB_SPI_DEVICE_HALFDUPLEX)) return ESP_ERR_INVALID_ARG;
+
+    // Speeds >=40MHz over GPIO matrix needs a dummy cycle, but these don't work for full-duplex connections.
+    if (((dev_config->flags & LB_SPI_DEVICE_HALFDUPLEX)==0) && (dev_config->clock_speed_hz > ((apbclk*2)/5)) && (!spihost[host]->no_gpio_matrix)) return ESP_ERR_INVALID_ARG;
+
+    //Allocate memory for device
+    spi_lobo_device_t *dev=malloc(sizeof(spi_lobo_device_t));
+    if (dev==NULL) return ESP_ERR_NO_MEM;
+
+    memset(dev, 0, sizeof(spi_lobo_device_t));
+    spihost[host]->device[freecs]=dev;
+
+    if (dev_config->duty_cycle_pos==0) dev_config->duty_cycle_pos=128;
+    dev->host=spihost[host];
+	dev->host_dev = host;
+
+    //We want to save a copy of the dev config in the dev struct.
+    memcpy(&dev->cfg, dev_config, sizeof(spi_lobo_device_interface_config_t));
+    //We want to save a copy of the bus config in the dev struct.
+    memcpy(&dev->bus_config, bus_config, sizeof(spi_lobo_bus_config_t));
+
+    //Set CS pin, CS options
+    if (dev_config->spics_io_num > 0) {
+        if (spihost[host]->no_gpio_matrix &&dev_config->spics_io_num == io_signal[host].spics0_native && freecs==0) {
+            //Again, the cs0s for all SPI peripherals map to pin mux source 1, so we use that instead of a define.
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[dev_config->spics_io_num], 1);
+        } else {
+            //Use GPIO matrix
+            PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[dev_config->spics_io_num], PIN_FUNC_GPIO);
+            gpio_set_direction(dev_config->spics_io_num, GPIO_MODE_OUTPUT);
+            gpio_matrix_out(dev_config->spics_io_num, io_signal[host].spics_out[freecs], false, false);
+        }
+    }
+    else if (dev_config->spics_ext_io_num >= 0) {
+		gpio_set_direction(dev_config->spics_ext_io_num, GPIO_MODE_OUTPUT);
+		gpio_set_level(dev_config->spics_ext_io_num, 1);
+	}
+    if (dev_config->flags & LB_SPI_DEVICE_CLK_AS_CS) {
+        spihost[host]->hw->pin.master_ck_sel |= (1<<freecs);
+    } else {
+        spihost[host]->hw->pin.master_ck_sel &= (1<<freecs);
+    }
+    if (dev_config->flags & LB_SPI_DEVICE_POSITIVE_CS) {
+        spihost[host]->hw->pin.master_cs_pol |= (1<<freecs);
+    } else {
+        spihost[host]->hw->pin.master_cs_pol &= (1<<freecs);
+    }
+
+    *handle = dev;
+    return ESP_OK;
+}
+
+//-------------------------------------------------------------------
+esp_err_t spi_lobo_bus_remove_device(spi_lobo_device_handle_t handle)
+{
+    int x;
+    if (handle == NULL) return ESP_ERR_INVALID_ARG;
+
+    //Remove device from list of csses and free memory
+    for (x=0; x<NO_DEV; x++) {
+        if (handle->host->device[x] == handle) handle->host->device[x]=NULL;
+    }
+	
+	// Check if all devices are removed from this host and free the bus if yes
+	for (x=0; x<NO_DEV; x++) {
+		if (spihost[handle->host_dev]->device[x] !=NULL) break;
+	}
+	if (x == NO_DEV) {
+		free(handle);
+		spi_lobo_bus_free(handle->host_dev, 1);
+	}
+	else free(handle);
+
+	return ESP_OK;
+}
+
+//-----------------------------------------------------------------
+static int IRAM_ATTR spi_freq_for_pre_n(int fapb, int pre, int n) {
+    return (fapb / (pre * n));
+}
+
+/*
+ * Set the SPI clock to a certain frequency. Returns the effective frequency set, which may be slightly
+ * different from the requested frequency.
+ */
+//-----------------------------------------------------------------------------------
+static int IRAM_ATTR spi_set_clock(spi_dev_t *hw, int fapb, int hz, int duty_cycle) {
+   int pre, n, h, l, eff_clk;
+
+    //In hw, n, h and l are 1-64, pre is 1-8K. Value written to register is one lower than used value.
+    if (hz>((fapb/4)*3)) {
+        //Using Fapb directly will give us the best result here.
+        hw->clock.clkcnt_l=0;
+        hw->clock.clkcnt_h=0;
+        hw->clock.clkcnt_n=0;
+        hw->clock.clkdiv_pre=0;
+        hw->clock.clk_equ_sysclk=1;
+        eff_clk=fapb;
+    } else {
+        //For best duty cycle resolution, we want n to be as close to 32 as possible, but
+        //we also need a pre/n combo that gets us as close as possible to the intended freq.
+        //To do this, we bruteforce n and calculate the best pre to go along with that.
+        //If there's a choice between pre/n combos that give the same result, use the one
+        //with the higher n.
+        int bestn=-1;
+        int bestpre=-1;
+        int besterr=0;
+        int errval;
+        for (n=1; n<=64; n++) {
+            //Effectively, this does pre=round((fapb/n)/hz).
+            pre=((fapb/n)+(hz/2))/hz;
+            if (pre<=0) pre=1;
+            if (pre>8192) pre=8192;
+            errval=abs(spi_freq_for_pre_n(fapb, pre, n)-hz);
+            if (bestn==-1 || errval<=besterr) {
+                besterr=errval;
+                bestn=n;
+                bestpre=pre;
+            }
+        }
+
+        n=bestn;
+        pre=bestpre;
+        l=n;
+        //This effectively does round((duty_cycle*n)/256)
+        h=(duty_cycle*n+127)/256;
+        if (h<=0) h=1;
+
+        hw->clock.clk_equ_sysclk=0;
+        hw->clock.clkcnt_n=n-1;
+        hw->clock.clkdiv_pre=pre-1;
+        hw->clock.clkcnt_h=h-1;
+        hw->clock.clkcnt_l=l-1;
+        eff_clk=spi_freq_for_pre_n(fapb, pre, n);
+    }
+    return eff_clk;
+}
+
+
+
+//------------------------------------------------------------------------------------
+esp_err_t IRAM_ATTR spi_lobo_device_select(spi_lobo_device_handle_t handle, int force)
+{
+	if (handle == NULL) return ESP_ERR_INVALID_ARG;
+
+	if ((handle->cfg.selected == 1) && (!force)) return ESP_OK;  // already selected
+
+	int i;
+	spi_lobo_host_t *host=(spi_lobo_host_t*)handle->host;
+
+	// find device's host bus
+	for (i=0; i<NO_DEV; i++) {
+		if (host->device[i] == handle) break;
+	}
+	if (i == NO_DEV) return ESP_ERR_INVALID_ARG;
+
+	if (!(xSemaphoreTake(host->spi_lobo_bus_mutex, SPI_SEMAPHORE_WAIT))) return ESP_ERR_INVALID_STATE;
+
+	// Check if previously used device's bus device is the same
+	if (memcmp(&host->cur_bus_config, &handle->bus_config, sizeof(spi_lobo_bus_config_t)) != 0) {
+		// device has different bus configuration, we need to reconfigure the bus
+		esp_err_t err = spi_lobo_bus_free(1, 0);
+		if (err) {
+			xSemaphoreGive(host->spi_lobo_bus_mutex);
+			return err;
+		}
+		err = spi_lobo_bus_initialize(i, &handle->bus_config, -1);
+		if (err) {
+			xSemaphoreGive(host->spi_lobo_bus_mutex);
+			return err;
+		}
+	}
+
+	//Reconfigure according to device settings, but only if the device changed or forced.
+	if ((force) || (host->device[host->cur_device] != handle)) {
+	    //Assumes a hardcoded 80MHz Fapb for now. ToDo: figure out something better once we have clock scaling working.
+		int apbclk=APB_CLK_FREQ;
+
+	    //Speeds >=40MHz over GPIO matrix needs a dummy cycle, but these don't work for full-duplex connections.
+	    if (((handle->cfg.flags & LB_SPI_DEVICE_HALFDUPLEX) == 0) && (handle->cfg.clock_speed_hz > ((apbclk*2)/5)) && (!host->no_gpio_matrix)) {
+	    	// set speed to 32 MHz
+	    	handle->cfg.clock_speed_hz = (apbclk*2)/5;
+	    }
+
+		int effclk=spi_set_clock(host->hw, apbclk, handle->cfg.clock_speed_hz, handle->cfg.duty_cycle_pos);
+		//Configure bit order
+		host->hw->ctrl.rd_bit_order=(handle->cfg.flags & LB_SPI_DEVICE_RXBIT_LSBFIRST)?1:0;
+		host->hw->ctrl.wr_bit_order=(handle->cfg.flags & LB_SPI_DEVICE_TXBIT_LSBFIRST)?1:0;
+		
+		//Configure polarity
+        //SPI iface needs to be configured for a delay in some cases.
+		int nodelay=0;
+        int extra_dummy=0;
+        if (host->no_gpio_matrix) {
+            if (effclk >= apbclk/2) {
+                nodelay=1;
+            }
+        } else {
+            if (effclk >= apbclk/2) {
+                nodelay=1;
+                extra_dummy=1;          //Note: This only works on half-duplex connections. spi_lobo_bus_add_device checks for this.
+            } else if (effclk >= apbclk/4) {
+                nodelay=1;
+            }
+        }
+		if (handle->cfg.mode==0) {
+			host->hw->pin.ck_idle_edge=0;
+			host->hw->user.ck_out_edge=0;
+			host->hw->ctrl2.miso_delay_mode=nodelay?0:2;
+		} else if (handle->cfg.mode==1) {
+			host->hw->pin.ck_idle_edge=0;
+			host->hw->user.ck_out_edge=1;
+			host->hw->ctrl2.miso_delay_mode=nodelay?0:1;
+		} else if (handle->cfg.mode==2) {
+			host->hw->pin.ck_idle_edge=1;
+			host->hw->user.ck_out_edge=1;
+			host->hw->ctrl2.miso_delay_mode=nodelay?0:1;
+		} else if (handle->cfg.mode==3) {
+			host->hw->pin.ck_idle_edge=1;
+			host->hw->user.ck_out_edge=0;
+			host->hw->ctrl2.miso_delay_mode=nodelay?0:2;
+		}
+
+		//Configure bit sizes, load addr and command
+		host->hw->user.usr_dummy=(handle->cfg.dummy_bits+extra_dummy)?1:0;
+		host->hw->user.usr_addr=(handle->cfg.address_bits)?1:0;
+		host->hw->user.usr_command=(handle->cfg.command_bits)?1:0;
+		host->hw->user1.usr_addr_bitlen=handle->cfg.address_bits-1;
+		host->hw->user1.usr_dummy_cyclelen=handle->cfg.dummy_bits+extra_dummy-1;
+		host->hw->user2.usr_command_bitlen=handle->cfg.command_bits-1;
+		//Configure misc stuff
+		host->hw->user.doutdin=(handle->cfg.flags & LB_SPI_DEVICE_HALFDUPLEX)?0:1;
+		host->hw->user.sio=(handle->cfg.flags & LB_SPI_DEVICE_3WIRE)?1:0;
+
+		host->hw->ctrl2.setup_time=handle->cfg.cs_ena_pretrans-1;
+		host->hw->user.cs_setup=handle->cfg.cs_ena_pretrans?1:0;
+		host->hw->ctrl2.hold_time=handle->cfg.cs_ena_posttrans-1;
+		host->hw->user.cs_hold=(handle->cfg.cs_ena_posttrans)?1:0;
+
+		//Configure CS pin
+		host->hw->pin.cs0_dis=(i==0)?0:1;
+		host->hw->pin.cs1_dis=(i==1)?0:1;
+		host->hw->pin.cs2_dis=(i==2)?0:1;
+		
+		host->cur_device = i;
+	}
+
+	if ((handle->cfg.spics_io_num < 0) && (handle->cfg.spics_ext_io_num > 0)) {
+		gpio_set_level(handle->cfg.spics_ext_io_num, 0);
+	}
+
+	handle->cfg.selected = 1;
+
+	return ESP_OK;
+}
+
+//---------------------------------------------------------------------------
+esp_err_t IRAM_ATTR spi_lobo_device_deselect(spi_lobo_device_handle_t handle)
+{
+	if (handle == NULL) return ESP_ERR_INVALID_ARG;
+
+	if (handle->cfg.selected == 0) return ESP_OK;  // already deselected
+
+	int i;
+	spi_lobo_host_t *host=(spi_lobo_host_t*)handle->host;
+
+	for (i=0; i<NO_DEV; i++) {
+		if (host->device[i] == handle) break;
+	}
+	if (i == NO_DEV) return ESP_ERR_INVALID_ARG;
+	
+	if (host->device[host->cur_device] == handle) {
+		if ((handle->cfg.spics_io_num < 0) && (handle->cfg.spics_ext_io_num > 0)) {
+			gpio_set_level(handle->cfg.spics_ext_io_num, 1);
+		}
+	}
+
+	handle->cfg.selected = 0;
+	xSemaphoreGive(host->spi_lobo_bus_mutex);
+
+	return ESP_OK;
+}
+
+//--------------------------------------------------------------------------------
+esp_err_t IRAM_ATTR spi_lobo_device_TakeSemaphore(spi_lobo_device_handle_t handle)
+{
+	if (!(xSemaphoreTake(handle->host->spi_lobo_bus_mutex, SPI_SEMAPHORE_WAIT))) return ESP_ERR_INVALID_STATE;
+	else return ESP_OK;
+}
+
+//---------------------------------------------------------------------------
+void IRAM_ATTR spi_lobo_device_GiveSemaphore(spi_lobo_device_handle_t handle)
+{
+	xSemaphoreTake(handle->host->spi_lobo_bus_mutex, portMAX_DELAY);
+}
+
+//----------------------------------------------------------
+uint32_t spi_lobo_get_speed(spi_lobo_device_handle_t handle)
+{
+	spi_lobo_host_t *host=(spi_lobo_host_t*)handle->host;
+	uint32_t speed = 0;
+	if (spi_lobo_device_select(handle, 0) == ESP_OK) {
+		if (host->hw->clock.clk_equ_sysclk == 1) speed = 80000000;
+		else speed =  80000000/(host->hw->clock.clkdiv_pre+1)/(host->hw->clock.clkcnt_n+1);
+	}
+	spi_lobo_device_deselect(handle);
+	return speed;
+}
+
+//--------------------------------------------------------------------------
+uint32_t spi_lobo_set_speed(spi_lobo_device_handle_t handle, uint32_t speed)
+{
+	spi_lobo_host_t *host=(spi_lobo_host_t*)handle->host;
+	uint32_t newspeed = 0;
+	if (spi_lobo_device_select(handle, 0) == ESP_OK) {
+		spi_lobo_device_deselect(handle);
+		handle->cfg.clock_speed_hz = speed;
+		if (spi_lobo_device_select(handle, 1) == ESP_OK) {
+			if (host->hw->clock.clk_equ_sysclk == 1) newspeed = 80000000;
+			else newspeed =  80000000/(host->hw->clock.clkdiv_pre+1)/(host->hw->clock.clkcnt_n+1);
+		}
+	}
+	spi_lobo_device_deselect(handle);
+	
+	return newspeed;
+}
+
+//-------------------------------------------------------------
+bool spi_lobo_uses_native_pins(spi_lobo_device_handle_t handle)
+{
+	return handle->host->no_gpio_matrix;
+}
+
+//-------------------------------------------------------------------
+void spi_lobo_get_native_pins(int host, int *sdi, int *sdo, int *sck)
+{
+	*sdo = io_signal[host].spid_native;
+	*sdi = io_signal[host].spiq_native;
+	*sck = io_signal[host].spiclk_native;
+}
+
+/*
+When using  'spi_lobo_transfer_data' function we can have several scenarios:
+
+A: Send only      (trans->rxlength = 0)
+B: Receive only   (trans->txlength = 0)
+C: Send & receive (trans->txlength > 0 & trans->rxlength > 0)
+D: No operation   (trans->txlength = 0 & trans->rxlength = 0)
+
+*/
+//----------------------------------------------------------------------------------------------------------
+esp_err_t IRAM_ATTR spi_lobo_transfer_data(spi_lobo_device_handle_t handle, spi_lobo_transaction_t *trans) {
+	if (!handle) return ESP_ERR_INVALID_ARG;
+
+	// *** For now we can only handle 8-bit bytes transmission
+	if (((trans->length % 8) != 0) || ((trans->rxlength % 8) != 0)) return ESP_ERR_INVALID_ARG;
+
+	spi_lobo_host_t *host=(spi_lobo_host_t*)handle->host;
+	esp_err_t ret;
+	uint8_t do_deselect = 0;
+    const uint8_t *txbuffer = NULL;
+	uint8_t *rxbuffer = NULL;
+
+	if (trans->flags & LB_SPI_TRANS_USE_TXDATA) {
+        // Send data from 'trans->tx_data'
+		txbuffer=(uint8_t*)&trans->tx_data[0];
+	} else {
+        // Send data from 'trans->tx_buffer'
+		txbuffer=(uint8_t*)trans->tx_buffer;
+	}
+	if (trans->flags & LB_SPI_TRANS_USE_RXDATA) {
+        // Receive data to 'trans->rx_data'
+		rxbuffer=(uint8_t*)&trans->rx_data[0];
+	} else {
+        // Receive data to 'trans->rx_buffer'
+		rxbuffer=(uint8_t*)trans->rx_buffer;
+	}
+
+	// ** Set transmit & receive length in bytes
+	uint32_t txlen = trans->length / 8;
+	uint32_t rxlen = trans->rxlength / 8;
+
+	if (txbuffer == NULL) txlen = 0;
+	if (rxbuffer == NULL) rxlen = 0;
+	if ((rxlen == 0) && (txlen == 0)) {
+        // ** NOTHING TO SEND or RECEIVE, return
+        return ESP_ERR_INVALID_ARG;
+    }
+
+    // If using 'trans->tx_data' and/or 'trans->rx_data', maximum 4 bytes can be sent/received
+	if ((txbuffer == &trans->tx_data[0]) && (txlen > 4)) return ESP_ERR_INVALID_ARG;
+	if ((rxbuffer == &trans->rx_data[0]) && (rxlen > 4)) return ESP_ERR_INVALID_ARG;
+
+	// --- Wait for SPI bus ready ---
+	while (host->hw->cmd.usr);
+
+    // ** If the device was not selected, select it
+	if (handle->cfg.selected == 0) {
+		ret = spi_lobo_device_select(handle, 0);
+		if (ret) return ret;
+		do_deselect = 1;     // We will deselect the device after the operation !
+	}
+
+	// ** Call pre-transmission callback, if any
+	if (handle->cfg.pre_cb) handle->cfg.pre_cb(trans);
+
+    // Test if operating in full duplex mode
+	uint8_t duplex = 1;
+	if (handle->cfg.flags & LB_SPI_DEVICE_HALFDUPLEX) duplex = 0; // Half duplex mode !
+
+    uint32_t bits, rdbits;
+	uint32_t wd;
+	uint8_t bc, rdidx;
+	uint32_t rdcount = rxlen;	// Total number of bytes to read
+	uint32_t count = 0;			// number of bytes transmitted
+	uint32_t rd_read = 0;		// Number of bytes read so far
+
+	host->hw->user.usr_mosi_highpart = 0; // use the whole spi buffer
+
+    // ** Check if address phase will be used
+	host->hw->user2.usr_command_value=trans->command;
+	if (handle->cfg.address_bits>32) {
+		host->hw->addr=trans->address >> 32;
+		host->hw->slv_wr_status=trans->address & 0xffffffff;
+	} else {
+		host->hw->addr=trans->address & 0xffffffff;
+	}
+
+	// Check if we have to transmit some data
+	if (txlen > 0) {
+		host->hw->user.usr_mosi = 1;
+		uint8_t idx;
+		bits = 0;				// remaining bits to send
+		idx = 0;				// index to spi hw data_buf (16 32-bit words, 64 bytes, 512 bits)
+
+        // ** Transmit 'txlen' bytes
+		while (count < txlen) {
+			wd = 0;
+			for (bc=0;bc<32;bc+=8) {
+				wd |= (uint32_t)txbuffer[count] << bc;
+				count++;                    // Increment sent data count
+				bits += 8;                  // Increment bits count
+				if (count == txlen) break;  // If all transmit data pushed to hw spi buffer break from the loop
+			}
+			host->hw->data_buf[idx] = wd;
+			idx++;
+			if (idx == 16) {
+				// hw SPI buffer full (all 64 bytes filled, START THE TRANSSACTION
+				host->hw->mosi_dlen.usr_mosi_dbitlen=bits-1;            // Set mosi dbitlen
+
+				if ((duplex) && (rdcount > 0)) {
+                    // In full duplex mode we are receiving while sending !
+					host->hw->miso_dlen.usr_miso_dbitlen = bits-1;      // Set miso dbitlen
+					host->hw->user.usr_miso = 1;
+				}
+				else {
+					host->hw->miso_dlen.usr_miso_dbitlen = 0;           // In half duplex mode nothing will be received
+					host->hw->user.usr_miso = 0;
+				}
+
+				// ** Start the transaction ***
+				host->hw->cmd.usr=1;
+                // Wait the transaction to finish
+				while (host->hw->cmd.usr);
+
+				if ((duplex) && (rdcount > 0)) {
+					// *** in full duplex mode transfer received data to input buffer ***
+					rdidx = 0;
+			    	while (bits > 0) {
+						wd = host->hw->data_buf[rdidx];
+						rdidx++;
+						for (bc=0;bc<32;bc+=8) { // get max 4 bytes
+							rxbuffer[rd_read++] = (uint8_t)((wd >> bc) & 0xFF);
+							rdcount--;
+							bits -= 8;
+							if (rdcount == 0) {
+								bits = 0;
+								break; // Finished reading data
+							}
+						}
+			    	}
+				}
+				bits = 0;   // nothing in hw spi buffer yet
+				idx = 0;    // start from the beginning of the hw spi buffer
+			}
+		}
+		// *** All transmit data are sent or pushed to hw spi buffer
+		// bits > 0  IF THERE ARE SOME DATA STILL WAITING IN THE HW SPI TRANSMIT BUFFER
+		if (bits > 0) {
+			// ** WE HAVE SOME DATA IN THE HW SPI TRANSMIT BUFFER
+			host->hw->mosi_dlen.usr_mosi_dbitlen = bits-1;          // Set mosi dbitlen
+
+			if ((duplex) && (rdcount > 0)) {
+                // In full duplex mode we are receiving while sending !
+				host->hw->miso_dlen.usr_miso_dbitlen = bits-1;      // Set miso dbitlen
+				host->hw->user.usr_miso = 1;
+			}
+			else {
+				host->hw->miso_dlen.usr_miso_dbitlen = 0;           // In half duplex mode nothing will be received
+				host->hw->user.usr_miso = 0;
+			}
+
+			// ** Start the transaction ***
+			host->hw->cmd.usr=1;
+            // Wait the transaction to finish
+			while (host->hw->cmd.usr);
+
+			if ((duplex) && (rdcount > 0)) {
+                // *** in full duplex mode transfer received data to input buffer ***
+				rdidx = 0;
+		    	while (bits > 0) {
+					wd = host->hw->data_buf[rdidx];
+					rdidx++;
+					for (bc=0;bc<32;bc+=8) { // get max 4 bytes
+						rxbuffer[rd_read++] = (uint8_t)((wd >> bc) & 0xFF);
+						rdcount--;
+						bits -= 8;
+						if (bits == 0) break;
+						if (rdcount == 0) {
+							bits = 0;
+							break; // Finished reading data
+						}
+					}
+		    	}
+			}
+		}
+		//if (duplex) rdcount = 0;  // In duplex mode receive only as many bytes as was transmitted
+	}
+
+	// ------------------------------------------------------------------------
+	// *** If rdcount = 0 we have nothing to receive and we exit the function
+    //     This is true if no data receive was requested,
+    //     or all the data was received in Full duplex mode during the transmission
+	// ------------------------------------------------------------------------
+	if (rdcount > 0) {
+		// ----------------------------------------------------------------------------------------------------------------
+		// *** rdcount > 0, we have to receive some data
+		//     This is true if we operate in Half duplex mode when receiving after transmission is done,
+		//     or not all data was received in Full duplex mode during the transmission (trans->rxlength > trans->txlength)
+		// ----------------------------------------------------------------------------------------------------------------
+		host->hw->user.usr_mosi = 0;  // do not send
+		host->hw->user.usr_miso = 1;  // do receive
+		while (rdcount > 0) {
+			if (rdcount <= 64) rdbits = rdcount * 8;
+			else rdbits = 64 * 8;
+
+			// Load receive buffer
+			host->hw->mosi_dlen.usr_mosi_dbitlen=0;
+			host->hw->miso_dlen.usr_miso_dbitlen=rdbits-1;
+
+			// ** Start the transaction ***
+			host->hw->cmd.usr=1;
+			// Wait the transaction to finish
+			while (host->hw->cmd.usr);
+
+			// *** transfer received data to input buffer ***
+			rdidx = 0;
+			while (rdbits > 0) {
+				wd = host->hw->data_buf[rdidx];
+				rdidx++;
+				for (bc=0;bc<32;bc+=8) {
+					rxbuffer[rd_read++] = (uint8_t)((wd >> bc) & 0xFF);
+					rdcount--;
+					rdbits -= 8;
+					if (rdcount == 0) {
+						rdbits = 0;
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// ** Call post-transmission callback, if any
+	if (handle->cfg.post_cb) handle->cfg.post_cb(trans);
+
+	if (do_deselect) {
+        // Spi device was selected in this function, we have to deselect it now
+		ret = spi_lobo_device_deselect(handle);
+		if (ret) return ret;
+	}
+
+	return ESP_OK;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/spidriver/spi_master_lobo.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,355 @@
+// Copyright 2010-2016 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+#ifndef _DRIVER_SPI_MASTER_LOBO_H_
+#define _DRIVER_SPI_MASTER_LOBO_H_
+
+#include "esp_err.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+#include "soc/spi_struct.h"
+
+#include "esp_intr.h"
+#include "esp_intr_alloc.h"
+#include "rom/lldesc.h"
+
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+//Maximum amount of bytes that can be put in one DMA descriptor
+#define SPI_MAX_DMA_LEN (4096-4)
+
+/**
+ * @brief Enum with the three SPI peripherals that are software-accessible in it
+ */
+typedef enum {
+    TFT_SPI_HOST=0,                 ///< SPI1, SPI; Cannot be used in this driver!
+    TFT_HSPI_HOST=1,                ///< SPI2, HSPI
+    TFT_VSPI_HOST=2                 ///< SPI3, VSPI
+} spi_lobo_host_device_t;
+
+
+/**
+ * @brief This is a configuration structure for a SPI bus.
+ *
+ * You can use this structure to specify the GPIO pins of the bus. Normally, the driver will use the
+ * GPIO matrix to route the signals. An exception is made when all signals either can be routed through 
+ * the IO_MUX or are -1. In that case, the IO_MUX is used, allowing for >40MHz speeds.
+ */
+typedef struct {
+    int mosi_io_num;                ///< GPIO pin for Master Out Slave In (=spi_d) signal, or -1 if not used.
+    int miso_io_num;                ///< GPIO pin for Master In Slave Out (=spi_q) signal, or -1 if not used.
+    int sclk_io_num;                ///< GPIO pin for Spi CLocK signal, or -1 if not used.
+    int quadwp_io_num;              ///< GPIO pin for WP (Write Protect) signal which is used as D2 in 4-bit communication modes, or -1 if not used.
+    int quadhd_io_num;              ///< GPIO pin for HD (HolD) signal which is used as D3 in 4-bit communication modes, or -1 if not used.
+    int max_transfer_sz;            ///< Maximum transfer size, in bytes. Defaults to 4094 if 0.
+} spi_lobo_bus_config_t;
+
+
+#define LB_SPI_DEVICE_TXBIT_LSBFIRST          (1<<0)  ///< Transmit command/address/data LSB first instead of the default MSB first
+#define LB_SPI_DEVICE_RXBIT_LSBFIRST          (1<<1)  ///< Receive data LSB first instead of the default MSB first
+#define LB_SPI_DEVICE_BIT_LSBFIRST            (SPI_TXBIT_LSBFIRST|SPI_RXBIT_LSBFIRST); ///< Transmit and receive LSB first
+#define LB_SPI_DEVICE_3WIRE                   (1<<2)  ///< Use spiq for both sending and receiving data
+#define LB_SPI_DEVICE_POSITIVE_CS             (1<<3)  ///< Make CS positive during a transaction instead of negative
+#define LB_SPI_DEVICE_HALFDUPLEX              (1<<4)  ///< Transmit data before receiving it, instead of simultaneously
+#define LB_SPI_DEVICE_CLK_AS_CS               (1<<5)  ///< Output clock on CS line if CS is active
+
+#define SPI_ERR_OTHER_CONFIG 7001
+
+typedef struct spi_lobo_transaction_t spi_lobo_transaction_t;
+typedef void(*spi_lobo_transaction_cb_t)(spi_lobo_transaction_t *trans);
+
+/**
+ * @brief This is a configuration for a SPI slave device that is connected to one of the SPI buses.
+ */
+typedef struct {
+    uint8_t command_bits;               ///< Amount of bits in command phase (0-16)
+    uint8_t address_bits;               ///< Amount of bits in address phase (0-64)
+    uint8_t dummy_bits;                 ///< Amount of dummy bits to insert between address and data phase
+    uint8_t mode;                       ///< SPI mode (0-3)
+    uint8_t duty_cycle_pos;             ///< Duty cycle of positive clock, in 1/256th increments (128 = 50%/50% duty). Setting this to 0 (=not setting it) is equivalent to setting this to 128.
+    uint8_t cs_ena_pretrans;            ///< Amount of SPI bit-cycles the cs should be activated before the transmission (0-16). This only works on half-duplex transactions.
+    uint8_t cs_ena_posttrans;           ///< Amount of SPI bit-cycles the cs should stay active after the transmission (0-16)
+    int clock_speed_hz;                 ///< Clock speed, in Hz
+    int spics_io_num;                   ///< CS GPIO pin for this device, handled by hardware; set to -1 if not used
+    int spics_ext_io_num;               ///< CS GPIO pin for this device, handled by software (spi_lobo_device_select/spi_lobo_device_deselect); only used if spics_io_num=-1
+    uint32_t flags;                     ///< Bitwise OR of LB_SPI_DEVICE_* flags
+    spi_lobo_transaction_cb_t pre_cb;   ///< Callback to be called before a transmission is started. This callback from 'spi_lobo_transfer_data' function.
+    spi_lobo_transaction_cb_t post_cb;  ///< Callback to be called after a transmission has completed. This callback from 'spi_lobo_transfer_data' function.
+    uint8_t selected;                   ///< **INTERNAL** 1 if the device's CS pin is active
+} spi_lobo_device_interface_config_t;
+
+
+#define LB_SPI_TRANS_MODE_DIO            (1<<0)  ///< Transmit/receive data in 2-bit mode
+#define LB_SPI_TRANS_MODE_QIO            (1<<1)  ///< Transmit/receive data in 4-bit mode
+#define LB_SPI_TRANS_MODE_DIOQIO_ADDR    (1<<2)  ///< Also transmit address in mode selected by SPI_MODE_DIO/SPI_MODE_QIO
+#define LB_SPI_TRANS_USE_RXDATA          (1<<3)  ///< Receive into rx_data member of spi_lobo_transaction_t instead into memory at rx_buffer.
+#define LB_SPI_TRANS_USE_TXDATA          (1<<4)  ///< Transmit tx_data member of spi_lobo_transaction_t instead of data at tx_buffer. Do not set tx_buffer when using this.
+
+/**
+ * This structure describes one SPI transmission
+ */
+struct spi_lobo_transaction_t {
+    uint32_t flags;                 ///< Bitwise OR of LB_SPI_TRANS_* flags
+    uint16_t command;               ///< Command data. Specific length was given when device was added to the bus.
+    uint64_t address;               ///< Address. Specific length was given when device was added to the bus.
+    size_t length;                  ///< Total data length to be transmitted to the device, in bits; if 0, no data is transmitted
+    size_t rxlength;                ///< Total data length to be received from the device, in bits; if 0, no data is received
+    void *user;                     ///< User-defined variable. Can be used to store eg transaction ID or data to be used by pre_cb and/or post_cb callbacks.
+    union {
+        const void *tx_buffer;      ///< Pointer to transmit buffer, or NULL for no MOSI phase
+        uint8_t tx_data[4];         ///< If SPI_USE_TXDATA is set, data set here is sent directly from this variable.
+    };
+    union {
+        void *rx_buffer;            ///< Pointer to receive buffer, or NULL for no MISO phase
+        uint8_t rx_data[4];         ///< If SPI_USE_RXDATA is set, data is received directly to this variable
+    };
+};
+
+#define NO_CS 3					    // Number of CS pins per SPI host
+#define NO_DEV 6				    // Number of spi devices per SPI host; more than 3 devices can be attached to the same bus if using software CS's
+#define SPI_SEMAPHORE_WAIT 2000     // Time in ms to wait for SPI mutex
+
+typedef struct spi_lobo_device_t spi_lobo_device_t;
+
+typedef struct {
+    spi_lobo_device_t *device[NO_DEV];
+    intr_handle_t intr;
+    spi_dev_t *hw;
+    //spi_lobo_transaction_t *cur_trans;
+    int cur_device;
+    lldesc_t *dmadesc_tx;
+    lldesc_t *dmadesc_rx;
+    bool no_gpio_matrix;
+    int dma_chan;
+    int max_transfer_sz;
+    QueueHandle_t spi_lobo_bus_mutex;
+    spi_lobo_bus_config_t cur_bus_config;
+} spi_lobo_host_t;
+
+struct spi_lobo_device_t {
+    spi_lobo_device_interface_config_t cfg;
+    spi_lobo_host_t *host;
+    spi_lobo_bus_config_t bus_config;
+	spi_lobo_host_device_t host_dev;
+};
+
+typedef spi_lobo_device_t* spi_lobo_device_handle_t;  ///< Handle for a device on a SPI bus
+typedef spi_lobo_host_t* spi_lobo_host_handle_t;
+typedef spi_lobo_device_interface_config_t* spi_lobo_device_interface_config_handle_t;
+
+
+/**
+ * @brief Add a device. This allocates a CS line for the device, allocates memory for the device structure and hooks
+ *        up the CS pin to whatever is specified.
+ *
+ * This initializes the internal structures for a device, plus allocates a CS pin on the indicated SPI master
+ * peripheral and routes it to the indicated GPIO. All SPI master devices have three hw CS pins and can thus control
+ * up to three devices. Software handled CS pin can also be used for additional devices on the same SPI bus.
+ * 
+ * ### If selected SPI host device bus is not yet initialized, it is initialized first with 'bus_config' function ###
+ *
+ * @note While in general, speeds up to 80MHz on the dedicated SPI pins and 40MHz on GPIO-matrix-routed pins are
+ *       supported, full-duplex transfers routed over the GPIO matrix only support speeds up to 26MHz.
+ *
+ * @param host SPI peripheral to allocate device on (HSPI or VSPI)
+ * @param dev_config SPI interface protocol config for the device
+ * @param bus_config Pointer to a spi_lobo_bus_config_t struct specifying how the host device bus should be initialized
+ * @param handle Pointer to variable to hold the device handle
+ * @return
+ *         - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *         - ESP_ERR_NOT_FOUND     if host doesn't have any free CS slots
+ *         - ESP_ERR_NO_MEM        if out of memory
+ *         - ESP_OK                on success
+ */
+esp_err_t spi_lobo_bus_add_device(spi_lobo_host_device_t host, spi_lobo_bus_config_t *bus_config, spi_lobo_device_interface_config_t *dev_config, spi_lobo_device_handle_t *handle);
+
+/**
+ * @brief Remove a device from the SPI bus. If after removal no other device is attached to the spi bus device, it is freed.
+ *
+ * @param handle Device handle to free
+ * @return
+ *         - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *         - ESP_ERR_INVALID_STATE if device already is freed
+ *         - ESP_OK                on success
+ */
+esp_err_t spi_lobo_bus_remove_device(spi_lobo_device_handle_t handle);
+
+
+/**
+ * @brief Return the actuall SPI bus speed for the spi device in Hz
+ *
+ * Some frequencies cannot be set, for example 30000000 will actually set SPI clock to 26666666 Hz
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * 
+ * @return 
+ *         - actuall SPI clock
+ */
+uint32_t spi_lobo_get_speed(spi_lobo_device_handle_t handle);
+
+/**
+ * @brief Set the new clock speed for the device, return the actuall SPI bus speed set, in Hz
+ *        This function can be used after the device is initialized
+ *
+ * Some frequencies cannot be set, for example 30000000 will actually set SPI clock to 26666666 Hz
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * @param speed  New device spi clock to be set in Hz
+ * 
+ * @return 
+ *         - actuall SPI clock
+ *         - 0 if speed cannot be set
+ */
+uint32_t spi_lobo_set_speed(spi_lobo_device_handle_t handle, uint32_t speed);
+
+/**
+ * @brief Select spi device for transmission
+ *
+ * It configures spi bus with selected spi device parameters if previously selected device was different than the current
+ * If device's spics_io_num=-1 and spics_ext_io_num > 0 'spics_ext_io_num' pin is set to active state (low)
+ * 
+ * spi bus device's semaphore is taken before selecting the device
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * @param force  configure spi bus even if the previous device was the same
+ * 
+ * @return 
+ *         - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *         - ESP_OK                on success
+ */
+esp_err_t spi_lobo_device_select(spi_lobo_device_handle_t handle, int force);
+
+/**
+ * @brief De-select spi device
+ *
+ * If device's spics_io_num=-1 and spics_ext_io_num > 0 'spics_ext_io_num' pin is set to inactive state (high)
+ * 
+ * spi bus device's semaphore is given after selecting the device
+ * 
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * 
+ * @return 
+ *         - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *         - ESP_OK                on success
+ */
+esp_err_t spi_lobo_device_deselect(spi_lobo_device_handle_t handle);
+
+
+/**
+ * @brief Check if spi bus uses native spi pins
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * 
+ * @return 
+ *         - true        if native spi pins are used
+ *         - false       if spi pins are routed through gpio matrix
+ */
+bool spi_lobo_uses_native_pins(spi_lobo_device_handle_t handle);
+
+/**
+ * @brief Get spi bus native spi pins
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * 
+ * @return 
+ *         places spi bus native pins in provided pointers
+ */
+void spi_lobo_get_native_pins(int host, int *sdi, int *sdo, int *sck);
+
+/**
+ * @brief Transimit and receive data to/from spi device based on transaction data
+ * 
+ * TRANSMIT 8-bit data to spi device from 'trans->tx_buffer' or 'trans->tx_data' (trans->lenght/8 bytes)
+ * and RECEIVE data to 'trans->rx_buffer' or 'trans->rx_data' (trans->rx_length/8 bytes)
+ * Lengths must be 8-bit multiples!
+ * If trans->rx_buffer is NULL or trans->rx_length is 0, only transmits data
+ * If trans->tx_buffer is NULL or trans->length is 0, only receives data
+ * If the device is in duplex mode (LB_SPI_DEVICE_HALFDUPLEX flag NOT set), data are transmitted and received simultaneously.
+ * If the device is in half duplex mode (LB_SPI_DEVICE_HALFDUPLEX flag IS set), data are received after transmission
+ * 'address', 'command' and 'dummy bits' are transmitted before data phase IF set in device's configuration
+ *   and IF 'trans->length' and 'trans->rx_length' are NOT both 0
+ * If device was not previously selected, it will be selected before transmission and deselected after transmission.
+ *
+ * @param handle Device handle obtained using spi_lobo_bus_add_device
+ * 
+ * @param trans Pointer to variable containing the description of the transaction that is executed
+ *
+ * @return
+ *         - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *         - ESP error code        if device cannot be selected
+ *         - ESP_OK                on success
+ *
+ */
+esp_err_t spi_lobo_transfer_data(spi_lobo_device_handle_t handle, spi_lobo_transaction_t *trans);
+
+
+/*
+ * SPI transactions uses the semaphore (taken in select function) to protect the transfer
+ */
+esp_err_t spi_lobo_device_TakeSemaphore(spi_lobo_device_handle_t handle);
+void spi_lobo_device_GiveSemaphore(spi_lobo_device_handle_t handle);
+
+
+/**
+ * @brief Setup a DMA link chain
+ *
+ * This routine will set up a chain of linked DMA descriptors in the array pointed to by
+ * ``dmadesc``. Enough DMA descriptors will be used to fit the buffer of ``len`` bytes in, and the
+ * descriptors will point to the corresponding positions in ``buffer`` and linked together. The
+ * end result is that feeding ``dmadesc[0]`` into DMA hardware results in the entirety ``len`` bytes
+ * of ``data`` being read or written.
+ *
+ * @param dmadesc Pointer to array of DMA descriptors big enough to be able to convey ``len`` bytes
+ * @param len Length of buffer
+ * @param data Data buffer to use for DMA transfer
+ * @param isrx True if data is to be written into ``data``, false if it's to be read from ``data``.
+ */
+void spi_lobo_setup_dma_desc_links(lldesc_t *dmadesc, int len, const uint8_t *data, bool isrx);
+
+/**
+ * @brief Check if a DMA reset is requested but has not completed yet
+ *
+ * @return True when a DMA reset is requested but hasn't completed yet. False otherwise.
+ */
+bool spi_lobo_dmaworkaround_reset_in_progress();
+
+
+/**
+ * @brief Mark a DMA channel as idle.
+ *
+ * A call to this function tells the workaround logic that this channel will
+ * not be affected by a global SPI DMA reset.
+ */
+void spi_lobo_dmaworkaround_idle(int dmachan);
+
+/**
+ * @brief Mark a DMA channel as active.
+ *
+ * A call to this function tells the workaround logic that this channel will
+ * be affected by a global SPI DMA reset, and a reset like that should not be attempted.
+ */
+void spi_lobo_dmaworkaround_transfer_active(int dmachan);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/DefaultFont.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,333 @@
+// Default font
+
+// ========================================================================
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// fonts. Individual characters do not have to be multiples of 8 bits wide.
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// Header Format:
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset	(start Y of visible pixels)
+// Width				(width of the visible pixels)
+// Height				(height of the visible pixels)
+// xOffset				(start X of visible pixels)
+// xDelta				(the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+// ========================================================================
+
+// dejavu
+// Point Size   : 12
+// Memory usage : 1158 bytes
+// # characters : 95
+
+const unsigned char tft_DefaultFont[] =
+{
+0x00, 0x0B, 0x86, 0x04,
+
+// ' '
+0x20,0x0A,0x00,0x00,0x00,0x04,
+
+// '!'
+0x21,0x01,0x01,0x09,0x02,0x05,
+0xFD,0x80,
+// '"'
+0x22,0x01,0x03,0x03,0x01,0x05,
+0xB6,0x80,
+// '#'
+0x23,0x02,0x08,0x08,0x01,0x0A,
+0x12,0x14,0x7F,0x24,0x24,0xFE,0x28,0x48,
+// '$'
+0x24,0x01,0x06,0x0B,0x02,0x08,
+0x21,0xCA,0xA8,0xE0,0xE2,0xAA,0x70,0x82,0x00,
+// '%'
+0x25,0x01,0x0A,0x09,0x00,0x0B,
+0x61,0x24,0x89,0x22,0x50,0x6D,0x82,0x91,0x24,0x49,0x21,0x80,
+// '&'
+0x26,0x01,0x09,0x09,0x01,0x0A,
+0x30,0x24,0x10,0x0C,0x05,0x14,0x4A,0x19,0x8C,0x7B,0x00,
+// '''
+0x27,0x01,0x01,0x03,0x01,0x03,
+0xE0,
+// '('
+0x28,0x00,0x03,0x0B,0x01,0x05,
+0x69,0x49,0x24,0x48,0x80,
+// ')'
+0x29,0x00,0x03,0x0B,0x01,0x05,
+0x89,0x12,0x49,0x4A,0x00,
+// '*'
+0x2A,0x01,0x05,0x06,0x01,0x06,
+0x25,0x5C,0xEA,0x90,
+// '+'
+0x2B,0x03,0x07,0x07,0x01,0x0A,
+0x10,0x20,0x47,0xF1,0x02,0x04,0x00,
+// ','
+0x2C,0x08,0x01,0x03,0x01,0x04,
+0xE0,
+// '-'
+0x2D,0x06,0x03,0x01,0x01,0x04,
+0xE0,
+// '.'
+0x2E,0x08,0x01,0x02,0x01,0x04,
+0xC0,
+// '/'
+0x2F,0x01,0x04,0x0A,0x00,0x04,
+0x11,0x22,0x24,0x44,0x88,
+// '0'
+0x30,0x01,0x06,0x09,0x01,0x08,
+0x79,0x28,0x61,0x86,0x18,0x52,0x78,
+// '1'
+0x31,0x01,0x05,0x09,0x01,0x08,
+0xE1,0x08,0x42,0x10,0x84,0xF8,
+// '2'
+0x32,0x01,0x07,0x09,0x01,0x08,
+0x79,0x18,0x10,0x20,0x82,0x08,0x20,0xFC,
+// '3'
+0x33,0x01,0x06,0x09,0x01,0x08,
+0x7A,0x10,0x41,0x38,0x30,0x63,0x78,
+// '4'
+0x34,0x01,0x06,0x09,0x01,0x08,
+0x18,0x62,0x92,0x4A,0x2F,0xC2,0x08,
+// '5'
+0x35,0x01,0x06,0x09,0x01,0x08,
+0xFA,0x08,0x3C,0x0C,0x10,0x63,0x78,
+// '6'
+0x36,0x01,0x06,0x09,0x01,0x08,
+0x39,0x18,0x3E,0xCE,0x18,0x53,0x78,
+// '7'
+0x37,0x01,0x06,0x09,0x01,0x08,
+0xFC,0x10,0x82,0x10,0x42,0x08,0x40,
+// '8'
+0x38,0x01,0x06,0x09,0x01,0x08,
+0x7B,0x38,0x73,0x7B,0x38,0x73,0x78,
+// '9'
+0x39,0x01,0x06,0x09,0x01,0x08,
+0x7B,0x28,0x61,0xCD,0xD0,0x62,0x70,
+// ':'
+0x3A,0x04,0x01,0x06,0x01,0x04,
+0xCC,
+// ';'
+0x3B,0x04,0x01,0x07,0x01,0x04,
+0xCE,
+// '<'
+0x3C,0x03,0x08,0x06,0x01,0x0A,
+0x03,0x1E,0xE0,0xE0,0x1E,0x03,
+// '='
+0x3D,0x05,0x08,0x03,0x01,0x0A,
+0xFF,0x00,0xFF,
+// '>'
+0x3E,0x03,0x08,0x06,0x01,0x0A,
+0xC0,0x78,0x07,0x07,0x78,0xC0,
+// '?'
+0x3F,0x01,0x05,0x09,0x00,0x06,
+0x74,0x42,0x22,0x10,0x04,0x20,
+// '@'
+0x40,0x01,0x0B,0x0B,0x01,0x0D,
+0x1F,0x06,0x19,0x01,0x46,0x99,0x13,0x22,0x64,0x54,0x6C,0x40,0x04,0x10,0x7C,0x00,
+// 'A'
+0x41,0x01,0x08,0x09,0x00,0x08,
+0x18,0x18,0x24,0x24,0x24,0x42,0x7E,0x42,0x81,
+// 'B'
+0x42,0x01,0x06,0x09,0x01,0x08,
+0xFA,0x18,0x61,0xFA,0x18,0x61,0xF8,
+// 'C'
+0x43,0x01,0x06,0x09,0x01,0x08,
+0x39,0x18,0x20,0x82,0x08,0x11,0x38,
+// 'D'
+0x44,0x01,0x07,0x09,0x01,0x09,
+0xF9,0x0A,0x0C,0x18,0x30,0x60,0xC2,0xF8,
+// 'E'
+0x45,0x01,0x06,0x09,0x01,0x08,
+0xFE,0x08,0x20,0xFE,0x08,0x20,0xFC,
+// 'F'
+0x46,0x01,0x05,0x09,0x01,0x07,
+0xFC,0x21,0x0F,0xC2,0x10,0x80,
+// 'G'
+0x47,0x01,0x07,0x09,0x01,0x09,
+0x3C,0x86,0x04,0x08,0xF0,0x60,0xA1,0x3C,
+// 'H'
+0x48,0x01,0x07,0x09,0x01,0x09,
+0x83,0x06,0x0C,0x1F,0xF0,0x60,0xC1,0x82,
+// 'I'
+0x49,0x01,0x01,0x09,0x01,0x03,
+0xFF,0x80,
+// 'J'
+0x4A,0x01,0x03,0x0B,0xFF,0x03,
+0x24,0x92,0x49,0x27,0x00,
+// 'K'
+0x4B,0x01,0x07,0x09,0x01,0x07,
+0x85,0x12,0x45,0x0C,0x14,0x24,0x44,0x84,
+// 'L'
+0x4C,0x01,0x05,0x09,0x01,0x06,
+0x84,0x21,0x08,0x42,0x10,0xF8,
+// 'M'
+0x4D,0x01,0x08,0x09,0x01,0x0A,
+0x81,0xC3,0xC3,0xA5,0xA5,0x99,0x99,0x81,0x81,
+// 'N'
+0x4E,0x01,0x07,0x09,0x01,0x09,
+0xC3,0x86,0x8D,0x19,0x31,0x62,0xC3,0x86,
+// 'O'
+0x4F,0x01,0x07,0x09,0x01,0x09,
+0x38,0x8A,0x0C,0x18,0x30,0x60,0xA2,0x38,
+// 'P'
+0x50,0x01,0x06,0x09,0x01,0x08,
+0xFA,0x38,0x63,0xFA,0x08,0x20,0x80,
+// 'Q'
+0x51,0x01,0x07,0x0B,0x01,0x09,
+0x38,0x8A,0x0C,0x18,0x30,0x60,0xA2,0x38,0x10,0x10,
+// 'R'
+0x52,0x01,0x07,0x09,0x01,0x08,
+0xF9,0x1A,0x14,0x6F,0x91,0x21,0x42,0x82,
+// 'S'
+0x53,0x01,0x06,0x09,0x01,0x08,
+0x7B,0x18,0x30,0x78,0x30,0x63,0x78,
+// 'T'
+0x54,0x01,0x07,0x09,0x00,0x07,
+0xFE,0x20,0x40,0x81,0x02,0x04,0x08,0x10,
+// 'U'
+0x55,0x01,0x07,0x09,0x01,0x09,
+0x83,0x06,0x0C,0x18,0x30,0x60,0xA2,0x38,
+// 'V'
+0x56,0x01,0x0A,0x09,0xFF,0x08,
+0x40,0x90,0x22,0x10,0x84,0x21,0x04,0x81,0x20,0x30,0x0C,0x00,
+// 'W'
+0x57,0x01,0x0B,0x09,0x00,0x0B,
+0x84,0x28,0x89,0x11,0x27,0x22,0xA8,0x55,0x0E,0xE0,0x88,0x11,0x00,
+// 'X'
+0x58,0x01,0x07,0x09,0x00,0x07,
+0xC6,0x88,0xA1,0xC1,0x07,0x0A,0x22,0x82,
+// 'Y'
+0x59,0x01,0x07,0x09,0x00,0x07,
+0x82,0x89,0x11,0x43,0x82,0x04,0x08,0x10,
+// 'Z'
+0x5A,0x01,0x07,0x09,0x01,0x09,
+0xFE,0x04,0x10,0x41,0x04,0x10,0x40,0xFE,
+// '['
+0x5B,0x01,0x02,0x0B,0x02,0x05,
+0xEA,0xAA,0xAC,
+// '\'
+0x5C,0x01,0x04,0x0A,0x00,0x04,
+0x88,0x44,0x42,0x22,0x11,
+// ']'
+0x5D,0x01,0x02,0x0B,0x01,0x05,
+0xD5,0x55,0x5C,
+// '^'
+0x5E,0x01,0x08,0x03,0x01,0x0A,
+0x18,0x24,0x42,
+// '_'
+0x5F,0x0C,0x06,0x01,0x00,0x06,
+0xFC,
+// '`'
+0x60,0x00,0x03,0x02,0x01,0x06,
+0x44,
+// 'a'
+0x61,0x03,0x06,0x07,0x01,0x08,
+0x7A,0x30,0x5F,0x86,0x37,0x40,
+// 'b'
+0x62,0x00,0x06,0x0A,0x01,0x08,
+0x82,0x08,0x2E,0xCA,0x18,0x61,0xCE,0xE0,
+// 'c'
+0x63,0x03,0x05,0x07,0x01,0x07,
+0x72,0x61,0x08,0x25,0xC0,
+// 'd'
+0x64,0x00,0x06,0x0A,0x01,0x08,
+0x04,0x10,0x5D,0xCE,0x18,0x61,0xCD,0xD0,
+// 'e'
+0x65,0x03,0x06,0x07,0x01,0x08,
+0x39,0x38,0x7F,0x81,0x13,0x80,
+// 'f'
+0x66,0x00,0x04,0x0A,0x00,0x04,
+0x34,0x4F,0x44,0x44,0x44,
+// 'g'
+0x67,0x03,0x06,0x0A,0x01,0x08,
+0x77,0x38,0x61,0x87,0x37,0x41,0x4C,0xE0,
+// 'h'
+0x68,0x00,0x06,0x0A,0x01,0x08,
+0x82,0x08,0x2E,0xC6,0x18,0x61,0x86,0x10,
+// 'i'
+0x69,0x01,0x01,0x09,0x01,0x03,
+0xBF,0x80,
+// 'j'
+0x6A,0x01,0x02,0x0C,0x00,0x03,
+0x45,0x55,0x56,
+// 'k'
+0x6B,0x00,0x06,0x0A,0x01,0x07,
+0x82,0x08,0x22,0x92,0x8E,0x28,0x92,0x20,
+// 'l'
+0x6C,0x00,0x01,0x0A,0x01,0x03,
+0xFF,0xC0,
+// 'm'
+0x6D,0x03,0x09,0x07,0x01,0x0B,
+0xB3,0x66,0x62,0x31,0x18,0x8C,0x46,0x22,
+// 'n'
+0x6E,0x03,0x06,0x07,0x01,0x08,
+0xBB,0x18,0x61,0x86,0x18,0x40,
+// 'o'
+0x6F,0x03,0x06,0x07,0x01,0x08,
+0x7B,0x38,0x61,0x87,0x37,0x80,
+// 'p'
+0x70,0x03,0x06,0x0A,0x01,0x08,
+0xBB,0x28,0x61,0x87,0x3B,0xA0,0x82,0x00,
+// 'q'
+0x71,0x03,0x06,0x0A,0x01,0x08,
+0x77,0x38,0x61,0x87,0x37,0x41,0x04,0x10,
+// 'r'
+0x72,0x03,0x04,0x07,0x01,0x05,
+0xBC,0x88,0x88,0x80,
+// 's'
+0x73,0x03,0x06,0x07,0x01,0x07,
+0x72,0x28,0x1C,0x0A,0x27,0x00,
+// 't'
+0x74,0x01,0x04,0x09,0x00,0x05,
+0x44,0xF4,0x44,0x44,0x30,
+// 'u'
+0x75,0x03,0x06,0x07,0x01,0x08,
+0x86,0x18,0x61,0x86,0x37,0x40,
+// 'v'
+0x76,0x03,0x08,0x07,0xFF,0x06,
+0x42,0x42,0x24,0x24,0x24,0x18,0x18,
+// 'w'
+0x77,0x03,0x09,0x07,0x00,0x09,
+0x88,0xC4,0x57,0x4A,0xA5,0x51,0x10,0x88,
+// 'x'
+0x78,0x03,0x06,0x07,0x00,0x06,
+0x85,0x24,0x8C,0x49,0x28,0x40,
+// 'y'
+0x79,0x03,0x08,0x0A,0xFF,0x06,
+0x42,0x42,0x24,0x24,0x14,0x18,0x08,0x08,0x10,0x60,
+// 'z'
+0x7A,0x03,0x05,0x07,0x00,0x05,
+0xF8,0x44,0x44,0x43,0xE0,
+// '{'
+0x7B,0x01,0x05,0x0B,0x02,0x08,
+0x19,0x08,0x42,0x60,0x84,0x21,0x06,
+// '|'
+0x7C,0x01,0x01,0x0C,0x02,0x04,
+0xFF,0xF0,
+// '}'
+0x7D,0x01,0x05,0x0B,0x01,0x08,
+0xC1,0x08,0x42,0x0C,0x84,0x21,0x30,
+// '~'
+0x7E,0x04,0x08,0x03,0x01,0x0A,
+0x00,0x71,0x8E,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/DejaVuSans18.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,322 @@
+// ============================================================================
+// Proportional font Header Format:
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+// ============================================================================
+
+// DejaVuSans
+// Point Size   : 18
+// Memory usage : 1828 bytes
+// # characters : 95
+
+const unsigned char tft_Dejavu18[] =
+{
+0x00, 0x12, 0x00, 0x00,
+
+// ' '
+0x20,0x0E,0x00,0x00,0x00,0x06,
+
+// '!'
+0x21,0x01,0x02,0x0D,0x03,0x07,
+0xFF,0xFF,0xC3,0xC0,
+// '"'
+0x22,0x01,0x06,0x05,0x01,0x08,
+0xCF,0x3C,0xF3,0xCC,
+// '#'
+0x23,0x00,0x0C,0x0E,0x01,0x0F,
+0x04,0x40,0x44,0x0C,0xC0,0xC8,0x7F,0xF7,0xFF,0x09,0x81,0x90,0xFF,0xEF,0xFE,0x13,0x03,0x30,0x32,0x02,0x20,
+// '$'
+0x24,0x00,0x0A,0x11,0x01,0x0B,
+0x08,0x02,0x03,0xE1,0xFC,0xE9,0x32,0x0F,0x81,0xF8,0x0F,0x02,0x60,0x9A,0x2E,0xFF,0x1F,0x80,0x80,0x20,0x08,0x00,
+// '%'
+0x25,0x01,0x0F,0x0D,0x01,0x11,
+0x78,0x10,0x90,0x43,0x31,0x86,0x62,0x0C,0xC8,0x19,0x10,0x1E,0x4F,0x01,0x12,0x02,0x66,0x08,0xCC,0x31,0x98,0x41,0x21,0x03,0xC0,
+// '&'
+0x26,0x01,0x0C,0x0D,0x01,0x0D,
+0x0F,0x01,0xF8,0x30,0x83,0x00,0x38,0x03,0xC0,0x7E,0x6C,0x76,0xC3,0xCC,0x18,0xE1,0xC7,0xFE,0x3E,0x70,
+// '''
+0x27,0x01,0x02,0x05,0x01,0x04,
+0xFF,0xC0,
+// '('
+0x28,0x00,0x04,0x10,0x02,0x07,
+0x32,0x66,0x4C,0xCC,0xCC,0xC4,0x66,0x23,
+// ')'
+0x29,0x00,0x04,0x10,0x01,0x07,
+0xC4,0x66,0x23,0x33,0x33,0x32,0x66,0x4C,
+// '*'
+0x2A,0x01,0x07,0x08,0x01,0x09,
+0x11,0x25,0x51,0xC3,0x8A,0xA4,0x88,
+// '+'
+0x2B,0x02,0x0C,0x0C,0x02,0x0F,
+0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x0F,0xFF,0xFF,0xF0,0x60,0x06,0x00,0x60,0x06,0x00,0x60,
+// ','
+0x2C,0x0C,0x03,0x04,0x01,0x06,
+0x6D,0x40,
+// '-'
+0x2D,0x08,0x05,0x02,0x01,0x07,
+0xFF,0xC0,
+// '.'
+0x2E,0x0C,0x02,0x02,0x02,0x06,
+0xF0,
+// '/'
+0x2F,0x01,0x06,0x0F,0x00,0x06,
+0x0C,0x31,0x86,0x18,0xE3,0x0C,0x31,0xC6,0x18,0x63,0x0C,0x00,
+// '0'
+0x30,0x01,0x09,0x0D,0x01,0x0B,
+0x3E,0x3F,0x98,0xD8,0x3C,0x1E,0x0F,0x07,0x83,0xC1,0xE0,0xD8,0xCF,0xE3,0xE0,
+// '1'
+0x31,0x01,0x08,0x0D,0x02,0x0B,
+0x38,0xF8,0xD8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0xFF,0xFF,
+// '2'
+0x32,0x01,0x09,0x0D,0x01,0x0B,
+0x7C,0x7F,0x21,0xC0,0x60,0x30,0x30,0x18,0x18,0x18,0x18,0x18,0x1F,0xEF,0xF0,
+// '3'
+0x33,0x01,0x09,0x0D,0x01,0x0B,
+0x7E,0x7F,0xA0,0xE0,0x30,0x39,0xF0,0xFC,0x07,0x01,0x80,0xE0,0xFF,0xE7,0xE0,
+// '4'
+0x34,0x01,0x0A,0x0D,0x01,0x0B,
+0x07,0x01,0xC0,0xB0,0x6C,0x13,0x08,0xC6,0x31,0x0C,0xFF,0xFF,0xF0,0x30,0x0C,0x03,0x00,
+// '5'
+0x35,0x01,0x08,0x0D,0x01,0x0B,
+0x7E,0x7E,0x60,0x60,0x7C,0x7E,0x47,0x03,0x03,0x03,0x87,0xFE,0x7C,
+// '6'
+0x36,0x01,0x09,0x0D,0x01,0x0B,
+0x1E,0x1F,0x9C,0x5C,0x0C,0x06,0xF3,0xFD,0xC7,0xC1,0xE0,0xD8,0xEF,0xE1,0xE0,
+// '7'
+0x37,0x01,0x08,0x0D,0x01,0x0B,
+0xFF,0xFF,0x06,0x06,0x06,0x0E,0x0C,0x0C,0x1C,0x18,0x18,0x38,0x30,
+// '8'
+0x38,0x01,0x09,0x0D,0x01,0x0B,
+0x3E,0x3F,0xB8,0xF8,0x3E,0x39,0xF1,0xFD,0xC7,0xC1,0xE0,0xF8,0xEF,0xE3,0xE0,
+// '9'
+0x39,0x01,0x09,0x0D,0x01,0x0B,
+0x3C,0x3F,0xB8,0xD8,0x3C,0x1F,0x1D,0xFE,0x7B,0x01,0x81,0xD1,0xCF,0xC3,0xC0,
+// ':'
+0x3A,0x05,0x02,0x09,0x02,0x06,
+0xF0,0x03,0xC0,
+// ';'
+0x3B,0x05,0x03,0x0B,0x01,0x06,
+0x6C,0x00,0x03,0x6A,0x00,
+// '<'
+0x3C,0x04,0x0B,0x0A,0x02,0x0F,
+0x00,0x20,0x3C,0x1F,0x1F,0x0F,0x81,0xF0,0x0F,0x80,0x3E,0x01,0xE0,0x04,
+// '='
+0x3D,0x05,0x0B,0x06,0x02,0x0F,
+0xFF,0xFF,0xFC,0x00,0x00,0x0F,0xFF,0xFF,0xC0,
+// '>'
+0x3E,0x04,0x0B,0x0A,0x02,0x0F,
+0x80,0x1E,0x01,0xF0,0x07,0xC0,0x3E,0x07,0xC3,0xE3,0xE0,0xF0,0x10,0x00,
+// '?'
+0x3F,0x01,0x07,0x0D,0x01,0x0A,
+0x79,0xFA,0x38,0x30,0x61,0x86,0x18,0x30,0x60,0x01,0x83,0x00,
+// '@'
+0x40,0x01,0x10,0x10,0x01,0x12,
+0x07,0xE0,0x1F,0xF8,0x3C,0x1C,0x70,0x06,0x60,0x07,0xE3,0x63,0xC7,0xE3,0xC6,0x63,0xC6,0x66,0xC7,0xFC,0xE3,0x70,0x60,0x00,0x70,0x00,0x3C,0x30,0x1F,0xF0,0x07,0xC0,
+// 'A'
+0x41,0x01,0x0C,0x0D,0x00,0x0C,
+0x06,0x00,0x60,0x0F,0x00,0xF0,0x19,0x81,0x98,0x19,0x83,0x0C,0x3F,0xC7,0xFE,0x60,0x66,0x06,0xC0,0x30,
+// 'B'
+0x42,0x01,0x09,0x0D,0x02,0x0C,
+0xFC,0x7F,0xB0,0xD8,0x6C,0x37,0xF3,0xF9,0x86,0xC1,0xE0,0xF0,0xFF,0xEF,0xE0,
+// 'C'
+0x43,0x01,0x0B,0x0D,0x01,0x0D,
+0x0F,0xC7,0xFD,0xC0,0xB0,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x0C,0x01,0xC0,0x9F,0xF0,0xFC,
+// 'D'
+0x44,0x01,0x0B,0x0D,0x02,0x0E,
+0xFE,0x1F,0xF3,0x07,0x60,0x6C,0x07,0x80,0xF0,0x1E,0x03,0xC0,0x78,0x1B,0x07,0x7F,0xCF,0xE0,
+// 'E'
+0x45,0x01,0x08,0x0D,0x02,0x0B,
+0xFF,0xFF,0xC0,0xC0,0xC0,0xFF,0xFF,0xC0,0xC0,0xC0,0xC0,0xFF,0xFF,
+// 'F'
+0x46,0x01,0x08,0x0D,0x02,0x0A,
+0xFF,0xFF,0xC0,0xC0,0xC0,0xFE,0xFE,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,
+// 'G'
+0x47,0x01,0x0B,0x0D,0x01,0x0E,
+0x0F,0xC7,0xFD,0xC0,0xB0,0x0C,0x01,0x87,0xF0,0xFE,0x03,0xC0,0x6C,0x0D,0xC1,0x9F,0xE0,0xF8,
+// 'H'
+0x48,0x01,0x0A,0x0D,0x02,0x0E,
+0xC0,0xF0,0x3C,0x0F,0x03,0xC0,0xFF,0xFF,0xFF,0x03,0xC0,0xF0,0x3C,0x0F,0x03,0xC0,0xC0,
+// 'I'
+0x49,0x01,0x02,0x0D,0x02,0x06,
+0xFF,0xFF,0xFF,0xC0,
+// 'J'
+0x4A,0x01,0x05,0x11,0xFF,0x06,
+0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0xFE,0xE0,
+// 'K'
+0x4B,0x01,0x0B,0x0D,0x02,0x0C,
+0xC1,0x98,0x63,0x18,0x66,0x0D,0x81,0xE0,0x3C,0x06,0xC0,0xCC,0x18,0xC3,0x0C,0x60,0xCC,0x0C,
+// 'L'
+0x4C,0x01,0x08,0x0D,0x02,0x0A,
+0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFF,0xFF,
+// 'M'
+0x4D,0x01,0x0C,0x0D,0x02,0x10,
+0xE0,0x7F,0x0F,0xF0,0xFD,0x8B,0xD9,0xBD,0x9B,0xCF,0x3C,0xF3,0xC6,0x3C,0x63,0xC0,0x3C,0x03,0xC0,0x30,
+// 'N'
+0x4E,0x01,0x0A,0x0D,0x02,0x0E,
+0xE0,0xF8,0x3F,0x0F,0xC3,0xD8,0xF6,0x3C,0xCF,0x1B,0xC6,0xF0,0xFC,0x3F,0x07,0xC1,0xC0,
+// 'O'
+0x4F,0x01,0x0C,0x0D,0x01,0x0E,
+0x1F,0x83,0xFC,0x70,0xE6,0x06,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x06,0x70,0xE3,0xFC,0x1F,0x80,
+// 'P'
+0x50,0x01,0x08,0x0D,0x02,0x0B,
+0xFC,0xFE,0xC7,0xC3,0xC3,0xC7,0xFE,0xFC,0xC0,0xC0,0xC0,0xC0,0xC0,
+// 'Q'
+0x51,0x01,0x0C,0x0F,0x01,0x0E,
+0x1F,0x83,0xFC,0x70,0xE6,0x06,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x06,0x70,0xE3,0xFC,0x1F,0x80,0x18,0x00,0xC0,
+// 'R'
+0x52,0x01,0x0A,0x0D,0x02,0x0D,
+0xFC,0x3F,0x8C,0x73,0x0C,0xC3,0x31,0xCF,0xE3,0xF0,0xC6,0x30,0xCC,0x33,0x06,0xC1,0xC0,
+// 'S'
+0x53,0x01,0x0A,0x0D,0x01,0x0B,
+0x3E,0x1F,0xCE,0x13,0x00,0xC0,0x1F,0x03,0xF0,0x0E,0x01,0x80,0x68,0x3B,0xFC,0x7E,0x00,
+// 'T'
+0x54,0x01,0x0C,0x0D,0x00,0x0C,
+0xFF,0xFF,0xFF,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,
+// 'U'
+0x55,0x01,0x0A,0x0D,0x02,0x0E,
+0xC0,0xF0,0x3C,0x0F,0x03,0xC0,0xF0,0x3C,0x0F,0x03,0xC0,0xF0,0x36,0x19,0xFE,0x1E,0x00,
+// 'V'
+0x56,0x01,0x0C,0x0D,0x00,0x0C,
+0xC0,0x36,0x06,0x60,0x66,0x06,0x30,0xC3,0x0C,0x19,0x81,0x98,0x19,0x80,0xF0,0x0F,0x00,0x60,0x06,0x00,
+// 'W'
+0x57,0x01,0x11,0x0D,0x01,0x13,
+0xC1,0xC1,0xE0,0xE0,0xD8,0xF8,0xCC,0x6C,0x66,0x36,0x33,0x1B,0x18,0xD8,0xD8,0x6C,0x6C,0x36,0x36,0x1F,0x1F,0x07,0x07,0x03,0x83,0x81,0xC1,0xC0,
+// 'X'
+0x58,0x01,0x0B,0x0D,0x01,0x0D,
+0x70,0xE6,0x18,0xE6,0x0D,0xC0,0xF0,0x1C,0x03,0x80,0x78,0x1B,0x07,0x30,0xC7,0x30,0x6E,0x0E,
+// 'Y'
+0x59,0x01,0x0C,0x0D,0x00,0x0C,
+0xE0,0x76,0x06,0x30,0xC1,0x98,0x19,0x80,0xF0,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,
+// 'Z'
+0x5A,0x01,0x0B,0x0D,0x01,0x0D,
+0xFF,0xFF,0xFC,0x07,0x01,0xC0,0x30,0x0E,0x03,0x80,0xE0,0x18,0x06,0x01,0xC0,0x7F,0xFF,0xFE,
+// '['
+0x5B,0x00,0x04,0x10,0x01,0x07,
+0xFF,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xFF,
+// '\'
+0x5C,0x01,0x06,0x0F,0x00,0x06,
+0xC3,0x06,0x18,0x61,0xC3,0x0C,0x30,0xE1,0x86,0x18,0x30,0xC0,
+// ']'
+0x5D,0x00,0x04,0x10,0x02,0x07,
+0xFF,0x33,0x33,0x33,0x33,0x33,0x33,0xFF,
+// '^'
+0x5E,0x01,0x0B,0x05,0x02,0x0F,
+0x0E,0x03,0xE0,0xC6,0x30,0x6C,0x06,
+// '_'
+0x5F,0x10,0x09,0x02,0x00,0x09,
+0xFF,0xFF,0xC0,
+// '`'
+0x60,0x00,0x04,0x03,0x02,0x09,
+0xC6,0x30,
+// 'a'
+0x61,0x04,0x08,0x0A,0x01,0x0A,
+0x3C,0x7E,0x47,0x03,0x3F,0xFF,0xC3,0xC7,0xFF,0x7B,
+// 'b'
+0x62,0x00,0x09,0x0E,0x02,0x0B,
+0xC0,0x60,0x30,0x18,0x0D,0xE7,0xFB,0x8F,0x83,0xC1,0xE0,0xF0,0x7C,0x7F,0xF6,0xF0,
+// 'c'
+0x63,0x04,0x08,0x0A,0x01,0x09,
+0x1E,0x7F,0x61,0xC0,0xC0,0xC0,0xC0,0x61,0x7F,0x1E,
+// 'd'
+0x64,0x00,0x09,0x0E,0x01,0x0B,
+0x01,0x80,0xC0,0x60,0x33,0xDB,0xFF,0x8F,0x83,0xC1,0xE0,0xF0,0x7C,0x77,0xF9,0xEC,
+// 'e'
+0x65,0x04,0x0A,0x0A,0x01,0x0B,
+0x1F,0x1F,0xE6,0x1F,0x03,0xFF,0xFF,0xFC,0x01,0x81,0x7F,0xC7,0xE0,
+// 'f'
+0x66,0x00,0x07,0x0E,0x00,0x06,
+0x1E,0x7C,0xC1,0x8F,0xFF,0xCC,0x18,0x30,0x60,0xC1,0x83,0x06,0x00,
+// 'g'
+0x67,0x04,0x09,0x0E,0x01,0x0B,
+0x3D,0xBF,0xF8,0xF8,0x3C,0x1E,0x0F,0x07,0xC7,0x7F,0x9E,0xC0,0x68,0x67,0xF1,0xF0,
+// 'h'
+0x68,0x00,0x08,0x0E,0x02,0x0B,
+0xC0,0xC0,0xC0,0xC0,0xDE,0xFE,0xE7,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,
+// 'i'
+0x69,0x00,0x02,0x0E,0x02,0x05,
+0xF0,0xFF,0xFF,0xF0,
+// 'j'
+0x6A,0x00,0x04,0x12,0x00,0x05,
+0x33,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0xEC,
+// 'k'
+0x6B,0x00,0x09,0x0E,0x02,0x0A,
+0xC0,0x60,0x30,0x18,0x0C,0x36,0x33,0x31,0xB0,0xF0,0x78,0x36,0x19,0x8C,0x66,0x18,
+// 'l'
+0x6C,0x00,0x02,0x0E,0x02,0x05,
+0xFF,0xFF,0xFF,0xF0,
+// 'm'
+0x6D,0x04,0x0E,0x0A,0x02,0x11,
+0xDC,0x7B,0xFB,0xEE,0x79,0xF0,0xC3,0xC3,0x0F,0x0C,0x3C,0x30,0xF0,0xC3,0xC3,0x0F,0x0C,0x30,
+// 'n'
+0x6E,0x04,0x08,0x0A,0x02,0x0B,
+0xDE,0xFE,0xE7,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,
+// 'o'
+0x6F,0x04,0x0A,0x0A,0x01,0x0B,
+0x1E,0x1F,0xE6,0x1B,0x03,0xC0,0xF0,0x3C,0x0D,0x86,0x7F,0x87,0x80,
+// 'p'
+0x70,0x04,0x09,0x0E,0x02,0x0B,
+0xDE,0x7F,0xB8,0xF8,0x3C,0x1E,0x0F,0x07,0xC7,0xFF,0x6F,0x30,0x18,0x0C,0x06,0x00,
+// 'q'
+0x71,0x04,0x09,0x0E,0x01,0x0B,
+0x3D,0xBF,0xF8,0xF8,0x3C,0x1E,0x0F,0x07,0xC7,0x7F,0x9E,0xC0,0x60,0x30,0x18,0x0C,
+// 'r'
+0x72,0x04,0x06,0x0A,0x02,0x08,
+0xDF,0xFE,0x30,0xC3,0x0C,0x30,0xC3,0x00,
+// 's'
+0x73,0x04,0x08,0x0A,0x01,0x08,
+0x7C,0xFE,0xC2,0xE0,0x7C,0x1E,0x06,0x86,0xFE,0x78,
+// 't'
+0x74,0x01,0x06,0x0D,0x01,0x07,
+0x61,0x86,0x3F,0xFD,0x86,0x18,0x61,0x86,0x1F,0x3C,
+// 'u'
+0x75,0x04,0x08,0x0A,0x02,0x0B,
+0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xE7,0x7F,0x7B,
+// 'v'
+0x76,0x04,0x0C,0x0A,0x00,0x0B,
+0x60,0x66,0x06,0x30,0xC3,0x0C,0x19,0x81,0x98,0x19,0x80,0xF0,0x0F,0x00,0x60,
+// 'w'
+0x77,0x04,0x0F,0x0A,0x01,0x10,
+0x63,0x8C,0xC7,0x19,0x8E,0x31,0xB6,0xC3,0x6D,0x86,0xDB,0x0F,0x1E,0x0E,0x38,0x1C,0x70,0x38,0xE0,
+// 'x'
+0x78,0x04,0x0A,0x0A,0x01,0x0B,
+0xE1,0xD8,0x63,0x30,0xCC,0x1E,0x07,0x83,0x30,0xCC,0x61,0xB8,0x70,
+// 'y'
+0x79,0x04,0x0C,0x0E,0x00,0x0B,
+0x60,0x66,0x06,0x30,0xC3,0x0C,0x19,0x81,0x98,0x0F,0x00,0xF0,0x06,0x00,0x60,0x06,0x00,0xC0,0x3C,0x03,0x80,
+// 'z'
+0x7A,0x04,0x08,0x0A,0x01,0x09,
+0xFF,0xFF,0x06,0x0C,0x1C,0x38,0x30,0x70,0xFF,0xFF,
+// '{'
+0x7B,0x00,0x08,0x11,0x02,0x0B,
+0x0F,0x1F,0x18,0x18,0x18,0x18,0x38,0xF0,0xF0,0x38,0x18,0x18,0x18,0x18,0x18,0x1F,0x0F,
+// '|'
+0x7C,0x00,0x02,0x12,0x02,0x06,
+0xFF,0xFF,0xFF,0xFF,0xF0,
+// '}'
+0x7D,0x00,0x08,0x11,0x02,0x0B,
+0xF0,0xF8,0x18,0x18,0x18,0x18,0x1C,0x0F,0x0F,0x1C,0x18,0x18,0x18,0x18,0x18,0xF8,0xF0,
+// '~'
+0x7E,0x05,0x0B,0x05,0x02,0x0F,
+0x00,0x0F,0x87,0xFF,0xC3,0xE0,0x00,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/DejaVuSans24.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,331 @@
+// ========================================================================
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// Header Format:
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+// ========================================================================
+
+// dejavu
+// Point Size   : 24
+// Memory usage : 2724 bytes
+// # characters : 95
+
+const unsigned char tft_Dejavu24[] =
+{
+0x00, 0x17, 0x00, 0x00,
+
+// ' '
+0x20,0x13,0x00,0x00,0x00,0x08,
+
+// '!'
+0x21,0x01,0x02,0x12,0x04,0x0A,
+0xFF,0xFF,0xFF,0x03,0xF0,
+// '"'
+0x22,0x01,0x06,0x07,0x02,0x0B,
+0xCF,0x3C,0xF3,0xCF,0x3C,0xC0,
+// '#'
+0x23,0x01,0x10,0x12,0x02,0x14,
+0x03,0x08,0x03,0x18,0x03,0x18,0x03,0x18,0x02,0x18,0x7F,0xFF,0x7F,0xFF,0x06,0x30,0x04,0x30,0x0C,0x20,0x0C,0x60,0xFF,0xFE,0xFF,0xFE,0x18,0x40,0x18,0xC0,0x18,0xC0,0x18,0xC0,0x10,0xC0,
+// '$'
+0x24,0x01,0x0B,0x16,0x02,0x0F,
+0x04,0x00,0x80,0x10,0x0F,0xC7,0xFD,0xC8,0xB1,0x06,0x20,0xE4,0x0F,0x80,0xFE,0x03,0xE0,0x4E,0x08,0xC1,0x1E,0x27,0xFF,0xC7,0xE0,0x10,0x02,0x00,0x40,0x08,0x00,
+// '%'
+0x25,0x01,0x14,0x12,0x01,0x17,
+0x3C,0x03,0x06,0x60,0x60,0xC3,0x06,0x0C,0x30,0xC0,0xC3,0x1C,0x0C,0x31,0x80,0xC3,0x38,0x0C,0x33,0x00,0x66,0x63,0xC3,0xC6,0x66,0x00,0xCC,0x30,0x1C,0xC3,0x01,0x8C,0x30,0x38,0xC3,0x03,0x0C,0x30,0x60,0xC3,0x06,0x06,0x60,0xC0,0x3C,
+// '&'
+0x26,0x01,0x10,0x12,0x01,0x13,
+0x07,0xC0,0x1F,0xE0,0x38,0x20,0x30,0x00,0x30,0x00,0x30,0x00,0x18,0x00,0x1C,0x00,0x3E,0x00,0x77,0x06,0xE3,0x86,0xC1,0xCC,0xC0,0xFC,0xC0,0x78,0xE0,0x78,0x70,0xFC,0x3F,0xCE,0x0F,0x87,
+// '''
+0x27,0x01,0x02,0x07,0x02,0x07,
+0xFF,0xFC,
+// '('
+0x28,0x01,0x05,0x15,0x02,0x09,
+0x19,0x8C,0xC6,0x31,0x18,0xC6,0x31,0x8C,0x61,0x0C,0x63,0x0C,0x61,0x80,
+// ')'
+0x29,0x01,0x05,0x15,0x02,0x09,
+0xC3,0x18,0x63,0x18,0x43,0x18,0xC6,0x31,0x8C,0x46,0x31,0x98,0xCC,0x00,
+// '*'
+0x2A,0x01,0x0B,0x0A,0x00,0x0C,
+0x04,0x00,0x83,0x11,0xBA,0xE1,0xF0,0x3E,0x1D,0x76,0x23,0x04,0x00,0x80,
+// '+'
+0x2B,0x03,0x10,0x10,0x03,0x14,
+0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0xFF,0xFF,0xFF,0xFF,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,
+// ','
+0x2C,0x10,0x03,0x06,0x02,0x08,
+0x6D,0xBD,0x80,
+// '-'
+0x2D,0x0B,0x06,0x02,0x01,0x09,
+0xFF,0xF0,
+// '.'
+0x2E,0x10,0x02,0x03,0x03,0x08,
+0xFC,
+// '/'
+0x2F,0x01,0x08,0x14,0x00,0x08,
+0x03,0x07,0x06,0x06,0x06,0x0C,0x0C,0x0C,0x18,0x18,0x18,0x18,0x30,0x30,0x30,0x60,0x60,0x60,0xE0,0xC0,
+// '0'
+0x30,0x01,0x0C,0x12,0x02,0x0F,
+0x0F,0x03,0xFC,0x70,0xE6,0x06,0x60,0x6C,0x03,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x06,0x60,0x67,0x0E,0x3F,0xC0,0xF0,
+// '1'
+0x31,0x01,0x0A,0x12,0x03,0x0F,
+0x3C,0x3F,0x0C,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0xFF,0xFF,0xF0,
+// '2'
+0x32,0x01,0x0C,0x12,0x02,0x0F,
+0x3F,0x0F,0xF8,0xC1,0xC0,0x0E,0x00,0x60,0x06,0x00,0x60,0x0C,0x01,0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xFF,0xEF,0xFE,
+// '3'
+0x33,0x01,0x0C,0x12,0x02,0x0F,
+0x3F,0x07,0xFC,0x41,0xC0,0x06,0x00,0x60,0x06,0x00,0x60,0x0C,0x1F,0x81,0xFC,0x00,0xE0,0x07,0x00,0x30,0x03,0x00,0x78,0x0E,0xFF,0xC3,0xF0,
+// '4'
+0x34,0x01,0x0D,0x12,0x01,0x0F,
+0x01,0xC0,0x1E,0x00,0xB0,0x0D,0x80,0xCC,0x06,0x60,0x63,0x03,0x18,0x30,0xC3,0x06,0x18,0x31,0x81,0x8F,0xFF,0xFF,0xFC,0x03,0x00,0x18,0x00,0xC0,0x06,0x00,
+// '5'
+0x35,0x01,0x0B,0x12,0x02,0x0F,
+0x7F,0xCF,0xF9,0x80,0x30,0x06,0x00,0xC0,0x1F,0xC3,0xFC,0x41,0xC0,0x1C,0x01,0x80,0x30,0x06,0x00,0xC0,0x3C,0x0E,0xFF,0x8F,0xC0,
+// '6'
+0x36,0x01,0x0C,0x12,0x02,0x0F,
+0x07,0xC1,0xFE,0x38,0x27,0x00,0x60,0x0C,0x00,0xCF,0x8D,0xFC,0xF8,0xEF,0x07,0xE0,0x3E,0x03,0xE0,0x36,0x03,0x70,0x77,0x8E,0x3F,0xC0,0xF8,
+// '7'
+0x37,0x01,0x0B,0x12,0x02,0x0F,
+0xFF,0xFF,0xFC,0x03,0x00,0x60,0x1C,0x03,0x00,0x60,0x18,0x03,0x00,0xE0,0x18,0x03,0x00,0xC0,0x18,0x07,0x00,0xC0,0x18,0x06,0x00,
+// '8'
+0x38,0x01,0x0C,0x12,0x02,0x0F,
+0x1F,0x87,0xFE,0x70,0xEC,0x03,0xC0,0x3C,0x03,0xC0,0x37,0x0E,0x3F,0xC3,0xFC,0x70,0xEC,0x03,0xC0,0x3C,0x03,0xC0,0x37,0x0E,0x7F,0xE1,0xF8,
+// '9'
+0x39,0x01,0x0C,0x12,0x02,0x0F,
+0x1F,0x03,0xFC,0x71,0xCE,0x0E,0xC0,0x6C,0x07,0xC0,0x7C,0x07,0xE0,0xF7,0x1F,0x3F,0xB1,0xF3,0x00,0x30,0x06,0x00,0xE4,0x1C,0x7F,0x83,0xE0,
+// ':'
+0x3A,0x07,0x02,0x0C,0x03,0x08,
+0xFC,0x00,0x3F,
+// ';'
+0x3B,0x07,0x03,0x0F,0x02,0x08,
+0x6D,0x80,0x00,0x0D,0xB7,0xB0,
+// '<'
+0x3C,0x05,0x0F,0x0D,0x03,0x14,
+0x00,0x02,0x00,0x3C,0x03,0xF0,0x3F,0x01,0xF8,0x1F,0x80,0x3C,0x00,0x7E,0x00,0x1F,0x80,0x0F,0xC0,0x03,0xF0,0x00,0xF0,0x00,0x20,
+// '='
+0x3D,0x08,0x0F,0x07,0x03,0x14,
+0xFF,0xFF,0xFF,0xFC,0x00,0x00,0x00,0x00,0x00,0x1F,0xFF,0xFF,0xFF,0x80,
+// '>'
+0x3E,0x05,0x0F,0x0D,0x03,0x14,
+0x80,0x01,0xE0,0x01,0xF8,0x00,0x7E,0x00,0x3F,0x00,0x0F,0xC0,0x07,0x80,0x3F,0x03,0xF0,0x1F,0x81,0xF8,0x07,0x80,0x08,0x00,0x00,
+// '?'
+0x3F,0x01,0x09,0x12,0x02,0x0D,
+0x3E,0x3F,0xB0,0xF0,0x30,0x18,0x0C,0x0C,0x0E,0x0E,0x0E,0x06,0x03,0x01,0x80,0x00,0x00,0x30,0x18,0x0C,0x00,
+// '@'
+0x40,0x02,0x15,0x15,0x02,0x18,
+0x00,0xFC,0x00,0x3F,0xF8,0x03,0xC0,0xF0,0x38,0x01,0xC3,0x80,0x07,0x38,0x79,0x99,0x8F,0xEC,0xFC,0x71,0xE3,0xC7,0x07,0x1E,0x30,0x18,0xF1,0x80,0xC7,0x8C,0x06,0x3C,0x70,0x73,0x71,0xC7,0xB9,0x8F,0xEF,0x8E,0x1E,0x70,0x38,0x00,0x00,0xE0,0x04,0x03,0xC0,0xE0,0x0F,0xFE,0x00,0x0F,0x80,0x00,
+// 'A'
+0x41,0x01,0x10,0x12,0x00,0x10,
+0x03,0xC0,0x03,0xC0,0x03,0xC0,0x07,0xE0,0x06,0x60,0x06,0x60,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x18,0x18,0x18,0x18,0x38,0x1C,0x3F,0xFC,0x3F,0xFC,0x60,0x06,0x60,0x06,0x60,0x06,0xC0,0x03,
+// 'B'
+0x42,0x01,0x0C,0x12,0x02,0x10,
+0xFF,0x0F,0xFC,0xC0,0xEC,0x06,0xC0,0x6C,0x06,0xC0,0x6C,0x0C,0xFF,0x8F,0xFC,0xC0,0x6C,0x03,0xC0,0x3C,0x03,0xC0,0x3C,0x06,0xFF,0xEF,0xF8,
+// 'C'
+0x43,0x01,0x0E,0x12,0x01,0x11,
+0x07,0xE0,0x7F,0xE3,0xC1,0xDC,0x01,0x60,0x01,0x80,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0x60,0x01,0x80,0x07,0x00,0x4F,0x07,0x1F,0xF8,0x1F,0x80,
+// 'D'
+0x44,0x01,0x0F,0x12,0x02,0x12,
+0xFF,0x81,0xFF,0xE3,0x01,0xE6,0x00,0xEC,0x00,0xD8,0x01,0xF0,0x01,0xE0,0x03,0xC0,0x07,0x80,0x0F,0x00,0x1E,0x00,0x3C,0x00,0xF8,0x01,0xB0,0x07,0x60,0x3C,0xFF,0xF1,0xFF,0x00,
+// 'E'
+0x45,0x01,0x0B,0x12,0x02,0x0F,
+0xFF,0xFF,0xFF,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xFF,0xDF,0xFB,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xFF,0xFF,0xFC,
+// 'F'
+0x46,0x01,0x0A,0x12,0x02,0x0E,
+0xFF,0xFF,0xFC,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,0xFF,0xBF,0xEC,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0x00,
+// 'G'
+0x47,0x01,0x0F,0x12,0x01,0x13,
+0x07,0xE0,0x3F,0xF0,0xE0,0x73,0x80,0x26,0x00,0x1C,0x00,0x30,0x00,0x60,0x00,0xC0,0x7F,0x80,0xFF,0x00,0x1E,0x00,0x36,0x00,0x6C,0x00,0xDC,0x01,0x9E,0x07,0x1F,0xFC,0x0F,0xE0,
+// 'H'
+0x48,0x01,0x0D,0x12,0x02,0x12,
+0xC0,0x1E,0x00,0xF0,0x07,0x80,0x3C,0x01,0xE0,0x0F,0x00,0x78,0x03,0xFF,0xFF,0xFF,0xF0,0x07,0x80,0x3C,0x01,0xE0,0x0F,0x00,0x78,0x03,0xC0,0x1E,0x00,0xC0,
+// 'I'
+0x49,0x01,0x02,0x12,0x02,0x07,
+0xFF,0xFF,0xFF,0xFF,0xF0,
+// 'J'
+0x4A,0x01,0x06,0x17,0xFE,0x07,
+0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x1B,0xEF,0x00,
+// 'K'
+0x4B,0x01,0x0F,0x12,0x02,0x10,
+0xC0,0x71,0x81,0xC3,0x07,0x06,0x1C,0x0C,0x70,0x19,0xC0,0x37,0x00,0x7C,0x00,0xF8,0x01,0xB0,0x03,0x38,0x06,0x38,0x0C,0x38,0x18,0x38,0x30,0x38,0x60,0x38,0xC0,0x39,0x80,0x38,
+// 'L'
+0x4C,0x01,0x0B,0x12,0x02,0x0D,
+0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xFF,0xFF,0xFC,
+// 'M'
+0x4D,0x01,0x10,0x12,0x02,0x15,
+0xE0,0x07,0xF0,0x0F,0xF0,0x0F,0xF8,0x1F,0xD8,0x1B,0xD8,0x1B,0xCC,0x33,0xCC,0x33,0xCC,0x33,0xC6,0x63,0xC6,0x63,0xC7,0xE3,0xC3,0xC3,0xC3,0xC3,0xC1,0x83,0xC0,0x03,0xC0,0x03,0xC0,0x03,
+// 'N'
+0x4E,0x01,0x0D,0x12,0x02,0x12,
+0xE0,0x1F,0x80,0xFC,0x07,0xF0,0x3D,0x81,0xE6,0x0F,0x30,0x78,0xC3,0xC6,0x1E,0x18,0xF0,0xC7,0x83,0x3C,0x19,0xE0,0x6F,0x03,0x78,0x0F,0xC0,0x7E,0x01,0xC0,
+// 'O'
+0x4F,0x01,0x10,0x12,0x01,0x13,
+0x07,0xE0,0x1F,0xF8,0x3C,0x3C,0x70,0x0E,0x60,0x06,0x60,0x06,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0x60,0x06,0x60,0x06,0x70,0x0E,0x3C,0x3C,0x1F,0xF8,0x07,0xE0,
+// 'P'
+0x50,0x01,0x0B,0x12,0x02,0x0E,
+0xFF,0x1F,0xFB,0x07,0x60,0x3C,0x07,0x80,0xF0,0x1E,0x0E,0xFF,0xDF,0xE3,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x18,0x00,
+// 'Q'
+0x51,0x01,0x10,0x15,0x01,0x13,
+0x07,0xE0,0x1F,0xF8,0x3C,0x3C,0x70,0x0E,0x60,0x06,0x60,0x06,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0x60,0x07,0x60,0x06,0x70,0x0E,0x3C,0x3C,0x1F,0xF8,0x07,0xF0,0x00,0x38,0x00,0x18,0x00,0x0C,
+// 'R'
+0x52,0x01,0x0D,0x12,0x02,0x11,
+0xFF,0x07,0xFE,0x30,0x31,0x80,0xCC,0x06,0x60,0x33,0x01,0x98,0x18,0xFF,0xC7,0xFC,0x30,0x71,0x81,0x8C,0x06,0x60,0x33,0x01,0xD8,0x06,0xC0,0x36,0x00,0xC0,
+// 'S'
+0x53,0x01,0x0C,0x12,0x02,0x0F,
+0x1F,0x87,0xFE,0x70,0x6C,0x00,0xC0,0x0C,0x00,0xC0,0x07,0x00,0x7F,0x01,0xFC,0x00,0xE0,0x07,0x00,0x30,0x03,0x00,0x3C,0x0E,0xFF,0xE3,0xF8,
+// 'T'
+0x54,0x01,0x0E,0x12,0x00,0x0F,
+0xFF,0xFF,0xFF,0xF0,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,
+// 'U'
+0x55,0x01,0x0D,0x12,0x02,0x12,
+0xC0,0x1E,0x00,0xF0,0x07,0x80,0x3C,0x01,0xE0,0x0F,0x00,0x78,0x03,0xC0,0x1E,0x00,0xF0,0x07,0x80,0x3C,0x01,0xE0,0x0D,0x80,0xCE,0x0E,0x3F,0xE0,0x7C,0x00,
+// 'V'
+0x56,0x01,0x10,0x12,0x00,0x10,
+0xC0,0x03,0x60,0x06,0x60,0x06,0x60,0x06,0x30,0x0C,0x30,0x0C,0x38,0x1C,0x18,0x18,0x18,0x18,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x06,0x60,0x06,0x60,0x07,0x60,0x03,0xC0,0x03,0xC0,0x03,0xC0,
+// 'W'
+0x57,0x01,0x16,0x12,0x01,0x18,
+0xC0,0x78,0x0F,0x01,0xE0,0x36,0x07,0x81,0x98,0x1E,0x06,0x60,0xEC,0x19,0x83,0x30,0x63,0x0C,0xC3,0x0C,0x33,0x0C,0x30,0xCE,0x30,0xC6,0x18,0xC1,0x98,0x66,0x06,0x61,0x98,0x19,0x86,0x60,0x6C,0x0D,0x80,0xF0,0x3C,0x03,0xC0,0xF0,0x0F,0x03,0xC0,0x38,0x07,0x00,
+// 'X'
+0x58,0x01,0x0F,0x12,0x01,0x11,
+0x70,0x0E,0x60,0x18,0x60,0x60,0xE1,0xC0,0xC7,0x00,0xCC,0x01,0xF0,0x01,0xE0,0x03,0x80,0x07,0x80,0x1F,0x00,0x37,0x00,0xC6,0x03,0x86,0x0E,0x0E,0x18,0x0C,0x60,0x0D,0xC0,0x1C,
+// 'Y'
+0x59,0x01,0x0E,0x12,0x00,0x0F,
+0xE0,0x1D,0x80,0x63,0x03,0x0E,0x1C,0x18,0x60,0x33,0x00,0xFC,0x01,0xE0,0x07,0x80,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,
+// 'Z'
+0x5A,0x01,0x0E,0x12,0x01,0x10,
+0xFF,0xFF,0xFF,0xF0,0x01,0x80,0x0E,0x00,0x70,0x01,0x80,0x0C,0x00,0x60,0x03,0x80,0x1C,0x00,0x60,0x03,0x00,0x18,0x00,0xE0,0x07,0x00,0x18,0x00,0xFF,0xFF,0xFF,0xF0,
+// '['
+0x5B,0x01,0x05,0x15,0x02,0x09,
+0xFF,0xF1,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0xFF,0x80,
+// '\'
+0x5C,0x01,0x08,0x14,0x00,0x08,
+0xC0,0xE0,0x60,0x60,0x60,0x30,0x30,0x30,0x18,0x18,0x18,0x18,0x0C,0x0C,0x0C,0x06,0x06,0x06,0x07,0x03,
+// ']'
+0x5D,0x01,0x05,0x15,0x02,0x09,
+0xFF,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC7,0xFF,0x80,
+// '^'
+0x5E,0x01,0x0F,0x07,0x03,0x14,
+0x03,0x80,0x0F,0x80,0x3B,0x80,0xE3,0x83,0x83,0x8E,0x03,0xB8,0x03,0x80,
+// '_'
+0x5F,0x17,0x0C,0x02,0x00,0x0C,
+0xFF,0xFF,0xFF,
+// '`'
+0x60,0x00,0x06,0x04,0x02,0x0C,
+0x60,0xC1,0x83,
+// 'a'
+0x61,0x06,0x0B,0x0D,0x01,0x0E,
+0x3F,0x0F,0xF9,0x03,0x00,0x30,0x06,0x3F,0xDF,0xFF,0x03,0xC0,0x78,0x1F,0x87,0xBF,0xF3,0xE6,
+// 'b'
+0x62,0x01,0x0C,0x12,0x02,0x0F,
+0xC0,0x0C,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0xF8,0xFF,0xCF,0x0E,0xE0,0x6C,0x03,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xE0,0x6F,0x0E,0xFF,0xCC,0xF8,
+// 'c'
+0x63,0x06,0x0A,0x0D,0x01,0x0D,
+0x0F,0x8F,0xF7,0x05,0x80,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x18,0x07,0x04,0xFF,0x0F,0x80,
+// 'd'
+0x64,0x01,0x0C,0x12,0x01,0x0F,
+0x00,0x30,0x03,0x00,0x30,0x03,0x00,0x31,0xF3,0x3F,0xF7,0x0F,0x60,0x7C,0x03,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0x60,0x77,0x0F,0x3F,0xF1,0xF3,
+// 'e'
+0x65,0x06,0x0C,0x0D,0x01,0x0E,
+0x0F,0x83,0xFC,0x70,0xE6,0x07,0xC0,0x3F,0xFF,0xFF,0xFC,0x00,0xC0,0x06,0x00,0x70,0x23,0xFE,0x0F,0xC0,
+// 'f'
+0x66,0x01,0x08,0x12,0x01,0x08,
+0x0F,0x1F,0x38,0x30,0x30,0xFF,0xFF,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,
+// 'g'
+0x67,0x06,0x0C,0x12,0x01,0x0F,
+0x1F,0x33,0xFF,0x70,0xF6,0x07,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x07,0x70,0xF3,0xFF,0x1F,0x30,0x03,0x00,0x72,0x0E,0x3F,0xC1,0xF8,
+// 'h'
+0x68,0x01,0x0B,0x12,0x02,0x0F,
+0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x9F,0x3F,0xF7,0x87,0xE0,0x78,0x0F,0x01,0xE0,0x3C,0x07,0x80,0xF0,0x1E,0x03,0xC0,0x78,0x0C,
+// 'i'
+0x69,0x01,0x02,0x12,0x02,0x07,
+0xFC,0x3F,0xFF,0xFF,0xF0,
+// 'j'
+0x6A,0x01,0x05,0x17,0xFF,0x07,
+0x18,0xC6,0x00,0x0C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x33,0xFB,0x80,
+// 'k'
+0x6B,0x01,0x0C,0x12,0x02,0x0E,
+0xC0,0x0C,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0x1C,0xC3,0x8C,0x70,0xCE,0x0D,0xC0,0xF8,0x0F,0x80,0xDC,0x0C,0xE0,0xC7,0x0C,0x38,0xC1,0xCC,0x0E,
+// 'l'
+0x6C,0x01,0x02,0x12,0x02,0x06,
+0xFF,0xFF,0xFF,0xFF,0xF0,
+// 'm'
+0x6D,0x06,0x14,0x0D,0x02,0x18,
+0xCF,0x87,0xCF,0xFC,0xFE,0xF0,0xF8,0x7E,0x07,0x03,0xC0,0x60,0x3C,0x06,0x03,0xC0,0x60,0x3C,0x06,0x03,0xC0,0x60,0x3C,0x06,0x03,0xC0,0x60,0x3C,0x06,0x03,0xC0,0x60,0x30,
+// 'n'
+0x6E,0x06,0x0B,0x0D,0x02,0x0F,
+0xCF,0x9F,0xFB,0xC3,0xF0,0x3C,0x07,0x80,0xF0,0x1E,0x03,0xC0,0x78,0x0F,0x01,0xE0,0x3C,0x06,
+// 'o'
+0x6F,0x06,0x0C,0x0D,0x01,0x0E,
+0x1F,0x83,0xFC,0x70,0xE6,0x06,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x06,0x70,0xE3,0xFC,0x1F,0x80,
+// 'p'
+0x70,0x06,0x0C,0x12,0x02,0x0F,
+0xCF,0x8F,0xFC,0xF0,0xEE,0x06,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x3E,0x06,0xF0,0xEF,0xFC,0xCF,0x8C,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0x00,
+// 'q'
+0x71,0x06,0x0C,0x12,0x01,0x0F,
+0x1F,0x33,0xFF,0x70,0xF6,0x07,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0xC0,0x36,0x07,0x70,0xF3,0xFF,0x1F,0x30,0x03,0x00,0x30,0x03,0x00,0x30,0x03,
+// 'r'
+0x72,0x06,0x08,0x0D,0x02,0x0A,
+0xCF,0xFF,0xF0,0xE0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,
+// 's'
+0x73,0x06,0x0B,0x0D,0x01,0x0C,
+0x3F,0x0F,0xF3,0x82,0x60,0x0C,0x00,0xF0,0x0F,0xC0,0x3C,0x00,0xC0,0x1A,0x07,0x7F,0xC7,0xF0,
+// 't'
+0x74,0x02,0x08,0x11,0x00,0x09,
+0x30,0x30,0x30,0x30,0xFF,0xFF,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x1F,0x0F,
+// 'u'
+0x75,0x06,0x0B,0x0D,0x02,0x0F,
+0xC0,0x78,0x0F,0x01,0xE0,0x3C,0x07,0x80,0xF0,0x1E,0x03,0xC0,0x78,0x1F,0x87,0xBF,0xF3,0xE6,
+// 'v'
+0x76,0x06,0x0D,0x0D,0x01,0x0F,
+0xC0,0x1B,0x01,0x98,0x0C,0xC0,0x63,0x06,0x18,0x30,0x63,0x03,0x18,0x18,0xC0,0x6C,0x03,0x60,0x1F,0x00,0x70,0x00,
+// 'w'
+0x77,0x06,0x12,0x0D,0x01,0x14,
+0xC1,0xE0,0xF0,0x78,0x36,0x1E,0x19,0x87,0x86,0x63,0x31,0x9C,0xCC,0xE3,0x33,0x30,0xCC,0xCC,0x36,0x1B,0x07,0x87,0x81,0xE1,0xE0,0x78,0x78,0x1C,0x0E,0x00,
+// 'x'
+0x78,0x06,0x0D,0x0D,0x01,0x0F,
+0xE0,0x3B,0x83,0x8E,0x38,0x31,0x80,0xD8,0x07,0xC0,0x1C,0x01,0xF0,0x1D,0xC0,0xC6,0x0C,0x18,0xE0,0xEE,0x03,0x80,
+// 'y'
+0x79,0x06,0x0D,0x12,0x01,0x0F,
+0xC0,0x1B,0x01,0x98,0x0C,0xE0,0xE3,0x06,0x18,0x70,0x63,0x03,0x18,0x0D,0x80,0x6C,0x03,0xE0,0x0E,0x00,0x70,0x03,0x00,0x18,0x01,0x80,0x7C,0x03,0xC0,0x00,
+// 'z'
+0x7A,0x06,0x0B,0x0D,0x01,0x0D,
+0xFF,0xFF,0xFC,0x03,0x00,0xE0,0x38,0x0E,0x03,0x80,0xE0,0x38,0x0E,0x01,0x80,0x7F,0xFF,0xFE,
+// '{'
+0x7B,0x01,0x09,0x16,0x03,0x0F,
+0x03,0x83,0xC3,0x81,0x80,0xC0,0x60,0x30,0x18,0x0C,0x0E,0x3E,0x1F,0x01,0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x01,0xC0,0x78,0x1C,
+// '|'
+0x7C,0x01,0x02,0x18,0x03,0x08,
+0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
+// '}'
+0x7D,0x01,0x09,0x16,0x03,0x0F,
+0xE0,0x78,0x0E,0x03,0x01,0x80,0xC0,0x60,0x30,0x18,0x0E,0x03,0xE1,0xF1,0xC0,0xC0,0x60,0x30,0x18,0x0C,0x06,0x07,0x0F,0x07,0x00,
+// '~'
+0x7E,0x09,0x0F,0x05,0x03,0x14,
+0x00,0x00,0x7C,0x05,0xFE,0x1E,0x1F,0xE0,0x0F,0x80,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/SmallFont.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,120 @@
+// SmallFont.c
+// Font type    : Full (95 characters)
+// Font size    : 8x12 pixels
+// Memory usage : 1144 bytes
+
+#if defined(__AVR__)
+	#include <avr/pgmspace.h>
+	#define fontdatatype const uint8_t
+#elif defined(__PIC32MX__)
+	#define PROGMEM
+	#define fontdatatype const unsigned char
+#elif defined(__arm__)
+	#define PROGMEM
+	#define fontdatatype const unsigned char
+#endif
+
+const unsigned char tft_SmallFont[1144] =
+{
+0x08,0x0C,0x20,0x5F,
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // <space>
+0x00,0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x00,  // !
+0x00,0x28,0x50,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // "
+0x00,0x00,0x28,0x28,0xFC,0x28,0x50,0xFC,0x50,0x50,0x00,0x00,  // #
+0x00,0x20,0x78,0xA8,0xA0,0x60,0x30,0x28,0xA8,0xF0,0x20,0x00,  // $
+0x00,0x00,0x48,0xA8,0xB0,0x50,0x28,0x34,0x54,0x48,0x00,0x00,  // %
+0x00,0x00,0x20,0x50,0x50,0x78,0xA8,0xA8,0x90,0x6C,0x00,0x00,  // &
+0x00,0x40,0x40,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // '
+0x00,0x04,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x08,0x04,0x00,  // (
+0x00,0x40,0x20,0x10,0x10,0x10,0x10,0x10,0x10,0x20,0x40,0x00,  // )
+0x00,0x00,0x00,0x20,0xA8,0x70,0x70,0xA8,0x20,0x00,0x00,0x00,  // *
+0x00,0x00,0x20,0x20,0x20,0xF8,0x20,0x20,0x20,0x00,0x00,0x00,  // +
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x40,0x80,  // ,
+0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,  // -
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,  // .
+0x00,0x08,0x10,0x10,0x10,0x20,0x20,0x40,0x40,0x40,0x80,0x00,  // /
+
+0x00,0x00,0x70,0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00,  // 0
+0x00,0x00,0x20,0x60,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00,  // 1
+0x00,0x00,0x70,0x88,0x88,0x10,0x20,0x40,0x80,0xF8,0x00,0x00,  // 2
+0x00,0x00,0x70,0x88,0x08,0x30,0x08,0x08,0x88,0x70,0x00,0x00,  // 3
+0x00,0x00,0x10,0x30,0x50,0x50,0x90,0x78,0x10,0x18,0x00,0x00,  // 4
+0x00,0x00,0xF8,0x80,0x80,0xF0,0x08,0x08,0x88,0x70,0x00,0x00,  // 5
+0x00,0x00,0x70,0x90,0x80,0xF0,0x88,0x88,0x88,0x70,0x00,0x00,  // 6
+0x00,0x00,0xF8,0x90,0x10,0x20,0x20,0x20,0x20,0x20,0x00,0x00,  // 7
+0x00,0x00,0x70,0x88,0x88,0x70,0x88,0x88,0x88,0x70,0x00,0x00,  // 8
+0x00,0x00,0x70,0x88,0x88,0x88,0x78,0x08,0x48,0x70,0x00,0x00,  // 9
+0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x20,0x00,0x00,  // :
+0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x20,0x20,0x00,  // ;
+0x00,0x04,0x08,0x10,0x20,0x40,0x20,0x10,0x08,0x04,0x00,0x00,  // <
+0x00,0x00,0x00,0x00,0xF8,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,  // =
+0x00,0x40,0x20,0x10,0x08,0x04,0x08,0x10,0x20,0x40,0x00,0x00,  // >
+0x00,0x00,0x70,0x88,0x88,0x10,0x20,0x20,0x00,0x20,0x00,0x00,  // ?
+
+0x00,0x00,0x70,0x88,0x98,0xA8,0xA8,0xB8,0x80,0x78,0x00,0x00,  // @
+0x00,0x00,0x20,0x20,0x30,0x50,0x50,0x78,0x48,0xCC,0x00,0x00,  // A
+0x00,0x00,0xF0,0x48,0x48,0x70,0x48,0x48,0x48,0xF0,0x00,0x00,  // B
+0x00,0x00,0x78,0x88,0x80,0x80,0x80,0x80,0x88,0x70,0x00,0x00,  // C
+0x00,0x00,0xF0,0x48,0x48,0x48,0x48,0x48,0x48,0xF0,0x00,0x00,  // D
+0x00,0x00,0xF8,0x48,0x50,0x70,0x50,0x40,0x48,0xF8,0x00,0x00,  // E
+0x00,0x00,0xF8,0x48,0x50,0x70,0x50,0x40,0x40,0xE0,0x00,0x00,  // F
+0x00,0x00,0x38,0x48,0x80,0x80,0x9C,0x88,0x48,0x30,0x00,0x00,  // G
+0x00,0x00,0xCC,0x48,0x48,0x78,0x48,0x48,0x48,0xCC,0x00,0x00,  // H
+0x00,0x00,0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0xF8,0x00,0x00,  // I
+0x00,0x00,0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x90,0xE0,0x00,  // J
+0x00,0x00,0xEC,0x48,0x50,0x60,0x50,0x50,0x48,0xEC,0x00,0x00,  // K
+0x00,0x00,0xE0,0x40,0x40,0x40,0x40,0x40,0x44,0xFC,0x00,0x00,  // L
+0x00,0x00,0xD8,0xD8,0xD8,0xD8,0xA8,0xA8,0xA8,0xA8,0x00,0x00,  // M
+0x00,0x00,0xDC,0x48,0x68,0x68,0x58,0x58,0x48,0xE8,0x00,0x00,  // N
+0x00,0x00,0x70,0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00,  // O
+
+0x00,0x00,0xF0,0x48,0x48,0x70,0x40,0x40,0x40,0xE0,0x00,0x00,  // P
+0x00,0x00,0x70,0x88,0x88,0x88,0x88,0xE8,0x98,0x70,0x18,0x00,  // Q
+0x00,0x00,0xF0,0x48,0x48,0x70,0x50,0x48,0x48,0xEC,0x00,0x00,  // R
+0x00,0x00,0x78,0x88,0x80,0x60,0x10,0x08,0x88,0xF0,0x00,0x00,  // S
+0x00,0x00,0xF8,0xA8,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00,  // T
+0x00,0x00,0xCC,0x48,0x48,0x48,0x48,0x48,0x48,0x30,0x00,0x00,  // U
+0x00,0x00,0xCC,0x48,0x48,0x50,0x50,0x30,0x20,0x20,0x00,0x00,  // V
+0x00,0x00,0xA8,0xA8,0xA8,0x70,0x50,0x50,0x50,0x50,0x00,0x00,  // W
+0x00,0x00,0xD8,0x50,0x50,0x20,0x20,0x50,0x50,0xD8,0x00,0x00,  // X
+0x00,0x00,0xD8,0x50,0x50,0x20,0x20,0x20,0x20,0x70,0x00,0x00,  // Y
+0x00,0x00,0xF8,0x90,0x10,0x20,0x20,0x40,0x48,0xF8,0x00,0x00,  // Z
+0x00,0x38,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x38,0x00,  // [
+0x00,0x40,0x40,0x40,0x20,0x20,0x10,0x10,0x10,0x08,0x00,0x00,  // <backslash>
+0x00,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x70,0x00,  // ]
+0x00,0x20,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ^
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC,  // _
+
+0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // `
+0x00,0x00,0x00,0x00,0x00,0x30,0x48,0x38,0x48,0x3C,0x00,0x00,  // a
+0x00,0x00,0xC0,0x40,0x40,0x70,0x48,0x48,0x48,0x70,0x00,0x00,  // b
+0x00,0x00,0x00,0x00,0x00,0x38,0x48,0x40,0x40,0x38,0x00,0x00,  // c
+0x00,0x00,0x18,0x08,0x08,0x38,0x48,0x48,0x48,0x3C,0x00,0x00,  // d
+0x00,0x00,0x00,0x00,0x00,0x30,0x48,0x78,0x40,0x38,0x00,0x00,  // e
+0x00,0x00,0x1C,0x20,0x20,0x78,0x20,0x20,0x20,0x78,0x00,0x00,  // f
+0x00,0x00,0x00,0x00,0x00,0x3C,0x48,0x30,0x40,0x78,0x44,0x38,  // g
+0x00,0x00,0xC0,0x40,0x40,0x70,0x48,0x48,0x48,0xEC,0x00,0x00,  // h
+0x00,0x00,0x20,0x00,0x00,0x60,0x20,0x20,0x20,0x70,0x00,0x00,  // i
+0x00,0x00,0x10,0x00,0x00,0x30,0x10,0x10,0x10,0x10,0x10,0xE0,  // j
+0x00,0x00,0xC0,0x40,0x40,0x5C,0x50,0x70,0x48,0xEC,0x00,0x00,  // k
+0x00,0x00,0xE0,0x20,0x20,0x20,0x20,0x20,0x20,0xF8,0x00,0x00,  // l
+0x00,0x00,0x00,0x00,0x00,0xF0,0xA8,0xA8,0xA8,0xA8,0x00,0x00,  // m
+0x00,0x00,0x00,0x00,0x00,0xF0,0x48,0x48,0x48,0xEC,0x00,0x00,  // n
+0x00,0x00,0x00,0x00,0x00,0x30,0x48,0x48,0x48,0x30,0x00,0x00,  // o
+
+0x00,0x00,0x00,0x00,0x00,0xF0,0x48,0x48,0x48,0x70,0x40,0xE0,  // p
+0x00,0x00,0x00,0x00,0x00,0x38,0x48,0x48,0x48,0x38,0x08,0x1C,  // q
+0x00,0x00,0x00,0x00,0x00,0xD8,0x60,0x40,0x40,0xE0,0x00,0x00,  // r
+0x00,0x00,0x00,0x00,0x00,0x78,0x40,0x30,0x08,0x78,0x00,0x00,  // s
+0x00,0x00,0x00,0x20,0x20,0x70,0x20,0x20,0x20,0x18,0x00,0x00,  // t
+0x00,0x00,0x00,0x00,0x00,0xD8,0x48,0x48,0x48,0x3C,0x00,0x00,  // u
+0x00,0x00,0x00,0x00,0x00,0xEC,0x48,0x50,0x30,0x20,0x00,0x00,  // v
+0x00,0x00,0x00,0x00,0x00,0xA8,0xA8,0x70,0x50,0x50,0x00,0x00,  // w
+0x00,0x00,0x00,0x00,0x00,0xD8,0x50,0x20,0x50,0xD8,0x00,0x00,  // x
+0x00,0x00,0x00,0x00,0x00,0xEC,0x48,0x50,0x30,0x20,0x20,0xC0,  // y
+0x00,0x00,0x00,0x00,0x00,0x78,0x10,0x20,0x20,0x78,0x00,0x00,  // z
+0x00,0x18,0x10,0x10,0x10,0x20,0x10,0x10,0x10,0x10,0x18,0x00,  // {
+0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,  // |
+0x00,0x60,0x20,0x20,0x20,0x10,0x20,0x20,0x20,0x20,0x60,0x00,  // }
+0x40,0xA4,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ~
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/Ubuntu16.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,331 @@
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// on Arduinos. It can however handle any ttf font that has been converted
+// using the conversion program. These could be fixed width or proportional 
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// Ubuntu16.c
+// Point Size   : 16
+// Memory usage : 1433 bytes
+// # characters : 95
+
+// Header Format (to make Arduino UTFT Compatible):
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+const unsigned char tft_Ubuntu16[] =
+{
+0x00, 0x10, 0x00, 0x00,
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+
+// ' '
+0x20,0x0D,0x00,0x00,0x00,0x04,
+
+// '!'
+0x21,0x02,0x01,0x0B,0x01,0x04,
+0xFC,0x60,
+// '"'
+0x22,0x00,0x04,0x04,0x01,0x07,
+0x99,0x99,
+// '#'
+0x23,0x02,0x09,0x0B,0x01,0x0B,
+0x11,0x08,0x84,0x5F,0xF2,0x21,0x10,0x89,0xFF,0x44,0x22,0x11,0x00,
+// '$'
+0x24,0x00,0x07,0x10,0x01,0x09,
+0x10,0x20,0xF6,0x08,0x10,0x18,0x08,0x0C,0x0C,0x08,0x3F,0xC2,0x04,0x00,
+// '%'
+0x25,0x02,0x0C,0x0B,0x01,0x0E,
+0x70,0x4D,0x88,0x89,0x08,0x90,0xDA,0x07,0x4E,0x05,0xB0,0x91,0x09,0x11,0x1B,0x20,0xE0,
+// '&'
+0x26,0x02,0x0A,0x0B,0x01,0x0B,
+0x3C,0x18,0x84,0x21,0x08,0x2C,0x0C,0x04,0x8A,0x10,0x83,0x30,0xC7,0xC8,
+// '''
+0x27,0x00,0x01,0x04,0x01,0x04,
+0xF0,
+// '('
+0x28,0x00,0x04,0x10,0x01,0x05,
+0x02,0x44,0x48,0x88,0x88,0x84,0x44,0x20,
+// ')'
+0x29,0x00,0x04,0x10,0x00,0x05,
+0x04,0x22,0x21,0x11,0x11,0x12,0x22,0x40,
+// '*'
+0x2A,0x02,0x09,0x06,0x00,0x08,
+0x08,0x24,0x8F,0x83,0x81,0x41,0x10,
+// '+'
+0x2B,0x05,0x07,0x07,0x01,0x09,
+0x10,0x20,0x47,0xF1,0x02,0x04,0x00,
+// ','
+0x2C,0x0B,0x02,0x05,0x00,0x04,
+0x54,0x80,
+// '-'
+0x2D,0x08,0x04,0x01,0x01,0x06,
+0xF0,
+// '.'
+0x2E,0x0B,0x01,0x02,0x01,0x04,
+0xC0,
+// '/'
+0x2F,0x00,0x07,0x10,0x00,0x06,
+0x02,0x08,0x10,0x20,0x81,0x02,0x08,0x10,0x40,0x81,0x04,0x08,0x10,0x40,
+// '0'
+0x30,0x02,0x07,0x0B,0x01,0x09,
+0x38,0x8B,0x1C,0x18,0x30,0x60,0xC1,0x86,0x88,0xE0,
+// '1'
+0x31,0x02,0x04,0x0B,0x01,0x09,
+0x13,0x59,0x11,0x11,0x11,0x10,
+// '2'
+0x32,0x02,0x06,0x0B,0x01,0x09,
+0x7A,0x30,0x41,0x08,0x21,0x08,0x42,0x0F,0xC0,
+// '3'
+0x33,0x02,0x07,0x0B,0x01,0x09,
+0x78,0x08,0x08,0x10,0x47,0x01,0x01,0x02,0x0B,0xE0,
+// '4'
+0x34,0x02,0x07,0x0B,0x01,0x09,
+0x04,0x18,0x51,0x22,0x48,0xA1,0x7F,0x04,0x08,0x10,
+// '5'
+0x35,0x02,0x07,0x0B,0x01,0x09,
+0x7E,0x81,0x02,0x07,0x81,0x80,0x81,0x02,0x0B,0xE0,
+// '6'
+0x36,0x02,0x07,0x0B,0x01,0x09,
+0x1C,0x61,0x00,0x0F,0x90,0xA0,0xC1,0x82,0x88,0xE0,
+// '7'
+0x37,0x02,0x07,0x0B,0x01,0x09,
+0xFE,0x04,0x10,0x40,0x82,0x04,0x08,0x20,0x40,0x80,
+// '8'
+0x38,0x02,0x07,0x0B,0x01,0x09,
+0x39,0x8A,0x0C,0x14,0x47,0x11,0x41,0x83,0x89,0xE0,
+// '9'
+0x39,0x02,0x07,0x0B,0x01,0x09,
+0x38,0x8A,0x0C,0x18,0x28,0x4F,0x81,0x04,0x11,0xC0,
+// ':'
+0x3A,0x05,0x01,0x08,0x01,0x04,
+0xC3,
+// ';'
+0x3B,0x05,0x02,0x0B,0x00,0x04,
+0x50,0x05,0x48,
+// '<'
+0x3C,0x05,0x08,0x07,0x01,0x09,
+0x02,0x0C,0x30,0x60,0x30,0x0C,0x02,
+// '='
+0x3D,0x06,0x07,0x04,0x01,0x09,
+0xFE,0x00,0x07,0xF0,
+// '>'
+0x3E,0x05,0x09,0x07,0x00,0x09,
+0x40,0x1C,0x01,0x80,0x70,0x61,0xC1,0x00,
+// '?'
+0x3F,0x02,0x06,0x0B,0x01,0x07,
+0x78,0x30,0x41,0x18,0xC2,0x00,0x00,0x82,0x00,
+// '@'
+0x40,0x02,0x0D,0x0D,0x01,0x0F,
+0x0F,0x81,0x83,0x10,0x0C,0x8F,0xA8,0x84,0xC8,0x26,0x41,0x32,0x09,0x88,0x5A,0x3F,0x90,0x00,0x60,0x00,0xFC,0x00,
+// 'A'
+0x41,0x02,0x0B,0x0B,0x00,0x0B,
+0x04,0x01,0xC0,0x28,0x08,0x81,0x10,0x61,0x08,0x21,0xFC,0x60,0x48,0x0B,0x00,0x80,
+// 'B'
+0x42,0x02,0x08,0x0B,0x01,0x0A,
+0xF8,0x86,0x82,0x82,0x86,0xFC,0x82,0x81,0x81,0x82,0xFC,
+// 'C'
+0x43,0x02,0x09,0x0B,0x01,0x0B,
+0x1F,0x10,0x10,0x10,0x08,0x04,0x02,0x01,0x00,0x40,0x30,0x07,0xC0,
+// 'D'
+0x44,0x02,0x09,0x0B,0x01,0x0B,
+0xFC,0x41,0x20,0x50,0x18,0x0C,0x06,0x03,0x01,0x81,0x41,0x3F,0x00,
+// 'E'
+0x45,0x02,0x07,0x0B,0x01,0x09,
+0xFF,0x02,0x04,0x08,0x1F,0xA0,0x40,0x81,0x03,0xF8,
+// 'F'
+0x46,0x02,0x07,0x0B,0x01,0x09,
+0xFF,0x02,0x04,0x08,0x1F,0xA0,0x40,0x81,0x02,0x00,
+// 'G'
+0x47,0x02,0x09,0x0B,0x01,0x0B,
+0x1F,0x10,0x10,0x10,0x08,0x04,0x02,0x03,0x01,0x40,0xB0,0x47,0xE0,
+// 'H'
+0x48,0x02,0x09,0x0B,0x01,0x0B,
+0x80,0xC0,0x60,0x30,0x18,0x0F,0xFE,0x03,0x01,0x80,0xC0,0x60,0x20,
+// 'I'
+0x49,0x02,0x01,0x0B,0x01,0x03,
+0xFF,0xE0,
+// 'J'
+0x4A,0x02,0x07,0x0B,0x00,0x08,
+0x02,0x04,0x08,0x10,0x20,0x40,0x81,0x02,0x09,0xE0,
+// 'K'
+0x4B,0x02,0x09,0x0B,0x01,0x0A,
+0x81,0x41,0x23,0x12,0x0A,0x06,0x02,0xC1,0x10,0x86,0x40,0xA0,0x20,
+// 'L'
+0x4C,0x02,0x07,0x0B,0x01,0x08,
+0x81,0x02,0x04,0x08,0x10,0x20,0x40,0x81,0x03,0xF8,
+// 'M'
+0x4D,0x02,0x0B,0x0B,0x01,0x0D,
+0x40,0x4C,0x19,0x01,0x28,0xA5,0x14,0x94,0xB2,0x9C,0x33,0x84,0x30,0x06,0x00,0x80,
+// 'N'
+0x4E,0x02,0x09,0x0B,0x01,0x0B,
+0x80,0xE0,0x68,0x32,0x19,0x0C,0x46,0x13,0x05,0x82,0xC0,0xE0,0x20,
+// 'O'
+0x4F,0x02,0x0B,0x0B,0x01,0x0D,
+0x1F,0x04,0x11,0x01,0x40,0x18,0x03,0x00,0x60,0x0C,0x01,0x40,0x44,0x10,0x7C,0x00,
+// 'P'
+0x50,0x02,0x08,0x0B,0x01,0x0A,
+0xFC,0x82,0x81,0x81,0x81,0x82,0xFC,0x80,0x80,0x80,0x80,
+// 'Q'
+0x51,0x02,0x0B,0x0E,0x01,0x0D,
+0x1F,0x04,0x11,0x01,0x40,0x18,0x03,0x00,0x60,0x0C,0x01,0x40,0x44,0x10,0x78,0x02,0x00,0x30,0x01,0x80,
+// 'R'
+0x52,0x02,0x09,0x0B,0x01,0x0A,
+0xFC,0x41,0x20,0x50,0x28,0x14,0x13,0xF1,0x08,0x82,0x40,0xA0,0x20,
+// 'S'
+0x53,0x02,0x08,0x0B,0x01,0x09,
+0x3C,0xC2,0x80,0x80,0x40,0x1C,0x06,0x02,0x02,0x06,0x78,
+// 'T'
+0x54,0x02,0x09,0x0B,0x00,0x09,
+0xFF,0x84,0x02,0x01,0x00,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x00,
+// 'U'
+0x55,0x02,0x09,0x0B,0x01,0x0B,
+0x80,0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x01,0x80,0xA0,0x8F,0x80,
+// 'V'
+0x56,0x02,0x09,0x0B,0x00,0x09,
+0x80,0xE0,0xD0,0x48,0x26,0x21,0x10,0x88,0x28,0x14,0x0E,0x02,0x00,
+// 'W'
+0x57,0x02,0x0D,0x0B,0x00,0x0D,
+0x80,0x0E,0x10,0xD0,0x84,0x8E,0x24,0x51,0x22,0x88,0xA2,0x85,0x14,0x38,0xE0,0xC2,0x04,0x10,
+// 'X'
+0x58,0x02,0x09,0x0B,0x00,0x09,
+0xC1,0xA0,0x88,0x86,0xC1,0x40,0x60,0x70,0x6C,0x22,0x20,0xB0,0x60,
+// 'Y'
+0x59,0x02,0x09,0x0B,0x00,0x09,
+0x80,0xA0,0x90,0x44,0x41,0x40,0xA0,0x20,0x10,0x08,0x04,0x02,0x00,
+// 'Z'
+0x5A,0x02,0x07,0x0B,0x01,0x09,
+0xFE,0x04,0x10,0x41,0x02,0x08,0x00,0x41,0x03,0xF8,
+// '['
+0x5B,0x00,0x03,0x10,0x02,0x05,
+0xF2,0x49,0x24,0x92,0x49,0x27,
+// '\'
+0x5C,0x00,0x07,0x10,0x00,0x06,
+0x80,0x81,0x02,0x02,0x04,0x08,0x08,0x10,0x10,0x20,0x40,0x40,0x81,0x01,
+// ']'
+0x5D,0x00,0x03,0x10,0x00,0x05,
+0xE4,0x92,0x49,0x24,0x92,0x4F,
+// '^'
+0x5E,0x02,0x07,0x06,0x01,0x09,
+0x10,0x70,0xA2,0x24,0x50,0x40,
+// '_'
+0x5F,0x0F,0x08,0x01,0x00,0x08,
+0xFF,
+// '`'
+0x60,0x01,0x04,0x03,0x01,0x06,
+0x86,0x10,
+// 'a'
+0x61,0x05,0x06,0x08,0x01,0x08,
+0x78,0x30,0x5F,0xC6,0x18,0x5F,
+// 'b'
+0x62,0x01,0x07,0x0C,0x01,0x09,
+0x81,0x02,0x04,0x0F,0x90,0xA0,0xC1,0x83,0x06,0x17,0xC0,
+// 'c'
+0x63,0x05,0x06,0x08,0x01,0x08,
+0x3D,0x08,0x20,0x82,0x04,0x0F,
+// 'd'
+0x64,0x01,0x07,0x0C,0x01,0x09,
+0x02,0x04,0x08,0x13,0xE8,0x60,0xC1,0x83,0x05,0x09,0xF0,
+// 'e'
+0x65,0x05,0x07,0x08,0x01,0x09,
+0x3C,0x8A,0x0F,0xF8,0x10,0x10,0x1E,
+// 'f'
+0x66,0x01,0x05,0x0C,0x01,0x06,
+0x7E,0x21,0x0F,0xC2,0x10,0x84,0x21,0x00,
+// 'g'
+0x67,0x05,0x07,0x0B,0x01,0x09,
+0x3E,0x86,0x0C,0x18,0x30,0x50,0x9F,0x02,0x0B,0xE0,
+// 'h'
+0x68,0x01,0x07,0x0C,0x01,0x09,
+0x81,0x02,0x04,0x0F,0x90,0xE0,0xC1,0x83,0x06,0x0C,0x10,
+// 'i'
+0x69,0x01,0x03,0x0C,0x00,0x03,
+0x48,0x04,0x92,0x49,0x20,
+// 'j'
+0x6A,0x01,0x04,0x0F,0xFF,0x03,
+0x22,0x00,0x22,0x22,0x22,0x22,0x22,0xC0,
+// 'k'
+0x6B,0x01,0x06,0x0C,0x01,0x08,
+0x82,0x08,0x20,0x8A,0x4A,0x30,0xA2,0x48,0xA1,
+// 'l'
+0x6C,0x01,0x04,0x0C,0x01,0x04,
+0x88,0x88,0x88,0x88,0x88,0x86,
+// 'm'
+0x6D,0x05,0x0B,0x08,0x01,0x0D,
+0xFB,0xD1,0x8E,0x10,0xC2,0x18,0x43,0x08,0x61,0x0C,0x21,
+// 'n'
+0x6E,0x05,0x07,0x08,0x01,0x09,
+0xFD,0x0E,0x0C,0x18,0x30,0x60,0xC1,
+// 'o'
+0x6F,0x05,0x08,0x08,0x01,0x0A,
+0x3C,0x42,0x81,0x81,0x81,0x81,0x42,0x3C,
+// 'p'
+0x70,0x05,0x07,0x0B,0x01,0x09,
+0xF9,0x0A,0x0C,0x18,0x30,0x61,0x7C,0x81,0x02,0x00,
+// 'q'
+0x71,0x05,0x07,0x0B,0x01,0x09,
+0x3E,0x86,0x0C,0x18,0x30,0x50,0x9F,0x02,0x04,0x08,
+// 'r'
+0x72,0x05,0x05,0x08,0x01,0x06,
+0xFC,0x21,0x08,0x42,0x10,
+// 's'
+0x73,0x05,0x05,0x08,0x01,0x07,
+0x7C,0x20,0xC3,0x04,0x3E,
+// 't'
+0x74,0x02,0x05,0x0B,0x01,0x07,
+0x84,0x21,0xF8,0x42,0x10,0x84,0x1E,
+// 'u'
+0x75,0x05,0x07,0x08,0x01,0x09,
+0x83,0x06,0x0C,0x18,0x30,0x50,0xBF,
+// 'v'
+0x76,0x05,0x07,0x08,0x00,0x07,
+0x83,0x05,0x12,0x22,0x85,0x0E,0x08,
+// 'w'
+0x77,0x05,0x0D,0x08,0x00,0x0D,
+0x82,0x0C,0x10,0x51,0xC4,0x8A,0x26,0x5B,0x14,0x50,0xE3,0x82,0x08,
+// 'x'
+0x78,0x05,0x08,0x08,0x00,0x08,
+0xC3,0x66,0x24,0x18,0x18,0x24,0x42,0xC3,
+// 'y'
+0x79,0x05,0x07,0x0B,0x00,0x07,
+0x82,0x89,0x12,0x22,0x85,0x04,0x08,0x10,0x43,0x00,
+// 'z'
+0x7A,0x05,0x06,0x08,0x01,0x08,
+0xFC,0x10,0x84,0x21,0x08,0x3F,
+// '{'
+0x7B,0x00,0x05,0x10,0x00,0x05,
+0x19,0x08,0x42,0x10,0x98,0x61,0x08,0x42,0x10,0x83,
+// '|'
+0x7C,0x00,0x01,0x10,0x02,0x05,
+0xFF,0xFF,
+// '}'
+0x7D,0x00,0x05,0x10,0x00,0x05,
+0xC1,0x08,0x42,0x10,0x83,0x31,0x08,0x42,0x10,0x98,
+// '~'
+0x7E,0x07,0x07,0x02,0x01,0x09,
+0x73,0x18,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/comic24.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,331 @@
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// on Arduinos. It can however handle any ttf font that has been converted
+// using the conversion program. These could be fixed width or proportional 
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// comic.c
+// Point Size   : 24
+// Memory usage : 2814 bytes
+// # characters : 95
+
+// Header Format (to make Arduino UTFT Compatible):
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+unsigned char tft_Comic24[] = 
+{
+0x00, 0x19, 0x00, 0x00,
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+
+// ' '
+0x20,0x15,0x00,0x00,0x00,0x07,
+
+// '!'
+0x21,0x02,0x02,0x14,0x01,0x06,
+0xFF,0xFF,0xFF,0xFC,0x2D,
+// '"'
+0x22,0x03,0x06,0x08,0x02,0x0A,
+0xCF,0x3C,0xF3,0xCF,0x3C,0xF3,
+// '#'
+0x23,0x03,0x14,0x12,0x01,0x14,
+0x01,0x81,0x80,0x18,0x18,0x01,0x81,0x80,0x30,0x30,0x03,0x03,0x07,0xFF,0xFF,0x7F,0xFF,0xF0,0x60,0x60,0x06,0x06,0x00,0xC0,0xC0,0x0C,0x0C,0x0F,0xFF,0xFE,0xFF,0xFF,0xE1,0x81,0x80,0x18,0x18,0x03,0x83,0x00,0x30,0x30,0x03,0x03,0x00,
+// '$'
+0x24,0x00,0x0B,0x19,0x02,0x11,
+0x0C,0x01,0x80,0x30,0x0F,0x83,0xFC,0xD9,0xBB,0x06,0x60,0xCC,0x19,0x83,0xB0,0x3F,0x83,0xFC,0x1B,0x83,0x18,0x63,0x0C,0x71,0x9F,0x37,0x7F,0xC3,0xF0,0x18,0x03,0x00,0x60,0x0C,0x00,
+// '%'
+0x25,0x01,0x11,0x14,0x02,0x14,
+0x00,0x00,0x00,0x0C,0x0E,0x0E,0x0F,0x86,0x0C,0x67,0x06,0x33,0x03,0x19,0x80,0xF9,0x80,0x38,0xC0,0x00,0xE0,0x00,0x60,0x00,0x70,0x00,0x31,0xE0,0x39,0xF8,0x19,0xCE,0x1C,0xC3,0x0C,0x61,0x86,0x39,0xC6,0x0F,0xC3,0x03,0xC0,
+// '&'
+0x26,0x03,0x0F,0x13,0x01,0x10,
+0x01,0xC0,0x07,0xC0,0x19,0x80,0x33,0x00,0x6E,0x00,0xF8,0x01,0xE0,0x07,0x80,0x1F,0x8C,0x73,0x19,0xC3,0x37,0x07,0xEC,0x07,0xD8,0x07,0x30,0x0E,0x38,0x7E,0x3F,0xEC,0x3F,0x0C,0x00,0x18,
+// '''
+0x27,0x03,0x02,0x06,0x03,0x09,
+0xFF,0xF0,
+// '('
+0x28,0x02,0x07,0x18,0x01,0x09,
+0x06,0x1C,0x71,0xC3,0x0E,0x18,0x30,0xE1,0x83,0x06,0x0C,0x18,0x30,0x60,0xE0,0xC1,0x83,0x83,0x83,0x87,0x83,
+// ')'
+0x29,0x02,0x06,0x18,0x02,0x09,
+0xC3,0x86,0x0C,0x30,0x61,0x86,0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x0C,0x61,0x86,0x31,0xCE,0x30,
+// '*'
+0x2A,0x03,0x0B,0x09,0x01,0x0D,
+0x0C,0x01,0x83,0xBF,0xFF,0xF3,0xFC,0x3C,0x0F,0xC3,0x9C,0x61,0x80,
+// '+'
+0x2B,0x09,0x0A,0x0A,0x00,0x0C,
+0x0C,0x03,0x00,0xC0,0x30,0xFF,0xFF,0xF0,0xC0,0x30,0x0C,0x03,0x00,
+// ','
+0x2C,0x13,0x04,0x06,0x02,0x07,
+0x37,0x66,0xEC,
+// '-'
+0x2D,0x0E,0x08,0x02,0x01,0x0A,
+0xFF,0xFF,
+// '.'
+0x2E,0x12,0x03,0x03,0x02,0x06,
+0xFF,0x80,
+// '/'
+0x2F,0x01,0x0A,0x15,0x01,0x0C,
+0x00,0x00,0x30,0x0C,0x06,0x01,0x80,0x60,0x30,0x0C,0x06,0x01,0x80,0xC0,0x30,0x18,0x06,0x03,0x00,0xC0,0x60,0x18,0x0E,0x03,0x00,0xC0,0x00,
+// '0'
+0x30,0x03,0x0D,0x12,0x01,0x0F,
+0x0F,0x80,0xFF,0x0E,0x18,0xE0,0x66,0x03,0x70,0x0F,0x00,0x78,0x03,0xC0,0x1E,0x00,0xF0,0x07,0x80,0x3C,0x03,0xB0,0x19,0x81,0xC7,0x1C,0x3F,0xC0,0x7C,0x00,
+// '1'
+0x31,0x03,0x06,0x12,0x03,0x0B,
+0x10,0xC7,0x3C,0xB0,0xC3,0x0C,0x30,0xC3,0x0C,0x30,0xC3,0x0C,0xFF,0xF0,
+// '2'
+0x32,0x03,0x0B,0x12,0x02,0x0F,
+0x1F,0x07,0xFB,0xC3,0xE0,0x30,0x06,0x00,0xC0,0x38,0x0E,0x07,0x81,0xE0,0xF8,0x3C,0x07,0x01,0xC0,0x30,0x06,0x00,0xFF,0xDF,0xFC,
+// '3'
+0x33,0x03,0x0B,0x12,0x02,0x0F,
+0x1F,0x0F,0xF9,0xC3,0x80,0x30,0x06,0x00,0xC0,0x78,0x7E,0x0F,0x80,0x78,0x03,0x80,0x30,0x06,0x00,0xF0,0x1F,0x0E,0x7F,0x83,0xE0,
+// '4'
+0x34,0x03,0x0D,0x12,0x02,0x0F,
+0x01,0xC0,0x0E,0x00,0xF0,0x0F,0x80,0x6C,0x07,0x60,0x33,0x03,0x98,0x38,0xC1,0x86,0x1C,0x31,0xFF,0xFF,0xFF,0x80,0x60,0x03,0x00,0x18,0x00,0xC0,0x06,0x00,
+// '5'
+0x35,0x02,0x0C,0x13,0x02,0x0F,
+0x00,0x0F,0xFE,0xFF,0xE6,0x00,0x60,0x0E,0x00,0xEF,0x8F,0xFC,0xF8,0x6E,0x07,0xC0,0x30,0x03,0x00,0x30,0x03,0x00,0x7C,0x06,0xE1,0xE7,0xFC,0x3F,0x00,
+// '6'
+0x36,0x03,0x0C,0x12,0x01,0x0F,
+0x03,0x00,0x70,0x0E,0x01,0xC0,0x38,0x03,0x00,0x60,0x06,0xF8,0xFF,0xEE,0x0E,0xC0,0x3C,0x03,0xC0,0x3C,0x03,0x60,0x77,0x0E,0x3F,0xC1,0xF8,
+// '7'
+0x37,0x02,0x0D,0x13,0x01,0x0F,
+0x00,0x07,0xFF,0xFF,0xFE,0x00,0xE0,0x0E,0x00,0x60,0x06,0x00,0x30,0x03,0x80,0x18,0x01,0xC0,0x0C,0x00,0x60,0x07,0x00,0x30,0x03,0x80,0x18,0x00,0xC0,0x04,0x00,
+// '8'
+0x38,0x02,0x0C,0x13,0x01,0x0F,
+0x00,0x00,0xFC,0x3F,0xE3,0x07,0x60,0x36,0x03,0x60,0x37,0x8F,0x3F,0xE1,0xFE,0x38,0xE7,0x07,0x60,0x36,0x03,0x60,0x36,0x03,0x30,0x63,0xFE,0x0F,0x80,
+// '9'
+0x39,0x03,0x0D,0x13,0x01,0x0F,
+0x0F,0x01,0xFE,0x1C,0x38,0xC0,0xCC,0x07,0x60,0x1B,0x00,0xD8,0x06,0xE0,0x73,0x87,0x8F,0xF8,0x3E,0xC0,0x0E,0x00,0x60,0x07,0x00,0xF0,0x1F,0x03,0xE0,0x1C,0x00,
+// ':'
+0x3A,0x09,0x03,0x0B,0x02,0x07,
+0xFF,0x80,0x00,0xFF,0x80,
+// ';'
+0x3B,0x09,0x04,0x0E,0x02,0x07,
+0xEE,0xE0,0x00,0x00,0x03,0x7E,0xCC,
+// '<'
+0x3C,0x09,0x07,0x0A,0x01,0x09,
+0x06,0x1C,0x71,0xC7,0x1E,0x1E,0x0E,0x0E,0x0C,
+// '='
+0x3D,0x0A,0x09,0x09,0x01,0x0C,
+0xFF,0xFF,0xC0,0x00,0x00,0x00,0x03,0xFF,0xFF,0x00,0x00,
+// '>'
+0x3E,0x08,0x08,0x0B,0x01,0x0A,
+0x60,0x70,0x38,0x3C,0x1E,0x0F,0x06,0x0C,0x38,0x70,0xC0,
+// '?'
+0x3F,0x04,0x0B,0x12,0x01,0x0D,
+0x1E,0x0F,0xE3,0xC6,0x60,0x60,0x06,0x00,0xC0,0x18,0x07,0x01,0xE0,0xF8,0x3E,0x0F,0x01,0x80,0x00,0x00,0x01,0x80,0x30,0x06,0x00,
+// '@'
+0x40,0x02,0x13,0x14,0x01,0x16,
+0x03,0xF8,0x01,0xFF,0xC0,0x78,0x3C,0x1C,0x01,0xC3,0x00,0x1C,0xC1,0xC1,0x98,0xF8,0x1E,0x3C,0x03,0xC6,0x30,0x79,0x8E,0x0F,0x31,0xC1,0xE6,0x78,0x6C,0x7F,0xFC,0xC7,0x3E,0x18,0x00,0x01,0x80,0x00,0x38,0x00,0x03,0xC0,0xE0,0x1F,0xFC,0x00,0xFE,0x00,
+// 'A'
+0x41,0x03,0x0E,0x12,0x01,0x11,
+0x00,0x80,0x07,0x00,0x1C,0x00,0xF0,0x03,0xC0,0x1D,0x80,0x76,0x03,0x98,0x0E,0x20,0x70,0xC1,0xFF,0x0F,0xFC,0x7C,0x19,0xC0,0x67,0x01,0xB8,0x07,0xE0,0x0F,0x00,0x30,
+// 'B'
+0x42,0x03,0x0B,0x13,0x03,0x0F,
+0x7C,0x1F,0xE3,0x0E,0x60,0xEC,0x0D,0x81,0xB0,0x36,0x0E,0xC3,0x9F,0xE3,0xFC,0x61,0xEC,0x0F,0x80,0xF0,0x1E,0x0E,0xC7,0xDF,0xE3,0xF0,0x00,
+// 'C'
+0x43,0x03,0x0D,0x12,0x01,0x0E,
+0x01,0xF8,0x3F,0xC3,0xC6,0x38,0x31,0x80,0x1C,0x01,0xC0,0x0C,0x00,0x60,0x06,0x00,0x30,0x01,0x80,0x0C,0x00,0x60,0x19,0x81,0xCE,0x3C,0x3F,0xC0,0xF8,0x00,
+// 'D'
+0x44,0x03,0x0D,0x12,0x02,0x11,
+0x60,0x07,0xC0,0x37,0x81,0x8F,0x0C,0x1C,0x60,0x73,0x01,0xD8,0x06,0xC0,0x1E,0x00,0xF0,0x07,0x80,0x3C,0x01,0xE0,0x1B,0x01,0xDC,0x1C,0xFF,0xC1,0xF8,0x00,
+// 'E'
+0x45,0x03,0x0D,0x12,0x02,0x0F,
+0xFF,0xF7,0xFF,0xF0,0x01,0x80,0x0C,0x00,0x60,0x03,0x00,0x18,0x7E,0xFF,0xF7,0xE0,0x30,0x01,0x80,0x0C,0x00,0x60,0x03,0x00,0x18,0x00,0x7F,0xF1,0xFF,0x80,
+// 'F'
+0x46,0x03,0x0C,0x12,0x02,0x0F,
+0xFF,0xCF,0xFF,0xC0,0x7C,0x00,0xC0,0x0C,0x00,0xC0,0x0D,0xFE,0xFF,0xEF,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0x00,0xC0,0x0C,0x00,
+// 'G'
+0x47,0x03,0x0F,0x12,0x01,0x10,
+0x03,0xE0,0x0F,0xF0,0x38,0xE0,0xE0,0x03,0x80,0x06,0x00,0x18,0x00,0x30,0x00,0x61,0xFF,0x9F,0xFF,0x3C,0x36,0x00,0x6C,0x01,0x98,0x07,0x30,0x0C,0x30,0x70,0x7F,0xC0,0x3E,0x00,
+// 'H'
+0x48,0x03,0x0F,0x12,0x02,0x12,
+0xC0,0x03,0x80,0x0F,0x00,0x1E,0x00,0x3C,0x00,0x78,0x00,0xF0,0x01,0xE0,0x03,0xC0,0xFF,0xFF,0xFF,0xFC,0x1E,0x00,0x3C,0x00,0x78,0x00,0xF0,0x01,0xE0,0x03,0xC0,0x07,0x80,0x0C,
+// 'I'
+0x49,0x03,0x0C,0x12,0x00,0x0D,
+0xFF,0xEF,0xFF,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0xFF,0xFF,0xFF,
+// 'J'
+0x4A,0x03,0x0E,0x12,0x01,0x10,
+0x1F,0xFC,0x7F,0xF0,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0x00,0xC0,0x03,0x00,0x0C,0x00,0x30,0xC0,0xC3,0x06,0x0E,0x18,0x1C,0x60,0x3F,0x80,0x3C,0x00,
+// 'K'
+0x4B,0x03,0x0C,0x12,0x03,0x0F,
+0xC0,0x6C,0x0E,0xC1,0xCC,0x38,0xC7,0x0C,0xE0,0xDC,0x0F,0x80,0xF0,0x0F,0x00,0xF8,0x0F,0xC0,0xDE,0x0C,0xF0,0xC7,0x8C,0x1E,0xC0,0xFC,0x07,
+// 'L'
+0x4C,0x03,0x0B,0x12,0x01,0x0D,
+0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xFF,0xEF,0xFC,
+// 'M'
+0x4D,0x03,0x13,0x13,0x01,0x15,
+0x0C,0x06,0x01,0x80,0xC0,0x78,0x3C,0x0F,0x07,0x81,0xE0,0xF0,0x3C,0x1E,0x07,0x83,0xC1,0xD8,0xEC,0x3B,0x1D,0x87,0x63,0xB0,0xCC,0xE6,0x38,0xDC,0x47,0x1B,0x8C,0xE3,0xF1,0xB8,0x3C,0x37,0x07,0x86,0xE0,0xF0,0x7C,0x1E,0x0F,0x01,0x81,0x80,
+// 'N'
+0x4E,0x03,0x11,0x12,0x01,0x13,
+0x60,0x01,0x38,0x00,0xDE,0x00,0x6F,0x00,0x37,0xC0,0x1B,0x70,0x0D,0x9C,0x06,0xCF,0x03,0x63,0x81,0xB0,0xE0,0xD8,0x38,0x6C,0x0E,0x36,0x03,0x9B,0x00,0xED,0x80,0x3E,0xC0,0x0F,0x60,0x03,0xB0,0x00,0xC0,
+// 'O'
+0x4F,0x03,0x11,0x12,0x01,0x13,
+0x01,0xF8,0x03,0xFF,0x07,0x81,0xC3,0x00,0x63,0x00,0x1B,0x80,0x0D,0x80,0x07,0xC0,0x03,0xC0,0x01,0xE0,0x00,0xF0,0x00,0xF8,0x00,0x6C,0x00,0x33,0x00,0x31,0xC0,0x38,0x70,0x78,0x1F,0xF8,0x03,0xF0,0x00,
+// 'P'
+0x50,0x03,0x0B,0x12,0x01,0x0D,
+0xFE,0x1F,0xF3,0x0F,0x60,0x7C,0x07,0x80,0xF0,0x1E,0x06,0xC3,0xDF,0xF3,0xF8,0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x18,0x00,
+// 'Q'
+0x51,0x03,0x14,0x17,0x01,0x15,
+0x01,0xF8,0x00,0x7F,0xE0,0x1E,0x07,0x03,0x80,0x18,0x30,0x01,0xC6,0x00,0x0C,0x60,0x00,0xEC,0x00,0x06,0xC0,0x00,0x6C,0x00,0x06,0xC0,0x00,0x6C,0x00,0x06,0x60,0xE0,0xE7,0x0F,0x0C,0x38,0x79,0xC1,0xC3,0xF8,0x0F,0xFF,0x00,0x3F,0x78,0x00,0x03,0xC0,0x00,0x1E,0x00,0x00,0xF0,0x00,0x07,0x00,0x00,0x20,
+// 'R'
+0x52,0x02,0x0D,0x13,0x01,0x0F,
+0x00,0x03,0xE0,0x3F,0xC1,0x8F,0x0C,0x0E,0x60,0x33,0x00,0xD8,0x06,0xC0,0x36,0x03,0xB0,0x79,0xFF,0x8F,0xF0,0x7F,0x83,0x1F,0x18,0x3C,0xC0,0xF6,0x01,0xF0,0x06,
+// 'S'
+0x53,0x03,0x0F,0x13,0x01,0x11,
+0x01,0xF0,0x07,0xF8,0x18,0x70,0x60,0x01,0x80,0x03,0x00,0x06,0x00,0x0E,0x00,0x0F,0xF0,0x07,0xF0,0x00,0xF0,0x00,0x70,0x00,0x60,0x00,0xD8,0x01,0xB8,0x06,0x78,0x3C,0x7F,0xE0,0x3F,0x00,
+// 'T'
+0x54,0x02,0x0F,0x13,0x01,0x10,
+0x00,0x01,0xFF,0xFD,0xFF,0xF8,0x18,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,
+// 'U'
+0x55,0x03,0x11,0x12,0x01,0x12,
+0x60,0x03,0x30,0x01,0x98,0x00,0xCC,0x00,0x66,0x00,0x33,0x00,0x19,0x80,0x0C,0xC0,0x06,0x60,0x03,0x30,0x01,0x98,0x01,0xCC,0x00,0xC7,0x00,0x61,0x80,0x70,0xE0,0x30,0x38,0x38,0x0F,0xF8,0x01,0xF0,0x00,
+// 'V'
+0x56,0x03,0x0E,0x13,0x02,0x10,
+0x80,0x0F,0x00,0x3C,0x01,0xB0,0x06,0x60,0x31,0x80,0xC6,0x03,0x0C,0x18,0x30,0x60,0xC1,0x81,0x8C,0x06,0x30,0x0D,0x80,0x36,0x00,0xF8,0x01,0xC0,0x07,0x00,0x08,0x00,0x00,0x00,
+// 'W'
+0x57,0x03,0x17,0x12,0x01,0x19,
+0xC0,0x20,0x0F,0xC0,0x60,0x19,0x81,0xC0,0x23,0x03,0x80,0xC6,0x07,0x01,0x86,0x1E,0x03,0x0C,0x36,0x0C,0x18,0x6C,0x18,0x11,0x98,0x60,0x33,0x30,0xC0,0x66,0x61,0x80,0xD8,0x66,0x01,0xB0,0xCC,0x01,0xC1,0xB0,0x03,0x83,0x60,0x07,0x07,0x80,0x0C,0x07,0x00,0x08,0x0E,0x00,
+// 'X'
+0x58,0x03,0x10,0x12,0x01,0x11,
+0x60,0x03,0x70,0x07,0x38,0x0E,0x1C,0x1C,0x0C,0x1C,0x0E,0x38,0x07,0x70,0x03,0xE0,0x01,0xC0,0x03,0xC0,0x07,0xE0,0x07,0x70,0x0E,0x38,0x1C,0x18,0x38,0x1C,0x70,0x0E,0xE0,0x07,0xC0,0x03,
+// 'Y'
+0x59,0x03,0x0F,0x13,0x00,0x10,
+0x60,0x06,0xE0,0x1D,0xC0,0x31,0xC0,0xE1,0xC1,0x83,0x83,0x03,0x8C,0x07,0x18,0x07,0x70,0x0F,0xC0,0x0F,0x80,0x0F,0x00,0x1C,0x00,0x38,0x00,0x60,0x01,0xC0,0x03,0x00,0x06,0x00,0x08,0x00,
+// 'Z'
+0x5A,0x03,0x0F,0x12,0x01,0x11,
+0xFF,0xFF,0xFF,0xFC,0x00,0xF0,0x03,0x80,0x0E,0x00,0x3C,0x00,0xF0,0x03,0xC0,0x07,0x00,0x1E,0x00,0x38,0x00,0xE0,0x03,0xC0,0x07,0x00,0x1C,0x00,0x70,0x00,0xFF,0xFF,0xFF,0xFC,
+// '['
+0x5B,0x01,0x07,0x1A,0x01,0x09,
+0x00,0xFD,0xFB,0x06,0x0C,0x18,0x30,0x60,0xC1,0x83,0x06,0x0C,0x18,0x30,0x60,0xC1,0x83,0x06,0x0C,0x18,0x3F,0x7E,0x00,
+// '\'
+0x5C,0x03,0x0B,0x14,0x02,0x0D,
+0xC0,0x18,0x01,0x80,0x30,0x03,0x00,0x60,0x06,0x00,0xC0,0x0C,0x01,0x80,0x18,0x03,0x00,0x20,0x06,0x00,0xC0,0x0C,0x01,0x80,0x18,0x03,0x00,0x60,
+// ']'
+0x5D,0x01,0x07,0x1A,0x02,0x09,
+0x01,0xFB,0xF0,0x60,0xC1,0x83,0x06,0x0C,0x18,0x30,0x60,0xC1,0x83,0x06,0x0C,0x18,0x30,0x60,0xC1,0x83,0x7E,0xFC,0x00,
+// '^'
+0x5E,0x02,0x0A,0x06,0x02,0x0E,
+0x0C,0x07,0x83,0xF1,0xCE,0xE1,0xF0,0x30,
+// '_'
+0x5F,0x16,0x0F,0x04,0x00,0x0F,
+0x00,0x01,0xFF,0xFF,0xFF,0xF8,0x00,0x00,
+// '`'
+0x60,0x02,0x05,0x06,0x02,0x0D,
+0xC7,0x1C,0x63,0x8C,
+// 'a'
+0x61,0x09,0x0B,0x0C,0x01,0x0C,
+0x0F,0x87,0xF9,0xE3,0x30,0x6E,0x0D,0x81,0xB0,0x36,0x06,0xC0,0xCC,0x39,0xFF,0x9F,0x30,
+// 'b'
+0x62,0x02,0x0C,0x13,0x01,0x0E,
+0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x78,0x7F,0xC7,0x8E,0x60,0x76,0x03,0x60,0x36,0x03,0x60,0x36,0x06,0x70,0xE7,0xFC,0x7F,0x00,
+// 'c'
+0x63,0x09,0x0A,0x0C,0x01,0x0C,
+0x0F,0x07,0xF3,0x0D,0x80,0x60,0x30,0x0C,0x03,0x00,0xC0,0x1C,0x33,0xFC,0x7C,
+// 'd'
+0x64,0x02,0x0C,0x13,0x01,0x0E,
+0x00,0x20,0x06,0x00,0x60,0x06,0x00,0x60,0x06,0x00,0x61,0xF6,0x3F,0xE7,0x0E,0x60,0x6C,0x06,0xC0,0x6C,0x06,0xC0,0x6E,0x06,0x70,0xE3,0xFE,0x1F,0x60,
+// 'e'
+0x65,0x09,0x0B,0x0C,0x01,0x0D,
+0x1F,0x07,0xF9,0xC7,0x30,0xEC,0x79,0xBE,0x3E,0x07,0x00,0xC0,0x6E,0x1D,0xFF,0x0F,0x80,
+// 'f'
+0x66,0x02,0x0A,0x14,0x01,0x0C,
+0x03,0x83,0xE0,0xE0,0x70,0x18,0x06,0x01,0x83,0xFF,0xFF,0xC6,0x01,0x80,0x60,0x18,0x06,0x01,0x80,0x60,0x18,0x06,0x01,0x80,0x60,
+// 'g'
+0x67,0x09,0x0A,0x13,0x02,0x0D,
+0x0F,0x0F,0xF7,0x0D,0x83,0xC0,0xF0,0x3C,0x1F,0x07,0xC1,0xD8,0xF7,0xEC,0xF3,0x00,0xC0,0x30,0x18,0x06,0x03,0xBF,0xC7,0xE0,
+// 'h'
+0x68,0x02,0x0B,0x13,0x01,0x0E,
+0x60,0x0C,0x01,0x80,0x30,0x06,0x00,0xC0,0x18,0x03,0x1E,0x6F,0xEF,0x8D,0xE1,0xB8,0x36,0x06,0xC0,0xD8,0x1B,0x03,0x60,0x6C,0x0D,0x81,0x80,
+// 'i'
+0x69,0x04,0x02,0x11,0x03,0x07,
+0xF0,0x3F,0xFF,0xFF,0xC0,
+// 'j'
+0x6A,0x04,0x08,0x18,0x00,0x0A,
+0x03,0x03,0x00,0x00,0x00,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0xC3,0xE3,0x77,0x7E,0x1C,
+// 'k'
+0x6B,0x03,0x0B,0x13,0x02,0x0E,
+0xC0,0x18,0x03,0x00,0x60,0x0C,0x01,0x80,0x30,0x36,0x0E,0xC7,0x99,0xE3,0x70,0x7E,0x0F,0xE1,0xCE,0x30,0xE6,0x0E,0xC0,0xF8,0x08,0x00,0x00,
+// 'l'
+0x6C,0x02,0x02,0x13,0x03,0x07,
+0xFF,0xFF,0xFF,0xFF,0xFC,
+// 'm'
+0x6D,0x09,0x10,0x0C,0x01,0x12,
+0x67,0x3C,0x6F,0xFE,0x7D,0xEE,0x79,0x86,0x71,0x86,0x61,0x86,0x61,0x86,0x61,0x86,0x61,0x86,0x61,0x86,0x61,0x86,0x61,0x86,
+// 'n'
+0x6E,0x09,0x0B,0x0C,0x01,0x0D,
+0x63,0x8D,0xF9,0xF1,0xBC,0x37,0x06,0xE0,0xD8,0x1B,0x03,0x60,0x6C,0x0D,0x81,0xB0,0x30,
+// 'o'
+0x6F,0x09,0x0C,0x0C,0x01,0x0D,
+0x0F,0x81,0xFC,0x38,0xC3,0x06,0x60,0x66,0x06,0x60,0x66,0x06,0x60,0xE3,0x1C,0x1F,0x80,0xF0,
+// 'p'
+0x70,0x08,0x0A,0x14,0x02,0x0D,
+0xC0,0x33,0xCF,0xFB,0xC6,0xC0,0xF0,0x3C,0x0F,0x03,0xC0,0xF0,0x7C,0x1B,0xFC,0xFE,0x30,0x0C,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,
+// 'q'
+0x71,0x08,0x0A,0x14,0x01,0x0C,
+0x00,0x03,0xF3,0xFD,0xE3,0x60,0xF8,0x3C,0x0F,0x03,0xC0,0xF0,0x76,0x1D,0xFF,0x1F,0x80,0x60,0x18,0x06,0x01,0x80,0x60,0x18,0x06,
+// 'r'
+0x72,0x09,0x09,0x0C,0x01,0x0B,
+0xCF,0x6F,0xFE,0x7C,0x3C,0x1E,0x03,0x01,0x80,0xC0,0x60,0x30,0x18,0x00,
+// 's'
+0x73,0x09,0x09,0x0C,0x02,0x0C,
+0x03,0x9F,0xDE,0x7C,0x3E,0x07,0xF0,0xFC,0x07,0x01,0xE0,0xFF,0xC7,0xC0,
+// 't'
+0x74,0x05,0x0A,0x10,0x00,0x0A,
+0x0C,0x03,0x00,0xC0,0x30,0xFF,0xFF,0xF0,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,
+// 'u'
+0x75,0x09,0x0B,0x0C,0x01,0x0C,
+0xC0,0xD8,0x1B,0x03,0x60,0x6C,0x0D,0x81,0xB0,0x36,0x06,0xC0,0xD8,0x19,0xFF,0x1F,0x60,
+// 'v'
+0x76,0x09,0x0B,0x0D,0x01,0x0C,
+0xC0,0x78,0x1F,0x83,0x30,0x67,0x1C,0x63,0x0C,0xE0,0xD8,0x1E,0x03,0xC0,0x30,0x06,0x00,0x00,
+// 'w'
+0x77,0x09,0x0F,0x0D,0x01,0x11,
+0xC1,0x87,0x83,0x0F,0x0E,0x1E,0x1C,0x66,0x7C,0xCC,0xD9,0x99,0x36,0x36,0x6C,0x7C,0xD8,0x70,0xE0,0xE1,0xC0,0x83,0x80,0x00,0x00,
+// 'x'
+0x78,0x09,0x0D,0x0D,0x01,0x0E,
+0x60,0x1B,0x81,0xCE,0x1C,0x39,0xC0,0xFC,0x03,0xC0,0x3C,0x03,0xF0,0x39,0xC3,0x87,0x38,0x1D,0x80,0x70,0x01,0x80,
+// 'y'
+0x79,0x09,0x0C,0x13,0x00,0x0D,
+0xC0,0x3E,0x07,0x60,0x67,0x0C,0x30,0xC3,0x98,0x19,0x81,0xD8,0x0F,0x00,0xF0,0x06,0x00,0x60,0x0C,0x00,0xC0,0x18,0x01,0x80,0x30,0x03,0x00,0x30,0x00,
+// 'z'
+0x7A,0x09,0x0B,0x0C,0x01,0x0D,
+0xFF,0xFF,0xFC,0x07,0x00,0xC0,0x30,0x0C,0x03,0x80,0xE0,0x38,0x0E,0x03,0xFF,0xFF,0xF0,
+// '{'
+0x7B,0x02,0x08,0x18,0x01,0x09,
+0x0F,0x1F,0x38,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x60,0xE0,0xE0,0x70,0x30,0x30,0x30,0x30,0x30,0x38,0x18,0x1F,0x07,
+// '|'
+0x7C,0x01,0x02,0x18,0x04,0x0A,
+0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
+// '}'
+0x7D,0x02,0x08,0x18,0x01,0x09,
+0x70,0xF8,0x1C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x06,0x07,0x07,0x0E,0x0C,0x0C,0x0C,0x0C,0x0C,0x1C,0x18,0xF8,0xE0,
+// '~'
+0x7E,0x0B,0x0C,0x05,0x01,0x0E,
+0x38,0x37,0xE3,0xE7,0x7C,0x3E,0x01,0xC0,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,7 @@
+#
+# Main Makefile. This is basically the same as a component makefile.
+#
+# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
+
+COMPONENT_SRCDIRS := . 
+COMPONENT_ADD_INCLUDEDIRS := .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/def_small.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,332 @@
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// on Arduinos. It can however handle any ttf font that has been converted
+// using the conversion program. These could be fixed width or proportional 
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// def_small.c
+// Point Size   : 9
+// Memory usage : 928 bytes
+// # characters : 95
+
+// Header Format (to make Arduino UTFT Compatible):
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+
+unsigned char tft_def_small[] = 
+{
+0x00, 0x08, 0x00, 0x00,
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+
+// ' '
+0x20,0x08,0x00,0x00,0x00,0x03,
+
+// '!'
+0x21,0x01,0x01,0x07,0x01,0x03,
+0xFA,
+// '"'
+0x22,0x01,0x03,0x02,0x01,0x04,
+0xB4,
+// '#'
+0x23,0x01,0x06,0x07,0x01,0x08,
+0x28,0xAF,0xCA,0xFD,0x45,0x00,
+// '$'
+0x24,0x01,0x06,0x08,0x00,0x06,
+0x21,0xEA,0x38,0x38,0xAF,0x08,
+// '%'
+0x25,0x01,0x08,0x07,0x00,0x09,
+0x44,0xA4,0xA8,0x5A,0x15,0x25,0x22,
+// '&'
+0x26,0x01,0x06,0x07,0x01,0x08,
+0x31,0x04,0x19,0x9E,0x66,0xC0,
+// '''
+0x27,0x01,0x01,0x02,0x01,0x02,
+0xC0,
+// '('
+0x28,0x00,0x02,0x08,0x01,0x04,
+0x4A,0xA1,
+// ')'
+0x29,0x00,0x02,0x08,0x01,0x04,
+0x85,0x52,
+// '*'
+0x2A,0x01,0x05,0x04,0x00,0x05,
+0xAB,0x9D,0x50,
+// '+'
+0x2B,0x03,0x05,0x05,0x01,0x08,
+0x21,0x3E,0x42,0x00,
+// ','
+0x2C,0x07,0x01,0x02,0x01,0x03,
+0xC0,
+// '-'
+0x2D,0x05,0x02,0x01,0x01,0x03,
+0xC0,
+// '.'
+0x2E,0x07,0x01,0x01,0x01,0x03,
+0x80,
+// '/'
+0x2F,0x01,0x03,0x07,0x00,0x03,
+0x25,0x25,0x20,
+// '0'
+0x30,0x01,0x04,0x07,0x01,0x06,
+0x69,0x99,0x99,0x60,
+// '1'
+0x31,0x01,0x03,0x07,0x02,0x06,
+0xC9,0x24,0xB8,
+// '2'
+0x32,0x01,0x05,0x07,0x01,0x06,
+0x64,0x84,0x44,0x43,0xC0,
+// '3'
+0x33,0x01,0x04,0x07,0x01,0x06,
+0x69,0x16,0x11,0x60,
+// '4'
+0x34,0x01,0x05,0x07,0x01,0x06,
+0x11,0x94,0xA9,0x7C,0x40,
+// '5'
+0x35,0x01,0x04,0x07,0x01,0x06,
+0xF8,0x8E,0x11,0xE0,
+// '6'
+0x36,0x01,0x04,0x07,0x01,0x06,
+0x7C,0x8E,0x99,0x60,
+// '7'
+0x37,0x01,0x04,0x07,0x01,0x06,
+0xF1,0x22,0x24,0x40,
+// '8'
+0x38,0x01,0x04,0x07,0x01,0x06,
+0x69,0x96,0x99,0x60,
+// '9'
+0x39,0x01,0x04,0x07,0x01,0x06,
+0x69,0x97,0x13,0xE0,
+// ':'
+0x3A,0x03,0x01,0x05,0x01,0x03,
+0x88,
+// ';'
+0x3B,0x03,0x01,0x06,0x01,0x03,
+0x8C,
+// '<'
+0x3C,0x03,0x06,0x05,0x01,0x08,
+0x04,0xEE,0x0E,0x04,
+// '='
+0x3D,0x04,0x06,0x03,0x01,0x08,
+0xFC,0x0F,0xC0,
+// '>'
+0x3E,0x03,0x06,0x05,0x01,0x08,
+0x81,0xC1,0xDC,0x80,
+// '?'
+0x3F,0x01,0x04,0x07,0x01,0x05,
+0xE1,0x24,0x40,0x40,
+// '@'
+0x40,0x01,0x08,0x08,0x01,0x0A,
+0x3C,0x42,0x9D,0xA5,0xA5,0x9E,0x40,0x38,
+// 'A'
+0x41,0x01,0x06,0x07,0x00,0x06,
+0x30,0xC4,0x92,0x7A,0x18,0x40,
+// 'B'
+0x42,0x01,0x05,0x07,0x01,0x07,
+0xF4,0x63,0xE8,0xC7,0xC0,
+// 'C'
+0x43,0x01,0x05,0x07,0x01,0x07,
+0x72,0x61,0x08,0x25,0xC0,
+// 'D'
+0x44,0x01,0x05,0x07,0x01,0x07,
+0xF4,0xE3,0x18,0xCF,0xC0,
+// 'E'
+0x45,0x01,0x04,0x07,0x01,0x06,
+0xF8,0x8F,0x88,0xF0,
+// 'F'
+0x46,0x01,0x04,0x07,0x01,0x06,
+0xF8,0x8F,0x88,0x80,
+// 'G'
+0x47,0x01,0x05,0x07,0x01,0x07,
+0x76,0x61,0x38,0xE5,0xC0,
+// 'H'
+0x48,0x01,0x05,0x07,0x01,0x07,
+0x8C,0x63,0xF8,0xC6,0x20,
+// 'I'
+0x49,0x01,0x01,0x07,0x01,0x03,
+0xFE,
+// 'J'
+0x4A,0x01,0x02,0x09,0x00,0x03,
+0x55,0x55,0x80,
+// 'K'
+0x4B,0x01,0x05,0x07,0x01,0x06,
+0x8C,0xA9,0x8A,0x4A,0x20,
+// 'L'
+0x4C,0x01,0x04,0x07,0x01,0x05,
+0x88,0x88,0x88,0xF0,
+// 'M'
+0x4D,0x01,0x06,0x07,0x01,0x08,
+0x87,0x3C,0xED,0xB6,0x18,0x40,
+// 'N'
+0x4E,0x01,0x05,0x07,0x01,0x07,
+0x8E,0x73,0x59,0xCE,0x20,
+// 'O'
+0x4F,0x01,0x05,0x07,0x01,0x07,
+0x76,0xE3,0x18,0xED,0xC0,
+// 'P'
+0x50,0x01,0x04,0x07,0x01,0x06,
+0xE9,0x9E,0x88,0x80,
+// 'Q'
+0x51,0x01,0x05,0x08,0x01,0x07,
+0x76,0xE3,0x18,0xE9,0xC2,
+// 'R'
+0x52,0x01,0x05,0x07,0x01,0x06,
+0xE4,0xA5,0xCA,0x4A,0x20,
+// 'S'
+0x53,0x01,0x06,0x07,0x01,0x07,
+0x72,0x28,0x1C,0x0A,0x27,0x00,
+// 'T'
+0x54,0x01,0x05,0x07,0x00,0x05,
+0xF9,0x08,0x42,0x10,0x80,
+// 'U'
+0x55,0x01,0x05,0x07,0x01,0x07,
+0x8C,0x63,0x18,0xC5,0xC0,
+// 'V'
+0x56,0x01,0x06,0x07,0x00,0x06,
+0x86,0x14,0x92,0x48,0xC3,0x00,
+// 'W'
+0x57,0x01,0x09,0x07,0xFF,0x07,
+0x49,0x24,0x8A,0x85,0x43,0xE0,0xA0,0x50,
+// 'X'
+0x58,0x01,0x06,0x07,0x00,0x06,
+0xCD,0x23,0x0C,0x31,0x28,0xC0,
+// 'Y'
+0x59,0x01,0x05,0x07,0x00,0x05,
+0x8A,0x9C,0x42,0x10,0x80,
+// 'Z'
+0x5A,0x01,0x05,0x07,0x00,0x05,
+0xF8,0x44,0x44,0x43,0xE0,
+// '['
+0x5B,0x01,0x02,0x08,0x01,0x04,
+0xEA,0xAB,
+// '\'
+0x5C,0x01,0x03,0x07,0x00,0x03,
+0x91,0x24,0x48,
+// ']'
+0x5D,0x01,0x02,0x08,0x01,0x04,
+0xD5,0x57,
+// '^'
+0x5E,0x01,0x06,0x02,0x01,0x08,
+0x31,0x20,
+// '_'
+0x5F,0x09,0x05,0x01,0x00,0x05,
+0xF8,
+// '`'
+0x60,0x00,0x02,0x02,0x01,0x05,
+0x90,
+// 'a'
+0x61,0x03,0x04,0x05,0x01,0x06,
+0x61,0x79,0xF0,
+// 'b'
+0x62,0x00,0x04,0x08,0x01,0x06,
+0x88,0x8E,0x99,0x9E,
+// 'c'
+0x63,0x03,0x04,0x05,0x01,0x06,
+0x78,0x88,0x70,
+// 'd'
+0x64,0x00,0x04,0x08,0x01,0x06,
+0x11,0x17,0x99,0x97,
+// 'e'
+0x65,0x03,0x04,0x05,0x01,0x06,
+0x69,0xF8,0x70,
+// 'f'
+0x66,0x00,0x04,0x08,0x00,0x03,
+0x34,0x4E,0x44,0x44,
+// 'g'
+0x67,0x03,0x04,0x07,0x01,0x06,
+0x79,0x99,0x71,0x60,
+// 'h'
+0x68,0x00,0x04,0x08,0x01,0x06,
+0x88,0x8E,0x99,0x99,
+// 'i'
+0x69,0x01,0x01,0x07,0x01,0x03,
+0xBE,
+// 'j'
+0x6A,0x01,0x02,0x09,0x00,0x03,
+0x45,0x55,0x80,
+// 'k'
+0x6B,0x00,0x04,0x08,0x01,0x05,
+0x88,0x89,0xAC,0xA9,
+// 'l'
+0x6C,0x00,0x01,0x08,0x01,0x03,
+0xFF,
+// 'm'
+0x6D,0x03,0x07,0x05,0x01,0x09,
+0xED,0x26,0x4C,0x99,0x20,
+// 'n'
+0x6E,0x03,0x04,0x05,0x01,0x06,
+0xE9,0x99,0x90,
+// 'o'
+0x6F,0x03,0x04,0x05,0x01,0x06,
+0x69,0x99,0x60,
+// 'p'
+0x70,0x03,0x04,0x07,0x01,0x06,
+0xE9,0x99,0xE8,0x80,
+// 'q'
+0x71,0x03,0x04,0x07,0x01,0x06,
+0x79,0x99,0x71,0x10,
+// 'r'
+0x72,0x03,0x03,0x05,0x01,0x04,
+0xF2,0x48,
+// 's'
+0x73,0x03,0x04,0x05,0x01,0x05,
+0x68,0x62,0xE0,
+// 't'
+0x74,0x02,0x04,0x06,0x00,0x04,
+0x4F,0x44,0x47,
+// 'u'
+0x75,0x03,0x04,0x05,0x01,0x06,
+0x99,0x99,0x70,
+// 'v'
+0x76,0x03,0x07,0x05,0xFF,0x05,
+0x44,0x98,0xA1,0xC1,0x00,
+// 'w'
+0x77,0x03,0x07,0x05,0x00,0x07,
+0x93,0x76,0xBA,0x24,0x40,
+// 'x'
+0x78,0x03,0x05,0x05,0x00,0x05,
+0x8A,0x88,0xA8,0x80,
+// 'y'
+0x79,0x03,0x07,0x07,0xFF,0x05,
+0x44,0x88,0xA1,0xC1,0x02,0x18,0x00,
+// 'z'
+0x7A,0x03,0x04,0x05,0x01,0x06,
+0xF1,0x24,0xF0,
+// '{'
+0x7B,0x01,0x03,0x08,0x01,0x05,
+0x69,0x64,0x93,
+// '|'
+0x7C,0x01,0x01,0x09,0x01,0x03,
+0xFF,0x80,
+// '}'
+0x7D,0x01,0x03,0x08,0x01,0x05,
+0xC9,0x34,0x96,
+// '~'
+0x7E,0x03,0x06,0x03,0x01,0x08,
+0x01,0x91,0x80,
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/minya24.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,331 @@
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// on Arduinos. It can however handle any ttf font that has been converted
+// using the conversion program. These could be fixed width or proportional 
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// minya24.c
+// Point Size   : 24
+// Memory usage : 2807 bytes
+// # characters : 95
+
+// Header Format (to make Arduino UTFT Compatible):
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+unsigned char tft_minya24[] = 
+{
+0x00, 0x15, 0x00, 0x00,
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+
+// ' '
+0x20,0x13,0x00,0x00,0x00,0x07,
+
+// '!'
+0x21,0x02,0x05,0x12,0x00,0x05,
+0x11,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x42,0x01,0xCE,0x73,0x80,
+// '"'
+0x22,0x01,0x06,0x08,0x00,0x06,
+0x01,0xA6,0xDB,0x6D,0xB6,0xC0,
+// '#'
+0x23,0x04,0x0C,0x0E,0x00,0x0B,
+0x04,0x80,0x6C,0x0C,0x80,0xD8,0x7F,0xE7,0xFE,0x1B,0x01,0xB0,0x7F,0xE7,0xFC,0x12,0x03,0x20,0x32,0x00,0x00,
+// '$'
+0x24,0x02,0x0A,0x11,0x00,0x0B,
+0x04,0x01,0x61,0xF8,0xFE,0x65,0x19,0x06,0x40,0xF0,0x1F,0x01,0xE0,0x4C,0x93,0x7C,0xCF,0xE0,0x60,0x10,0x04,0x00,
+// '%'
+0x25,0x01,0x0D,0x14,0x01,0x0F,
+0x00,0x01,0xC1,0x1F,0x19,0x8C,0xCC,0x6C,0x63,0x61,0x36,0x0F,0xB0,0x1B,0x00,0x18,0x01,0x80,0x0C,0x00,0xCE,0x06,0xF8,0x6C,0x66,0x63,0x33,0x13,0x0F,0x98,0x38,0x00,0x00,
+// '&'
+0x26,0x02,0x0E,0x11,0x00,0x0D,
+0x0E,0x00,0x7C,0x01,0xB0,0x06,0xC0,0x1E,0x00,0x38,0x00,0xC3,0x07,0x8C,0x37,0x60,0xCD,0x86,0x1E,0x18,0x70,0x41,0xE1,0x07,0xC6,0x33,0x9F,0x86,0x38,0x00,
+// '''
+0x27,0x02,0x03,0x08,0x00,0x03,
+0x6D,0xB6,0xD8,
+// '('
+0x28,0x01,0x05,0x14,0x02,0x07,
+0x00,0x8E,0x66,0x33,0x18,0xC6,0x31,0x8C,0x61,0x0C,0x63,0x8C,0x00,
+// ')'
+0x29,0x01,0x06,0x15,0x00,0x07,
+0x01,0x86,0x0C,0x18,0x61,0x82,0x08,0x30,0xC2,0x08,0x61,0x86,0x30,0xC6,0x10,0x00,
+// '*'
+0x2A,0x04,0x0A,0x0D,0x01,0x0B,
+0x08,0x03,0x04,0xC1,0xF6,0x7F,0x07,0x81,0xC0,0xF8,0x3F,0x1B,0xCC,0xD8,0x30,0x0C,0x00,
+// '+'
+0x2B,0x06,0x0A,0x0A,0x01,0x0B,
+0x00,0x03,0x00,0xC0,0x30,0x7F,0xBF,0xE0,0xC0,0x30,0x0C,0x00,0x00,
+// ','
+0x2C,0x10,0x05,0x07,0x00,0x05,
+0x33,0x9C,0x63,0x20,0x00,
+// '-'
+0x2D,0x09,0x07,0x02,0x00,0x07,
+0x7D,0xF8,
+// '.'
+0x2E,0x10,0x04,0x04,0x01,0x05,
+0x6E,0xE6,
+// '/'
+0x2F,0x01,0x0C,0x13,0x00,0x0B,
+0x00,0x00,0x06,0x00,0x60,0x0C,0x00,0xC0,0x18,0x01,0x80,0x30,0x03,0x00,0x60,0x06,0x00,0xC0,0x0C,0x01,0x80,0x18,0x03,0x00,0x30,0x06,0x00,0x40,0x00,
+// '0'
+0x30,0x08,0x0B,0x0B,0x00,0x0B,
+0x0E,0x03,0xE0,0xC6,0x30,0x66,0x0C,0xC0,0x98,0x33,0x06,0x61,0xC7,0xF0,0x7C,0x00,
+// '1'
+0x31,0x08,0x0A,0x0D,0x00,0x09,
+0x04,0x1F,0x03,0xC0,0x30,0x0C,0x03,0x00,0xC0,0x30,0x0C,0x03,0x07,0xF9,0xFE,0x00,0x00,
+// '2'
+0x32,0x06,0x0A,0x0E,0x00,0x0A,
+0x0E,0x07,0xC3,0x30,0xCC,0x03,0x01,0x80,0x60,0x30,0x18,0x0C,0x03,0x01,0xF8,0x7F,0xC0,0x20,
+// '3'
+0x33,0x08,0x09,0x10,0x00,0x08,
+0x78,0x3F,0x81,0x81,0x81,0xC0,0xC0,0xE0,0x78,0x06,0x01,0x01,0x80,0xC0,0xE1,0xE0,0xC0,0x00,
+// '4'
+0x34,0x03,0x0B,0x13,0x00,0x0A,
+0x00,0x00,0xC0,0x18,0x03,0x00,0x60,0x08,0x03,0x00,0x60,0x0D,0x83,0x20,0x64,0x18,0x83,0x10,0xC6,0x1F,0xF3,0xFE,0x03,0x00,0x60,0x0C,0x00,
+// '5'
+0x35,0x09,0x0A,0x0E,0x00,0x0A,
+0x3F,0x8F,0xE3,0x00,0x80,0x60,0x1F,0x87,0xF0,0x06,0x01,0x80,0x60,0x19,0x86,0x7F,0x07,0x80,
+// '6'
+0x36,0x03,0x0A,0x12,0x00,0x0A,
+0x00,0x01,0x80,0xE0,0x30,0x18,0x06,0x03,0x00,0xC0,0x60,0x1B,0x87,0xF1,0x86,0x61,0x90,0x66,0x19,0x86,0x3F,0x07,0x80,
+// '7'
+0x37,0x09,0x0A,0x0E,0x00,0x0A,
+0x7F,0x9F,0xF0,0x18,0x0C,0x06,0x01,0x80,0xC0,0x30,0x18,0x06,0x01,0x80,0x40,0x10,0x0C,0x00,
+// '8'
+0x38,0x03,0x0B,0x11,0x00,0x0B,
+0x0F,0x07,0xF0,0xC3,0x30,0x66,0x0C,0x63,0x87,0xE0,0xF8,0x39,0xCE,0x19,0x81,0x30,0x34,0x06,0xC1,0x98,0x71,0xFC,0x1F,0x00,
+// '9'
+0x39,0x07,0x0A,0x11,0x00,0x0A,
+0x1E,0x0F,0xC6,0x19,0x86,0x41,0x98,0x66,0x18,0xFE,0x1D,0x80,0x40,0x30,0x0C,0x06,0x03,0x01,0xC0,0x20,0x00,0x00,
+// ':'
+0x3A,0x0A,0x05,0x0A,0x00,0x05,
+0x33,0x9C,0x60,0x00,0xCE,0x71,0x80,
+// ';'
+0x3B,0x09,0x05,0x0D,0x00,0x06,
+0x33,0xDE,0x60,0x00,0x0E,0x73,0x8C,0xC0,0x00,
+// '<'
+0x3C,0x06,0x0A,0x0C,0x00,0x09,
+0x00,0x00,0xE0,0x70,0x38,0x1C,0x1C,0x03,0x00,0x70,0x0E,0x01,0xC0,0x10,0x00,
+// '='
+0x3D,0x09,0x08,0x06,0x01,0x0A,
+0xFF,0xFE,0x00,0x00,0x7F,0xFF,
+// '>'
+0x3E,0x06,0x0A,0x0C,0x00,0x09,
+0x00,0x18,0x03,0x80,0x70,0x0E,0x00,0xE0,0x30,0x38,0x1C,0x0E,0x02,0x00,0x00,
+// '?'
+0x3F,0x02,0x09,0x12,0x00,0x09,
+0x1E,0x1F,0x98,0x6C,0x30,0x18,0x18,0x38,0x30,0x30,0x18,0x0C,0x02,0x01,0x00,0x00,0x60,0x78,0x3C,0x0C,0x00,
+// '@'
+0x40,0x02,0x11,0x11,0x00,0x11,
+0x01,0xF0,0x03,0xFE,0x03,0x03,0x83,0x00,0xC3,0x04,0x31,0x1F,0x19,0x9F,0x0C,0xCC,0x86,0x4C,0x43,0x24,0x21,0x12,0x39,0x89,0xF7,0x84,0x71,0x83,0x00,0x00,0xC1,0x80,0x3F,0xC0,0x0F,0x80,0x00,
+// 'A'
+0x41,0x02,0x12,0x13,0x00,0x10,
+0x00,0x00,0x01,0xF0,0x00,0x7E,0x00,0x03,0x80,0x01,0xA0,0x00,0x6C,0x00,0x1B,0x00,0x0C,0x40,0x03,0x18,0x01,0x86,0x00,0x61,0x80,0x1F,0xE0,0x0F,0xFC,0x03,0x03,0x01,0x80,0xC0,0x60,0x10,0x18,0x1F,0x9F,0xC7,0xEF,0xF8,0x00,
+// 'B'
+0x42,0x02,0x0E,0x13,0x00,0x0E,
+0x7F,0x81,0xFF,0x81,0x86,0x06,0x18,0x18,0xC0,0x66,0x01,0xFC,0x07,0xFC,0x1C,0x30,0x60,0x61,0x81,0x86,0x06,0x18,0x18,0x60,0x61,0x81,0x06,0x0C,0x18,0xE0,0xFE,0x03,0xE0,0x00,
+// 'C'
+0x43,0x02,0x0F,0x12,0x00,0x0F,
+0x03,0xF0,0x1F,0xE0,0x70,0xC1,0x80,0x83,0x00,0x04,0x00,0x18,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x06,0x00,0x0E,0x01,0x0E,0x0E,0x0F,0xF8,0x07,0xE0,
+// 'D'
+0x44,0x02,0x0F,0x13,0x00,0x0F,
+0x3F,0xC0,0x7F,0xE0,0x60,0xE0,0xC0,0xC1,0x80,0xC3,0x01,0x86,0x03,0x0C,0x02,0x18,0x04,0x30,0x08,0x60,0x30,0xC0,0x61,0x80,0x83,0x03,0x06,0x0C,0x0C,0x38,0x18,0xE0,0xFF,0x81,0xF8,0x00,
+// 'E'
+0x45,0x02,0x0D,0x13,0x00,0x0D,
+0x3F,0xF1,0xFF,0x86,0x0C,0x30,0x61,0x80,0x0C,0x40,0x62,0x03,0xF0,0x1F,0x80,0xC4,0x06,0x20,0x30,0x01,0x80,0x0C,0x00,0x60,0xC3,0x06,0x7F,0xF3,0xFF,0x80,0x00,
+// 'F'
+0x46,0x01,0x0E,0x13,0x00,0x0E,
+0x00,0x01,0xFF,0xE7,0xFF,0x86,0x06,0x18,0x18,0x63,0x01,0x8C,0x03,0xF0,0x0F,0xC0,0x63,0x01,0x8C,0x06,0x30,0x18,0x00,0x60,0x01,0x80,0x06,0x00,0x3F,0x81,0xFE,0x00,0x00,0x00,
+// 'G'
+0x47,0x01,0x11,0x14,0x00,0x11,
+0x00,0x00,0x03,0xC8,0x03,0xFC,0x03,0x0E,0x03,0x03,0x01,0x00,0x01,0x80,0x00,0xC0,0x00,0x60,0x03,0x30,0x3F,0xD8,0x1F,0x0C,0x01,0x86,0x00,0xC3,0x80,0x60,0xC0,0x30,0x70,0x38,0x1C,0x3C,0x07,0xF6,0x01,0xF3,0x80,0x01,0xE0,
+// 'H'
+0x48,0x01,0x10,0x13,0x00,0x10,
+0x00,0x00,0x7C,0x0F,0x1C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x08,0x0C,0x08,0x0F,0xF8,0x0F,0xF8,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x1A,0x7C,0x1F,0x7C,0x10,
+// 'I'
+0x49,0x02,0x08,0x13,0x00,0x08,
+0x7E,0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,
+// 'J'
+0x4A,0x01,0x0C,0x14,0x00,0x0B,
+0x00,0x00,0x7E,0x07,0xE0,0x18,0x01,0x80,0x18,0x01,0x80,0x18,0x00,0x80,0x08,0x00,0x80,0x18,0x61,0x84,0x18,0x41,0x8C,0x18,0xC3,0x06,0x70,0x7E,0x01,0x80,
+// 'K'
+0x4B,0x02,0x11,0x13,0x00,0x10,
+0x7E,0x7E,0x3F,0x3E,0x06,0x06,0x03,0x06,0x01,0x86,0x00,0xC6,0x00,0x66,0x00,0x37,0x00,0x1F,0x80,0x0E,0x60,0x06,0x10,0x03,0x0C,0x01,0x06,0x00,0x81,0x00,0x60,0x80,0x30,0x66,0x7E,0x1B,0x3F,0x0F,0x80,0x01,0x80,
+// 'L'
+0x4C,0x02,0x0D,0x11,0x00,0x0D,
+0x7E,0x03,0xF0,0x06,0x00,0x30,0x01,0x80,0x0C,0x00,0x60,0x03,0x00,0x18,0x00,0xC0,0x06,0x00,0x30,0x01,0x81,0x0C,0x08,0x60,0x4F,0xFF,0x7F,0xF8,
+// 'M'
+0x4D,0x02,0x14,0x12,0x00,0x13,
+0x7C,0x07,0x87,0xC0,0x7C,0x0C,0x06,0x00,0xE0,0xE0,0x0E,0x0E,0x00,0xE0,0xE0,0x1B,0x1E,0x01,0xB1,0x60,0x0B,0x16,0x00,0x9B,0x60,0x09,0xA6,0x00,0x9E,0x60,0x08,0xE6,0x00,0x8C,0x60,0x08,0xC6,0x03,0x8C,0x7E,0x78,0x47,0xE0,0x00,0x00,
+// 'N'
+0x4E,0x01,0x12,0x13,0x00,0x12,
+0x00,0x00,0x1F,0x03,0xF3,0xC0,0x7C,0x38,0x10,0x0E,0x04,0x03,0xC1,0x00,0xB0,0x40,0x26,0x10,0x09,0x84,0x02,0x31,0x00,0x8C,0x40,0x21,0x90,0x08,0x74,0x06,0x0F,0x01,0x81,0xC0,0x60,0x70,0x78,0x0C,0x3E,0x03,0x00,0x00,0x40,
+// 'O'
+0x4F,0x02,0x11,0x12,0x00,0x11,
+0x03,0xE0,0x07,0xFC,0x07,0x07,0x06,0x01,0x82,0x00,0x63,0x00,0x11,0x80,0x0C,0x80,0x06,0x40,0x03,0x20,0x01,0x98,0x00,0xCC,0x00,0x66,0x00,0x31,0x80,0x30,0xE0,0x18,0x38,0x18,0x0F,0xF8,0x01,0xF0,0x00,
+// 'P'
+0x50,0x02,0x0C,0x12,0x00,0x0C,
+0x7F,0x07,0xFC,0x10,0x61,0x03,0x10,0x31,0x03,0x10,0x31,0x06,0x10,0xE1,0xFC,0x1E,0x01,0x00,0x18,0x01,0x80,0x18,0x01,0x80,0x7E,0x07,0xF0,
+// 'Q'
+0x51,0x02,0x13,0x15,0x00,0x11,
+0x07,0xE0,0x01,0xFE,0x00,0x60,0x70,0x18,0x06,0x06,0x00,0x60,0xC0,0x0C,0x18,0x00,0xC2,0x00,0x18,0x40,0x03,0x08,0x0C,0x61,0x83,0xCC,0x30,0xCD,0x83,0x09,0xE0,0x60,0x3C,0x06,0x07,0x00,0x7F,0xE3,0x07,0xEC,0x40,0x01,0x98,0x00,0x33,0x00,0x03,0xC0,0x00,0x30,
+// 'R'
+0x52,0x02,0x0F,0x13,0x00,0x0F,
+0x7F,0x80,0xFF,0xE0,0x60,0xE0,0xC0,0x61,0x80,0xC3,0x01,0x86,0x07,0x0C,0x1C,0x1F,0xF0,0x3F,0x80,0x66,0x00,0xC4,0x01,0x8C,0x63,0x18,0xC6,0x11,0x8C,0x33,0x7E,0x7C,0xFC,0x70,0x00,0x00,
+// 'S'
+0x53,0x01,0x0D,0x13,0x00,0x0D,
+0x00,0x60,0x7B,0x07,0xF8,0x71,0xC3,0x06,0x18,0x00,0xC0,0x03,0x80,0x0E,0x00,0x1C,0x00,0x78,0x00,0xC0,0x03,0x30,0x19,0x80,0xCC,0x06,0x30,0x70,0xFF,0x03,0xE0,
+// 'T'
+0x54,0x01,0x0F,0x12,0x00,0x0F,
+0x00,0x00,0xFF,0xF9,0xFF,0xF3,0x18,0x66,0x30,0xCC,0x61,0x90,0xC3,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x0F,0xE0,0x1F,0xC0,
+// 'U'
+0x55,0x02,0x10,0x11,0x00,0x0F,
+0x7C,0x7E,0x7C,0x7E,0x30,0x08,0x30,0x08,0x30,0x08,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x10,0x08,0x18,0x18,0x18,0x18,0x0C,0x30,0x0F,0xF0,0x03,0xC0,
+// 'V'
+0x56,0x01,0x10,0x12,0x00,0x10,
+0x00,0x7E,0xFE,0x7E,0x7E,0x18,0x18,0x30,0x18,0x30,0x18,0x20,0x08,0x60,0x0C,0x60,0x0C,0x60,0x0C,0x40,0x0C,0xC0,0x04,0xC0,0x04,0xC0,0x06,0x80,0x07,0x80,0x07,0x80,0x03,0x00,0x03,0x00,
+// 'W'
+0x57,0x01,0x14,0x13,0x00,0x14,
+0x00,0x00,0x0F,0xC4,0xFF,0xF0,0x4F,0xE3,0x06,0x10,0x30,0xE3,0x01,0x8E,0x30,0x18,0xE3,0x01,0x8E,0x30,0x19,0xA3,0x01,0x9A,0x20,0x09,0xA6,0x00,0xD2,0x60,0x0D,0x36,0x00,0xF3,0x40,0x0F,0x34,0x00,0x63,0xC0,0x06,0x1C,0x00,0x61,0x80,0x00,0x18,0x00,
+// 'X'
+0x58,0x01,0x10,0x13,0x00,0x10,
+0x00,0x00,0x7F,0x7F,0x7E,0x7E,0x0C,0x30,0x06,0x20,0x06,0x60,0x03,0xC0,0x01,0x80,0x01,0x80,0x03,0x80,0x02,0xC0,0x06,0x40,0x04,0x60,0x0C,0x60,0x18,0x30,0x18,0x30,0x7C,0x30,0xFC,0xFE,0x00,0xFF,
+// 'Y'
+0x59,0x01,0x11,0x13,0x00,0x10,
+0x00,0x00,0x7F,0x3F,0xBF,0x07,0x03,0x03,0x00,0xC1,0x00,0x31,0x80,0x18,0x80,0x06,0xC0,0x01,0xC0,0x00,0xE0,0x00,0x30,0x00,0x30,0x00,0x18,0x00,0x0C,0x00,0x06,0x00,0x03,0x00,0x07,0x80,0x03,0xF8,0x00,0x7C,0x00,
+// 'Z'
+0x5A,0x02,0x0E,0x12,0x00,0x0D,
+0x7F,0xF9,0xFF,0xE6,0x03,0x18,0x18,0x60,0xC1,0x07,0x00,0x18,0x00,0xC0,0x06,0x00,0x18,0x00,0xC0,0x06,0x00,0x18,0x30,0xC0,0xC3,0x03,0x1F,0xFE,0x7F,0xF8,0x00,0x60,
+// '['
+0x5B,0x01,0x08,0x14,0x01,0x08,
+0x00,0x7C,0x7C,0x40,0x40,0x40,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x7E,0x00,
+// '\'
+0x5C,0x01,0x0A,0x13,0x00,0x0A,
+0x00,0x10,0x06,0x01,0x80,0x30,0x0C,0x01,0x80,0x60,0x0C,0x03,0x00,0x60,0x18,0x06,0x00,0xC0,0x30,0x06,0x01,0x80,0x60,0x08,
+// ']'
+0x5D,0x01,0x08,0x14,0x00,0x08,
+0x00,0x7C,0x7E,0x04,0x04,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x7C,0xFC,
+// '^'
+0x5E,0x03,0x0B,0x0B,0x00,0x0B,
+0x00,0x00,0x40,0x18,0x03,0x00,0xF0,0x32,0x0C,0x63,0x86,0x60,0xC0,0x0C,0x00,0x00,
+// '_'
+0x5F,0x15,0x11,0x02,0xFF,0x0F,
+0x7F,0xFF,0x3F,0xFF,0x80,
+// '`'
+0x60,0x00,0x06,0x07,0x00,0x06,
+0x01,0x87,0x0C,0x18,0x20,0x00,
+// 'a'
+0x61,0x06,0x0C,0x0D,0x00,0x0B,
+0x0E,0x03,0xF0,0x31,0x06,0x10,0x23,0x80,0xF8,0x3D,0x87,0x10,0x61,0x0C,0x18,0x63,0xA7,0xFE,0x1C,0xC0,
+// 'b'
+0x62,0x01,0x0C,0x14,0x00,0x0C,
+0x00,0x07,0xC0,0x3C,0x01,0x80,0x18,0x01,0x80,0x18,0x01,0x98,0x1F,0xC1,0xC6,0x18,0x21,0x83,0x10,0x31,0x03,0x18,0x31,0x83,0x18,0x6F,0xC6,0xF7,0xC0,0x30,
+// 'c'
+0x63,0x06,0x0B,0x0D,0x00,0x0B,
+0x06,0x03,0xF0,0xC7,0x31,0xE4,0x1C,0x80,0x30,0x06,0x00,0x60,0x0C,0x01,0xC3,0x1F,0xC0,0xF0,
+// 'd'
+0x64,0x01,0x0D,0x13,0x00,0x0D,
+0x00,0x00,0x1E,0x00,0xF0,0x01,0x80,0x0C,0x00,0x60,0x03,0x01,0x98,0x3E,0x81,0x9C,0x18,0x60,0xC3,0x04,0x08,0x60,0x41,0x02,0x0C,0x30,0x71,0x91,0xFF,0x87,0x1C,
+// 'e'
+0x65,0x07,0x0B,0x0C,0x00,0x0B,
+0x0F,0x03,0xF0,0xC3,0x30,0x66,0x1C,0xDE,0x1E,0x03,0x00,0x60,0x06,0x18,0x7E,0x07,0x80,
+// 'f'
+0x66,0x02,0x0A,0x12,0x00,0x08,
+0x07,0x07,0xE1,0xB8,0x4E,0x10,0x04,0x07,0xE1,0xF8,0x18,0x06,0x01,0x80,0x60,0x18,0x06,0x01,0x81,0xF8,0x7E,0x00,0x00,
+// 'g'
+0x67,0x05,0x0A,0x12,0x00,0x0A,
+0x00,0x00,0x20,0x18,0x7E,0x3F,0x18,0x64,0x09,0x02,0x61,0x9F,0xC1,0xE0,0x0C,0x01,0x84,0x67,0x19,0x8C,0x7E,0x0F,0x00,
+// 'h'
+0x68,0x02,0x0E,0x12,0x00,0x0D,
+0x78,0x01,0xE0,0x01,0x80,0x06,0x00,0x19,0xC0,0x2F,0x80,0xE6,0x07,0x08,0x18,0x20,0x60,0x81,0x82,0x06,0x08,0x18,0x20,0x60,0x81,0x82,0x1F,0x9E,0x7E,0x78,0x00,0x00,
+// 'i'
+0x69,0x02,0x08,0x11,0x00,0x08,
+0x30,0x78,0x78,0x30,0x00,0x78,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7F,
+// 'j'
+0x6A,0x02,0x09,0x15,0xFE,0x07,
+0x06,0x07,0x03,0x81,0xC0,0x00,0xF8,0x7C,0x06,0x03,0x01,0x80,0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x01,0x99,0x8F,0xC3,0xC0,
+// 'k'
+0x6B,0x02,0x0D,0x12,0x00,0x0D,
+0x78,0x03,0xC0,0x06,0x00,0x37,0xC1,0xBC,0x0C,0xC0,0x6C,0x03,0xC0,0x1C,0x00,0xC0,0x07,0x00,0x3E,0x01,0xB8,0x08,0x61,0xC1,0xCF,0x07,0x00,0x10,0x00,0x00,
+// 'l'
+0x6C,0x02,0x08,0x12,0x00,0x08,
+0x78,0x78,0x18,0x08,0x08,0x08,0x08,0x08,0x08,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,
+// 'm'
+0x6D,0x08,0x14,0x0D,0x00,0x14,
+0x40,0xC3,0x0F,0xBE,0xF8,0x1E,0x78,0xC1,0xC7,0x0C,0x18,0x20,0xC1,0x82,0x0C,0x10,0x20,0xC1,0x82,0x08,0x18,0x60,0x81,0x84,0x18,0x7E,0x71,0xE7,0xE7,0x9E,0x00,0x00,0x00,
+// 'n'
+0x6E,0x06,0x0E,0x0E,0x00,0x0D,
+0x79,0xC1,0xEF,0x81,0xE6,0x07,0x08,0x18,0x30,0x60,0xC1,0x82,0x06,0x08,0x18,0x20,0x60,0x81,0x82,0x1F,0x1E,0x7E,0x78,0x00,0x00,
+// 'o'
+0x6F,0x08,0x0B,0x0C,0x00,0x0C,
+0x0F,0x03,0xF8,0xC3,0x30,0x36,0x06,0x80,0xD0,0x1B,0x03,0x60,0x66,0x18,0xFE,0x07,0x80,
+// 'p'
+0x70,0x07,0x0B,0x10,0x00,0x0B,
+0x07,0x1F,0xF1,0xE3,0x18,0x23,0x06,0x60,0xCC,0x19,0x82,0x30,0xC6,0x18,0xFE,0x1B,0x83,0x00,0x60,0x1C,0x07,0x80,
+// 'q'
+0x71,0x06,0x0D,0x11,0x00,0x0B,
+0x0C,0x01,0xF7,0x0C,0xF8,0xC3,0x04,0x18,0x20,0x41,0x02,0x08,0x10,0x60,0x83,0x04,0x0C,0x60,0x7F,0x01,0xE8,0x00,0xC0,0x06,0xC0,0x1E,0x00,0xE0,
+// 'r'
+0x72,0x08,0x0A,0x0D,0x00,0x09,
+0x77,0x3F,0xE3,0xB8,0xCE,0x33,0x0C,0x03,0x00,0xC0,0x30,0x04,0x07,0xC1,0xF0,0x00,0x00,
+// 's'
+0x73,0x05,0x0A,0x0E,0x00,0x0A,
+0x03,0x00,0xC1,0xF0,0xFC,0x63,0x18,0x47,0x00,0xF8,0x07,0x80,0x64,0x19,0x86,0x7F,0x07,0x80,
+// 't'
+0x74,0x02,0x09,0x13,0x00,0x09,
+0x30,0x18,0x0C,0x06,0x02,0x07,0xE3,0xF8,0x40,0x20,0x10,0x08,0x04,0x02,0x11,0x18,0x84,0x62,0x33,0x0F,0x83,0x00,
+// 'u'
+0x75,0x06,0x0E,0x0E,0x00,0x0D,
+0x00,0x03,0xE7,0xC7,0x9F,0x0C,0x30,0x30,0xC0,0xC3,0x03,0x0C,0x0C,0x30,0x30,0xC0,0xC3,0x03,0x0C,0x0E,0x7C,0x1F,0xF8,0x3C,0x00,
+// 'v'
+0x76,0x07,0x0E,0x0D,0x00,0x0D,
+0x01,0xFB,0xF3,0xEF,0xC6,0x06,0x18,0x18,0x60,0x31,0x00,0xCC,0x01,0x30,0x06,0x80,0x0A,0x00,0x38,0x00,0xE0,0x01,0x00,
+// 'w'
+0x77,0x08,0x11,0x0D,0x00,0x11,
+0x04,0x1F,0xFF,0x6F,0xBE,0x31,0x83,0x1C,0xC1,0x9E,0x40,0x4D,0x20,0x34,0xB0,0x1A,0x78,0x05,0x3C,0x03,0x8E,0x01,0x86,0x00,0x43,0x00,0x00,0x00,
+// 'x'
+0x78,0x08,0x0D,0x0D,0x00,0x0D,
+0x0C,0xFB,0xE7,0xDE,0x18,0x31,0x80,0xD8,0x03,0x80,0x18,0x01,0xE0,0x19,0x81,0x8C,0x0E,0x7D,0xF3,0xE4,0x00,0x00,
+// 'y'
+0x79,0x06,0x0F,0x11,0x00,0x0D,
+0x7C,0x01,0xF8,0x00,0xC3,0xF0,0xC7,0xE1,0x82,0x01,0x0C,0x03,0x18,0x06,0x20,0x06,0xC0,0x0D,0x00,0x0E,0x00,0x18,0x00,0x30,0x06,0xC0,0x1F,0x00,0x3E,0x00,0x30,0x00,
+// 'z'
+0x7A,0x08,0x0B,0x0B,0x00,0x0B,
+0x7F,0xCF,0xF9,0x86,0x31,0x86,0x60,0x1C,0x07,0x00,0xC6,0x30,0xCF,0xF9,0xFF,0x00,
+// '{'
+0x7B,0x02,0x09,0x12,0x00,0x08,
+0x07,0x07,0x83,0x01,0x01,0x80,0xC0,0x60,0xE0,0x70,0x0C,0x06,0x03,0x01,0x80,0xC0,0x60,0x30,0x1F,0x03,0x80,
+// '|'
+0x7C,0x02,0x03,0x12,0x01,0x05,
+0x49,0x24,0x92,0x6D,0xB6,0xDB,0x6C,
+// '}'
+0x7D,0x02,0x07,0x11,0x00,0x07,
+0x30,0xF0,0x20,0x41,0x83,0x02,0x06,0x0E,0x18,0x60,0xC0,0x81,0x83,0x3C,0x78,
+// '~'
+0x7E,0x09,0x0D,0x04,0x00,0x0D,
+0x18,0x03,0xF1,0x98,0xFC,0x83,0xC0,
+
+// Terminator
+0xFF
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/stmpe610.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,71 @@
+/*
+    STMPE610 Touch controller constants
+*/
+
+#ifndef _STMPE610_H
+#define _STMPE610_H
+
+#include <stdint.h>
+
+#define STMPE610_SPI_MODE              1
+
+// Identification registers
+#define STMPE610_REG_CHP_ID         0x00 // 16-bit
+#define STMPE610_REG_ID_VER         0x02
+
+// System registers
+#define STMPE610_REG_SYS_CTRL1      0x03
+#define STMPE610_REG_SYS_CTRL2      0x04
+#define STMPE610_REG_SPI_CFG        0x08
+
+// Interrupt control registers
+#define STMPE610_REG_INT_CTRL       0x09
+#define STMPE610_REG_INT_EN         0x0A
+#define STMPE610_REG_INT_STA        0x0B
+#define STMPE610_REG_GPIO_INT_EN    0x0C
+#define STMPE610_REG_GPIO_INT_STA   0x0D
+#define STMPE610_REG_ADC_INT_EN     0x0E
+#define STMPE610_REG_ADC_INT_STA    0x0F
+
+// GPIO registers
+#define STMPE610_REG_GPIO_SET_PIN   0x10
+#define STMPE610_REG_GPIO_CLR_PIN   0x11
+#define STMPE610_REG_GPIO_MP_STA    0x12
+#define STMPE610_REG_GPIO_DIR       0x13
+#define STMPE610_REG_GPIO_ED        0x14
+#define STMPE610_REG_GPIO_RE        0x15
+#define STMPE610_REG_GPIO_FE        0x16
+#define STMPE610_REG_GPIO_AF        0x17
+
+// ADC registers
+#define STMPE610_REG_ADC_CTRL1      0x20
+#define STMPE610_REG_ADC_CTRL2      0x21
+#define STMPE610_REG_ADC_CAPT       0x22
+#define STMPE610_REG_ADC_DATA_CH0   0x30 // 16-bit
+#define STMPE610_REG_ADC_DATA_CH1   0x32 // 16-bit
+#define STMPE610_REG_ADC_DATA_CH4   0x38 // 16-bit
+#define STMPE610_REG_ADC_DATA_CH5   0x3A // 16-bit
+#define STMPE610_REG_ADC_DATA_CH6   0x3C // 16-bit
+#define STMPE610_REG_ADC_DATA_CH7   0x3E // 16-bit
+
+// Touchscreen registers
+#define STMPE610_REG_TSC_CTRL       0x40
+#define STMPE610_REG_TSC_CFG        0x41
+#define STMPE610_REG_WDW_TR_X       0x42 // 16-bit
+#define STMPE610_REG_WDW_TR_Y       0x44 // 16-bit
+#define STMPE610_REG_WDW_BL_X       0x46 // 16-bit
+#define STMPE610_REG_WDW_BL_Y       0x48 // 16-bit
+#define STMPE610_REG_FIFO_TH        0x4A
+#define STMPE610_REG_FIFO_STA       0x4B
+#define STMPE610_REG_FIFO_SIZE      0x4C
+#define STMPE610_REG_TSC_DATA_X     0x4D // 16-bit
+#define STMPE610_REG_TSC_DATA_Y     0x4F // 16-bit
+#define STMPE610_REG_TSC_DATA_Z     0x51
+#define STMPE610_REG_TSC_DATA_XYZ   0x52 // 32-bit
+#define STMPE610_REG_TSC_FRACT_XYZ  0x56
+#define STMPE610_REG_TSC_DATA       0x57
+#define STMPE610_REG_TSC_I_DRIVE    0x58
+#define STMPE610_REG_TSC_SHIELD     0x59
+
+
+#endif /* _STMPE610_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/tft.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2986 @@
+/* TFT module
+ *
+ *  Author: LoBo (loboris@gmail.com, loboris.github)
+ *
+ *  Module supporting SPI TFT displays based on ILI9341 & ILI9488 controllers
+*/
+
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <string.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_system.h"
+#include "tft.h"
+#include "time.h"
+#include <math.h>
+#include "rom/tjpgd.h"
+#include "esp_heap_caps.h"
+#include "tftspi.h"
+
+#include "vnc-server.h"
+
+#define DEG_TO_RAD 0.01745329252
+#define RAD_TO_DEG 57.295779513
+#define deg_to_rad 0.01745329252 + 3.14159265359
+#define swap(a, b) { int16_t t = a; a = b; b = t; }
+#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
+#if !defined(max)
+#define max(A,B) ( (A) > (B) ? (A):(B))
+#endif
+#if !defined(min)
+#define min(A,B) ( (A) < (B) ? (A):(B))
+#endif
+
+// Embedded fonts
+extern uint8_t tft_SmallFont[];
+extern uint8_t tft_DefaultFont[];
+extern uint8_t tft_Dejavu18[];
+extern uint8_t tft_Dejavu24[];
+extern uint8_t tft_Ubuntu16[];
+extern uint8_t tft_Comic24[];
+extern uint8_t tft_minya24[];
+extern uint8_t tft_tooney32[];
+extern uint8_t tft_def_small[];
+
+// ==== Color definitions constants ==============
+const color_t TFT_BLACK       = {   0,   0,   0 };
+const color_t TFT_NAVY        = {   0,   0, 128 };
+const color_t TFT_DARKGREEN   = {   0, 128,   0 };
+const color_t TFT_DARKCYAN    = {   0, 128, 128 };
+const color_t TFT_MAROON      = { 128,   0,   0 };
+const color_t TFT_PURPLE      = { 128,   0, 128 };
+const color_t TFT_OLIVE       = { 128, 128,   0 };
+const color_t TFT_LIGHTGREY   = { 192, 192, 192 };
+const color_t TFT_DARKGREY    = { 128, 128, 128 };
+const color_t TFT_BLUE        = {   0,   0, 255 };
+const color_t TFT_GREEN       = {   0, 255,   0 };
+const color_t TFT_CYAN        = {   0, 255, 255 };
+const color_t TFT_RED         = { 255,   0,   0 };
+const color_t TFT_MAGENTA     = { 255,   0, 255 };
+const color_t TFT_YELLOW      = { 255, 255,   0 };
+const color_t TFT_WHITE       = { 255, 255, 255 };
+const color_t TFT_ORANGE      = { 255, 164,   0 };
+const color_t TFT_GREENYELLOW = { 172, 255,  44 };
+const color_t TFT_PINK        = { 255, 192, 202 };
+// ===============================================
+
+// ==============================================================
+// ==== Set default values of global variables ==================
+uint8_t orientation = LANDSCAPE;// screen orientation
+uint16_t font_rotate = 0;		// font rotation
+uint8_t	font_transparent = 0;
+uint8_t	font_forceFixed = 0;
+uint8_t	text_wrap = 0;			// character wrapping to new line
+color_t	_fg = {  0, 255,   0};
+color_t _bg = {  0,   0,   0};
+uint8_t image_debug = 0;
+
+float _angleOffset = DEFAULT_ANGLE_OFFSET;
+
+int	TFT_X = 0;
+int	TFT_Y = 0;
+
+uint16_t tp_xleft = 300;
+uint16_t tp_xright = 3550;
+uint16_t tp_ytop = 3800;
+uint16_t tp_ybottom = 300;
+
+dispWin_t dispWin = {
+  .x1 = 0,
+  .y1 = 0,
+  .x2 = DEFAULT_TFT_DISPLAY_WIDTH -1,
+  .y2 = DEFAULT_TFT_DISPLAY_HEIGHT -1,
+};
+
+Font cfont = {
+	.font = tft_DefaultFont,
+	.x_size = 0,
+	.y_size = 0x0B,
+	.offset = 0,
+	.numchars = 95,
+	.bitmap = 1,
+};
+
+uint8_t font_buffered_char = 1;
+uint8_t font_line_space = 0;
+// ==============================================================
+
+
+typedef struct {
+      uint8_t charCode;
+      int adjYOffset;
+      int width;
+      int height;
+      int xOffset;
+      int xDelta;
+      uint16_t dataPtr;
+} propFont;
+
+static dispWin_t dispWinTemp;
+
+static uint8_t *userfont = NULL;
+static int TFT_OFFSET = 0;
+static propFont	fontChar;
+static float _arcAngleMax = DEFAULT_ARC_ANGLE_MAX;
+
+
+// =========================================================================
+// ** All drawings are clipped to 'dispWin' **
+// ** All x,y coordinates in public functions are relative to clip window **
+// =========== : Public functions
+// ----------- : Local functions
+// =========================================================================
+
+
+// Compare two colors; return 0 if equal
+//============================================
+int TFT_compare_colors(color_t c1, color_t c2)
+{
+	if ((c1.r & 0xFC) != (c2.r & 0xFC)) return 1;
+	if ((c1.g & 0xFC) != (c2.g & 0xFC)) return 1;
+	if ((c1.b & 0xFC) != (c2.b & 0xFC)) return 1;
+
+	return 0;
+}
+
+// draw color pixel on screen
+//------------------------------------------------------------------------
+static void _drawPixel(int16_t x, int16_t y, color_t color, uint8_t sel) {
+
+	if ((x < dispWin.x1) || (y < dispWin.y1) || (x > dispWin.x2) || (y > dispWin.y2)) return;
+	drawPixel(x, y, color, sel);
+	VncDrawPixel(x, y, VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+//====================================================================
+void TFT_drawPixel(int16_t x, int16_t y, color_t color, uint8_t sel) {
+
+	_drawPixel(x+dispWin.x1, y+dispWin.y1, color, sel);
+}
+
+//===========================================
+color_t TFT_readPixel(int16_t x, int16_t y) {
+
+  if ((x < dispWin.x1) || (y < dispWin.y1) || (x > dispWin.x2) || (y > dispWin.y2)) return TFT_BLACK;
+
+  return readPixel(x, y);
+}
+
+//--------------------------------------------------------------------------
+static void _drawFastVLine(int16_t x, int16_t y, int16_t h, color_t color) {
+	// clipping
+	if ((x < dispWin.x1) || (x > dispWin.x2) || (y > dispWin.y2)) return;
+	if (y < dispWin.y1) {
+		h -= (dispWin.y1 - y);
+		y = dispWin.y1;
+	}
+	if (h < 0) h = 0;
+	if ((y + h) > (dispWin.y2+1)) h = dispWin.y2 - y + 1;
+	if (h == 0) h = 1;
+	TFT_pushColorRep(x, y, x, y+h-1, color, (uint32_t)h);
+	VncDrawVertLine(x, y, y+h-1, VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+//--------------------------------------------------------------------------
+static void _drawFastHLine(int16_t x, int16_t y, int16_t w, color_t color) {
+	// clipping
+	if ((y < dispWin.y1) || (x > dispWin.x2) || (y > dispWin.y2)) return;
+	if (x < dispWin.x1) {
+		w -= (dispWin.x1 - x);
+		x = dispWin.x1;
+	}
+	if (w < 0) w = 0;
+	if ((x + w) > (dispWin.x2+1)) w = dispWin.x2 - x + 1;
+	if (w == 0) w = 1;
+
+	TFT_pushColorRep(x, y, x+w-1, y, color, (uint32_t)w);
+	VncDrawHorzLine(x, x+w-1, y, VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+//======================================================================
+void TFT_drawFastVLine(int16_t x, int16_t y, int16_t h, color_t color) {
+	_drawFastVLine(x+dispWin.x1, y+dispWin.y1, h, color);
+}
+
+//======================================================================
+void TFT_drawFastHLine(int16_t x, int16_t y, int16_t w, color_t color) {
+	_drawFastHLine(x+dispWin.x1, y+dispWin.y1, w, color);
+}
+
+// Bresenham's algorithm - thx wikipedia - speed enhanced by Bodmer this uses
+// the eficient FastH/V Line draw routine for segments of 2 pixels or more
+//----------------------------------------------------------------------------------
+static void _drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, color_t color)
+{
+  if (x0 == x1) {
+	  if (y0 <= y1) _drawFastVLine(x0, y0, y1-y0, color);
+	  else _drawFastVLine(x0, y1, y0-y1, color);
+	  return;
+  }
+  if (y0 == y1) {
+	  if (x0 <= x1) _drawFastHLine(x0, y0, x1-x0, color);
+	  else _drawFastHLine(x1, y0, x0-x1, color);
+	  return;
+  }
+
+  int steep = 0;
+  if (abs(y1 - y0) > abs(x1 - x0)) steep = 1;
+  if (steep) {
+    swap(x0, y0);
+    swap(x1, y1);
+  }
+  if (x0 > x1) {
+    swap(x0, x1);
+    swap(y0, y1);
+  }
+
+  int16_t dx = x1 - x0, dy = abs(y1 - y0);
+  int16_t err = dx >> 1, ystep = -1, xs = x0, dlen = 0;
+
+  if (y0 < y1) ystep = 1;
+
+  // Split into steep and not steep for FastH/V separation
+  if (steep) {
+    for (; x0 <= x1; x0++) {
+      dlen++;
+      err -= dy;
+      if (err < 0) {
+        err += dx;
+        if (dlen == 1) _drawPixel(y0, xs, color, 1);
+        else _drawFastVLine(y0, xs, dlen, color);
+        dlen = 0; y0 += ystep; xs = x0 + 1;
+      }
+    }
+    if (dlen) _drawFastVLine(y0, xs, dlen, color);
+  }
+  else
+  {
+    for (; x0 <= x1; x0++) {
+      dlen++;
+      err -= dy;
+      if (err < 0) {
+        err += dx;
+        if (dlen == 1) _drawPixel(xs, y0, color, 1);
+        else _drawFastHLine(xs, y0, dlen, color);
+        dlen = 0; y0 += ystep; xs = x0 + 1;
+      }
+    }
+    if (dlen) _drawFastHLine(xs, y0, dlen, color);
+  }
+}
+
+//==============================================================================
+void TFT_drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, color_t color)
+{
+	_drawLine(x0+dispWin.x1, y0+dispWin.y1, x1+dispWin.x1, y1+dispWin.y1, color);
+}
+
+// fill a rectangle
+//--------------------------------------------------------------------------------
+static void _fillRect(int16_t x, int16_t y, int16_t w, int16_t h, color_t color) {
+	// clipping
+	if ((x >= dispWin.x2) || (y > dispWin.y2)) return;
+
+	if (x < dispWin.x1) {
+		w -= (dispWin.x1 - x);
+		x = dispWin.x1;
+	}
+	if (y < dispWin.y1) {
+		h -= (dispWin.y1 - y);
+		y = dispWin.y1;
+	}
+	if (w < 0) w = 0;
+	if (h < 0) h = 0;
+
+	if ((x + w) > (dispWin.x2+1)) w = dispWin.x2 - x + 1;
+	if ((y + h) > (dispWin.y2+1)) h = dispWin.y2 - y + 1;
+	if (w == 0) w = 1;
+	if (h == 0) h = 1;
+	TFT_pushColorRep(x, y, x+w-1, y+h-1, color, (uint32_t)(h*w));
+	VncFillRect(x, y, x+w-1, y+h-1, VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+//============================================================================
+void TFT_fillRect(int16_t x, int16_t y, int16_t w, int16_t h, color_t color) {
+	_fillRect(x+dispWin.x1, y+dispWin.y1, w, h, color);
+}
+
+//==================================
+void TFT_fillScreen(color_t color) {
+	TFT_pushColorRep(0, 0, _width-1, _height-1, color, (uint32_t)(_height*_width));
+	VncCls(VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+//==================================
+void TFT_fillWindow(color_t color) {
+	TFT_pushColorRep(dispWin.x1, dispWin.y1, dispWin.x2, dispWin.y2,
+			color, (uint32_t)((dispWin.x2-dispWin.x1+1) * (dispWin.y2-dispWin.y1+1)));
+	VncFillRect(dispWin.x1, dispWin.y1, dispWin.x2, dispWin.y2, VNC_RGB2COL(color.r, color.g, color.b));
+}
+
+// ^^^============= Basics drawing functions ================================^^^
+
+
+// ================ Graphics drawing functions ==================================
+
+//-----------------------------------------------------------------------------------
+static void _drawRect(uint16_t x1,uint16_t y1,uint16_t w,uint16_t h, color_t color) {
+  _drawFastHLine(x1,y1,w, color);
+  _drawFastVLine(x1+w-1,y1,h, color);
+  _drawFastHLine(x1,y1+h-1,w, color);
+  _drawFastVLine(x1,y1,h, color);
+}
+
+//===============================================================================
+void TFT_drawRect(uint16_t x1,uint16_t y1,uint16_t w,uint16_t h, color_t color) {
+	_drawRect(x1+dispWin.x1, y1+dispWin.y1, w, h, color);
+}
+
+//-------------------------------------------------------------------------------------------------
+static void drawCircleHelper(int16_t x0, int16_t y0, int16_t r, uint8_t cornername, color_t color)
+{
+	int16_t f = 1 - r;
+	int16_t ddF_x = 1;
+	int16_t ddF_y = -2 * r;
+	int16_t x = 0;
+	int16_t y = r;
+
+	disp_select();
+	while (x < y) {
+		if (f >= 0) {
+			y--;
+			ddF_y += 2;
+			f += ddF_y;
+		}
+		x++;
+		ddF_x += 2;
+		f += ddF_x;
+		if (cornername & 0x4) {
+			_drawPixel(x0 + x, y0 + y, color, 0);
+			_drawPixel(x0 + y, y0 + x, color, 0);
+		}
+		if (cornername & 0x2) {
+			_drawPixel(x0 + x, y0 - y, color, 0);
+			_drawPixel(x0 + y, y0 - x, color, 0);
+		}
+		if (cornername & 0x8) {
+			_drawPixel(x0 - y, y0 + x, color, 0);
+			_drawPixel(x0 - x, y0 + y, color, 0);
+		}
+		if (cornername & 0x1) {
+			_drawPixel(x0 - y, y0 - x, color, 0);
+			_drawPixel(x0 - x, y0 - y, color, 0);
+		}
+	}
+	disp_deselect();
+}
+
+// Used to do circles and roundrects
+//----------------------------------------------------------------------------------------------------------------
+static void fillCircleHelper(int16_t x0, int16_t y0, int16_t r,	uint8_t cornername, int16_t delta, color_t color)
+{
+	int16_t f = 1 - r;
+	int16_t ddF_x = 1;
+	int16_t ddF_y = -2 * r;
+	int16_t x = 0;
+	int16_t y = r;
+	int16_t ylm = x0 - r;
+
+	while (x < y) {
+		if (f >= 0) {
+			if (cornername & 0x1) _drawFastVLine(x0 + y, y0 - x, 2 * x + 1 + delta, color);
+			if (cornername & 0x2) _drawFastVLine(x0 - y, y0 - x, 2 * x + 1 + delta, color);
+			ylm = x0 - y;
+			y--;
+			ddF_y += 2;
+			f += ddF_y;
+		}
+		x++;
+		ddF_x += 2;
+		f += ddF_x;
+
+		if ((x0 - x) > ylm) {
+			if (cornername & 0x1) _drawFastVLine(x0 + x, y0 - y, 2 * y + 1 + delta, color);
+			if (cornername & 0x2) _drawFastVLine(x0 - x, y0 - y, 2 * y + 1 + delta, color);
+		}
+	}
+}
+
+// Draw a rounded rectangle
+//=============================================================================================
+void TFT_drawRoundRect(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t r, color_t color)
+{
+	x += dispWin.x1;
+	y += dispWin.y1;
+
+	// smarter version
+	_drawFastHLine(x + r, y, w - 2 * r, color);			// Top
+	_drawFastHLine(x + r, y + h - 1, w - 2 * r, color);	// Bottom
+	_drawFastVLine(x, y + r, h - 2 * r, color);			// Left
+	_drawFastVLine(x + w - 1, y + r, h - 2 * r, color);	// Right
+
+	// draw four corners
+	drawCircleHelper(x + r, y + r, r, 1, color);
+	drawCircleHelper(x + w - r - 1, y + r, r, 2, color);
+	drawCircleHelper(x + w - r - 1, y + h - r - 1, r, 4, color);
+	drawCircleHelper(x + r, y + h - r - 1, r, 8, color);
+}
+
+// Fill a rounded rectangle
+//=============================================================================================
+void TFT_fillRoundRect(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t r, color_t color)
+{
+	x += dispWin.x1;
+	y += dispWin.y1;
+
+	// smarter version
+	_fillRect(x + r, y, w - 2 * r, h, color);
+
+	// draw four corners
+	fillCircleHelper(x + w - r - 1, y + r, r, 1, h - 2 * r - 1, color);
+	fillCircleHelper(x + r, y + r, r, 2, h - 2 * r - 1, color);
+}
+
+
+
+
+//-----------------------------------------------------------------------------------------------
+static void _drawLineByAngle(int16_t x, int16_t y, int16_t angle, uint16_t length, color_t color)
+{
+	_drawLine(
+		x,
+		y,
+		x + length * cos((angle + _angleOffset) * DEG_TO_RAD),
+		y + length * sin((angle + _angleOffset) * DEG_TO_RAD), color);
+}
+
+//---------------------------------------------------------------------------------------------------------------
+static void _DrawLineByAngle(int16_t x, int16_t y, int16_t angle, uint16_t start, uint16_t length, color_t color)
+{
+	_drawLine(
+		x + start * cos((angle + _angleOffset) * DEG_TO_RAD),
+		y + start * sin((angle + _angleOffset) * DEG_TO_RAD),
+		x + (start + length) * cos((angle + _angleOffset) * DEG_TO_RAD),
+		y + (start + length) * sin((angle + _angleOffset) * DEG_TO_RAD), color);
+}
+
+//===========================================================================================================
+void TFT_drawLineByAngle(uint16_t x, uint16_t y, uint16_t start, uint16_t len, uint16_t angle, color_t color)
+{
+	x += dispWin.x1;
+	y += dispWin.y1;
+
+	if (start == 0) _drawLineByAngle(x, y, angle, len, color);
+	else _DrawLineByAngle(x, y, angle, start, len, color);
+}
+
+
+// Draw a triangle
+//--------------------------------------------------------------------------------------------------------------------
+static void _drawTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color)
+{
+	_drawLine(x0, y0, x1, y1, color);
+	_drawLine(x1, y1, x2, y2, color);
+	_drawLine(x2, y2, x0, y0, color);
+}
+
+//================================================================================================================
+void TFT_drawTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color)
+{
+	x0 += dispWin.x1;
+	y0 += dispWin.y1;
+	x1 += dispWin.x1;
+	y1 += dispWin.y1;
+	x2 += dispWin.x1;
+	y2 += dispWin.y1;
+
+	_drawLine(x0, y0, x1, y1, color);
+	_drawLine(x1, y1, x2, y2, color);
+	_drawLine(x2, y2, x0, y0, color);
+}
+
+// Fill a triangle
+//--------------------------------------------------------------------------------------------------------------------
+static void _fillTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color)
+{
+  int16_t a, b, y, last;
+
+  // Sort coordinates by Y order (y2 >= y1 >= y0)
+  if (y0 > y1) {
+    swap(y0, y1); swap(x0, x1);
+  }
+  if (y1 > y2) {
+    swap(y2, y1); swap(x2, x1);
+  }
+  if (y0 > y1) {
+    swap(y0, y1); swap(x0, x1);
+  }
+
+  if(y0 == y2) { // Handle awkward all-on-same-line case as its own thing
+    a = b = x0;
+    if(x1 < a)      a = x1;
+    else if(x1 > b) b = x1;
+    if(x2 < a)      a = x2;
+    else if(x2 > b) b = x2;
+    _drawFastHLine(a, y0, b-a+1, color);
+    return;
+  }
+
+  int16_t
+    dx01 = x1 - x0,
+    dy01 = y1 - y0,
+    dx02 = x2 - x0,
+    dy02 = y2 - y0,
+    dx12 = x2 - x1,
+    dy12 = y2 - y1;
+  int32_t
+    sa   = 0,
+    sb   = 0;
+
+  // For upper part of triangle, find scanline crossings for segments
+  // 0-1 and 0-2.  If y1=y2 (flat-bottomed triangle), the scanline y1
+  // is included here (and second loop will be skipped, avoiding a /0
+  // error there), otherwise scanline y1 is skipped here and handled
+  // in the second loop...which also avoids a /0 error here if y0=y1
+  // (flat-topped triangle).
+  if(y1 == y2) last = y1;   // Include y1 scanline
+  else         last = y1-1; // Skip it
+
+  for(y=y0; y<=last; y++) {
+    a   = x0 + sa / dy01;
+    b   = x0 + sb / dy02;
+    sa += dx01;
+    sb += dx02;
+    /* longhand:
+    a = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
+    b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
+    */
+    if(a > b) swap(a,b);
+    _drawFastHLine(a, y, b-a+1, color);
+  }
+
+  // For lower part of triangle, find scanline crossings for segments
+  // 0-2 and 1-2.  This loop is skipped if y1=y2.
+  sa = dx12 * (y - y1);
+  sb = dx02 * (y - y0);
+  for(; y<=y2; y++) {
+    a   = x1 + sa / dy12;
+    b   = x0 + sb / dy02;
+    sa += dx12;
+    sb += dx02;
+    /* longhand:
+    a = x1 + (x2 - x1) * (y - y1) / (y2 - y1);
+    b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
+    */
+    if(a > b) swap(a,b);
+    _drawFastHLine(a, y, b-a+1, color);
+  }
+}
+
+//================================================================================================================
+void TFT_fillTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color)
+{
+	_fillTriangle(
+			x0 + dispWin.x1, y0 + dispWin.y1,
+			x1 + dispWin.x1, y1 + dispWin.y1,
+			x2 + dispWin.x1, y2 + dispWin.y1,
+			color);
+}
+
+//====================================================================
+void TFT_drawCircle(int16_t x, int16_t y, int radius, color_t color) {
+	x += dispWin.x1;
+	y += dispWin.y1;
+	int f = 1 - radius;
+	int ddF_x = 1;
+	int ddF_y = -2 * radius;
+	int x1 = 0;
+	int y1 = radius;
+
+	disp_select();
+	_drawPixel(x, y + radius, color, 0);
+	_drawPixel(x, y - radius, color, 0);
+	_drawPixel(x + radius, y, color, 0);
+	_drawPixel(x - radius, y, color, 0);
+	while(x1 < y1) {
+		if (f >= 0) {
+			y1--;
+			ddF_y += 2;
+			f += ddF_y;
+		}
+		x1++;
+		ddF_x += 2;
+		f += ddF_x;
+		_drawPixel(x + x1, y + y1, color, 0);
+		_drawPixel(x - x1, y + y1, color, 0);
+		_drawPixel(x + x1, y - y1, color, 0);
+		_drawPixel(x - x1, y - y1, color, 0);
+		_drawPixel(x + y1, y + x1, color, 0);
+		_drawPixel(x - y1, y + x1, color, 0);
+		_drawPixel(x + y1, y - x1, color, 0);
+		_drawPixel(x - y1, y - x1, color, 0);
+	}
+  disp_deselect();
+}
+
+//====================================================================
+void TFT_fillCircle(int16_t x, int16_t y, int radius, color_t color) {
+	x += dispWin.x1;
+	y += dispWin.y1;
+
+	_drawFastVLine(x, y-radius, 2*radius+1, color);
+	fillCircleHelper(x, y, radius, 3, 0, color);
+}
+
+//----------------------------------------------------------------------------------------------------------------
+static void _draw_ellipse_section(uint16_t x, uint16_t y, uint16_t x0, uint16_t y0, color_t color, uint8_t option)
+{
+	disp_select();
+    // upper right
+    if ( option & TFT_ELLIPSE_UPPER_RIGHT ) _drawPixel(x0 + x, y0 - y, color, 0);
+    // upper left
+    if ( option & TFT_ELLIPSE_UPPER_LEFT ) _drawPixel(x0 - x, y0 - y, color, 0);
+    // lower right
+    if ( option & TFT_ELLIPSE_LOWER_RIGHT ) _drawPixel(x0 + x, y0 + y, color, 0);
+    // lower left
+    if ( option & TFT_ELLIPSE_LOWER_LEFT ) _drawPixel(x0 - x, y0 + y, color, 0);
+	disp_deselect();
+}
+
+//=====================================================================================================
+void TFT_drawEllipse(uint16_t x0, uint16_t y0, uint16_t rx, uint16_t ry, color_t color, uint8_t option)
+{
+	x0 += dispWin.x1;
+	y0 += dispWin.y1;
+
+	uint16_t x, y;
+	int32_t xchg, ychg;
+	int32_t err;
+	int32_t rxrx2;
+	int32_t ryry2;
+	int32_t stopx, stopy;
+
+	rxrx2 = rx;
+	rxrx2 *= rx;
+	rxrx2 *= 2;
+
+	ryry2 = ry;
+	ryry2 *= ry;
+	ryry2 *= 2;
+
+	x = rx;
+	y = 0;
+
+	xchg = 1;
+	xchg -= rx;
+	xchg -= rx;
+	xchg *= ry;
+	xchg *= ry;
+
+	ychg = rx;
+	ychg *= rx;
+
+	err = 0;
+
+	stopx = ryry2;
+	stopx *= rx;
+	stopy = 0;
+
+	while( stopx >= stopy ) {
+		_draw_ellipse_section(x, y, x0, y0, color, option);
+		y++;
+		stopy += rxrx2;
+		err += ychg;
+		ychg += rxrx2;
+		if ( 2*err+xchg > 0 ) {
+			x--;
+			stopx -= ryry2;
+			err += xchg;
+			xchg += ryry2;
+		}
+	}
+
+	x = 0;
+	y = ry;
+
+	xchg = ry;
+	xchg *= ry;
+
+	ychg = 1;
+	ychg -= ry;
+	ychg -= ry;
+	ychg *= rx;
+	ychg *= rx;
+
+	err = 0;
+
+	stopx = 0;
+
+	stopy = rxrx2;
+	stopy *= ry;
+
+	while( stopx <= stopy ) {
+		_draw_ellipse_section(x, y, x0, y0, color, option);
+		x++;
+		stopx += ryry2;
+		err += xchg;
+		xchg += ryry2;
+		if ( 2*err+ychg > 0 ) {
+			y--;
+			stopy -= rxrx2;
+			err += ychg;
+			ychg += rxrx2;
+		}
+	}
+}
+
+//-----------------------------------------------------------------------------------------------------------------------
+static void _draw_filled_ellipse_section(uint16_t x, uint16_t y, uint16_t x0, uint16_t y0, color_t color, uint8_t option)
+{
+    // upper right
+    if ( option & TFT_ELLIPSE_UPPER_RIGHT ) _drawFastVLine(x0+x, y0-y, y+1, color);
+    // upper left
+    if ( option & TFT_ELLIPSE_UPPER_LEFT ) _drawFastVLine(x0-x, y0-y, y+1, color);
+    // lower right
+    if ( option & TFT_ELLIPSE_LOWER_RIGHT ) _drawFastVLine(x0+x, y0, y+1, color);
+    // lower left
+    if ( option & TFT_ELLIPSE_LOWER_LEFT ) _drawFastVLine(x0-x, y0, y+1, color);
+}
+
+//=====================================================================================================
+void TFT_fillEllipse(uint16_t x0, uint16_t y0, uint16_t rx, uint16_t ry, color_t color, uint8_t option)
+{
+	x0 += dispWin.x1;
+	y0 += dispWin.y1;
+
+	uint16_t x, y;
+	int32_t xchg, ychg;
+	int32_t err;
+	int32_t rxrx2;
+	int32_t ryry2;
+	int32_t stopx, stopy;
+
+	rxrx2 = rx;
+	rxrx2 *= rx;
+	rxrx2 *= 2;
+
+	ryry2 = ry;
+	ryry2 *= ry;
+	ryry2 *= 2;
+
+	x = rx;
+	y = 0;
+
+	xchg = 1;
+	xchg -= rx;
+	xchg -= rx;
+	xchg *= ry;
+	xchg *= ry;
+
+	ychg = rx;
+	ychg *= rx;
+
+	err = 0;
+
+	stopx = ryry2;
+	stopx *= rx;
+	stopy = 0;
+
+	while( stopx >= stopy ) {
+		_draw_filled_ellipse_section(x, y, x0, y0, color, option);
+		y++;
+		stopy += rxrx2;
+		err += ychg;
+		ychg += rxrx2;
+		if ( 2*err+xchg > 0 ) {
+			x--;
+			stopx -= ryry2;
+			err += xchg;
+			xchg += ryry2;
+		}
+	}
+
+	x = 0;
+	y = ry;
+
+	xchg = ry;
+	xchg *= ry;
+
+	ychg = 1;
+	ychg -= ry;
+	ychg -= ry;
+	ychg *= rx;
+	ychg *= rx;
+
+	err = 0;
+
+	stopx = 0;
+
+	stopy = rxrx2;
+	stopy *= ry;
+
+	while( stopx <= stopy ) {
+		_draw_filled_ellipse_section(x, y, x0, y0, color, option);
+		x++;
+		stopx += ryry2;
+		err += xchg;
+		xchg += ryry2;
+		if ( 2*err+ychg > 0 ) {
+			y--;
+			stopy -= rxrx2;
+			err += ychg;
+			ychg += rxrx2;
+		}
+	}
+}
+
+
+// ==== ARC DRAWING ===================================================================
+
+//---------------------------------------------------------------------------------------------------------------------------------
+static void _fillArcOffsetted(uint16_t cx, uint16_t cy, uint16_t radius, uint16_t thickness, float start, float end, color_t color)
+{
+	//float sslope = (float)cos_lookup(start) / (float)sin_lookup(start);
+	//float eslope = (float)cos_lookup(end) / (float)sin_lookup(end);
+	float sslope = (cos(start/_arcAngleMax * 2 * PI) * _arcAngleMax) / (sin(start/_arcAngleMax * 2 * PI) * _arcAngleMax) ;
+	float eslope = (cos(end/_arcAngleMax * 2 * PI) * _arcAngleMax) / (sin(end/_arcAngleMax * 2 * PI) * _arcAngleMax);
+
+	if (end == 360) eslope = -1000000;
+
+	int ir2 = (radius - thickness) * (radius - thickness);
+	int or2 = radius * radius;
+
+	disp_select();
+	for (int x = -radius; x <= radius; x++) {
+		for (int y = -radius; y <= radius; y++) {
+			int x2 = x * x;
+			int y2 = y * y;
+
+			if (
+				(x2 + y2 < or2 && x2 + y2 >= ir2) &&
+				(
+				(y > 0 && start < 180 && x <= y * sslope) ||
+				(y < 0 && start > 180 && x >= y * sslope) ||
+				(y < 0 && start <= 180) ||
+				(y == 0 && start <= 180 && x < 0) ||
+				(y == 0 && start == 0 && x > 0)
+				) &&
+				(
+				(y > 0 && end < 180 && x >= y * eslope) ||
+				(y < 0 && end > 180 && x <= y * eslope) ||
+				(y > 0 && end >= 180) ||
+				(y == 0 && end >= 180 && x < 0) ||
+				(y == 0 && start == 0 && x > 0)
+				)
+				)
+				_drawPixel(cx+x, cy+y, color, 0);
+		}
+	}
+	disp_deselect();
+}
+
+
+//===========================================================================================================================
+void TFT_drawArc(uint16_t cx, uint16_t cy, uint16_t r, uint16_t th, float start, float end, color_t color, color_t fillcolor)
+{
+	cx += dispWin.x1;
+	cy += dispWin.y1;
+
+	if (th < 1) th = 1;
+	if (th > r) th = r;
+
+	int f = TFT_compare_colors(fillcolor, color);
+
+	float astart = fmodf(start, _arcAngleMax);
+	float aend = fmodf(end, _arcAngleMax);
+
+	astart += _angleOffset;
+	aend += _angleOffset;
+
+	if (astart < 0) astart += (float)360;
+	if (aend < 0) aend += (float)360;
+
+	if (aend == 0) aend = (float)360;
+
+	if (astart > aend) {
+		_fillArcOffsetted(cx, cy, r, th, astart, _arcAngleMax, fillcolor);
+		_fillArcOffsetted(cx, cy, r, th, 0, aend, fillcolor);
+		if (f) {
+			_fillArcOffsetted(cx, cy, r, 1, astart, _arcAngleMax, color);
+			_fillArcOffsetted(cx, cy, r, 1, 0, aend, color);
+			_fillArcOffsetted(cx, cy, r-th, 1, astart, _arcAngleMax, color);
+			_fillArcOffsetted(cx, cy, r-th, 1, 0, aend, color);
+		}
+	}
+	else {
+		_fillArcOffsetted(cx, cy, r, th, astart, aend, fillcolor);
+		if (f) {
+			_fillArcOffsetted(cx, cy, r, 1, astart, aend, color);
+			_fillArcOffsetted(cx, cy, r-th, 1, astart, aend, color);
+		}
+	}
+	if (f) {
+		_drawLine(cx + (r-th) * cos(astart * DEG_TO_RAD), cy + (r-th) * sin(astart * DEG_TO_RAD),
+			cx + (r-1) * cos(astart * DEG_TO_RAD), cy + (r-1) * sin(astart * DEG_TO_RAD), color);
+		_drawLine(cx + (r-th) * cos(aend * DEG_TO_RAD), cy + (r-th) * sin(aend * DEG_TO_RAD),
+			cx + (r-1) * cos(aend * DEG_TO_RAD), cy + (r-1) * sin(aend * DEG_TO_RAD), color);
+	}
+}
+
+//=============================================================================================================
+void TFT_drawPolygon(int cx, int cy, int sides, int diameter, color_t color, color_t fill, int rot, uint8_t th)
+{
+	cx += dispWin.x1;
+	cy += dispWin.y1;
+
+	int deg = rot - _angleOffset;
+	int f = TFT_compare_colors(fill, color);
+
+	if (sides < MIN_POLIGON_SIDES) sides = MIN_POLIGON_SIDES;	// This ensures the minimum side number
+	if (sides > MAX_POLIGON_SIDES) sides = MAX_POLIGON_SIDES;	// This ensures the maximum side number
+
+	int Xpoints[sides], Ypoints[sides];							// Set the arrays based on the number of sides entered
+	int rads = 360 / sides;										// This equally spaces the points.
+
+	for (int idx = 0; idx < sides; idx++) {
+		Xpoints[idx] = cx + sin((float)(idx*rads + deg) * deg_to_rad) * diameter;
+		Ypoints[idx] = cy + cos((float)(idx*rads + deg) * deg_to_rad) * diameter;
+	}
+
+	// Draw the polygon on the screen.
+	if (f) {
+		for(int idx = 0; idx < sides; idx++) {
+			if((idx+1) < sides) _fillTriangle(cx,cy,Xpoints[idx],Ypoints[idx],Xpoints[idx+1],Ypoints[idx+1], fill);
+			else _fillTriangle(cx,cy,Xpoints[idx],Ypoints[idx],Xpoints[0],Ypoints[0], fill);
+		}
+	}
+
+	if (th) {
+		for (int n=0; n<th; n++) {
+			if (n > 0) {
+				for (int idx = 0; idx < sides; idx++) {
+					Xpoints[idx] = cx + sin((float)(idx*rads + deg) * deg_to_rad) * (diameter-n);
+					Ypoints[idx] = cy + cos((float)(idx*rads + deg) * deg_to_rad) * (diameter-n);
+				}
+			}
+			for(int idx = 0; idx < sides; idx++) {
+				if( (idx+1) < sides)
+					_drawLine(Xpoints[idx],Ypoints[idx],Xpoints[idx+1],Ypoints[idx+1], color); // draw the lines
+				else
+					_drawLine(Xpoints[idx],Ypoints[idx],Xpoints[0],Ypoints[0], color); // finishes the last line to close up the polygon.
+			}
+		}
+	}
+}
+
+/*
+// Similar to the Polygon function.
+//=====================================================================================
+void TFT_drawStar(int cx, int cy, int diameter, color_t color, bool fill, float factor)
+{
+	cx += dispWin.x1;
+	cy += dispWin.y1;
+
+	factor = constrain(factor, 1.0, 4.0);
+	uint8_t sides = 5;
+	uint8_t rads = 360 / sides;
+
+	int Xpoints_O[sides], Ypoints_O[sides], Xpoints_I[sides], Ypoints_I[sides];//Xpoints_T[5], Ypoints_T[5];
+
+	for(int idx = 0; idx < sides; idx++) {
+		// makes the outer points
+		Xpoints_O[idx] = cx + sin((float)(idx*rads + 72) * deg_to_rad) * diameter;
+		Ypoints_O[idx] = cy + cos((float)(idx*rads + 72) * deg_to_rad) * diameter;
+		// makes the inner points
+		Xpoints_I[idx] = cx + sin((float)(idx*rads + 36) * deg_to_rad) * ((float)(diameter)/factor);
+		// 36 is half of 72, and this will allow the inner and outer points to line up like a triangle.
+		Ypoints_I[idx] = cy + cos((float)(idx*rads + 36) * deg_to_rad) * ((float)(diameter)/factor);
+	}
+
+	for(int idx = 0; idx < sides; idx++) {
+		if((idx+1) < sides) {
+			if(fill) {// this part below should be self explanatory. It fills in the star.
+				_fillTriangle(cx,cy,Xpoints_I[idx],Ypoints_I[idx],Xpoints_O[idx],Ypoints_O[idx], color);
+				_fillTriangle(cx,cy,Xpoints_O[idx],Ypoints_O[idx],Xpoints_I[idx+1],Ypoints_I[idx+1], color);
+			}
+			else {
+				_drawLine(Xpoints_O[idx],Ypoints_O[idx],Xpoints_I[idx+1],Ypoints_I[idx+1], color);
+				_drawLine(Xpoints_I[idx],Ypoints_I[idx],Xpoints_O[idx],Ypoints_O[idx], color);
+			}
+		}
+		else {
+			if(fill) {
+				_fillTriangle(cx,cy,Xpoints_I[0],Ypoints_I[0],Xpoints_O[idx],Ypoints_O[idx], color);
+				_fillTriangle(cx,cy,Xpoints_O[idx],Ypoints_O[idx],Xpoints_I[idx],Ypoints_I[idx], color);
+			}
+			else {
+				_drawLine(Xpoints_O[idx],Ypoints_O[idx],Xpoints_I[idx],Ypoints_I[idx], color);
+				_drawLine(Xpoints_I[0],Ypoints_I[0],Xpoints_O[idx],Ypoints_O[idx], color);
+			}
+		}
+	}
+}
+*/
+
+// ================ Font and string functions ==================================
+
+//--------------------------------------------------------
+static int load_file_font(const char * fontfile, int info)
+{
+	int err = 0;
+	char err_msg[256] = {'\0'};
+
+	if (userfont != NULL) {
+		free(userfont);
+		userfont = NULL;
+	}
+
+    struct stat sb;
+
+    // Open the file
+    FILE *fhndl = fopen(fontfile, "r");
+    if (!fhndl) {
+    	sprintf(err_msg, "Error opening font file '%s'", fontfile);
+		err = 1;
+		goto exit;
+    }
+
+	// Get file size
+    if (stat(fontfile, &sb) != 0) {
+    	sprintf(err_msg, "Error getting font file size");
+		err = 2;
+		goto exit;
+    }
+	int fsize = sb.st_size;
+	if (fsize < 30) {
+		sprintf(err_msg, "Error getting font file size");
+		err = 3;
+		goto exit;
+	}
+
+	userfont = malloc(fsize+4);
+	if (userfont == NULL) {
+		sprintf(err_msg, "Font memory allocation error");
+		fclose(fhndl);
+		err = 4;
+		goto exit;
+	}
+
+	int read = fread(userfont, 1, fsize, fhndl);
+
+	fclose(fhndl);
+
+	if (read != fsize) {
+		sprintf(err_msg, "Font read error");
+		err = 5;
+		goto exit;
+	}
+
+	userfont[read] = 0;
+	if (strstr((char *)(userfont+read-8), "RPH_font") == NULL) {
+		sprintf(err_msg, "Font ID not found");
+		err = 6;
+		goto exit;
+	}
+
+	// Check size
+	int size = 0;
+	int numchar = 0;
+	int width = userfont[0];
+	int height = userfont[1];
+	uint8_t first = 255;
+	uint8_t last = 0;
+	//int offst = 0;
+	int pminwidth = 255;
+	int pmaxwidth = 0;
+
+	if (width != 0) {
+		// Fixed font
+		numchar = userfont[3];
+		first = userfont[2];
+		last = first + numchar - 1;
+		size = ((width * height * numchar) / 8) + 4;
+	}
+	else {
+		// Proportional font
+		size = 4; // point at first char data
+		uint8_t charCode;
+		int charwidth;
+
+		do {
+		    charCode = userfont[size];
+		    charwidth = userfont[size+2];
+
+		    if (charCode != 0xFF) {
+		    	numchar++;
+		    	if (charwidth != 0) size += ((((charwidth * userfont[size+3])-1) / 8) + 7);
+		    	else size += 6;
+
+		    	if (info) {
+	    			if (charwidth > pmaxwidth) pmaxwidth = charwidth;
+	    			if (charwidth < pminwidth) pminwidth = charwidth;
+	    			if (charCode < first) first = charCode;
+	    			if (charCode > last) last = charCode;
+	    		}
+		    }
+		    else size++;
+		  } while ((size < (read-8)) && (charCode != 0xFF));
+	}
+
+	if (size != (read-8)) {
+		sprintf(err_msg, "Font size error: found %d expected %d)", size, (read-8));
+		err = 7;
+		goto exit;
+	}
+
+	if (info) {
+		if (width != 0) {
+			printf("Fixed width font:\r\n  size: %d  width: %d  height: %d  characters: %d (%d~%d)\n",
+					size, width, height, numchar, first, last);
+		}
+		else {
+			printf("Proportional font:\r\n  size: %d  width: %d~%d  height: %d  characters: %d (%d~%d)\n",
+					size, pminwidth, pmaxwidth, height, numchar, first, last);
+		}
+	}
+
+exit:
+	if (err) {
+		if (userfont) {
+			free(userfont);
+			userfont = NULL;
+		}
+		if (info) printf("Error: %d [%s]\r\n", err, err_msg);
+	}
+	return err;
+}
+
+//------------------------------------------------
+int compile_font_file(char *fontfile, uint8_t dbg)
+{
+	int err = 0;
+	char err_msg[128] = {'\0'};
+	char outfile[128] = {'\0'};
+	size_t len;
+    struct stat sb;
+    FILE *ffd = NULL;
+    FILE *ffd_out = NULL;
+    char *sourcebuf = NULL;
+
+    len = strlen(fontfile);
+
+	// check here that filename end with ".c".
+	if ((len < 3) || (len > 125) || (strcmp(fontfile + len - 2, ".c") != 0)) {
+		sprintf(err_msg, "not a .c file");
+		err = 1;
+		goto exit;
+	}
+
+	sprintf(outfile, "%s", fontfile);
+	sprintf(outfile+strlen(outfile)-1, "fon");
+
+	// Open the source file
+    if (stat(fontfile, &sb) != 0) {
+    	sprintf(err_msg, "Error opening source file '%s'", fontfile);
+    	err = 2;
+		goto exit;
+    }
+    // Open the file
+    ffd = fopen(fontfile, "rb");
+    if (!ffd) {
+    	sprintf(err_msg, "Error opening source file '%s'", fontfile);
+    	err = 3;
+		goto exit;
+    }
+
+	// Open the font file
+    ffd_out= fopen(outfile, "wb");
+	if (!ffd_out) {
+		sprintf(err_msg, "error opening destination file");
+		err = 4;
+		goto exit;
+	}
+
+	// Get file size
+	int fsize = sb.st_size;
+	if (fsize <= 0) {
+		sprintf(err_msg, "source file size error");
+		err = 5;
+		goto exit;
+	}
+
+	sourcebuf = malloc(fsize+4);
+	if (sourcebuf == NULL) {
+		sprintf(err_msg, "memory allocation error");
+		err = 6;
+		goto exit;
+	}
+	char *fbuf = sourcebuf;
+
+	int rdsize = fread(fbuf, 1, fsize, ffd);
+	fclose(ffd);
+	ffd = NULL;
+
+	if (rdsize != fsize) {
+		sprintf(err_msg, "error reading from source file");
+		err = 7;
+		goto exit;
+	}
+
+	*(fbuf+rdsize) = '\0';
+
+	fbuf = strchr(fbuf, '{');			// beginning of font data
+	char *fend = strstr(fbuf, "};");	// end of font data
+
+	if ((fbuf == NULL) || (fend == NULL) || ((fend-fbuf) < 22)) {
+		sprintf(err_msg, "wrong source file format");
+		err = 8;
+		goto exit;
+	}
+
+	fbuf++;
+	*fend = '\0';
+	char hexstr[5] = {'\0'};
+	int lastline = 0;
+
+	fbuf = strstr(fbuf, "0x");
+	int size = 0;
+	char *nextline;
+	char *numptr;
+
+	int bptr = 0;
+
+	while ((fbuf != NULL) && (fbuf < fend) && (lastline == 0)) {
+		nextline = strchr(fbuf, '\n'); // beginning of the next line
+		if (nextline == NULL) {
+			nextline = fend-1;
+			lastline++;
+		}
+		else nextline++;
+
+		while (fbuf < nextline) {
+			numptr = strstr(fbuf, "0x");
+			if ((numptr == NULL) || ((fbuf+4) > nextline)) numptr = strstr(fbuf, "0X");
+			if ((numptr != NULL) && ((numptr+4) <= nextline)) {
+				fbuf = numptr;
+				if (bptr >= 128) {
+					// buffer full, write to file
+                    if (fwrite(outfile, 1, 128, ffd_out) != 128) goto error;
+					bptr = 0;
+					size += 128;
+				}
+				memcpy(hexstr, fbuf, 4);
+				hexstr[4] = 0;
+				outfile[bptr++] = (uint8_t)strtol(hexstr, NULL, 0);
+				fbuf += 4;
+			}
+			else fbuf = nextline;
+		}
+		fbuf = nextline;
+	}
+
+	if (bptr > 0) {
+		size += bptr;
+        if (fwrite(outfile, 1, bptr, ffd_out) != bptr) goto error;
+	}
+
+	// write font ID
+	sprintf(outfile, "RPH_font");
+    if (fwrite(outfile, 1, 8, ffd_out) != 8) goto error;
+
+    fclose(ffd_out);
+    ffd_out = NULL;
+
+	// === Test compiled font ===
+	sprintf(outfile, "%s", fontfile);
+	sprintf(outfile+strlen(outfile)-1, "fon");
+
+	uint8_t *uf = userfont; // save userfont pointer
+	userfont = NULL;
+	if (load_file_font(outfile, 1) != 0) {
+		sprintf(err_msg, "Error compiling file!");
+		err = 10;
+	}
+	else {
+		free(userfont);
+		sprintf(err_msg, "File compiled successfully.");
+	}
+	userfont = uf; // restore userfont
+
+	goto exit;
+
+error:
+	sprintf(err_msg, "error writing to destination file");
+	err = 9;
+
+exit:
+	if (sourcebuf) free(sourcebuf);
+	if (ffd) fclose(ffd);
+	if (ffd_out) fclose(ffd_out);
+
+	if (dbg) printf("%s\r\n", err_msg);
+
+	return err;
+}
+
+
+// -----------------------------------------------------------------------------------------
+// Individual Proportional Font Character Format:
+// -----------------------------------------------------------------------------------------
+// Character Code
+// yOffset				(start Y of visible pixels)
+// Width				(width of the visible pixels)
+// Height				(height of the visible pixels)
+// xOffset				(start X of visible pixels)
+// xDelta				(the distance to move the cursor. Effective width of the character.)
+// Data[n]
+// -----------------------------------------------------------------------------------------
+
+//---------------------------------------------------------------------------------------------
+// Character drawing rectangle is (0, 0) (xDelta-1, cfont.y_size-1)
+// Character visible pixels rectangle is (xOffset, yOffset) (xOffset+Width-1, yOffset+Height-1)
+//---------------------------------------------------------------------------------------------
+
+//----------------------------------
+void getFontCharacters(uint8_t *buf)
+{
+    if (cfont.bitmap == 2) {
+    	//For 7 segment font only characters 0,1,2,3,4,5,6,7,8,9, . , - , : , / are available.
+		for (uint8_t n=0; n < 11; n++) {
+			buf[n] = n + 0x30;
+		}
+		buf[11] = '.';
+		buf[12] = '-';
+		buf[13] = '/';
+		buf[14] = '\0';
+    	return;
+    }
+
+    if (cfont.x_size > 0) {
+		for (uint8_t n=0; n < cfont.numchars; n++) {
+			buf[n] = cfont.offset + n;
+		}
+		buf[cfont.numchars] = '\0';
+		return;
+	}
+
+	uint16_t tempPtr = 4; // point at first char data
+	uint8_t cc, cw, ch, n;
+
+	n = 0;
+    cc = cfont.font[tempPtr++];
+    while (cc != 0xFF)  {
+    	cfont.numchars++;
+        tempPtr++;
+        cw = cfont.font[tempPtr++];
+        ch = cfont.font[tempPtr++];
+        tempPtr++;
+        tempPtr++;
+		if (cw != 0) {
+			// packed bits
+			tempPtr += (((cw * ch)-1) / 8) + 1;
+		}
+		buf[n++] = cc;
+	    cc = cfont.font[tempPtr++];
+	}
+	buf[n] = '\0';
+}
+
+// Set max width & height of the proportional font
+//-----------------------------
+static void getMaxWidthHeight()
+{
+	uint16_t tempPtr = 4; // point at first char data
+	uint8_t cc, cw, ch, cd, cy;
+
+	cfont.numchars = 0;
+	cfont.max_x_size = 0;
+
+    cc = cfont.font[tempPtr++];
+    while (cc != 0xFF)  {
+    	cfont.numchars++;
+        cy = cfont.font[tempPtr++];
+        cw = cfont.font[tempPtr++];
+        ch = cfont.font[tempPtr++];
+        tempPtr++;
+        cd = cfont.font[tempPtr++];
+        cy += ch;
+		if (cw > cfont.max_x_size) cfont.max_x_size = cw;
+		if (cd > cfont.max_x_size) cfont.max_x_size = cd;
+		if (ch > cfont.y_size) cfont.y_size = ch;
+		if (cy > cfont.y_size) cfont.y_size = cy;
+		if (cw != 0) {
+			// packed bits
+			tempPtr += (((cw * ch)-1) / 8) + 1;
+		}
+	    cc = cfont.font[tempPtr++];
+	}
+    cfont.size = tempPtr;
+}
+
+// Return the Glyph data for an individual character in the proportional font
+//------------------------------------
+static uint8_t getCharPtr(uint8_t c) {
+  uint16_t tempPtr = 4; // point at first char data
+
+  do {
+	fontChar.charCode = cfont.font[tempPtr++];
+    if (fontChar.charCode == 0xFF) return 0;
+
+    fontChar.adjYOffset = cfont.font[tempPtr++];
+    fontChar.width = cfont.font[tempPtr++];
+    fontChar.height = cfont.font[tempPtr++];
+    fontChar.xOffset = cfont.font[tempPtr++];
+    fontChar.xOffset = fontChar.xOffset < 0x80 ? fontChar.xOffset : -(0xFF - fontChar.xOffset);
+    fontChar.xDelta = cfont.font[tempPtr++];
+
+    if (c != fontChar.charCode && fontChar.charCode != 0xFF) {
+      if (fontChar.width != 0) {
+        // packed bits
+        tempPtr += (((fontChar.width * fontChar.height)-1) / 8) + 1;
+      }
+    }
+  } while ((c != fontChar.charCode) && (fontChar.charCode != 0xFF));
+
+  fontChar.dataPtr = tempPtr;
+  if (c == fontChar.charCode) {
+    if (font_forceFixed > 0) {
+      // fix width & offset for forced fixed width
+      fontChar.xDelta = cfont.max_x_size;
+      fontChar.xOffset = (fontChar.xDelta - fontChar.width) / 2;
+    }
+  }
+  else return 0;
+
+  return 1;
+}
+
+/*
+//-----------------------
+static void _testFont() {
+  if (cfont.x_size) {
+	  printf("FONT TEST: fixed font\r\n");
+	  return;
+  }
+  uint16_t tempPtr = 4; // point at first char data
+  uint8_t c = 0x20;
+  for (c=0x20; c <0xFF; c++) {
+	fontChar.charCode = cfont.font[tempPtr++];
+    if (fontChar.charCode == 0xFF) break;
+    if (fontChar.charCode != c) {
+    	printf("FONT TEST: last sequential char: %d, expected %d\r\n", fontChar.charCode, c);
+    	break;
+    }
+    c = fontChar.charCode;
+    fontChar.adjYOffset = cfont.font[tempPtr++];
+    fontChar.width = cfont.font[tempPtr++];
+    fontChar.height = cfont.font[tempPtr++];
+    fontChar.xOffset = cfont.font[tempPtr++];
+    fontChar.xOffset = fontChar.xOffset < 0x80 ? fontChar.xOffset : -(0xFF - fontChar.xOffset);
+    fontChar.xDelta = cfont.font[tempPtr++];
+
+    if (fontChar.charCode != 0xFF) {
+      if (fontChar.width != 0) {
+        // packed bits
+        tempPtr += (((fontChar.width * fontChar.height)-1) / 8) + 1;
+      }
+    }
+  }
+  printf("FONT TEST: W=%d  H=%d last char: %d [%c]; length: %d\r\n", cfont.max_x_size, cfont.y_size, c, c, tempPtr);
+}
+*/
+
+//===================================================
+void TFT_setFont(uint8_t font, const char *font_file)
+{
+    cfont.font = NULL;
+
+    if (font == FONT_7SEG) {
+	cfont.bitmap = 2;
+	cfont.x_size = 24;
+	cfont.y_size = 6;
+	cfont.offset = 0;
+	cfont.color  = _fg;
+    } else {
+	if (font == USER_FONT) {
+	    if (load_file_font(font_file, 0) != 0)
+		cfont.font = tft_DefaultFont;
+	    else
+		cfont.font = userfont;
+	} else if (font == DEJAVU18_FONT)
+	    cfont.font = tft_Dejavu18;
+	else if (font == DEJAVU24_FONT) 
+	    cfont.font = tft_Dejavu24;
+	else if (font == UBUNTU16_FONT) 
+	    cfont.font = tft_Ubuntu16;
+	else if (font == COMIC24_FONT)
+	    cfont.font = tft_Comic24;
+	else if (font == MINYA24_FONT)
+	    cfont.font = tft_minya24;
+	else if (font == TOONEY32_FONT)
+	    cfont.font = tft_tooney32;
+	else if (font == SMALL_FONT)
+	    cfont.font = tft_SmallFont;
+	else if (font == DEF_SMALL_FONT)
+	    cfont.font = tft_def_small;
+	else
+	    cfont.font = tft_DefaultFont;
+
+	cfont.bitmap = 1;
+	cfont.x_size = cfont.font[0];
+	cfont.y_size = cfont.font[1];
+	if (cfont.x_size > 0) {
+	    cfont.offset = cfont.font[2];
+	    cfont.numchars = cfont.font[3];
+	    cfont.size = cfont.x_size * cfont.y_size * cfont.numchars;
+	} else {
+	    cfont.offset = 4;
+	    getMaxWidthHeight();
+	}
+	//_testFont();
+    }
+}
+
+
+
+// -----------------------------------------------------------------------------------------
+// Individual Proportional Font Character Format:
+// -----------------------------------------------------------------------------------------
+// Character Code
+// yOffset				(start Y of visible pixels)
+// Width				(width of the visible pixels)
+// Height				(height of the visible pixels)
+// xOffset				(start X of visible pixels)
+// xDelta				(the distance to move the cursor. Effective width of the character.)
+// Data[n]
+// -----------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+// Character drawing rectangle is (0, 0) (xDelta-1, cfont.y_size-1)
+// Character visible pixels rectangle is (xOffset, yOffset) (xOffset+Width-1, yOffset+Height-1)
+//---------------------------------------------------------------------------------------------
+
+// print non-rotated proportional character
+// character is already in fontChar
+//----------------------------------------------
+static int printProportionalChar(int x, int y) {
+    uint8_t	ch = 0;
+    int		i, j, char_width;
+
+    char_width = ((fontChar.width > fontChar.xDelta) ? fontChar.width : fontChar.xDelta);
+
+    if ((font_buffered_char) && (!font_transparent)) {
+	int len, bufPos;
+
+	// === buffer Glyph data for faster sending ===
+	len = char_width * cfont.y_size;
+	color_t *color_line = heap_caps_malloc(len*3, MALLOC_CAP_DMA);
+	if (color_line) {
+	    // fill with background color
+	    for (int n = 0; n < len; n++) {
+		color_line[n] = _bg;
+	    }
+	    // set character pixels to foreground color
+	    uint8_t mask = 0x80;
+	    for (j = 0; j < fontChar.height; j++) {
+		for (i = 0; i < fontChar.width; i++) {
+		    if (((i + (j*fontChar.width)) % 8) == 0) {
+			mask = 0x80;
+			ch = cfont.font[fontChar.dataPtr++];
+		    }
+		    if ((ch & mask) != 0) {
+			// visible pixel
+			bufPos = ((j + fontChar.adjYOffset) * char_width) + (fontChar.xOffset + i);  // bufY + bufX
+			color_line[bufPos] = _fg;
+			VncDrawPixel(x + (fontChar.xOffset + i), y + (j + fontChar.adjYOffset), VNC_RGB2COL(_fg.r, _fg.g, _fg.b));
+		    } else {
+			VncDrawPixel(x + (fontChar.xOffset + i), y + (j + fontChar.adjYOffset), VNC_RGB2COL(_bg.r, _bg.g, _bg.b));
+		    }
+		    mask >>= 1;
+		}
+	    }
+	    // send to display in one transaction
+	    disp_select();
+	    send_data(x, y, x+char_width-1, y+cfont.y_size-1, len, color_line);
+	    disp_deselect();
+	    free(color_line);
+
+	    return char_width;
+	}
+    }
+
+    int cx, cy;
+
+    if (!font_transparent)
+	_fillRect(x, y, char_width+1, cfont.y_size, _bg);
+
+    // draw Glyph
+    uint8_t mask = 0x80;
+    disp_select();
+    for (j=0; j < fontChar.height; j++) {
+		for (i=0; i < fontChar.width; i++) {
+			if (((i + (j*fontChar.width)) % 8) == 0) {
+				mask = 0x80;
+				ch = cfont.font[fontChar.dataPtr++];
+			}
+
+			if ((ch & mask) !=0) {
+				cx = (uint16_t)(x+fontChar.xOffset+i);
+				cy = (uint16_t)(y+j+fontChar.adjYOffset);
+				_drawPixel(cx, cy, _fg, 0);
+			}
+			mask >>= 1;
+		}
+    }
+    disp_deselect();
+
+    return char_width;
+}
+
+
+
+// non-rotated fixed width character
+//----------------------------------------------
+static void printChar(uint8_t c, int x, int y) {
+    uint8_t	i, j, ch, fz, mask;
+    uint16_t	k, temp, cx, cy, len;
+
+    // fz = bytes per char row
+    fz = cfont.x_size/8;
+    if (cfont.x_size % 8)
+	fz++;
+
+    // get character position in buffer
+    temp = ((c-cfont.offset)*((fz)*cfont.y_size))+4;
+
+    if ((font_buffered_char) && (!font_transparent)) {
+	// === buffer Glyph data for faster sending ===
+	len = cfont.x_size * cfont.y_size;
+	color_t *color_line = heap_caps_malloc(len*3, MALLOC_CAP_DMA);
+	if (color_line) {
+	    // fill with background color
+	    for (int n = 0; n < len; n++) {
+		color_line[n] = _bg;
+	    }
+	    // set character pixels to foreground color
+	    for (j = 0; j < cfont.y_size; j++) {
+		for (k = 0; k < fz; k++) {
+		    ch = cfont.font[temp + k];
+		    mask = 0x80;
+		    for (i = 0; i < 8; i++) {
+			if ((ch & mask) !=0) {
+			    color_line[(j*cfont.x_size) + (i+(k*8))] = _fg;
+			    VncDrawPixel(x+i+(k*8), y+j, VNC_RGB2COL(_fg.r, _fg.g, _fg.b));
+			} else {
+			    VncDrawPixel(x+i+(k*8), y+j, VNC_RGB2COL(_bg.r, _bg.g, _bg.b));
+			}
+			mask >>= 1;
+		    }
+		}
+		temp += (fz);
+	    }
+	    // send to display in one transaction
+	    disp_select();
+	    send_data(x, y, x+cfont.x_size-1, y+cfont.y_size-1, len, color_line);
+	    disp_deselect();
+	    free(color_line);
+
+	    return;
+	}
+    }
+
+    if (!font_transparent)
+	_fillRect(x, y, cfont.x_size, cfont.y_size, _bg);
+
+    disp_select();
+    for (j = 0; j < cfont.y_size; j++) {
+	for (k = 0; k < fz; k++) {
+	    ch = cfont.font[temp + k];
+	    mask = 0x80;
+	    for (i = 0; i < 8; i++) {
+		if ((ch & mask) !=0) {
+		    cx = (uint16_t)(x+i+(k*8));
+		    cy = (uint16_t)(y+j);
+		    _drawPixel(cx, cy, _fg, 0);
+		}
+		mask >>= 1;
+	    }
+	}
+	temp += (fz);
+    }
+    disp_deselect();
+}
+
+
+
+// print rotated proportional character
+// character is already in fontChar
+//---------------------------------------------------
+static int rotatePropChar(int x, int y, int offset) {
+  uint8_t ch = 0;
+  double radian = font_rotate * DEG_TO_RAD;
+  float cos_radian = cos(radian);
+  float sin_radian = sin(radian);
+
+  uint8_t mask = 0x80;
+  disp_select();
+  for (int j=0; j < fontChar.height; j++) {
+    for (int i=0; i < fontChar.width; i++) {
+      if (((i + (j*fontChar.width)) % 8) == 0) {
+        mask = 0x80;
+        ch = cfont.font[fontChar.dataPtr++];
+      }
+
+      int newX = (int)(x + (((offset + i) * cos_radian) - ((j+fontChar.adjYOffset)*sin_radian)));
+      int newY = (int)(y + (((j+fontChar.adjYOffset) * cos_radian) + ((offset + i) * sin_radian)));
+
+      if ((ch & mask) != 0) _drawPixel(newX,newY,_fg, 0);
+      else if (!font_transparent) _drawPixel(newX,newY,_bg, 0);
+
+      mask >>= 1;
+    }
+  }
+  disp_deselect();
+
+  return fontChar.xDelta+1;
+}
+
+// rotated fixed width character
+//--------------------------------------------------------
+static void rotateChar(uint8_t c, int x, int y, int pos) {
+  uint8_t i,j,ch,fz,mask;
+  uint16_t temp;
+  int newx,newy;
+  double radian = font_rotate*0.0175;
+  float cos_radian = cos(radian);
+  float sin_radian = sin(radian);
+  int zz;
+
+  if( cfont.x_size < 8 ) fz = cfont.x_size;
+  else fz = cfont.x_size/8;
+  temp=((c-cfont.offset)*((fz)*cfont.y_size))+4;
+
+  disp_select();
+  for (j=0; j<cfont.y_size; j++) {
+    for (zz=0; zz<(fz); zz++) {
+      ch = cfont.font[temp+zz];
+      mask = 0x80;
+      for (i=0; i<8; i++) {
+        newx=(int)(x+(((i+(zz*8)+(pos*cfont.x_size))*cos_radian)-((j)*sin_radian)));
+        newy=(int)(y+(((j)*cos_radian)+((i+(zz*8)+(pos*cfont.x_size))*sin_radian)));
+
+        if ((ch & mask) != 0) _drawPixel(newx,newy,_fg, 0);
+        else if (!font_transparent) _drawPixel(newx,newy,_bg, 0);
+        mask >>= 1;
+      }
+    }
+    temp+=(fz);
+  }
+  disp_deselect();
+  // calculate x,y for the next char
+  TFT_X = (int)(x + ((pos+1) * cfont.x_size * cos_radian));
+  TFT_Y = (int)(y + ((pos+1) * cfont.x_size * sin_radian));
+}
+
+//----------------------
+static int _7seg_width()
+{
+	return (2 * (2 * cfont.y_size + 1)) + cfont.x_size;
+}
+
+//-----------------------
+static int _7seg_height()
+{
+	return (3 * (2 * cfont.y_size + 1)) + (2 * cfont.x_size);
+}
+
+// Returns the string width in pixels.
+// Useful for positions strings on the screen.
+//===============================
+int TFT_getStringWidth(char* str)
+{
+    int strWidth = 0;
+
+	if (cfont.bitmap == 2) strWidth = ((_7seg_width()+2) * strlen(str)) - 2;	// 7-segment font
+	else if (cfont.x_size != 0) strWidth = strlen(str) * cfont.x_size;			// fixed width font
+	else {
+		// calculate the width of the string of proportional characters
+		char* tempStrptr = str;
+		while (*tempStrptr != 0) {
+			if (getCharPtr(*tempStrptr++)) {
+				strWidth += (((fontChar.width > fontChar.xDelta) ? fontChar.width : fontChar.xDelta) + 1);
+			}
+		}
+		strWidth--;
+	}
+	return strWidth;
+}
+
+//===============================================
+void TFT_clearStringRect(int x, int y, char *str)
+{
+	int w = TFT_getStringWidth(str);
+	int h = TFT_getfontheight();
+	TFT_fillRect(x+dispWin.x1, y+dispWin.y1, w, h, _bg);
+}
+
+//==============================================================================
+/**
+ * bit-encoded bar position of all digits' bcd segments
+ *
+ *           6
+ * 		  +-----+
+ * 		3 |  .	| 2
+ * 		  +--5--+
+ * 		1 |  .	| 0
+ * 		  +--.--+
+ * 		     4
+ */
+static const uint16_t font_bcd[] = {
+  0x200, // 0010 0000 0000  // -
+  0x080, // 0000 1000 0000  // .
+  0x06C, // 0100 0110 1100  // /, degree
+  0x05f, // 0000 0101 1111, // 0
+  0x005, // 0000 0000 0101, // 1
+  0x076, // 0000 0111 0110, // 2
+  0x075, // 0000 0111 0101, // 3
+  0x02d, // 0000 0010 1101, // 4
+  0x079, // 0000 0111 1001, // 5
+  0x07b, // 0000 0111 1011, // 6
+  0x045, // 0000 0100 0101, // 7
+  0x07f, // 0000 0111 1111, // 8
+  0x07d, // 0000 0111 1101  // 9
+  0x900  // 1001 0000 0000  // :
+};
+
+//-----------------------------------------------------------------------------------------------
+static void barVert(int16_t x, int16_t y, int16_t w, int16_t l, color_t color, color_t outline) {
+  _fillTriangle(x+1, y+2*w, x+w, y+w+1, x+2*w-1, y+2*w, color);
+  _fillTriangle(x+1, y+2*w+l+1, x+w, y+3*w+l, x+2*w-1, y+2*w+l+1, color);
+  _fillRect(x, y+2*w+1, 2*w+1, l, color);
+  if (cfont.offset) {
+    _drawTriangle(x+1, y+2*w, x+w, y+w+1, x+2*w-1, y+2*w, outline);
+    _drawTriangle(x+1, y+2*w+l+1, x+w, y+3*w+l, x+2*w-1, y+2*w+l+1, outline);
+    _drawRect(x, y+2*w+1, 2*w+1, l, outline);
+  }
+}
+
+//----------------------------------------------------------------------------------------------
+static void barHor(int16_t x, int16_t y, int16_t w, int16_t l, color_t color, color_t outline) {
+  _fillTriangle(x+2*w, y+2*w-1, x+w+1, y+w, x+2*w, y+1, color);
+  _fillTriangle(x+2*w+l+1, y+2*w-1, x+3*w+l, y+w, x+2*w+l+1, y+1, color);
+  _fillRect(x+2*w+1, y, l, 2*w+1, color);
+  if (cfont.offset) {
+    _drawTriangle(x+2*w, y+2*w-1, x+w+1, y+w, x+2*w, y+1, outline);
+    _drawTriangle(x+2*w+l+1, y+2*w-1, x+3*w+l, y+w, x+2*w+l+1, y+1, outline);
+    _drawRect(x+2*w+1, y, l, 2*w+1, outline);
+  }
+}
+
+//--------------------------------------------------------------------------------------------
+static void _draw7seg(int16_t x, int16_t y, int8_t num, int16_t w, int16_t l, color_t color) {
+  /* TODO: clipping */
+  if (num < 0x2D || num > 0x3A) return;
+
+  int16_t c = font_bcd[num-0x2D];
+  int16_t d = 2*w+l+1;
+
+  // === Clear unused segments ===
+  if (!(c & 0x001)) barVert(x+d, y+d, w, l, _bg, _bg);
+  if (!(c & 0x002)) barVert(x,   y+d, w, l, _bg, _bg);
+  if (!(c & 0x004)) barVert(x+d, y, w, l, _bg, _bg);
+  if (!(c & 0x008)) barVert(x,   y, w, l, _bg, _bg);
+  if (!(c & 0x010)) barHor(x, y+2*d, w, l, _bg, _bg);
+  if (!(c & 0x020)) barHor(x, y+d, w, l, _bg, _bg);
+  if (!(c & 0x040)) barHor(x, y, w, l, _bg, _bg);
+
+  if (!(c & 0x080)) {
+    // low point
+    _fillRect(x+(d/2), y+2*d, 2*w+1, 2*w+1, _bg);
+    if (cfont.offset) _drawRect(x+(d/2), y+2*d, 2*w+1, 2*w+1, _bg);
+  }
+  if (!(c & 0x100)) {
+    // down middle point
+    _fillRect(x+(d/2), y+d+2*w+1, 2*w+1, l/2, _bg);
+    if (cfont.offset) _drawRect(x+(d/2), y+d+2*w+1, 2*w+1, l/2, _bg);
+  }
+  if (!(c & 0x800)) {
+	// up middle point
+    _fillRect(x+(d/2), y+(2*w)+1+(l/2), 2*w+1, l/2, _bg);
+    if (cfont.offset) _drawRect(x+(d/2), y+(2*w)+1+(l/2), 2*w+1, l/2, _bg);
+  }
+  if (!(c & 0x200)) {
+    // middle, minus
+    _fillRect(x+2*w+1, y+d, l, 2*w+1, _bg);
+    if (cfont.offset) _drawRect(x+2*w+1, y+d, l, 2*w+1, _bg);
+  }
+
+  // === Draw used segments ===
+  if (c & 0x001) barVert(x+d, y+d, w, l, color, cfont.color);	// down right
+  if (c & 0x002) barVert(x,   y+d, w, l, color, cfont.color);	// down left
+  if (c & 0x004) barVert(x+d, y, w, l, color, cfont.color);		// up right
+  if (c & 0x008) barVert(x,   y, w, l, color, cfont.color);		// up left
+  if (c & 0x010) barHor(x, y+2*d, w, l, color, cfont.color);	// down
+  if (c & 0x020) barHor(x, y+d, w, l, color, cfont.color);		// middle
+  if (c & 0x040) barHor(x, y, w, l, color, cfont.color);		// up
+
+  if (c & 0x080) {
+    // low point
+    _fillRect(x+(d/2), y+2*d, 2*w+1, 2*w+1, color);
+    if (cfont.offset) _drawRect(x+(d/2), y+2*d, 2*w+1, 2*w+1, cfont.color);
+  }
+  if (c & 0x100) {
+    // down middle point
+    _fillRect(x+(d/2), y+d+2*w+1, 2*w+1, l/2, color);
+    if (cfont.offset) _drawRect(x+(d/2), y+d+2*w+1, 2*w+1, l/2, cfont.color);
+  }
+  if (c & 0x800) {
+	// up middle point
+    _fillRect(x+(d/2), y+(2*w)+1+(l/2), 2*w+1, l/2, color);
+    if (cfont.offset) _drawRect(x+(d/2), y+(2*w)+1+(l/2), 2*w+1, l/2, cfont.color);
+  }
+  if (c & 0x200) {
+    // middle, minus
+    _fillRect(x+2*w+1, y+d, l, 2*w+1, color);
+    if (cfont.offset) _drawRect(x+2*w+1, y+d, l, 2*w+1, cfont.color);
+  }
+}
+//==============================================================================
+
+//======================================
+void TFT_print(char *st, int x, int y) {
+	int stl, i, tmpw, tmph, fh;
+	uint8_t ch;
+
+	if (cfont.bitmap == 0) return; // wrong font selected
+
+	// ** Rotated strings cannot be aligned
+	if ((font_rotate != 0) && ((x <= CENTER) || (y <= CENTER))) return;
+
+	if ((x < LASTX) || (font_rotate == 0)) TFT_OFFSET = 0;
+
+	if ((x >= LASTX) && (x < LASTY)) x = TFT_X + (x-LASTX);
+	else if (x > CENTER) x += dispWin.x1;
+
+	if (y >= LASTY) y = TFT_Y + (y-LASTY);
+	else if (y > CENTER) y += dispWin.y1;
+
+	// ** Get number of characters in string to print
+	stl = strlen(st);
+
+	// ** Calculate CENTER, RIGHT or BOTTOM position
+	tmpw = TFT_getStringWidth(st);	// string width in pixels
+	fh = cfont.y_size;			// font height
+	if ((cfont.x_size != 0) && (cfont.bitmap == 2)) {
+		// 7-segment font
+		fh = (3 * (2 * cfont.y_size + 1)) + (2 * cfont.x_size);  // 7-seg character height
+	}
+
+	if (x == RIGHT) x = dispWin.x2 - tmpw + dispWin.x1;
+	else if (x == CENTER) x = (((dispWin.x2 - dispWin.x1 + 1) - tmpw) / 2) + dispWin.x1;
+
+	if (y == BOTTOM) y = dispWin.y2 - fh + dispWin.y1;
+	else if (y==CENTER) y = (((dispWin.y2 - dispWin.y1 + 1) - (fh/2)) / 2) + dispWin.y1;
+
+	if (x < dispWin.x1) x = dispWin.x1;
+	if (y < dispWin.y1) y = dispWin.y1;
+	if ((x > dispWin.x2) || (y > dispWin.y2)) return;
+
+	TFT_X = x;
+	TFT_Y = y;
+
+	// ** Adjust y position
+	tmph = cfont.y_size; // font height
+	// for non-proportional fonts, char width is the same for all chars
+	tmpw = cfont.x_size;
+	if (cfont.x_size != 0) {
+		if (cfont.bitmap == 2) {	// 7-segment font
+			tmpw = _7seg_width();	// character width
+			tmph = _7seg_height();	// character height
+		}
+	}
+	else TFT_OFFSET = 0;	// fixed font; offset not needed
+
+	if ((TFT_Y + tmph - 1) > dispWin.y2) return;
+
+	int offset = TFT_OFFSET;
+
+	for (i=0; i<stl; i++) {
+		ch = st[i]; // get string character
+
+		if (ch == 0x0D) { // === '\r', erase to eol ====
+			if ((!font_transparent) && (font_rotate==0)) _fillRect(TFT_X, TFT_Y,  dispWin.x2+1-TFT_X, tmph, _bg);
+		}
+
+		else if (ch == 0x0A) { // ==== '\n', new line ====
+			if (cfont.bitmap == 1) {
+				TFT_Y += tmph + font_line_space;
+				if (TFT_Y > (dispWin.y2-tmph)) break;
+				TFT_X = dispWin.x1;
+			}
+		}
+
+		else { // ==== other characters ====
+			if (cfont.x_size == 0) {
+				// for proportional font get character data to 'fontChar'
+				if (getCharPtr(ch)) tmpw = fontChar.xDelta;
+				else continue;
+			}
+
+			// check if character can be displayed in the current line
+			if ((TFT_X+tmpw) > (dispWin.x2)) {
+				if (text_wrap == 0) break;
+				TFT_Y += tmph + font_line_space;
+				if (TFT_Y > (dispWin.y2-tmph)) break;
+				TFT_X = dispWin.x1;
+			}
+
+			// Let's print the character
+			if (cfont.x_size == 0) {
+				// == proportional font
+				if (font_rotate == 0) TFT_X += printProportionalChar(TFT_X, TFT_Y) + 1;
+				else {
+					// rotated proportional font
+					offset += rotatePropChar(x, y, offset);
+					TFT_OFFSET = offset;
+				}
+			}
+			else {
+				if (cfont.bitmap == 1) {
+					// == fixed font
+					if ((ch < cfont.offset) || ((ch-cfont.offset) > cfont.numchars)) ch = cfont.offset;
+					if (font_rotate == 0) {
+						printChar(ch, TFT_X, TFT_Y);
+						TFT_X += tmpw;
+					}
+					else rotateChar(ch, x, y, i);
+				}
+				else if (cfont.bitmap == 2) {
+					// == 7-segment font ==
+					_draw7seg(TFT_X, TFT_Y, ch, cfont.y_size, cfont.x_size, _fg);
+					TFT_X += (tmpw + 2);
+				}
+			}
+		}
+	}
+}
+
+
+// ================ Service functions ==========================================
+
+// Change the screen rotation.
+// Input: m new rotation value (0 to 3)
+//=================================
+void TFT_setRotation(uint8_t rot) {
+    if (rot > 3) {
+        uint8_t madctl = (rot & 0xF8); // for testing, manually set MADCTL register
+		if (disp_select() == ESP_OK) {
+			disp_spi_transfer_cmd_data(TFT_MADCTL, &madctl, 1);
+			disp_deselect();
+		}
+    }
+	else {
+		orientation = rot;
+        _tft_setRotation(rot);
+	}
+
+	dispWin.x1 = 0;
+	dispWin.y1 = 0;
+	dispWin.x2 = _width-1;
+	dispWin.y2 = _height-1;
+
+	TFT_fillScreen(_bg);
+}
+
+// Send the command to invert all of the colors.
+// Input: i 0 to disable inversion; non-zero to enable inversion
+//==========================================
+void TFT_invertDisplay(const uint8_t mode) {
+  if ( mode == INVERT_ON ) disp_spi_transfer_cmd(TFT_INVONN);
+  else disp_spi_transfer_cmd(TFT_INVOFF);
+}
+
+// Select gamma curve
+// Input: gamma = 0~3
+//==================================
+void TFT_setGammaCurve(uint8_t gm) {
+  uint8_t gamma_curve = 1 << (gm & 0x03);
+  disp_spi_transfer_cmd_data(TFT_CMD_GAMMASET, &gamma_curve, 1);
+}
+
+//===========================================================
+color_t HSBtoRGB(float _hue, float _sat, float _brightness) {
+ float red = 0.0;
+ float green = 0.0;
+ float blue = 0.0;
+
+ if (_sat == 0.0) {
+   red = _brightness;
+   green = _brightness;
+   blue = _brightness;
+ } else {
+   if (_hue == 360.0) {
+     _hue = 0;
+   }
+
+   int slice = (int)(_hue / 60.0);
+   float hue_frac = (_hue / 60.0) - slice;
+
+   float aa = _brightness * (1.0 - _sat);
+   float bb = _brightness * (1.0 - _sat * hue_frac);
+   float cc = _brightness * (1.0 - _sat * (1.0 - hue_frac));
+
+   switch(slice) {
+     case 0:
+         red = _brightness;
+         green = cc;
+         blue = aa;
+         break;
+     case 1:
+         red = bb;
+         green = _brightness;
+         blue = aa;
+         break;
+     case 2:
+         red = aa;
+         green = _brightness;
+         blue = cc;
+         break;
+     case 3:
+         red = aa;
+         green = bb;
+         blue = _brightness;
+         break;
+     case 4:
+         red = cc;
+         green = aa;
+         blue = _brightness;
+         break;
+     case 5:
+         red = _brightness;
+         green = aa;
+         blue = bb;
+         break;
+     default:
+         red = 0.0;
+         green = 0.0;
+         blue = 0.0;
+         break;
+   }
+ }
+
+ color_t color;
+ color.r = ((uint8_t)(red * 255.0)) & 0xFC;
+ color.g = ((uint8_t)(green * 255.0)) & 0xFC;
+ color.b = ((uint8_t)(blue * 255.0)) & 0xFC;
+
+ return color;
+}
+//=====================================================================
+void TFT_setclipwin(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
+{
+	dispWin.x1 = x1;
+	dispWin.y1 = y1;
+	dispWin.x2 = x2;
+	dispWin.y2 = y2;
+
+	if (dispWin.x2 >= _width) dispWin.x2 = _width-1;
+	if (dispWin.y2 >= _height) dispWin.y2 = _height-1;
+	if (dispWin.x1 > dispWin.x2) dispWin.x1 = dispWin.x2;
+	if (dispWin.y1 > dispWin.y2) dispWin.y1 = dispWin.y2;
+}
+
+//=====================
+void TFT_resetclipwin()
+{
+	dispWin.x2 = _width-1;
+	dispWin.y2 = _height-1;
+	dispWin.x1 = 0;
+	dispWin.y1 = 0;
+}
+
+//==========================================================================
+void set_7seg_font_atrib(uint8_t l, uint8_t w, int outline, color_t color) {
+	if (cfont.bitmap != 2) return;
+
+	if (l < 6) l = 6;
+    if (l > 40) l = 40;
+    if (w < 1) w = 1;
+    if (w > (l/2)) w = l/2;
+    if (w > 12) w = 12;
+
+    cfont.x_size = l;
+	cfont.y_size = w;
+	cfont.offset = outline;
+	cfont.color  = color;
+}
+
+//==========================================
+int TFT_getfontsize(int *width, int* height)
+{
+  if (cfont.bitmap == 1) {
+    if (cfont.x_size != 0) *width = cfont.x_size;	// fixed width font
+    else *width = cfont.max_x_size;					// proportional font
+    *height = cfont.y_size;
+  }
+  else if (cfont.bitmap == 2) {
+	// 7-segment font
+    *width = _7seg_width();
+    *height = _7seg_height();
+  }
+  else {
+    *width = 0;
+    *height = 0;
+    return 0;
+  }
+  return 1;
+}
+
+//=====================
+int TFT_getfontheight()
+{
+  if (cfont.bitmap == 1) return cfont.y_size;			// Bitmap font
+  else if (cfont.bitmap == 2) return _7seg_height();	// 7-segment font
+  return 0;
+}
+
+//====================
+void TFT_saveClipWin()
+{
+	dispWinTemp.x1 = dispWin.x1;
+	dispWinTemp.y1 = dispWin.y1;
+	dispWinTemp.x2 = dispWin.x2;
+	dispWinTemp.y2 = dispWin.y2;
+}
+
+//=======================
+void TFT_restoreClipWin()
+{
+	dispWin.x1 = dispWinTemp.x1;
+	dispWin.y1 = dispWinTemp.y1;
+	dispWin.x2 = dispWinTemp.x2;
+	dispWin.y2 = dispWinTemp.y2;
+}
+
+
+// ================ JPG SUPPORT ================================================
+// User defined device identifier
+typedef struct {
+	FILE		*fhndl;			// File handler for input function
+    int			x;				// image top left point X position
+    int			y;				// image top left point Y position
+    uint8_t		*membuff;		// memory buffer containing the image
+    uint32_t	bufsize;		// size of the memory buffer
+    uint32_t	bufptr;			// memory buffer current position
+    color_t		*linbuf[2];		// memory buffer used for display output
+    uint8_t		linbuf_idx;
+} JPGIODEV;
+
+
+// User defined call-back function to input JPEG data from file
+//---------------------
+static UINT tjd_input (
+	JDEC* jd,		// Decompression object
+	BYTE* buff,		// Pointer to the read buffer (NULL:skip)
+	UINT nd			// Number of bytes to read/skip from input stream
+)
+{
+	int rb = 0;
+	// Device identifier for the session (5th argument of jd_prepare function)
+	JPGIODEV *dev = (JPGIODEV*)jd->device;
+
+	if (buff) {	// Read nd bytes from the input strem
+		rb = fread(buff, 1, nd, dev->fhndl);
+		return rb;	// Returns actual number of bytes read
+	}
+	else {	// Remove nd bytes from the input stream
+		if (fseek(dev->fhndl, nd, SEEK_CUR) >= 0) return nd;
+		else return 0;
+	}
+}
+
+// User defined call-back function to input JPEG data from memory buffer
+//-------------------------
+static UINT tjd_buf_input (
+	JDEC* jd,		// Decompression object
+	BYTE* buff,		// Pointer to the read buffer (NULL:skip)
+	UINT nd			// Number of bytes to read/skip from input stream
+)
+{
+	// Device identifier for the session (5th argument of jd_prepare function)
+	JPGIODEV *dev = (JPGIODEV*)jd->device;
+	if (!dev->membuff) return 0;
+	if (dev->bufptr >= (dev->bufsize + 2)) return 0; // end of stream
+
+	if ((dev->bufptr + nd) > (dev->bufsize + 2)) nd = (dev->bufsize + 2) - dev->bufptr;
+
+	if (buff) {	// Read nd bytes from the input strem
+		memcpy(buff, dev->membuff + dev->bufptr, nd);
+		dev->bufptr += nd;
+		return nd;	// Returns number of bytes read
+	}
+	else {	// Remove nd bytes from the input stream
+		dev->bufptr += nd;
+		return nd;
+	}
+}
+
+// User defined call-back function to output RGB bitmap to display device
+//----------------------
+static UINT tjd_output (
+	JDEC* jd,		// Decompression object of current session
+	void* bitmap,	// Bitmap data to be output
+	JRECT* rect		// Rectangular region to output
+)
+{
+	// Device identifier for the session (5th argument of jd_prepare function)
+	JPGIODEV *dev = (JPGIODEV*)jd->device;
+
+	// ** Put the rectangular into the display device **
+	int x;
+	int y;
+	int dleft, dtop, dright, dbottom;
+	BYTE *src = (BYTE*)bitmap;
+
+	int left = rect->left + dev->x;
+	int top = rect->top + dev->y;
+	int right = rect->right + dev->x;
+	int bottom = rect->bottom + dev->y;
+
+	if ((left > dispWin.x2) || (top > dispWin.y2)) return 1;	// out of screen area, return
+	if ((right < dispWin.x1) || (bottom < dispWin.y1)) return 1;// out of screen area, return
+
+	if (left < dispWin.x1) dleft = dispWin.x1;
+	else dleft = left;
+	if (top < dispWin.y1) dtop = dispWin.y1;
+	else dtop = top;
+	if (right > dispWin.x2) dright = dispWin.x2;
+	else dright = right;
+	if (bottom > dispWin.y2) dbottom = dispWin.y2;
+	else dbottom = bottom;
+
+	if ((dleft > dispWin.x2) || (dtop > dispWin.y2)) return 1;		// out of screen area, return
+	if ((dright < dispWin.x1) || (dbottom < dispWin.y1)) return 1;	// out of screen area, return
+
+	uint32_t len = ((dright-dleft+1) * (dbottom-dtop+1));	// calculate length of data
+
+
+	if ((len > 0) && (len <= JPG_IMAGE_LINE_BUF_SIZE)) {
+		uint8_t *dest = (uint8_t *)(dev->linbuf[dev->linbuf_idx]);
+
+		for (y = top; y <= bottom; y++) {
+			for (x = left; x <= right; x++) {
+				// Clip to display area
+				if ((x >= dleft) && (y >= dtop) && (x <= dright) && (y <= dbottom)) {
+					*dest++ = (*src++) & 0xFC;
+					*dest++ = (*src++) & 0xFC;
+					*dest++ = (*src++) & 0xFC;
+				}
+				else src += 3; // skip
+			}
+		}
+		wait_trans_finish(1);
+		send_data(dleft, dtop, dright, dbottom, len, dev->linbuf[dev->linbuf_idx]);
+		dev->linbuf_idx = ((dev->linbuf_idx + 1) & 1);
+	}
+	else {
+		wait_trans_finish(1);
+		printf("Data size error: %d jpg: (%d,%d,%d,%d) disp: (%d,%d,%d,%d)\r\n", len, left,top,right,bottom, dleft,dtop,dright,dbottom);
+		return 0;  // stop decompression
+	}
+
+	return 1;	// Continue to decompression
+}
+
+// tft.jpgimage(X, Y, scale, file_name, buf, size]
+// X & Y can be < 0 !
+//==================================================================================
+void TFT_jpg_image(int x, int y, uint8_t scale, char *fname, uint8_t *buf, int size)
+{
+	JPGIODEV dev;
+    struct stat sb;
+	char *work = NULL;		// Pointer to the working buffer (must be 4-byte aligned)
+	UINT sz_work = 3800;	// Size of the working buffer (must be power of 2)
+	JDEC jd;				// Decompression object (70 bytes)
+	JRESULT rc;
+
+	dev.linbuf[0] = NULL;
+	dev.linbuf[1] = NULL;
+    dev.linbuf_idx = 0;
+
+   	dev.fhndl = NULL;
+    if (fname == NULL) {
+    	// image from buffer
+        dev.membuff = buf;
+        dev.bufsize = size;
+        dev.bufptr = 0;
+    }
+    else {
+    	// image from file
+        dev.membuff = NULL;
+        dev.bufsize = 0;
+        dev.bufptr = 0;
+
+        if (stat(fname, &sb) != 0) {
+        	if (image_debug) printf("File error: %ss\r\n", strerror(errno));
+            goto exit;
+        }
+
+        dev.fhndl = fopen(fname, "r");
+        if (!dev.fhndl) {
+        	if (image_debug) printf("Error opening file: %s\r\n", strerror(errno));
+            goto exit;
+        }
+    }
+
+	if (scale > 3) scale = 3;
+
+	work = malloc(sz_work);
+	if (work) {
+		if (dev.membuff) rc = jd_prepare(&jd, tjd_buf_input, (void *)work, sz_work, &dev);
+		else rc = jd_prepare(&jd, tjd_input, (void *)work, sz_work, &dev);
+		if (rc == JDR_OK) {
+			if (x == CENTER) x = ((dispWin.x2 - dispWin.x1 + 1 - (int)(jd.width >> scale)) / 2) + dispWin.x1;
+			else if (x == RIGHT) x = dispWin.x2 + 1 - (int)(jd.width >> scale);
+
+			if (y == CENTER) y = ((dispWin.y2 - dispWin.y1 + 1 - (int)(jd.height >> scale)) / 2) + dispWin.y1;
+			else if (y == BOTTOM) y = dispWin.y2 + 1 - (int)(jd.height >> scale);
+
+			if (x < ((dispWin.x2-1) * -1)) x = (dispWin.x2-1) * -1;
+			if (y < ((dispWin.y2-1)) * -1) y = (dispWin.y2-1) * -1;
+			if (x > (dispWin.x2-1)) x = dispWin.x2 - 1;
+			if (y > (dispWin.y2-1)) y = dispWin.y2-1;
+
+			dev.x = x;
+			dev.y = y;
+
+			dev.linbuf[0] = heap_caps_malloc(JPG_IMAGE_LINE_BUF_SIZE*3, MALLOC_CAP_DMA);
+			if (dev.linbuf[0] == NULL) {
+				if (image_debug) printf("Error allocating line buffer #0\r\n");
+				goto exit;
+			}
+			dev.linbuf[1] = heap_caps_malloc(JPG_IMAGE_LINE_BUF_SIZE*3, MALLOC_CAP_DMA);
+			if (dev.linbuf[1] == NULL) {
+				if (image_debug) printf("Error allocating line buffer #1\r\n");
+				goto exit;
+			}
+
+			// Start to decode the JPEG file
+			disp_select();
+			rc = jd_decomp(&jd, tjd_output, scale);
+			disp_deselect();
+
+			if (rc != JDR_OK) {
+				if (image_debug) printf("jpg decompression error %d\r\n", rc);
+			}
+			if (image_debug) printf("Jpg size: %dx%d, position; %d,%d, scale: %d, bytes used: %d\r\n", jd.width, jd.height, x, y, scale, jd.sz_pool);
+		}
+		else {
+			if (image_debug) printf("jpg prepare error %d\r\n", rc);
+		}
+	}
+	else {
+		if (image_debug) printf("work buffer allocation error\r\n");
+	}
+
+exit:
+	if (work) free(work);  // free work buffer
+	if (dev.linbuf[0]) free(dev.linbuf[0]);
+	if (dev.linbuf[1]) free(dev.linbuf[1]);
+    if (dev.fhndl) fclose(dev.fhndl);  // close input file
+}
+
+
+//====================================================================================
+int TFT_bmp_image(int x, int y, uint8_t scale, char *fname, uint8_t *imgbuf, int size)
+{
+	FILE *fhndl = NULL;
+	struct stat sb;
+	int i, err=0;
+	int img_xsize, img_ysize, img_xstart, img_xlen, img_ystart, img_ylen;
+	int img_pos, img_pix_pos, scan_lines, rd_len;
+	uint8_t tmpc;
+	uint16_t wtemp;
+	uint32_t temp;
+	int disp_xstart, disp_xend, disp_ystart, disp_yend;
+	uint8_t buf[56];
+	char err_buf[64];
+	uint8_t *line_buf[2] = {NULL,NULL};
+	uint8_t lb_idx = 0;
+	uint8_t *scale_buf = NULL;
+	uint8_t scale_pix;
+	uint16_t co[3] = {0,0,0};			// RGB sum
+	uint8_t npix;
+
+	if (scale > 7) scale = 7;
+	scale_pix = scale+1;	// scale factor ( 1~8 )
+
+    if (fname) {
+    	// * File name is given, reading image from file
+    	if (stat(fname, &sb) != 0) {
+			sprintf(err_buf, "opening file");
+    		err = -1;
+    		goto exit;
+    	}
+    	size = sb.st_size;
+		fhndl = fopen(fname, "r");
+		if (!fhndl) {
+			sprintf(err_buf, "opening file");
+			err = -2;
+			goto exit;
+		}
+
+		i = fread(buf, 1, 54, fhndl);  // read header
+    }
+    else {
+    	// * Reading image from buffer
+    	if ((imgbuf) && (size > 54)) {
+    		memcpy(buf, imgbuf, 54);
+    		i = 54;
+    	}
+    	else i = 0;
+    }
+
+    sprintf(err_buf, "reading header");
+	if (i != 54) {err = -3;	goto exit;}
+
+	// ** Check image header and get image properties
+	if ((buf[0] != 'B') || (buf[1] != 'M')) {err=-4; goto exit;} // accept only images with 'BM' id
+
+	memcpy(&temp, buf+2, 4);				// file size
+	if (temp != size) {err=-5; goto exit;}
+
+	memcpy(&img_pos, buf+10, 4);			// start of pixel data
+
+	memcpy(&temp, buf+14, 4);				// BMP header size
+	if (temp != 40) {err=-6; goto exit;}
+
+	memcpy(&wtemp, buf+26, 2);				// the number of color planes
+	if (wtemp != 1) {err=-7; goto exit;}
+
+	memcpy(&wtemp, buf+28, 2);				// the number of bits per pixel
+	if (wtemp != 24) {err=-8; goto exit;}
+
+	memcpy(&temp, buf+30, 4);				// the compression method being used
+	if (temp != 0) {err=-9; goto exit;}
+
+	memcpy(&img_xsize, buf+18, 4);			// the bitmap width in pixels
+	memcpy(&img_ysize, buf+22, 4);			// the bitmap height in pixels
+
+
+	// * scale image dimensions
+
+	img_xlen = img_xsize / scale_pix;		// image display horizontal size
+	img_ylen = img_ysize / scale_pix;		// image display vertical size
+
+	if (x == CENTER) x = ((dispWin.x2 - dispWin.x1 + 1 - img_xlen) / 2) + dispWin.x1;
+	else if (x == RIGHT) x = dispWin.x2 + 1 - img_xlen;
+
+	if (y == CENTER) y = ((dispWin.y2 - dispWin.y1 + 1 - img_ylen) / 2) + dispWin.y1;
+	else if (y == BOTTOM) y = dispWin.y2 + 1 - img_ylen;
+
+	if ((x < ((dispWin.x2 + 1) * -1)) || (x > (dispWin.x2 + 1)) || (y < ((dispWin.y2 + 1) * -1)) || (y > (dispWin.y2 + 1))) {
+		sprintf(err_buf, "out of display area (%d,%d", x, y);
+		err = -10;
+		goto exit;
+	}
+
+	// ** set display and image areas
+	if (x < dispWin.x1) {
+		disp_xstart = dispWin.x1;
+		img_xstart = -x;	// image pixel line X offset
+		img_xlen += x;
+	}
+	else {
+		disp_xstart = x;
+		img_xstart = 0;
+	}
+	if (y < dispWin.y1) {
+		disp_ystart = dispWin.y1;
+		img_ystart = -y;	// image pixel line Y offset
+		img_ylen += y;
+	}
+	else {
+		disp_ystart = y;
+		img_ystart = 0;
+	}
+	disp_xend = disp_xstart + img_xlen - 1;
+	disp_yend = disp_ystart + img_ylen - 1;
+	if (disp_xend > dispWin.x2) {
+		disp_xend = dispWin.x2;
+		img_xlen = disp_xend - disp_xstart + 1;
+	}
+	if (disp_yend > dispWin.y2) {
+		disp_yend = dispWin.y2;
+		img_ylen = disp_yend - disp_ystart + 1;
+	}
+
+	if ((img_xlen < 8) || (img_ylen < 8) || (img_xstart >= (img_xsize-2)) || ((img_ysize - img_ystart) < 2)) {
+		sprintf(err_buf, "image too small");
+		err = -11;
+		goto exit;
+	}
+
+	// ** Allocate memory for 2 lines of image pixels
+	line_buf[0] = heap_caps_malloc(img_xsize*3, MALLOC_CAP_DMA);
+	if (line_buf[0] == NULL) {
+	    sprintf(err_buf, "allocating line buffer #1");
+		err=-12;
+		goto exit;
+	}
+
+	line_buf[1] = heap_caps_malloc(img_xsize*3, MALLOC_CAP_DMA);
+	if (line_buf[1] == NULL) {
+	    sprintf(err_buf, "allocating line buffer #2");
+		err=-13;
+		goto exit;
+	}
+
+	if (scale) {
+		// Allocate memory for scale buffer
+		rd_len = img_xlen * 3 * scale_pix;
+		scale_buf = malloc(rd_len*scale_pix);
+		if (scale_buf == NULL) {
+			sprintf(err_buf, "allocating scale buffer");
+			err=-14;
+			goto exit;
+		}
+	}
+	else rd_len = img_xlen * 3;
+
+	// ** ***************************************************** **
+	// ** BMP images are stored in file from LAST to FIRST line **
+	// ** ***************************************************** **
+
+	/* Used variables:
+		img_xsize		horizontal image size in pixels
+		img_ysize		number of image lines
+		img_xlen 		image display horizontal scaled size in pixels
+		img_ylen		image display vertical scaled size in pixels
+		img_xstart		first pixel in line to be displayed
+		img_ystart		first image line to be displayed
+		img_xlen		number of pixels in image line to be displayed, starting with 'img_xstart'
+		img_ylen		number of lines in image to be displayed, starting with 'img_ystart'
+		rd_len			length of color data which are read from image line in bytes
+	 */
+
+	// Set position in image to the first color data (beginning of the LAST line)
+	img_pos += (img_ystart * (img_xsize*3));
+	if (fhndl) {
+		if (fseek(fhndl, img_pos, SEEK_SET) != 0) {
+			sprintf(err_buf, "file seek at %d", img_pos);
+			err = -15;
+			goto exit;
+		}
+	}
+
+	if (image_debug) printf("BMP: image size: (%d,%d) scale: %d disp size: (%d,%d) img xofs: %d img yofs: %d at: %d,%d; line buf: 2* %d scale buf: %d\r\n",
+			img_xsize, img_ysize, scale_pix, img_xlen, img_ylen, img_xstart, img_ystart, disp_xstart, disp_ystart, img_xsize*3, ((scale) ? (rd_len*scale_pix) : 0));
+
+	// * Select the display
+	disp_select();
+
+	while ((disp_yend >= disp_ystart) && ((img_pos + (img_xsize*3)) <= size)) {
+		if (img_pos > size) {
+			sprintf(err_buf, "EOF reached: %d > %d", img_pos, size);
+			err = -16;
+			goto exit1;
+		}
+		if (scale == 0) {
+			// Read the line of color data into color buffer
+			if (fhndl) {
+				i = fread(line_buf[lb_idx], 1, img_xsize*3, fhndl);  // read line from file
+				if (i != (img_xsize*3)) {
+					sprintf(err_buf, "file read at %d (%d<>%d)", img_pos, i, img_xsize*3);
+					err = -16;
+					goto exit1;
+				}
+			}
+			else memcpy(line_buf[lb_idx], imgbuf+img_pos, img_xsize*3);
+
+			if (img_xstart > 0)	memmove(line_buf[lb_idx], line_buf[lb_idx]+(img_xstart*3), rd_len);
+			// Convert colors BGR-888 (BMP) -> RGB-888 (DISPLAY) ===
+			for (i=0; i < rd_len; i += 3) {
+				tmpc = line_buf[lb_idx][i+2] & 0xfc;				// save R
+				line_buf[lb_idx][i+2] = line_buf[lb_idx][i] & 0xfc;	// B -> R
+				line_buf[lb_idx][i] = tmpc;							// R -> B
+				line_buf[lb_idx][i+1] &= 0xfc;						// G
+			}
+			img_pos += (img_xsize*3);
+		}
+		else {
+			// scale image, read 'scale_pix' lines and find the average color
+			for (scan_lines=0; scan_lines<scale_pix; scan_lines++) {
+				if (img_pos > size) break;
+				if (fhndl) {
+					i = fread(line_buf[lb_idx], 1, img_xsize*3, fhndl);  // read line from file
+					if (i != (img_xsize*3)) {
+						sprintf(err_buf, "file read at %d (%d<>%d)", img_pos, i, img_xsize*3);
+						err = -17;
+						goto exit1;
+					}
+				}
+				else memcpy(line_buf[lb_idx], imgbuf+img_pos, img_xsize*3);
+				img_pos += (img_xsize*3);
+
+				// copy only data which are displayed to scale buffer
+				memcpy(scale_buf + (rd_len * scan_lines), line_buf[lb_idx]+img_xstart, rd_len);
+			}
+
+			// Populate display line buffer
+			for (int n=0;n<(img_xlen*3);n += 3) {
+				memset(co, 0, sizeof(co));	// initialize color sum
+				npix = 0;					// initialize number of pixels in scale rectangle
+
+				// sum all pixels in scale rectangle
+				for (int sc_line=0; sc_line<scan_lines; sc_line++) {
+					// Get colors position in scale buffer
+					img_pix_pos = (rd_len * sc_line) + (n * scale_pix);
+
+					for (int sc_col=0; sc_col<scale_pix; sc_col++) {
+						co[0] += scale_buf[img_pix_pos];
+						co[1] += scale_buf[img_pix_pos + 1];
+						co[2] += scale_buf[img_pix_pos + 2];
+						npix++;
+					}
+				}
+				// Place the average in display buffer, convert BGR-888 (BMP) -> RGB-888 (DISPLAY)
+				line_buf[lb_idx][n+2] = (uint8_t)(co[0] / npix);	// B
+				line_buf[lb_idx][n+1] = (uint8_t)(co[1] / npix);	// G
+				line_buf[lb_idx][n] = (uint8_t)(co[2] / npix);		// R
+			}
+		}
+
+		wait_trans_finish(1);
+		send_data(disp_xstart, disp_yend, disp_xend, disp_yend, img_xlen, (color_t *)line_buf[lb_idx]);
+		lb_idx = (lb_idx + 1) & 1;  // change buffer
+
+		disp_yend--;
+	}
+	err = 0;
+exit1:
+	disp_deselect();
+exit:
+	if (scale_buf) free(scale_buf);
+	if (line_buf[0]) free(line_buf[0]);
+	if (line_buf[1]) free(line_buf[1]);
+	if (fhndl) fclose(fhndl);
+	if ((err) && (image_debug)) printf("Error: %d [%s]\r\n", err, err_buf);
+
+	return err;
+}
+
+
+// ============= Touch panel functions =========================================
+
+#if USE_TOUCH == TOUCH_TYPE_XPT2046
+//-------------------------------------------------------
+static int tp_get_data_xpt2046(uint8_t type, int samples)
+{
+	if (ts_spi == NULL) return 0;
+
+	int n, result, val = 0;
+	uint32_t i = 0;
+	uint32_t vbuf[18];
+	uint32_t minval, maxval, dif;
+
+    if (samples < 3) samples = 1;
+    if (samples > 18) samples = 18;
+
+    // one dummy read
+    result = touch_get_data(type);
+
+    // read data
+	while (i < 10) {
+    	minval = 5000;
+    	maxval = 0;
+		// get values
+		for (n=0;n<samples;n++) {
+		    result = touch_get_data(type);
+			if (result < 0) break;
+
+			vbuf[n] = result;
+			if (result < minval) minval = result;
+			if (result > maxval) maxval = result;
+		}
+		if (result < 0) break;
+		dif = maxval - minval;
+		if (dif < 40) break;
+		i++;
+    }
+	if (result < 0) return -1;
+
+	if (samples > 2) {
+		// remove one min value
+		for (n = 0; n < samples; n++) {
+			if (vbuf[n] == minval) {
+				vbuf[n] = 5000;
+				break;
+			}
+		}
+		// remove one max value
+		for (n = 0; n < samples; n++) {
+			if (vbuf[n] == maxval) {
+				vbuf[n] = 5000;
+				break;
+			}
+		}
+		for (n = 0; n < samples; n++) {
+			if (vbuf[n] < 5000) val += vbuf[n];
+		}
+		val /= (samples-2);
+	}
+	else val = vbuf[0];
+
+    return val;
+}
+
+//-----------------------------------------------
+static int TFT_read_touch_xpt2046(int *x, int* y)
+{
+    int res = 0, result = -1;
+	
+    if (spi_lobo_device_select(ts_spi, 0) != ESP_OK)
+	return 0;
+
+    result = tp_get_data_xpt2046(0xB0, 3);  	// Z; pressure; touch detect
+    if (result <= 15) goto exit;		// Z was 50, but near the origin it's just above 10.
+   						// 26-6-2018 from 10 to 15.
+
+	// touch panel pressed
+	result = tp_get_data_xpt2046(0xD0, 6);
+	if (result < 0) goto exit;
+	*x = result;
+
+	result = tp_get_data_xpt2046(0x90, 6);
+	if (result < 0) goto exit;
+	*y = result;
+	res = 1;
+
+exit:
+	spi_lobo_device_deselect(ts_spi);
+	return res;
+}
+#endif
+
+//=============================================
+int TFT_read_touch(int *x, int* y, uint8_t raw)
+{
+    *x = 0;
+    *y = 0;
+	
+    if (ts_spi == NULL)
+	return 0;
+#if USE_TOUCH == TOUCH_TYPE_NONE
+    return 0;
+#else
+    int result = -1;
+    int X=0, Y=0;
+
+#if USE_TOUCH == TOUCH_TYPE_XPT2046
+    result = TFT_read_touch_xpt2046(&X, &Y);
+    if (result == 0)
+	return 0;
+#elif USE_TOUCH == TOUCH_TYPE_STMPE610
+    uint32_t tp_calx = TP_CALX_STMPE610;
+    uint32_t tp_caly = TP_CALY_STMPE610;
+    uint16_t Xx, Yy, Z=0;
+    result = stmpe610_get_touch(&Xx, &Yy, &Z);
+    if (result == 0) return 0;
+    X = Xx;
+    Y = Yy;
+#else
+    return 0;
+#endif
+
+    if (raw) {
+    	*x = X;
+    	*y = Y;
+    	return 1;
+    }
+
+    // Calibrate the result
+    int tmp;
+    int xleft   = tp_xleft;   //(tp_calx >> 16) & 0x3FFF;
+    int xright  = tp_xright;  //tp_calx & 0x3FFF;
+    int ytop    = tp_ytop;    //(tp_caly >> 16) & 0x3FFF;
+    int ybottom = tp_ybottom; //tp_caly & 0x3FFF;
+
+   if (((xright - xleft) <= 0) || ((ytop - ybottom) <= 0))
+	return 0;
+
+#if USE_TOUCH == TOUCH_TYPE_XPT2046
+//  printf("Raw %dx%d ", X, Y);
+    // Received coordinates are always in portrait, origin left bottom.
+    int width = DEFAULT_TFT_DISPLAY_WIDTH;
+    int height = DEFAULT_TFT_DISPLAY_HEIGHT;
+    X = ((X - xleft) * width)  / (xright - xleft);
+    Y = ((Y - ybottom) * height) / (ytop - ybottom);
+
+    if (X < 0)
+	X = 0;
+    if (X > width-1)
+	X = width-1;
+    if (Y < 0)
+	Y = 0;
+    if (Y > height-1)
+	Y = height-1;
+
+    switch (orientation) {
+	case PORTRAIT:
+		Y = height - Y - 1;
+		break;
+	case LANDSCAPE:
+		tmp = X;
+		X = height - Y - 1;
+		Y = width - tmp - 1;
+		break;
+	case PORTRAIT_FLIP:
+		X = width - X - 1;
+		break;
+	case LANDSCAPE_FLIP:
+		tmp = X;
+		X = Y;
+		Y = tmp;
+		break;
+    }
+//  printf("Cal %dx%d %dx%d  XxY %dx%d\n", xleft, ybottom, xright, ytop, X, Y);
+#elif USE_TOUCH == TOUCH_TYPE_STMPE610
+    int width = _width;
+    int height = _height;
+    if (_width > _height) {
+	width = _height;
+	height = _width;
+    }
+    X = ((X - xleft) * width) / (xright - xleft);
+    Y = ((Y - ytop) * height) / (ybottom - ytop);
+
+    if (X < 0) X = 0;
+    if (X > width-1) X = width-1;
+    if (Y < 0) Y = 0;
+    if (Y > height-1) Y = height-1;
+
+    switch (orientation) {
+	case PORTRAIT_FLIP:
+		X = width - X - 1;
+		Y = height - Y - 1;
+		break;
+	case LANDSCAPE:
+		tmp = X;
+		X = Y;
+		Y = width - tmp -1;
+		break;
+	case LANDSCAPE_FLIP:
+		tmp = X;
+		X = height - Y -1;
+		Y = tmp;
+		break;
+    }
+#endif
+    *x = X;
+    *y = Y;
+    return 1;
+#endif
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/tft.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,583 @@
+/**
+ * @file tft.h
+ * @brief High level TFT functions
+ * @author LoBo 04/2017
+ * @see https://github.com/loboris
+ */
+
+#ifndef _TFT_H_
+#define _TFT_H_
+
+#include <stdlib.h>
+#include "tftspi.h"
+
+typedef struct {
+	uint16_t        x1;
+	uint16_t        y1;
+	uint16_t        x2;
+	uint16_t        y2;
+} dispWin_t;
+
+typedef struct {
+	uint8_t 	*font;
+	uint8_t 	x_size;
+	uint8_t 	y_size;
+	uint8_t		offset;
+	uint16_t	numchars;
+	uint16_t	size;
+	uint8_t 	max_x_size;
+	uint8_t		bitmap;
+	color_t		color;
+} Font;
+
+
+//==========================================================================================
+// ==== Global variables ===================================================================
+//==========================================================================================
+extern uint8_t   orientation;			///< current screen orientation
+extern uint16_t  font_rotate;			///< current font font_rotate angle (0~395)
+extern uint8_t   font_transparent;		///< if not 0 draw fonts transparent
+extern uint8_t   font_forceFixed;		///< if not zero force drawing proportional fonts with fixed width
+extern uint8_t   font_buffered_char;
+extern uint8_t   font_line_space;		///< additional spacing between text lines; added to font height
+extern uint8_t   text_wrap;			///< if not 0 wrap long text to the new line, else clip
+extern color_t   _fg;				///< current foreground color for fonts
+extern color_t   _bg;				///< current background for non transparent fonts
+extern dispWin_t dispWin;			///< display clip window
+extern float	  _angleOffset;			///< angle offset for arc, polygon and line by angle functions
+extern uint8_t	  image_debug;			///< print debug messages during image decode if set to 1
+
+extern Font cfont;				///< Current font structure
+
+extern int	TFT_X;				///< X position of the next character after TFT_print() function
+extern int	TFT_Y;				///< Y position of the next character after TFT_print() function
+
+// =========================================================================================
+
+
+// Buffer is created during jpeg decode for sending data
+// Total size of the buffer is  2 * (JPG_IMAGE_LINE_BUF_SIZE * 3)
+// The size must be multiple of 256 bytes !!
+#define JPG_IMAGE_LINE_BUF_SIZE 512
+
+// --- Constants for ellipse function ---
+#define TFT_ELLIPSE_UPPER_RIGHT 0x01
+#define TFT_ELLIPSE_UPPER_LEFT  0x02
+#define TFT_ELLIPSE_LOWER_LEFT  0x04
+#define TFT_ELLIPSE_LOWER_RIGHT 0x08
+
+// Constants for Arc function
+// number representing the maximum angle (e.g. if 100, then if you pass in start=0 and end=50, you get a half circle)
+// this can be changed with setArcParams function at runtime
+#define DEFAULT_ARC_ANGLE_MAX 360
+// rotational offset in degrees defining position of value 0 (-90 will put it at the top of circle)
+// this can be changed with setAngleOffset function at runtime
+#define DEFAULT_ANGLE_OFFSET -90
+
+#define PI 3.14159265359
+
+#define MIN_POLIGON_SIDES	3
+#define MAX_POLIGON_SIDES	60
+
+// === Color names constants ===
+extern const color_t TFT_BLACK;
+extern const color_t TFT_NAVY;
+extern const color_t TFT_DARKGREEN;
+extern const color_t TFT_DARKCYAN;
+extern const color_t TFT_MAROON;
+extern const color_t TFT_PURPLE;
+extern const color_t TFT_OLIVE;
+extern const color_t TFT_LIGHTGREY;
+extern const color_t TFT_DARKGREY;
+extern const color_t TFT_BLUE;
+extern const color_t TFT_GREEN;
+extern const color_t TFT_CYAN;
+extern const color_t TFT_RED;
+extern const color_t TFT_MAGENTA;
+extern const color_t TFT_YELLOW;
+extern const color_t TFT_WHITE;
+extern const color_t TFT_ORANGE;
+extern const color_t TFT_GREENYELLOW;
+extern const color_t TFT_PINK;
+
+// === Color invert constants ===
+#define INVERT_ON		1
+#define INVERT_OFF		0
+
+// === Special coordinates constants ===
+#define CENTER	-9003
+#define RIGHT	-9004
+#define BOTTOM	-9004
+
+#define LASTX	7000
+#define LASTY	8000
+
+// === Embedded fonts constants ===
+#define DEFAULT_FONT		0	///< 12 points 95 characters DejaVu font.
+#define DEJAVU18_FONT		1	///< 18 points 95 characters DejaVuSans font.
+#define DEJAVU24_FONT		2	///< 24 points 95 characters DejaVu font.
+#define UBUNTU16_FONT		3	///< 16 points 95 characters Ubuntu font.
+#define COMIC24_FONT		4	///< 24 points 95 characters Comic font.
+#define MINYA24_FONT		5	///< 24 points 95 characters Minya font.
+#define TOONEY32_FONT		6	///< 32 points 95 characters Tooney font.
+#define SMALL_FONT		7	///< 8x12 pixels 95 characters fixed font.
+#define DEF_SMALL_FONT		8	///<  9 points 95 characters font.
+#define FONT_7SEG		9	///< Variable size 14 characters 7 segment font.
+#define USER_FONT		10	///< font will be read from file
+
+
+
+// ===== PUBLIC FUNCTIONS =========================================================================
+
+
+/**
+ * @brief Draw pixel at given x,y coordinates
+ * @param x horizontal position
+ * @param y vertical position
+ * @param color pixel color
+ * @param sel if not 0 activate CS before and deactivat after sending pixel data to display
+ *            when sending multiple pixels it is faster to activate the CS first,
+ *            send all pixels an deactivate CS after all pixela was sent
+ */
+void TFT_drawPixel(int16_t x, int16_t y, color_t color, uint8_t sel);
+
+/**
+ * @brief Read pixel color value from display GRAM at given x,y coordinates
+ * @param[in] x horizontal position
+ * @param[in] y vertical position
+ */
+color_t TFT_readPixel(int16_t x, int16_t y);
+
+/**
+ * @brief Draw vertical line at given x,y coordinates
+ * 
+ * @param x horizontal start position
+ * @param y vertical start position
+ * @param h line height in pixels
+ * @param color line color
+ */
+void TFT_drawFastVLine(int16_t x, int16_t y, int16_t h, color_t color);
+
+/**
+ * @brief Draw horizontal line at given x,y coordinates
+ * 
+ * @param x horizontal start position
+ * @param y vertical start position
+ * @param w line width in pixels
+ * @param color line color
+ */
+void TFT_drawFastHLine(int16_t x, int16_t y, int16_t w, color_t color);
+
+/**
+ * @brief Draw line on screen
+ * 
+ * @param x0 horizontal start position
+ * @param y0 vertical start position
+ * @param x1 horizontal end position
+ * @param y1 vertical end position
+ * @param color line color
+ */
+void TFT_drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, color_t color);
+
+
+/**
+ * @brief Draw line on screen from (x,y) point at given angle
+ *        Line drawing angle starts at lower right quadrant of the screen and is offseted by
+ *        '_angleOffset' global variable (default: -90 degrees)
+ *
+ * @param x horizontal start position
+ * @param y vertical start position
+ * @param start start offset from (x,y)
+ * @param len length of the line
+ * @param angle line angle in degrees
+ * @param color line color
+ */
+void TFT_drawLineByAngle(uint16_t x, uint16_t y, uint16_t start, uint16_t len, uint16_t angle, color_t color);
+
+/**
+ * @brief Fill given rectangular screen region with color
+ * 
+ * @param x horizontal rect start position
+ * @param y vertical rect start position
+ * @param w rectangle width
+ * @param h rectangle height
+ * @param color fill color
+ */
+void TFT_fillRect(int16_t x, int16_t y, int16_t w, int16_t h, color_t color);
+
+/**
+ * @brief Draw rectangle on screen
+ * 
+ * @param x horizontal rect start position
+ * @param y vertical rect start position
+ * @param w rectangle width
+ * @param h rectangle height
+ * @param color rect line color
+ */
+void TFT_drawRect(uint16_t x1,uint16_t y1,uint16_t w,uint16_t h, color_t color);
+
+/**
+ * @brief Draw rectangle with rounded corners on screen
+ * 
+ * @param x horizontal rect start position
+ * @param y vertical rect start position
+ * @param w rectangle width
+ * @param h rectangle height
+ * @param r corner radius
+ * @param color rectangle color
+ */
+void TFT_drawRoundRect(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t r, color_t color);
+
+/**
+ * @brief Fill given rectangular screen region with rounded corners with color
+ * 
+ * @param x horizontal rect start position
+ * @param y vertical rect start position
+ * @param w rectangle width
+ * @param h rectangle height
+ * @param r corner radius
+ * @param color fill color
+ */
+void TFT_fillRoundRect(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t r, color_t color);
+
+/**
+ * @brief Fill the whole screen with color
+ * 
+ * @param color fill color
+ */
+void TFT_fillScreen(color_t color);
+
+/**
+ * @brief Fill the current clip window with color
+ *
+ * @param color fill color
+ */
+void TFT_fillWindow(color_t color);
+
+/**
+ * @brief Draw triangle on screen
+ * 
+ * @param x0 first triangle point x position
+ * @param y0 first triangle point y position
+ * @param x0 second triangle point x position
+ * @param y0 second triangle point y position
+ * @param x0 third triangle point x position
+ * @param y0 third triangle point y position
+ * @param color triangle color
+ */
+void TFT_drawTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color);
+
+/**
+ * @brief Fill triangular screen region with color
+ * 
+ * @param x0 first triangle point x position
+ * @param y0 first triangle point y position
+ * @param x0 second triangle point x position
+ * @param y0 second triangle point y position
+ * @param x0 third triangle point x position
+ * @param y0 third triangle point y position
+ * @param color fill color
+ */
+void TFT_fillTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, color_t color);
+
+/**
+ * @brief Draw circle on screen
+ * 
+ * @param x circle center x position
+ * @param y circle center x position
+ * @param r circle radius
+ * @param color circle color
+ */
+void TFT_drawCircle(int16_t x, int16_t y, int radius, color_t color);
+
+/**
+ * @brief Fill circle on screen with color
+ * 
+ * @param x circle center x position
+ * @param y circle center x position
+ * @param r circle radius
+ * @param color circle fill color
+ */
+void TFT_fillCircle(int16_t x, int16_t y, int radius, color_t color);
+
+/**
+ * @brief Draw ellipse on screen
+ * 
+ * @param x0 ellipse center x position
+ * @param y0 ellipse center x position
+ * @param rx ellipse horizontal radius
+ * @param ry ellipse vertical radius
+ * @param option drawing options, multiple options can be combined
+ *
+ *               1 (TFT_ELLIPSE_UPPER_RIGHT) draw upper right corner
+ *               2 (TFT_ELLIPSE_UPPER_LEFT)  draw upper left corner
+ *               4 (TFT_ELLIPSE_LOWER_LEFT)  draw lower left corner
+ *               8 (TFT_ELLIPSE_LOWER_RIGHT) draw lower right corner
+ *             to draw the whole ellipse use option value 15 (1 | 2 | 4 | 8)
+ * @param color: circle color
+ */
+void TFT_drawEllipse(uint16_t x0, uint16_t y0, uint16_t rx, uint16_t ry, color_t color, uint8_t option);
+
+/**
+ * @brief Fill elliptical region on screen
+ * 
+ * @param x0 ellipse center x position
+ * @param y0 ellipse center x position
+ * @param rx ellipse horizontal radius
+ * @param ry ellipse vertical radius
+ * @param option drawing options, multiple options can be combined
+ *
+ *              1 (TFT_ELLIPSE_UPPER_RIGHT) fill upper right corner
+ *              2 (TFT_ELLIPSE_UPPER_LEFT)  fill upper left corner
+ *              4 (TFT_ELLIPSE_LOWER_LEFT)  fill lower left corner
+ *              8 (TFT_ELLIPSE_LOWER_RIGHT) fill lower right corner
+ *            to fill the whole ellipse use option value 15 (1 | 2 | 4 | 8)
+ * @param color fill color
+ */
+void TFT_fillEllipse(uint16_t x0, uint16_t y0, uint16_t rx, uint16_t ry, color_t color, uint8_t option);
+
+
+/**
+ * @brief Draw circle arc on screen
+ *         Arc drawing angle starts at lower right quadrant of the screen and is offseted by
+ *         '_angleOffset' global variable (default: -90 degrees)
+ *
+ * @param cx arc center X position
+ * @param cy arc center Y position
+ * @param th thickness of the drawn arc
+ * @param ry arc vertical radius
+ * @param start arc start angle in degrees
+ * @param end arc end angle in degrees
+ * @param color arc outline color
+ * @param fillcolor arc fill color
+ */
+void TFT_drawArc(uint16_t cx, uint16_t cy, uint16_t r, uint16_t th, float start, float end, color_t color, color_t fillcolor);
+
+/**
+ * @brief Draw polygon on screen
+ *
+ * @param cx polygon center X position
+ * @param cy arc center Y position
+ * @param sides number of polygon sides; MAX_POLIGON_SIDES ~ MAX_POLIGON_SIDES (3 ~ 60)
+ * @param diameter diameter of the circle inside which the polygon is drawn
+ * @param color polygon outline color
+ * @param fill polygon fill color; if same as color, polygon is not filled
+ * @param deg polygon rotation angle; 0 ~ 360
+ * @param th thickness of the polygon outline
+ */
+void TFT_drawPolygon(int cx, int cy, int sides, int diameter, color_t color, color_t fill, int deg, uint8_t th);
+
+/**
+ * @brief Set the font used for writing the text to display.
+ *
+ * ------------------------------------------------------------------------------------
+ * For 7 segment font only characters 0,1,2,3,4,5,6,7,8,9, . , - , : , / are available.
+ *   Character ‘/‘ draws the degree sign.
+ * ------------------------------------------------------------------------------------
+ *
+ * @param font font number; use defined font names
+ * @param font_file pointer to font file name; NULL for embeded fonts
+ */
+void TFT_setFont(uint8_t font, const char *font_file);
+
+/**
+ * @brief Returns current font height & width in pixels.
+ *
+ * @param[in] width pointer to returned font width
+ * @param[in] height pointer to returned font height
+ */
+int TFT_getfontsize(int *width, int* height);
+
+/**
+ * @brief Returns current font height in pixels.
+ * @return The font height in pixels.
+ */
+int TFT_getfontheight();
+
+/**
+ * @brief Write text to display.
+ *
+ * Rotation of the displayed text depends on 'font_rotate' variable (0~360)
+ * if 'font_transparent' variable is set to 1, no background pixels will be printed
+ *
+ * If the text does not fit the screen width it will be clipped (if text_wrap=0),
+ * or continued on next line (if text_wrap=1)
+ *
+ * Two special characters are allowed in strings:
+ *
+ * 		‘\r’ CR (0x0D), clears the display to EOL
+ * 		‘\n’ LF (ox0A), continues to the new line, x=0
+ *
+ * @param st pointer to null terminated string to be printed
+ * @param x horizontal position of the upper left point in pixels. Special values can be entered:
+ *
+ *		CENTER, centers the text
+ *		RIGHT, right justifies the text
+ *		LASTX, continues from last X position; offset can be used: LASTX+n
+ *
+ * @param y vertical position of the upper left point in pixels. Special values can be entered:
+ *
+ *		CENTER, centers the text
+ *		BOTTOM, bottom justifies the text
+ *		LASTY, continues from last Y position; offset can be used: LASTY+n
+ *
+ */
+void TFT_print(char *st, int x, int y);
+
+/**
+ * @brief Set atributes for 7 segment vector font
+ * @note 7 segment font must be the current font to this function to have effect
+ *
+ * @param l 6~40; distance between bars in pixels
+ * @param w 1~12, max l/2;  bar width in pixels
+ * @param outline draw font outline if set to 1
+ * @param color	font outline color, only used if outline=1
+ */
+void set_7seg_font_atrib(uint8_t l, uint8_t w, int outline, color_t color);
+
+/**
+ * @brief Sets the clipping area coordinates.
+ *        All writing to screen is clipped to that area.
+ *        Starting x & y in all functions will be adjusted to the clipping area.
+ *
+ * @param x1,y1	upper left point of the clipping area
+ * @param x2,y2	bottom right point of the clipping area
+ */
+void TFT_setclipwin(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
+
+/**
+ * @brief Resets the clipping area to full screen (0,0),(_wodth,_height)
+ */
+void TFT_resetclipwin();
+
+/**
+ * @brief Save current clipping area to temporary variable
+ */
+void TFT_saveClipWin();
+
+/**
+ * @brief Restore current clipping area from temporary variable
+ */
+void TFT_restoreClipWin();
+
+/**
+ * @brief Set the screen rotation
+ *        Also resets the clip window and clears the screen with current background color
+ *
+ * @param rot 0~3; screen rotation; use defined rotation constants:
+ * 	      PORTRAIT, LANDSCAPE, PORTRAIT_FLIP, LANDSCAPE_FLIP
+ */
+void TFT_setRotation(uint8_t rot);
+
+/**
+ * @brief Set inverted/normal colors
+ *
+ * @param mode 0 or 1; use defined constants: INVERT_ON or INVERT_OFF
+ */
+void TFT_invertDisplay(const uint8_t mode);
+
+/**
+ * @brief Select gamma curve
+ * @param gm gama curve, values 0~3
+ */
+void TFT_setGammaCurve(uint8_t gm);
+
+/**
+ * @brief Compare two color structures
+ * @return 0 if equal, 1 if not equal
+ *
+ * @param c1, c2 colors to be compared
+ */
+int TFT_compare_colors(color_t c1, color_t c2);
+
+/**
+ * @brief returns the string width in pixels. Useful for positions strings on the screen.
+ * @return The string width.
+ */
+int TFT_getStringWidth(char* str);
+
+
+/**
+ * @brief Fills the rectangle occupied by string with current background color
+ * @param x X position
+ * @param Y Y position
+ * @param str The string
+ */
+void TFT_clearStringRect(int x, int y, char *str);
+
+/**
+ * @brief Converts the components of a color, as specified by the HSB model,
+ *        to an equivalent set of values for the default RGB model.
+ *
+ *        The color structure that is returned by HSBtoRGB encodes the value of a color as R, G & B component
+ *
+ * @param _hue any number, the floor of this number is subtracted from it to create a fraction between 0 and 1.
+ * 	       This fractional number is then multiplied by 360 to produce the hue angle in the HSB color model.
+ * @param _sat 0 ~ 1.0
+ * @param _brightness 0 ~ 1.0
+ */
+color_t HSBtoRGB(float _hue, float _sat, float _brightness);
+
+/**
+ * @brief Decodes and displays JPG image
+ *        Limits:
+ * 		Baseline only. Progressive and Lossless JPEG format are not supported.
+ *		Image size: Up to 65520 x 65520 pixels
+ *		Color space: YCbCr three components only. Gray scale image is not supported.
+ *		Sampling factor: 4:4:4, 4:2:2 or 4:2:0.
+ *
+ * @param x image left position; constants CENTER & RIGHT can be used; negative value is accepted
+ * @param y image top position;  constants CENTER & BOTTOM can be used; negative value is accepted
+ * @param scale image scale factor: 0~3; if scale>0, image is scaled by factor 1/(2^scale) (1/2, 1/4 or 1/8)
+ * @param fname pointer to the name of the file from which the image will be read
+ *   		if set to NULL, image will be read from memory buffer pointed to by 'buf'
+ * @param buf pointer to the memory buffer from which the image will be read; used if fname=NULL
+ * @param size size of the memory buffer from which the image will be read; used if fname=NULL & buf!=NULL
+ */
+void TFT_jpg_image(int x, int y, uint8_t scale, char *fname, uint8_t *buf, int size);
+
+/**
+ * @brief Decodes and displays BMP image
+ *        Only uncompressed RGB 24-bit with no color space information BMP images can be displayed
+ *
+ * @param x image left position; constants CENTER & RIGHT can be used; negative value is accepted
+ * @param y image top position;  constants CENTER & BOTTOM can be used; negative value is accepted
+ * @param scale image scale factor: 0~7; if scale>0, image is scaled by factor 1/(scale+1)
+ * @param fname pointer to the name of the file from which the image will be read
+ *   		if set to NULL, image will be read from memory buffer pointed to by 'imgbuf'
+ * @param imgbuf pointer to the memory buffer from which the image will be read; used if fname=NULL
+ * @param size size of the memory buffer from which the image will be read; used if fname=NULL & imgbuf!=NULL
+ */
+int TFT_bmp_image(int x, int y, uint8_t scale, char *fname, uint8_t *imgbuf, int size);
+
+/**
+ * @brief Get the touch panel coordinates.
+ *        The coordinates are adjusted to screen orientation if raw=0
+ *
+ * @param[in] x pointer to X coordinate
+ * @param[in] y pointer to Y coordinate
+ * @param raw if 0 returns calibrated screen coordinates; if 1 returns raw touch controller coordinates
+ * @return 0 if touch panel is not touched; x=y=0
+ *         1 if touch panel is touched; x&y are the valid coordinates
+ */
+int TFT_read_touch(int *x, int* y, uint8_t raw);
+
+
+/**
+ * @brief Compile font c source file to .fnt file
+ *        which can be used in TFT_setFont() function to select external font
+ *        Created file have the same name as source file and extension .fnt
+ *
+ * @param fontfile pointer to c source font file name; must have .c extension
+ * @param dbg if set to 1, prints debug information
+ *
+ * @return 0 on success, err no on error
+ */
+int compile_font_file(char *fontfile, uint8_t dbg);
+
+/**
+ * @brief Get all font's characters to buffer
+ */
+void getFontCharacters(uint8_t *buf);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/tftspi.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,951 @@
+/*
+ *  Author: LoBo (loboris@gmail.com, loboris.github)
+ *
+ *  Module supporting SPI TFT displays based on ILI9341 & ILI9488 controllers
+ * 
+ * HIGH SPEED LOW LEVEL DISPLAY FUNCTIONS
+ * USING DIRECT or DMA SPI TRANSFER MODEs
+ *
+*/
+
+#include <string.h>
+#include "tftspi.h"
+#include "esp_system.h"
+#include "freertos/task.h"
+#include "esp_heap_caps.h"
+#include "soc/spi_reg.h"
+
+// ====================================================
+// ==== Global variables, default values ==============
+
+// Converts colors to grayscale if set to 1
+uint8_t gray_scale = 0;
+// Spi clock for reading data from display memory in Hz
+uint32_t max_rdclock = 8000000;
+
+// Default display dimensions
+int _width = DEFAULT_TFT_DISPLAY_WIDTH;
+int _height = DEFAULT_TFT_DISPLAY_HEIGHT;
+
+// Display type, DISP_TYPE_ILI9488 or DISP_TYPE_ILI9341
+uint8_t tft_disp_type = DEFAULT_DISP_TYPE;
+
+// Spi device handles for display and touch screen
+spi_lobo_device_handle_t disp_spi = NULL;
+spi_lobo_device_handle_t ts_spi = NULL;
+
+// ====================================================
+
+
+static color_t *trans_cline = NULL;
+static uint8_t _dma_sending = 0;
+
+// RGB to GRAYSCALE constants
+// 0.2989  0.5870  0.1140
+#define GS_FACT_R 0.2989
+#define GS_FACT_G 0.4870
+#define GS_FACT_B 0.2140
+
+
+
+// ==== Functions =====================
+
+//------------------------------------------------------
+esp_err_t IRAM_ATTR wait_trans_finish(uint8_t free_line)
+{
+	// Wait for SPI bus ready
+	while (disp_spi->host->hw->cmd.usr);
+	if ((free_line) && (trans_cline)) {
+		free(trans_cline);
+		trans_cline = NULL;
+	}
+	if (_dma_sending) {
+	    //Tell common code DMA workaround that our DMA channel is idle. If needed, the code will do a DMA reset.
+	    if (disp_spi->host->dma_chan) spi_lobo_dmaworkaround_idle(disp_spi->host->dma_chan);
+
+	    // Reset DMA
+		disp_spi->host->hw->dma_conf.val |= SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST;
+		disp_spi->host->hw->dma_out_link.start=0;
+		disp_spi->host->hw->dma_in_link.start=0;
+		disp_spi->host->hw->dma_conf.val &= ~(SPI_OUT_RST|SPI_IN_RST|SPI_AHBM_RST|SPI_AHBM_FIFO_RST);
+		disp_spi->host->hw->dma_conf.out_data_burst_en=1;
+		_dma_sending = 0;
+	}
+    return ESP_OK;
+}
+
+//-------------------------------
+esp_err_t IRAM_ATTR disp_select()
+{
+	wait_trans_finish(1);
+	return spi_lobo_device_select(disp_spi, 0);
+}
+
+//---------------------------------
+esp_err_t IRAM_ATTR disp_deselect()
+{
+	wait_trans_finish(1);
+	return spi_lobo_device_deselect(disp_spi);
+}
+
+//---------------------------------------------------------------------------------------------------
+static void IRAM_ATTR _spi_transfer_start(spi_lobo_device_handle_t spi_dev, int wrbits, int rdbits) {
+	// Load send buffer
+	spi_dev->host->hw->user.usr_mosi_highpart = 0;
+	spi_dev->host->hw->mosi_dlen.usr_mosi_dbitlen = wrbits-1;
+	spi_dev->host->hw->user.usr_mosi = 1;
+    if (rdbits) {
+        spi_dev->host->hw->miso_dlen.usr_miso_dbitlen = rdbits;
+        spi_dev->host->hw->user.usr_miso = 1;
+    }
+    else {
+        spi_dev->host->hw->miso_dlen.usr_miso_dbitlen = 0;
+        spi_dev->host->hw->user.usr_miso = 0;
+    }
+	// Start transfer
+	spi_dev->host->hw->cmd.usr = 1;
+    // Wait for SPI bus ready
+	while (spi_dev->host->hw->cmd.usr);
+}
+
+// Send 1 byte display command, display must be selected
+//------------------------------------------------
+void IRAM_ATTR disp_spi_transfer_cmd(int8_t cmd) {
+	// Wait for SPI bus ready
+	while (disp_spi->host->hw->cmd.usr);
+
+	// Set DC to 0 (command mode);
+    gpio_set_level(PIN_NUM_DC, 0);
+
+    disp_spi->host->hw->data_buf[0] = (uint32_t)cmd;
+    _spi_transfer_start(disp_spi, 8, 0);
+}
+
+// Send command with data to display, display must be selected
+//----------------------------------------------------------------------------------
+void IRAM_ATTR disp_spi_transfer_cmd_data(int8_t cmd, uint8_t *data, uint32_t len) {
+	// Wait for SPI bus ready
+	while (disp_spi->host->hw->cmd.usr);
+
+    // Set DC to 0 (command mode);
+    gpio_set_level(PIN_NUM_DC, 0);
+
+    disp_spi->host->hw->data_buf[0] = (uint32_t)cmd;
+    _spi_transfer_start(disp_spi, 8, 0);
+
+	if ((len == 0) || (data == NULL)) return;
+
+    // Set DC to 1 (data mode);
+	gpio_set_level(PIN_NUM_DC, 1);
+
+	uint8_t idx=0, bidx=0;
+	uint32_t bits=0;
+	uint32_t count=0;
+	uint32_t wd = 0;
+	while (count < len) {
+		// get data byte from buffer
+		wd |= (uint32_t)data[count] << bidx;
+    	count++;
+    	bits += 8;
+		bidx += 8;
+    	if (count == len) {
+    		disp_spi->host->hw->data_buf[idx] = wd;
+    		break;
+    	}
+		if (bidx == 32) {
+			disp_spi->host->hw->data_buf[idx] = wd;
+			idx++;
+			bidx = 0;
+			wd = 0;
+		}
+    	if (idx == 16) {
+    		// SPI buffer full, send data
+			_spi_transfer_start(disp_spi, bits, 0);
+    		
+			bits = 0;
+    		idx = 0;
+			bidx = 0;
+    	}
+    }
+    if (bits > 0) _spi_transfer_start(disp_spi, bits, 0);
+}
+
+// Set the address window for display write & read commands, display must be selected
+//---------------------------------------------------------------------------------------------------
+static void IRAM_ATTR disp_spi_transfer_addrwin(uint16_t x1, uint16_t x2, uint16_t y1, uint16_t y2) {
+	uint32_t wd;
+
+    taskDISABLE_INTERRUPTS();
+	// Wait for SPI bus ready
+	while (disp_spi->host->hw->cmd.usr);
+    gpio_set_level(PIN_NUM_DC, 0);
+
+	disp_spi->host->hw->data_buf[0] = (uint32_t)TFT_CASET;
+	disp_spi->host->hw->user.usr_mosi_highpart = 0;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 7;
+	disp_spi->host->hw->user.usr_mosi = 1;
+	disp_spi->host->hw->miso_dlen.usr_miso_dbitlen = 0;
+	disp_spi->host->hw->user.usr_miso = 0;
+
+	disp_spi->host->hw->cmd.usr = 1; // Start transfer
+
+	wd = (uint32_t)(x1>>8);
+	wd |= (uint32_t)(x1&0xff) << 8;
+	wd |= (uint32_t)(x2>>8) << 16;
+	wd |= (uint32_t)(x2&0xff) << 24;
+
+	while (disp_spi->host->hw->cmd.usr); // wait transfer end
+	gpio_set_level(PIN_NUM_DC, 1);
+	disp_spi->host->hw->data_buf[0] = wd;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 31;
+	disp_spi->host->hw->cmd.usr = 1; // Start transfer
+
+    while (disp_spi->host->hw->cmd.usr);
+    gpio_set_level(PIN_NUM_DC, 0);
+    disp_spi->host->hw->data_buf[0] = (uint32_t)TFT_PASET;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 7;
+	disp_spi->host->hw->cmd.usr = 1; // Start transfer
+
+	wd = (uint32_t)(y1>>8);
+	wd |= (uint32_t)(y1&0xff) << 8;
+	wd |= (uint32_t)(y2>>8) << 16;
+	wd |= (uint32_t)(y2&0xff) << 24;
+
+	while (disp_spi->host->hw->cmd.usr);
+	gpio_set_level(PIN_NUM_DC, 1);
+
+	disp_spi->host->hw->data_buf[0] = wd;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 31;
+	disp_spi->host->hw->cmd.usr = 1; // Start transfer
+	while (disp_spi->host->hw->cmd.usr);
+    taskENABLE_INTERRUPTS();
+}
+
+// Convert color to gray scale
+//----------------------------------------------
+static color_t IRAM_ATTR color2gs(color_t color)
+{
+	color_t _color;
+    float gs_clr = GS_FACT_R * color.r + GS_FACT_G * color.g + GS_FACT_B * color.b;
+    if (gs_clr > 255) gs_clr = 255;
+
+    _color.r = (uint8_t)gs_clr;
+    _color.g = (uint8_t)gs_clr;
+    _color.b = (uint8_t)gs_clr;
+
+    return _color;
+}
+
+// Set display pixel at given coordinates to given color
+//------------------------------------------------------------------------
+void IRAM_ATTR drawPixel(int16_t x, int16_t y, color_t color, uint8_t sel)
+{
+	if (!(disp_spi->cfg.flags & LB_SPI_DEVICE_HALFDUPLEX)) return;
+
+	if (sel) {
+		if (disp_select()) return;
+	}
+	else wait_trans_finish(1);
+
+	uint32_t wd = 0;
+    color_t _color = color;
+	if (gray_scale) _color = color2gs(color);
+
+    taskDISABLE_INTERRUPTS();
+	disp_spi_transfer_addrwin(x, x+1, y, y+1);
+
+	// Send RAM WRITE command
+    gpio_set_level(PIN_NUM_DC, 0);
+    disp_spi->host->hw->data_buf[0] = (uint32_t)TFT_RAMWR;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 7;
+	disp_spi->host->hw->cmd.usr = 1;		// Start transfer
+	while (disp_spi->host->hw->cmd.usr);	// Wait for SPI bus ready
+
+	wd = (uint32_t)_color.r;
+	wd |= (uint32_t)_color.g << 8;
+	wd |= (uint32_t)_color.b << 16;
+
+    // Set DC to 1 (data mode);
+	gpio_set_level(PIN_NUM_DC, 1);
+
+	disp_spi->host->hw->data_buf[0] = wd;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 23;
+	disp_spi->host->hw->cmd.usr = 1;		// Start transfer
+	while (disp_spi->host->hw->cmd.usr);	// Wait for SPI bus ready
+
+    taskENABLE_INTERRUPTS();
+   if (sel) disp_deselect();
+}
+
+//-----------------------------------------------------------
+static void IRAM_ATTR _dma_send(uint8_t *data, uint32_t size)
+{
+    //Fill DMA descriptors
+    spi_lobo_dmaworkaround_transfer_active(disp_spi->host->dma_chan); //mark channel as active
+    spi_lobo_setup_dma_desc_links(disp_spi->host->dmadesc_tx, size, data, false);
+    disp_spi->host->hw->user.usr_mosi_highpart=0;
+    disp_spi->host->hw->dma_out_link.addr=(int)(&disp_spi->host->dmadesc_tx[0]) & 0xFFFFF;
+    disp_spi->host->hw->dma_out_link.start=1;
+    disp_spi->host->hw->user.usr_mosi_highpart=0;
+
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = (size * 8) - 1;
+
+	_dma_sending = 1;
+	// Start transfer
+	disp_spi->host->hw->cmd.usr = 1;
+}
+
+//---------------------------------------------------------------------------
+static void IRAM_ATTR _direct_send(color_t *color, uint32_t len, uint8_t rep)
+{
+	uint32_t cidx = 0;	// color buffer index
+	uint32_t wd = 0;
+	int idx = 0;
+	int bits = 0;
+	int wbits = 0;
+
+    taskDISABLE_INTERRUPTS();
+	color_t _color = color[0];
+	if ((rep) && (gray_scale)) _color = color2gs(color[0]);
+
+	while (len) {
+		// ** Get color data from color buffer **
+		if (rep == 0) {
+			if (gray_scale) _color = color2gs(color[cidx]);
+			else _color = color[cidx];
+		}
+
+		wd |= (uint32_t)_color.r << wbits;
+		wbits += 8;
+		if (wbits == 32) {
+			bits += wbits;
+			wbits = 0;
+			disp_spi->host->hw->data_buf[idx++] = wd;
+			wd = 0;
+		}
+		wd |= (uint32_t)_color.g << wbits;
+		wbits += 8;
+		if (wbits == 32) {
+			bits += wbits;
+			wbits = 0;
+			disp_spi->host->hw->data_buf[idx++] = wd;
+			wd = 0;
+		}
+		wd |= (uint32_t)_color.b << wbits;
+		wbits += 8;
+		if (wbits == 32) {
+			bits += wbits;
+			wbits = 0;
+			disp_spi->host->hw->data_buf[idx++] = wd;
+			wd = 0;
+		}
+    	len--;					// Decrement colors counter
+        if (rep == 0) cidx++;	// if not repeating color, increment color buffer index
+    }
+	if (bits) {
+		while (disp_spi->host->hw->cmd.usr);						// Wait for SPI bus ready
+		disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = bits-1;	// set number of bits to be sent
+        disp_spi->host->hw->cmd.usr = 1;							// Start transfer
+	}
+    taskENABLE_INTERRUPTS();
+}
+
+// ================================================================
+// === Main function to send data to display ======================
+// If  rep==true:  repeat sending color data to display 'len' times
+// If rep==false:  send 'len' color data from color buffer to display
+// ** Device must already be selected and address window set **
+// ================================================================
+//----------------------------------------------------------------------------------------------
+static void IRAM_ATTR _TFT_pushColorRep(color_t *color, uint32_t len, uint8_t rep, uint8_t wait)
+{
+	if (len == 0) return;
+	if (!(disp_spi->cfg.flags & LB_SPI_DEVICE_HALFDUPLEX)) return;
+
+	// Send RAM WRITE command
+    gpio_set_level(PIN_NUM_DC, 0);
+    disp_spi->host->hw->data_buf[0] = (uint32_t)TFT_RAMWR;
+	disp_spi->host->hw->mosi_dlen.usr_mosi_dbitlen = 7;
+	disp_spi->host->hw->cmd.usr = 1;		// Start transfer
+	while (disp_spi->host->hw->cmd.usr);	// Wait for SPI bus ready
+
+	gpio_set_level(PIN_NUM_DC, 1);								// Set DC to 1 (data mode);
+
+	if ((len*24) <= 512) {
+
+		_direct_send(color, len, rep);
+
+	}
+	else if (rep == 0)  {
+		// ==== use DMA transfer ====
+		// ** Prepare data
+		if (gray_scale) {
+			for (int n=0; n<len; n++) {
+				color[n] = color2gs(color[n]);
+			}
+	    }
+
+	    _dma_send((uint8_t *)color, len*3);
+	}
+	else {
+		// ==== Repeat color, more than 512 bits total ====
+
+		color_t _color;
+		uint32_t buf_colors;
+		int buf_bytes, to_send;
+
+		/*
+		to_send = len;
+		while (to_send > 0) {
+			wait_trans_finish(0);
+			_direct_send(color, ((to_send > 21) ? 21 : to_send), rep);
+			to_send -= 21;
+		}
+		*/
+
+		buf_colors = ((len > (_width*2)) ? (_width*2) : len);
+		buf_bytes = buf_colors * 3;
+
+		// Prepare color buffer of maximum 2 color lines
+		trans_cline = heap_caps_malloc(buf_bytes, MALLOC_CAP_DMA);
+		if (trans_cline == NULL) return;
+
+		// Prepare fill color
+		if (gray_scale) _color = color2gs(color[0]);
+		else _color = color[0];
+
+		// Fill color buffer with fill color
+		for (uint32_t i=0; i<buf_colors; i++) {
+			trans_cline[i] = _color;
+		}
+
+		// Send 'len' colors
+		to_send = len;
+		while (to_send > 0) {
+			wait_trans_finish(0);
+			_dma_send((uint8_t *)trans_cline, ((to_send > buf_colors) ? buf_bytes : (to_send*3)));
+			to_send -= buf_colors;
+		}
+	}
+
+	if (wait) wait_trans_finish(1);
+}
+
+// Write 'len' color data to TFT 'window' (x1,y2),(x2,y2)
+//-------------------------------------------------------------------------------------------
+void IRAM_ATTR TFT_pushColorRep(int x1, int y1, int x2, int y2, color_t color, uint32_t len)
+{
+	if (disp_select() != ESP_OK) return;
+
+	// ** Send address window **
+	disp_spi_transfer_addrwin(x1, x2, y1, y2);
+
+	_TFT_pushColorRep(&color, len, 1, 1);
+
+	disp_deselect();
+}
+
+// Write 'len' color data to TFT 'window' (x1,y2),(x2,y2) from given buffer
+// ** Device must already be selected **
+//-----------------------------------------------------------------------------------
+void IRAM_ATTR send_data(int x1, int y1, int x2, int y2, uint32_t len, color_t *buf)
+{
+	// ** Send address window **
+	disp_spi_transfer_addrwin(x1, x2, y1, y2);
+	_TFT_pushColorRep(buf, len, 0, 0);
+}
+
+// Reads 'len' pixels/colors from the TFT's GRAM 'window'
+// 'buf' is an array of bytes with 1st byte reserved for reading 1 dummy byte
+// and the rest is actually an array of color_t values
+//--------------------------------------------------------------------------------------------
+int IRAM_ATTR read_data(int x1, int y1, int x2, int y2, int len, uint8_t *buf, uint8_t set_sp)
+{
+	spi_lobo_transaction_t t;
+	uint32_t current_clock = 0;
+
+    memset(&t, 0, sizeof(t));  //Zero out the transaction
+	memset(buf, 0, len*sizeof(color_t));
+
+	if (set_sp) {
+		if (disp_deselect() != ESP_OK) return -1;
+		// Change spi clock if needed
+		current_clock = spi_lobo_get_speed(disp_spi);
+		if (max_rdclock < current_clock) spi_lobo_set_speed(disp_spi, max_rdclock);
+	}
+
+	if (disp_select() != ESP_OK) return -2;
+
+	// ** Send address window **
+	disp_spi_transfer_addrwin(x1, x2, y1, y2);
+
+    // ** GET pixels/colors **
+	disp_spi_transfer_cmd(TFT_RAMRD);
+
+    t.length=0;                //Send nothing
+    t.tx_buffer=NULL;
+    t.rxlength=8*((len*3)+1);  //Receive size in bits
+    t.rx_buffer=buf;
+    //t.user = (void*)1;
+
+	esp_err_t res = spi_lobo_transfer_data(disp_spi, &t); // Receive using direct mode
+
+	disp_deselect();
+
+	if (set_sp) {
+		// Restore spi clock if needed
+		if (max_rdclock < current_clock) spi_lobo_set_speed(disp_spi, current_clock);
+	}
+
+    return res;
+}
+
+// Reads one pixel/color from the TFT's GRAM at position (x,y)
+//-----------------------------------------------
+color_t IRAM_ATTR readPixel(int16_t x, int16_t y)
+{
+    uint8_t color_buf[sizeof(color_t)+1] = {0};
+
+    read_data(x, y, x+1, y+1, 1, color_buf, 1);
+
+    color_t color;
+	color.r = color_buf[1];
+	color.g = color_buf[2];
+	color.b = color_buf[3];
+	return color;
+}
+
+// get 16-bit data from touch controller for specified type
+// ** Touch device must already be selected **
+//----------------------------------------
+int IRAM_ATTR touch_get_data(uint8_t type)
+{
+    /*
+    esp_err_t ret;
+    spi_lobo_transaction_t t;
+    memset(&t, 0, sizeof(t));            //Zero out the transaction
+    uint8_t rxdata[2] = {0};
+
+    // send command byte & receive 2 byte response
+    t.rxlength=8*2;
+    t.rx_buffer=&rxdata;
+    t.command = type;
+
+    ret = spi_lobo_transfer_data(ts_spi, &t);    // Transmit using direct mode
+
+    if (ret != ESP_OK) res = -1;
+    res = (((int)(rxdata[0] << 8) | (int)(rxdata[1])) >> 4);
+*/
+//    spi_lobo_device_select(ts_spi, 0);
+
+    ts_spi->host->hw->data_buf[0] = type;
+    _spi_transfer_start(ts_spi, 24, 24);
+
+//    printf("touch_get_data(%02X) %06x %02x %02x %02x ", type, ts_spi->host->hw->data_buf[0],
+//		    (ts_spi->host->hw->data_buf[0] & 0x0ff), 
+//		    (ts_spi->host->hw->data_buf[0] >> 11 & 0x0ff), 
+//		    (ts_spi->host->hw->data_buf[0] >> 19 & 0x0ff) );
+    uint16_t res = (uint16_t)(((ts_spi->host->hw->data_buf[0] >> 11 & 0x0ff) << 8) | (ts_spi->host->hw->data_buf[0] >> 19 & 0x0ff));
+//    printf("res=%d %04x\n", res, res);
+//    spi_lobo_device_deselect(ts_spi);
+    return res;
+}
+
+// ==== STMPE610 ===============================================================
+
+
+// ----- STMPE610 --------------------------------------------------------------------------
+
+// Send 1 byte display command, display must be selected
+//---------------------------------------------------------
+static void IRAM_ATTR stmpe610_write_reg(uint8_t reg, uint8_t val) {
+
+    spi_lobo_device_select(ts_spi, 0);
+
+    ts_spi->host->hw->data_buf[0] = (val << 8) | reg;
+    _spi_transfer_start(ts_spi, 16, 0);
+
+    spi_lobo_device_deselect(ts_spi);
+}
+
+//-----------------------------------------------
+static uint8_t IRAM_ATTR stmpe610_read_byte(uint8_t reg) {
+    spi_lobo_device_select(ts_spi, 0);
+
+    ts_spi->host->hw->data_buf[0] = (reg << 8) | (reg | 0x80);
+    _spi_transfer_start(ts_spi, 16, 16);
+    uint8_t res = ts_spi->host->hw->data_buf[0] >> 8;
+
+    spi_lobo_device_deselect(ts_spi);
+    return res;
+}
+
+//-----------------------------------------
+static uint16_t IRAM_ATTR stmpe610_read_word(uint8_t reg) {
+    spi_lobo_device_select(ts_spi, 0);
+
+    ts_spi->host->hw->data_buf[0] = ((((reg+1) << 8) | ((reg+1) | 0x80)) << 16) | (reg << 8) | (reg | 0x80);
+    _spi_transfer_start(ts_spi, 32, 32);
+    uint16_t res = (uint16_t)(ts_spi->host->hw->data_buf[0] & 0xFF00);
+    res |= (uint16_t)(ts_spi->host->hw->data_buf[0] >> 24);
+
+    spi_lobo_device_deselect(ts_spi);
+    return res;
+}
+
+//-----------------------
+uint32_t stmpe610_getID()
+{
+    uint16_t tid = stmpe610_read_word(0);
+    uint8_t tver = stmpe610_read_byte(2);
+    return (tid << 8) | tver;
+}
+
+//==================
+void stmpe610_Init()
+{
+    stmpe610_write_reg(STMPE610_REG_SYS_CTRL1, 0x02);        // Software chip reset
+    vTaskDelay(10 / portTICK_RATE_MS);
+
+    stmpe610_write_reg(STMPE610_REG_SYS_CTRL2, 0x04);        // Temperature sensor clock off, GPIO clock off, touch clock on, ADC clock on
+
+    stmpe610_write_reg(STMPE610_REG_INT_EN, 0x00);           // Don't Interrupt on INT pin
+
+    stmpe610_write_reg(STMPE610_REG_ADC_CTRL1, 0x48);        // ADC conversion time = 80 clock ticks, 12-bit ADC, internal voltage refernce
+    vTaskDelay(2 / portTICK_RATE_MS);
+    stmpe610_write_reg(STMPE610_REG_ADC_CTRL2, 0x01);        // ADC speed 3.25MHz
+    stmpe610_write_reg(STMPE610_REG_GPIO_AF, 0x00);          // GPIO alternate function - OFF
+    stmpe610_write_reg(STMPE610_REG_TSC_CFG, 0xE3);          // Averaging 8, touch detect delay 1ms, panel driver settling time 1ms
+    stmpe610_write_reg(STMPE610_REG_FIFO_TH, 0x01);          // FIFO threshold = 1
+    stmpe610_write_reg(STMPE610_REG_FIFO_STA, 0x01);         // FIFO reset enable
+    stmpe610_write_reg(STMPE610_REG_FIFO_STA, 0x00);         // FIFO reset disable
+    stmpe610_write_reg(STMPE610_REG_TSC_FRACT_XYZ, 0x07);    // Z axis data format
+    stmpe610_write_reg(STMPE610_REG_TSC_I_DRIVE, 0x01);      // max 50mA touchscreen line current
+    stmpe610_write_reg(STMPE610_REG_TSC_CTRL, 0x30);         // X&Y&Z, 16 reading window
+    stmpe610_write_reg(STMPE610_REG_TSC_CTRL, 0x31);         // X&Y&Z, 16 reading window, TSC enable
+    stmpe610_write_reg(STMPE610_REG_INT_STA, 0xFF);          // Clear all interrupts
+    stmpe610_write_reg(STMPE610_REG_INT_CTRL, 0x00);         // Level interrupt, disable interrupts
+}
+
+//===========================================================
+int stmpe610_get_touch(uint16_t *x, uint16_t *y, uint16_t *z)
+{
+	if (!(stmpe610_read_byte(STMPE610_REG_TSC_CTRL) & 0x80)) return 0;
+
+    // Get touch data
+    uint8_t fifo_size = stmpe610_read_byte(STMPE610_REG_FIFO_SIZE);
+    while (fifo_size < 2) {
+    	if (!(stmpe610_read_byte(STMPE610_REG_TSC_CTRL) & 0x80)) return 0;
+        fifo_size = stmpe610_read_byte(STMPE610_REG_FIFO_SIZE);
+    }
+    while (fifo_size > 120) {
+    	if (!(stmpe610_read_byte(STMPE610_REG_TSC_CTRL) & 0x80)) return 0;
+        *x = stmpe610_read_word(STMPE610_REG_TSC_DATA_X);
+        *y = stmpe610_read_word(STMPE610_REG_TSC_DATA_Y);
+        *z = stmpe610_read_byte(STMPE610_REG_TSC_DATA_Z);
+        fifo_size = stmpe610_read_byte(STMPE610_REG_FIFO_SIZE);
+    }
+    for (uint8_t i=0; i < (fifo_size-1); i++) {
+        *x = stmpe610_read_word(STMPE610_REG_TSC_DATA_X);
+        *y = stmpe610_read_word(STMPE610_REG_TSC_DATA_Y);
+        *z = stmpe610_read_byte(STMPE610_REG_TSC_DATA_Z);
+    }
+
+    *x = 4096 - *x;
+    /*
+    // Clear the rest of the fifo
+    {
+        stmpe610_write_reg(STMPE610_REG_FIFO_STA, 0x01);		// FIFO reset enable
+        stmpe610_write_reg(STMPE610_REG_FIFO_STA, 0x00);		// FIFO reset disable
+    }
+    */
+	return 1;
+}
+
+// ==== STMPE610 ===========================================================================
+
+
+// Find maximum spi clock for successful read from display RAM
+// ** Must be used AFTER the display is initialized **
+//======================
+uint32_t find_rd_speed()
+{
+	esp_err_t ret;
+	color_t color;
+	uint32_t max_speed = 1000000;
+    uint32_t change_speed, cur_speed;
+    int line_check;
+    color_t *color_line = NULL;
+    uint8_t *line_rdbuf = NULL;
+    uint8_t gs = gray_scale;
+
+    gray_scale = 0;
+    cur_speed = spi_lobo_get_speed(disp_spi);
+
+	color_line = malloc(_width*3);
+    if (color_line == NULL) goto exit;
+
+    line_rdbuf = malloc((_width*3)+1);
+	if (line_rdbuf == NULL) goto exit;
+
+	color_t *rdline = (color_t *)(line_rdbuf+1);
+
+	// Fill test line with colors
+	color = (color_t){0xEC,0xA8,0x74};
+	for (int x=0; x<_width; x++) {
+		color_line[x] = color;
+	}
+
+	// Find maximum read spi clock
+	for (uint32_t speed=2000000; speed<=cur_speed; speed += 1000000) {
+		change_speed = spi_lobo_set_speed(disp_spi, speed);
+		if (change_speed == 0) goto exit;
+
+		memset(line_rdbuf, 0, _width*sizeof(color_t)+1);
+
+		if (disp_select()) goto exit;
+		// Write color line
+		send_data(0, _height/2, _width-1, _height/2, _width, color_line);
+		if (disp_deselect()) goto exit;
+
+		// Read color line
+		ret = read_data(0, _height/2, _width-1, _height/2, _width, line_rdbuf, 0);
+
+		// Compare
+		line_check = 0;
+		if (ret == ESP_OK) {
+			for (int y=0; y<_width; y++) {
+				if ((color_line[y].r & 0xFC) != (rdline[y].r & 0xFC)) line_check = 1;
+				else if ((color_line[y].g & 0xFC) != (rdline[y].g & 0xFC)) line_check = 1;
+				else if ((color_line[y].b & 0xFC) != (rdline[y].b & 0xFC)) line_check =  1;
+				if (line_check) break;
+			}
+		}
+		else line_check = ret;
+
+		if (line_check) break;
+		max_speed = speed;
+	}
+
+exit:
+    gray_scale = gs;
+	if (line_rdbuf) free(line_rdbuf);
+	if (color_line) free(color_line);
+
+	// restore spi clk
+	change_speed = spi_lobo_set_speed(disp_spi, cur_speed);
+
+	return max_speed;
+}
+
+//---------------------------------------------------------------------------
+// Companion code to the initialization table.
+// Reads and issues a series of LCD commands stored in byte array
+//---------------------------------------------------------------------------
+static void commandList(spi_lobo_device_handle_t spi, const uint8_t *addr) {
+  uint8_t  numCommands, numArgs, cmd;
+  uint16_t ms;
+
+  numCommands = *addr++;				// Number of commands to follow
+  while(numCommands--) {				// For each command...
+    cmd = *addr++;						// save command
+    numArgs  = *addr++;					// Number of args to follow
+    ms       = numArgs & TFT_CMD_DELAY;	// If high bit set, delay follows args
+    numArgs &= ~TFT_CMD_DELAY;			// Mask out delay bit
+
+	disp_spi_transfer_cmd_data(cmd, (uint8_t *)addr, numArgs);
+
+	addr += numArgs;
+
+    if(ms) {
+      ms = *addr++;              // Read post-command delay time (ms)
+      if(ms == 255) ms = 500;    // If 255, delay for 500 ms
+	  vTaskDelay(ms / portTICK_RATE_MS);
+    }
+  }
+}
+
+//==================================
+void _tft_setRotation(uint8_t rot) {
+	uint8_t rotation = rot & 3; // can't be higher than 3
+	uint8_t send = 1;
+	uint8_t madctl = 0;
+	uint16_t tmp;
+
+    if ((rotation & 1)) {
+        // in landscape modes must be width > height
+        if (_width < _height) {
+            tmp = _width;
+            _width  = _height;
+            _height = tmp;
+        }
+    }
+    else {
+        // in portrait modes must be width < height
+        if (_width > _height) {
+            tmp = _width;
+            _width  = _height;
+            _height = tmp;
+        }
+    }
+    #if TFT_INVERT_ROTATION
+    switch (rotation) {
+        case PORTRAIT:
+        madctl = (MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE:
+        madctl = (MADCTL_MX | TFT_RGB_BGR);
+        break;
+        case PORTRAIT_FLIP:
+        madctl = (MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE_FLIP:
+        madctl = (MADCTL_MY | TFT_RGB_BGR);
+        break;
+    }
+    #elif TFT_INVERT_ROTATION1
+    switch (rotation) {
+        case PORTRAIT:
+        madctl = (MADCTL_MY | MADCTL_MX | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE:
+        madctl = (MADCTL_MY | MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case PORTRAIT_FLIP:
+        madctl = (TFT_RGB_BGR);
+        break;
+        case LANDSCAPE_FLIP:
+        madctl = (MADCTL_MX | MADCTL_MV | TFT_RGB_BGR);
+        break;
+    }
+    #elif TFT_INVERT_ROTATION2
+    switch (rotation) {
+        case PORTRAIT:
+        madctl = (MADCTL_MX | MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE:
+        madctl = (TFT_RGB_BGR);
+        break;
+        case PORTRAIT_FLIP:
+        madctl = (MADCTL_MY | MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE_FLIP:
+        madctl = (MADCTL_MY | MADCTL_MX | TFT_RGB_BGR);
+        break;
+    }
+    #else
+    switch (rotation) {
+        case PORTRAIT:
+        madctl = (MADCTL_MX | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE:
+        madctl = (MADCTL_MV | TFT_RGB_BGR);
+        break;
+        case PORTRAIT_FLIP:
+        madctl = (MADCTL_MY | TFT_RGB_BGR);
+        break;
+        case LANDSCAPE_FLIP:
+        madctl = (MADCTL_MX | MADCTL_MY | MADCTL_MV | TFT_RGB_BGR);
+        break;
+    }
+    #endif
+	if (send) {
+		if (disp_select() == ESP_OK) {
+			disp_spi_transfer_cmd_data(TFT_MADCTL, &madctl, 1);
+			disp_deselect();
+		}
+	}
+
+}
+
+//=================
+void TFT_PinsInit()
+{
+    // Route all used pins to GPIO control
+    gpio_pad_select_gpio(PIN_NUM_CS);
+    gpio_pad_select_gpio(PIN_NUM_MISO);
+    gpio_pad_select_gpio(PIN_NUM_MOSI);
+    gpio_pad_select_gpio(PIN_NUM_CLK);
+    gpio_pad_select_gpio(PIN_NUM_DC);
+
+    gpio_set_direction(PIN_NUM_MISO, GPIO_MODE_INPUT);
+    gpio_set_pull_mode(PIN_NUM_MISO, GPIO_PULLUP_ONLY);
+    gpio_set_direction(PIN_NUM_CS, GPIO_MODE_OUTPUT);
+    gpio_set_direction(PIN_NUM_MOSI, GPIO_MODE_OUTPUT);
+    gpio_set_direction(PIN_NUM_CLK, GPIO_MODE_OUTPUT);
+    gpio_set_direction(PIN_NUM_DC, GPIO_MODE_OUTPUT);
+    gpio_set_level(PIN_NUM_DC, 0);
+#if USE_TOUCH
+    gpio_pad_select_gpio(PIN_NUM_TCS);
+    gpio_set_direction(PIN_NUM_TCS, GPIO_MODE_OUTPUT);
+#endif    
+#if PIN_NUM_BCKL
+    gpio_pad_select_gpio(PIN_NUM_BCKL);
+    gpio_set_direction(PIN_NUM_BCKL, GPIO_MODE_OUTPUT);
+    gpio_set_level(PIN_NUM_BCKL, PIN_BCKL_OFF);
+#endif
+
+#if PIN_NUM_RST
+    gpio_pad_select_gpio(PIN_NUM_RST);
+    gpio_set_direction(PIN_NUM_RST, GPIO_MODE_OUTPUT);
+    gpio_set_level(PIN_NUM_RST, 0);
+#endif
+}
+
+// Initialize the display
+// ====================
+void TFT_display_init()
+{
+    esp_err_t ret;
+
+#if PIN_NUM_RST
+    //Reset the display
+    gpio_set_level(PIN_NUM_RST, 0);
+    vTaskDelay(20 / portTICK_RATE_MS);
+    gpio_set_level(PIN_NUM_RST, 1);
+    vTaskDelay(150 / portTICK_RATE_MS);
+#endif
+
+    ret = disp_select();
+    assert(ret==ESP_OK);
+    //Send all the initialization commands
+	if (tft_disp_type == DISP_TYPE_ILI9341) {
+		commandList(disp_spi, ILI9341_init);
+	}
+	else if (tft_disp_type == DISP_TYPE_ILI9488) {
+		commandList(disp_spi, ILI9488_init);
+	}
+	else if (tft_disp_type == DISP_TYPE_ST7789V) {
+		commandList(disp_spi, ST7789V_init);
+	}
+	else if (tft_disp_type == DISP_TYPE_ST7735) {
+		commandList(disp_spi, STP7735_init);
+	}
+	else if (tft_disp_type == DISP_TYPE_ST7735R) {
+		commandList(disp_spi, STP7735R_init);
+		commandList(disp_spi, Rcmd2green);
+		commandList(disp_spi, Rcmd3);
+	}
+	else if (tft_disp_type == DISP_TYPE_ST7735B) {
+		commandList(disp_spi, STP7735R_init);
+		commandList(disp_spi, Rcmd2red);
+		commandList(disp_spi, Rcmd3);
+	    uint8_t dt = 0xC0;
+		disp_spi_transfer_cmd_data(TFT_MADCTL, &dt, 1);
+	}
+	else assert(0);
+
+    ret = disp_deselect();
+	assert(ret==ESP_OK);
+
+	// Clear screen
+    _tft_setRotation(PORTRAIT);
+	TFT_pushColorRep(0, 0, _width-1, _height-1, (color_t){0,0,0}, (uint32_t)(_height*_width));
+
+	///Enable backlight
+#if PIN_NUM_BCKL
+    gpio_set_level(PIN_NUM_BCKL, PIN_BCKL_ON);
+#endif
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/tftspi.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,674 @@
+/*
+ * 
+ * HIGH SPEED LOW LEVEL DISPLAY FUNCTIONS USING DIRECT TRANSFER MODE
+ *
+*/
+
+#ifndef _TFTSPI_H_
+#define _TFTSPI_H_
+
+#include "tftspi.h"
+#include "spi_master_lobo.h"
+#include "sdkconfig.h"
+#include "stmpe610.h"
+
+#define TOUCH_TYPE_NONE		0
+#define TOUCH_TYPE_XPT2046	1
+#define TOUCH_TYPE_STMPE610	2
+
+//#define TP_CALX_STMPE610	21368532
+//#define TP_CALY_STMPE610	11800144
+
+// === Screen orientation constants ===
+#define PORTRAIT	0
+#define LANDSCAPE	1
+#define PORTRAIT_FLIP	2
+#define LANDSCAPE_FLIP	3
+
+#define DISP_TYPE_ILI9341	0
+#define DISP_TYPE_ILI9488	1
+#define DISP_TYPE_ST7789V	2
+#define DISP_TYPE_ST7735	3
+#define DISP_TYPE_ST7735R	4
+#define DISP_TYPE_ST7735B	5
+#define DISP_TYPE_MAX		6
+
+#if CONFIG_EXAMPLE_DISPLAY_TYPE == 1
+
+// ** Set the correct configuration for ESP32-WROVER-KIT v3
+// --------------------------------------------------------
+#define DEFAULT_DISP_TYPE           DISP_TYPE_ST7789V
+#define DEFAULT_TFT_DISPLAY_WIDTH   240
+#define DEFAULT_TFT_DISPLAY_HEIGHT  320
+#define DISP_COLOR_BITS_24          0x66
+#define DEFAULT_GAMMA_CURVE         0
+#define DEFAULT_SPI_CLOCK           26000000
+#define TFT_INVERT_ROTATION         0
+#define TFT_INVERT_ROTATION1        1
+#define TFT_RGB_BGR                 0x00
+
+#define USE_TOUCH	TOUCH_TYPE_NONE
+
+#define PIN_NUM_MISO 25		// SPI MISO
+#define PIN_NUM_MOSI 23		// SPI MOSI
+#define PIN_NUM_CLK  19		// SPI CLOCK pin
+#define PIN_NUM_CS   22		// Display CS pin
+#define PIN_NUM_DC   21		// Display command/data pin
+#define PIN_NUM_TCS   0		// Touch screen CS pin
+
+#define PIN_NUM_RST  18  	// GPIO used for RESET control
+#define PIN_NUM_BCKL  5     // GPIO used for backlight control
+#define PIN_BCKL_ON   0     // GPIO value for backlight ON
+#define PIN_BCKL_OFF  1     // GPIO value for backlight OFF
+// --------------------------------------------------------
+
+#elif CONFIG_EXAMPLE_DISPLAY_TYPE == 2
+
+// ** Set the correct configuration for Adafruit TFT Feather
+// ---------------------------------------------------------
+#define DEFAULT_DISP_TYPE   DISP_TYPE_ILI9341
+#define DEFAULT_TFT_DISPLAY_WIDTH   240
+#define DEFAULT_TFT_DISPLAY_HEIGHT  320
+#define DISP_COLOR_BITS_24          0x66
+#define DEFAULT_GAMMA_CURVE         0
+#define DEFAULT_SPI_CLOCK           26000000
+#define TFT_INVERT_ROTATION         0
+#define TFT_INVERT_ROTATION1        0
+#define TFT_RGB_BGR                 0x08
+
+#define USE_TOUCH                   TOUCH_TYPE_STMPE610
+
+#define PIN_NUM_MISO 19		// SPI MISO
+#define PIN_NUM_MOSI 18		// SPI MOSI
+#define PIN_NUM_CLK  5		// SPI CLOCK pin
+#define PIN_NUM_CS   15		// Display CS pin
+#define PIN_NUM_DC   33		// Display command/data pin
+#define PIN_NUM_TCS  32		// Touch screen CS pin (NOT used if USE_TOUCH=0)
+
+#define PIN_NUM_RST  0  	// GPIO used for RESET control (#16)
+#define PIN_NUM_BCKL 0  	// GPIO used for backlight control
+#define PIN_BCKL_ON  0  	// GPIO value for backlight ON
+#define PIN_BCKL_OFF 1  	// GPIO value for backlight OFF
+// ---------------------------------------------------------
+
+#elif CONFIG_EXAMPLE_DISPLAY_TYPE == 3
+
+// ** Set the correct configuration for M5Stack TFT
+// ---------------------------------------------------------
+#define DEFAULT_DISP_TYPE   DISP_TYPE_ILI9341
+#define DEFAULT_TFT_DISPLAY_WIDTH   320
+#define DEFAULT_TFT_DISPLAY_HEIGHT  240
+#define DISP_COLOR_BITS_24          0x66
+#define DEFAULT_GAMMA_CURVE         0
+#define DEFAULT_SPI_CLOCK           26000000
+#define TFT_INVERT_ROTATION         0
+#define TFT_INVERT_ROTATION1        0
+#define TFT_INVERT_ROTATION2        1   // Adapte M5Stack TFT
+#define TFT_RGB_BGR                 0x08
+
+#define USE_TOUCH                   TOUCH_TYPE_NONE
+
+#define PIN_NUM_MISO 19		// SPI MISO
+#define PIN_NUM_MOSI 23		// SPI MOSI
+#define PIN_NUM_CLK  18		// SPI CLOCK pin
+#define PIN_NUM_CS   14		// Display CS pin
+#define PIN_NUM_DC   27		// Display command/data pin
+#define PIN_NUM_TCS  0		// Touch screen CS pin (NOT used if USE_TOUCH=0)
+
+#define PIN_NUM_RST  33  	// GPIO used for RESET control (#16)
+#define PIN_NUM_BCKL 32  	// GPIO used for backlight control
+#define PIN_BCKL_ON  1  	// GPIO value for backlight ON
+#define PIN_BCKL_OFF 0  	// GPIO value for backlight OFF
+// ---------------------------------------------------------
+
+#else
+
+// Configuration for other boards, set the correct values for the display used
+//----------------------------------------------------------------------------
+#define DISP_COLOR_BITS_24	0x66
+//#define DISP_COLOR_BITS_16	0x55  // Do not use!
+
+// #############################################
+// ### Set to 1 for some displays,           ###
+//     for example the one on ESP-WROWER-KIT ###
+// #############################################
+#define TFT_INVERT_ROTATION 0
+#define TFT_INVERT_ROTATION1 0
+
+// ################################################
+// ### SET TO 0X00 FOR DISPLAYS WITH RGB MATRIX ###
+// ### SET TO 0X08 FOR DISPLAYS WITH BGR MATRIX ###
+// ### For ESP-WROWER-KIT set to 0x00           ###
+// ################################################
+#define TFT_RGB_BGR 0x08
+
+// ##############################################################
+// ### Define ESP32 SPI pins to which the display is attached ###
+// ##############################################################
+
+// The pins configured here are the native spi pins for HSPI interface
+// Any other valid pin combination can be used
+#define PIN_NUM_MISO 19		// SPI MISO
+#define PIN_NUM_MOSI 23		// SPI MOSI
+#define PIN_NUM_CLK  18		// SPI CLOCK pin
+#define PIN_NUM_CS   5		// Display CS pin
+#define PIN_NUM_DC   4		// Display command/data pin
+#define PIN_NUM_TCS  22		// Touch screen CS pin (NOT used if USE_TOUCH=0)
+
+// --------------------------------------------------------------
+// ** Set Reset and Backlight pins to 0 if not used !
+// ** If you want to use them, set them to some valid GPIO number
+#define PIN_NUM_RST  0  	// GPIO used for RESET control
+
+#define PIN_NUM_BCKL 21  	// GPIO used for backlight control
+#define PIN_BCKL_ON  1  	// GPIO value for backlight ON
+#define PIN_BCKL_OFF 0  	// GPIO value for backlight OFF
+// --------------------------------------------------------------
+
+// #######################################################
+// Set this to 1 if you want to use touch screen functions
+// #######################################################
+#define USE_TOUCH	TOUCH_TYPE_XPT2046
+// #######################################################
+
+// #######################################################################
+// Default display width (smaller dimension) and height (larger dimension)
+// #######################################################################
+#define DEFAULT_TFT_DISPLAY_WIDTH  240
+#define DEFAULT_TFT_DISPLAY_HEIGHT 320
+// #######################################################################
+
+#define DEFAULT_GAMMA_CURVE 0
+#define DEFAULT_SPI_CLOCK   26000000
+#define DEFAULT_DISP_TYPE   DISP_TYPE_ILI9341
+//----------------------------------------------------------------------------
+
+#endif  // CONFIG_EXAMPLE_ESP_WROVER_KIT
+
+
+// ##############################################################
+// #### Global variables                                     ####
+// ##############################################################
+
+// ==== Converts colors to grayscale if 1 =======================
+extern uint8_t gray_scale;
+
+// ==== Spi clock for reading data from display memory in Hz ====
+extern uint32_t max_rdclock;
+
+// ==== Display dimensions in pixels ============================
+extern int _width;
+extern int _height;
+
+// ==== Display type, DISP_TYPE_ILI9488 or DISP_TYPE_ILI9341 ====
+extern uint8_t tft_disp_type;
+
+// ==== Spi device handles for display and touch screen =========
+extern spi_lobo_device_handle_t disp_spi;
+extern spi_lobo_device_handle_t ts_spi;
+
+// ##############################################################
+
+// 24-bit color type structure
+typedef struct __attribute__((__packed__)) {
+//typedef struct {
+	uint8_t r;
+	uint8_t g;
+	uint8_t b;
+} color_t ;
+
+// ==== Display commands constants ====
+#define TFT_INVOFF     0x20
+#define TFT_INVONN     0x21
+#define TFT_DISPOFF    0x28
+#define TFT_DISPON     0x29
+#define TFT_MADCTL	   0x36
+#define TFT_PTLAR 	   0x30
+#define TFT_ENTRYM 	   0xB7
+
+#define TFT_CMD_NOP			0x00
+#define TFT_CMD_SWRESET		0x01
+#define TFT_CMD_RDDID		0x04
+#define TFT_CMD_RDDST		0x09
+
+#define TFT_CMD_SLPIN		0x10
+#define TFT_CMD_SLPOUT		0x11
+#define TFT_CMD_PTLON		0x12
+#define TFT_CMD_NORON		0x13
+
+#define TFT_CMD_RDMODE		0x0A
+#define TFT_CMD_RDMADCTL	0x0B
+#define TFT_CMD_RDPIXFMT	0x0C
+#define TFT_CMD_RDIMGFMT	0x0D
+#define TFT_CMD_RDSELFDIAG  0x0F
+
+#define TFT_CMD_GAMMASET	0x26
+
+#define TFT_CMD_FRMCTR1		0xB1
+#define TFT_CMD_FRMCTR2		0xB2
+#define TFT_CMD_FRMCTR3		0xB3
+#define TFT_CMD_INVCTR		0xB4
+#define TFT_CMD_DFUNCTR		0xB6
+
+#define TFT_CMD_PWCTR1		0xC0
+#define TFT_CMD_PWCTR2		0xC1
+#define TFT_CMD_PWCTR3		0xC2
+#define TFT_CMD_PWCTR4		0xC3
+#define TFT_CMD_PWCTR5		0xC4
+#define TFT_CMD_VMCTR1		0xC5
+#define TFT_CMD_VMCTR2		0xC7
+
+#define TFT_CMD_RDID1		0xDA
+#define TFT_CMD_RDID2		0xDB
+#define TFT_CMD_RDID3		0xDC
+#define TFT_CMD_RDID4		0xDD
+
+#define TFT_CMD_GMCTRP1		0xE0
+#define TFT_CMD_GMCTRN1		0xE1
+
+#define TFT_CMD_POWERA		0xCB
+#define TFT_CMD_POWERB		0xCF
+#define TFT_CMD_POWER_SEQ	0xED
+#define TFT_CMD_DTCA		0xE8
+#define TFT_CMD_DTCB		0xEA
+#define TFT_CMD_PRC			0xF7
+#define TFT_CMD_3GAMMA_EN	0xF2
+
+#define ST_CMD_VCOMS       0xBB
+#define ST_CMD_FRCTRL2      0xC6
+#define ST_CMD_PWCTR1		0xD0
+
+#define ST7735_FRMCTR1 0xB1
+#define ST7735_FRMCTR2 0xB2
+#define ST7735_FRMCTR3 0xB3
+#define ST7735_INVCTR  0xB4
+#define ST7735_DISSET5 0xB6
+
+#define ST7735_PWCTR1  0xC0
+#define ST7735_PWCTR2  0xC1
+#define ST7735_PWCTR3  0xC2
+#define ST7735_PWCTR4  0xC3
+#define ST7735_PWCTR5  0xC4
+#define ST7735_VMCTR1  0xC5
+
+#define ST7735_RDID1   0xDA
+#define ST7735_RDID2   0xDB
+#define ST7735_RDID3   0xDC
+#define ST7735_RDID4   0xDD
+#define ST7735_NOP     0x00
+#define ST7735_SWRESET 0x01
+#define ST7735_RDDID   0x04
+#define ST7735_RDDST   0x09
+
+#define ST7735_SLPIN   0x10
+#define ST7735_SLPOUT  0x11
+#define ST7735_PTLON   0x12
+#define ST7735_NORON   0x13
+#define ST7735_PWCTR6  0xFC
+#define ST7735_GMCTRP1 0xE0
+#define ST7735_GMCTRN1 0xE1
+
+#define MADCTL_MY  0x80
+#define MADCTL_MX  0x40
+#define MADCTL_MV  0x20
+#define MADCTL_ML  0x10
+#define MADCTL_MH  0x04
+
+#define TFT_CASET		0x2A
+#define TFT_PASET		0x2B
+#define TFT_RAMWR		0x2C
+#define TFT_RAMRD		0x2E
+#define TFT_CMD_PIXFMT	0x3A
+
+#define TFT_CMD_DELAY	0x80
+
+
+// Initialization sequence for ILI7749
+// ====================================
+static const uint8_t ST7789V_init[] = {
+#if PIN_NUM_RST
+  15,                   					        // 15 commands in list
+#else
+  16,                   					        // 16 commands in list
+  TFT_CMD_SWRESET, TFT_CMD_DELAY,					//  1: Software reset, no args, w/delay
+  200,												//     200 ms delay
+#endif
+  TFT_CMD_FRMCTR2, 5, 0x0c, 0x0c, 0x00, 0x33, 0x33,
+  TFT_ENTRYM, 1, 0x45,
+  ST_CMD_VCOMS, 1, 0x2B,
+  TFT_CMD_PWCTR1, 1, 0x2C,
+  TFT_CMD_PWCTR3, 2, 0x01, 0xff,
+  TFT_CMD_PWCTR4, 1, 0x11,
+  TFT_CMD_PWCTR5, 1, 0x20,
+  ST_CMD_FRCTRL2, 1, 0x0f,
+  ST_CMD_PWCTR1, 2, 0xA4, 0xA1,
+  TFT_CMD_GMCTRP1, 14, 0xD0, 0x00, 0x05, 0x0E, 0x15, 0x0D, 0x37, 0x43, 0x47, 0x09, 0x15, 0x12, 0x16, 0x19,
+  TFT_CMD_GMCTRN1, 14, 0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19,
+  TFT_MADCTL, 1, (MADCTL_MX | TFT_RGB_BGR),			// Memory Access Control (orientation)
+  TFT_CMD_PIXFMT, 1, DISP_COLOR_BITS_24,            // *** INTERFACE PIXEL FORMAT: 0x66 -> 18 bit; 0x55 -> 16 bit
+  TFT_CMD_SLPOUT, TFT_CMD_DELAY, 120,				//  Sleep out,	//  120 ms delay
+  TFT_DISPON, TFT_CMD_DELAY, 120,
+};
+
+// Initialization sequence for ILI7341
+// ====================================
+static const uint8_t ILI9341_init[] = {
+#if PIN_NUM_RST
+  23,                   					        // 24 commands in list
+#else
+  24,                   					        // 24 commands in list
+  TFT_CMD_SWRESET, TFT_CMD_DELAY,					//  1: Software reset, no args, w/delay
+  250,												//     200 ms delay
+#endif
+  TFT_CMD_POWERA, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
+  TFT_CMD_POWERB, 3, 0x00, 0XC1, 0X30,
+  0xEF, 3, 0x03, 0x80, 0x02,
+  TFT_CMD_DTCA, 3, 0x85, 0x00, 0x78,
+  TFT_CMD_DTCB, 2, 0x00, 0x00,
+  TFT_CMD_POWER_SEQ, 4, 0x64, 0x03, 0X12, 0X81,
+  TFT_CMD_PRC, 1, 0x20,
+  TFT_CMD_PWCTR1, 1, 0x23,							//Power control VRH[5:0]
+  TFT_CMD_PWCTR2, 1, 0x10,							//Power control SAP[2:0];BT[3:0]
+  TFT_CMD_VMCTR1, 2, 0x3e, 0x28,					//VCM control
+  TFT_CMD_VMCTR2, 1, 0x86,							//VCM control2
+  TFT_MADCTL, 1,									// Memory Access Control (orientation)
+  (MADCTL_MX | TFT_RGB_BGR),
+  // *** INTERFACE PIXEL FORMAT: 0x66 -> 18 bit; 0x55 -> 16 bit
+  TFT_CMD_PIXFMT, 1, DISP_COLOR_BITS_24,
+  TFT_INVOFF, 0,
+  TFT_CMD_FRMCTR1, 2, 0x00, 0x18,
+  TFT_CMD_DFUNCTR, 4, 0x08, 0x82, 0x27, 0x00,		// Display Function Control
+  TFT_PTLAR, 4, 0x00, 0x00, 0x01, 0x3F,
+  TFT_CMD_3GAMMA_EN, 1, 0x00,						// 3Gamma Function: Disable (0x02), Enable (0x03)
+  TFT_CMD_GAMMASET, 1, 0x01,						//Gamma curve selected (0x01, 0x02, 0x04, 0x08)
+  TFT_CMD_GMCTRP1, 15,   							//Positive Gamma Correction
+  0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00,
+  TFT_CMD_GMCTRN1, 15,   							//Negative Gamma Correction
+  0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F,
+  TFT_CMD_SLPOUT, TFT_CMD_DELAY,					//  Sleep out
+  200,			 									//  120 ms delay
+  TFT_DISPON, TFT_CMD_DELAY, 200,
+};
+
+// Initialization sequence for ILI9488
+// ====================================
+static const uint8_t ILI9488_init[] = {
+#if PIN_NUM_RST
+  17,                   					        // 17 commands in list
+#else
+  18,                   					        // 18 commands in list
+  TFT_CMD_SWRESET, TFT_CMD_DELAY,					//  1: Software reset, no args, w/delay
+  200,												//     200 ms delay
+#endif
+  0xE0, 15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F,
+  0xE1, 15,	0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F,
+  0xC0, 2,   //Power Control 1
+	0x17,    //Vreg1out
+	0x15,    //Verg2out
+
+  0xC1, 1,   //Power Control 2
+	0x41,    //VGH,VGL
+
+  0xC5, 3,   //Power Control 3
+	0x00,
+	0x12,    //Vcom
+	0x80,
+
+#if TFT_INVERT_ROTATION
+  TFT_MADCTL, 1, (MADCTL_MV | TFT_RGB_BGR),			// Memory Access Control (orientation), set to portrait
+#else
+  TFT_MADCTL, 1, (MADCTL_MX | TFT_RGB_BGR),			// Memory Access Control (orientation), set to portrait
+#endif
+
+  // *** INTERFACE PIXEL FORMAT: 0x66 -> 18 bit;
+  TFT_CMD_PIXFMT, 1, DISP_COLOR_BITS_24,
+
+  0xB0, 1,   // Interface Mode Control
+	0x00,    // 0x80: SDO NOT USE; 0x00 USE SDO
+
+  0xB1, 1,   //Frame rate
+	0xA0,    //60Hz
+
+  0xB4, 1,   //Display Inversion Control
+	0x02,    //2-dot
+
+  0xB6, 2,   //Display Function Control  RGB/MCU Interface Control
+	0x02,    //MCU
+	0x02,    //Source,Gate scan direction
+
+  0xE9, 1,   // Set Image Function
+	0x00,    // Disable 24 bit data
+
+  0x53, 1,   // Write CTRL Display Value
+	0x28,    // BCTRL && DD on
+
+  0x51, 1,   // Write Display Brightness Value
+	0x7F,    //
+
+  0xF7, 4,   // Adjust Control
+	0xA9,
+	0x51,
+	0x2C,
+	0x02,    // D7 stream, loose
+
+
+  0x11, TFT_CMD_DELAY,  //Exit Sleep
+    120,
+  0x29, 0,      //Display on
+
+};
+
+
+// Initialization commands for 7735B screens
+// ------------------------------------
+static const uint8_t STP7735_init[] = {
+#if PIN_NUM_RST
+  16,                       // 17 commands in list
+#else
+  17,						// 18 commands in list:
+  ST7735_SLPOUT,   TFT_CMD_DELAY,	//  2: Out of sleep mode, no args, w/delay
+  255,			           			//     255 = 500 ms delay
+#endif
+  TFT_CMD_PIXFMT, 1+TFT_CMD_DELAY,	//  3: Set color mode, 1 arg + delay:
+  0x06, 							//     18-bit color 6-6-6 color format
+  10,	          					//     10 ms delay
+  ST7735_FRMCTR1, 3+TFT_CMD_DELAY,	//  4: Frame rate control, 3 args + delay:
+  0x00,						//     fastest refresh
+  0x06,						//     6 lines front porch
+  0x03,						//     3 lines back porch
+  10,						//     10 ms delay
+  TFT_MADCTL , 1      ,		//  5: Memory access ctrl (directions), 1 arg:
+  0x08,						//     Row addr/col addr, bottom to top refresh
+  ST7735_DISSET5, 2      ,	//  6: Display settings #5, 2 args, no delay:
+  0x15,						//     1 clk cycle nonoverlap, 2 cycle gate
+  // rise, 3 cycle osc equalize
+  0x02,						//     Fix on VTL
+  ST7735_INVCTR , 1      ,	//  7: Display inversion control, 1 arg:
+  0x0,						//     Line inversion
+  ST7735_PWCTR1 , 2+TFT_CMD_DELAY,	//  8: Power control, 2 args + delay:
+  0x02,						//     GVDD = 4.7V
+  0x70,						//     1.0uA
+  10,						//     10 ms delay
+  ST7735_PWCTR2 , 1      ,	//  9: Power control, 1 arg, no delay:
+  0x05,						//     VGH = 14.7V, VGL = -7.35V
+  ST7735_PWCTR3 , 2      ,	// 10: Power control, 2 args, no delay:
+  0x01,						//     Opamp current small
+  0x02,						//     Boost frequency
+  ST7735_VMCTR1 , 2+TFT_CMD_DELAY,	// 11: Power control, 2 args + delay:
+  0x3C,						//     VCOMH = 4V
+  0x38,						//     VCOML = -1.1V
+  10,						//     10 ms delay
+  ST7735_PWCTR6 , 2      ,	// 12: Power control, 2 args, no delay:
+  0x11, 0x15,
+  ST7735_GMCTRP1,16      ,	// 13: Magical unicorn dust, 16 args, no delay:
+  0x09, 0x16, 0x09, 0x20,	//     (seriously though, not sure what
+  0x21, 0x1B, 0x13, 0x19,	//      these config values represent)
+  0x17, 0x15, 0x1E, 0x2B,
+  0x04, 0x05, 0x02, 0x0E,
+  ST7735_GMCTRN1,16+TFT_CMD_DELAY,	// 14: Sparkles and rainbows, 16 args + delay:
+  0x0B, 0x14, 0x08, 0x1E,	//     (ditto)
+  0x22, 0x1D, 0x18, 0x1E,
+  0x1B, 0x1A, 0x24, 0x2B,
+  0x06, 0x06, 0x02, 0x0F,
+  10,						//     10 ms delay
+  TFT_CASET  , 4      , 	// 15: Column addr set, 4 args, no delay:
+  0x00, 0x02,				//     XSTART = 2
+  0x00, 0x81,				//     XEND = 129
+  TFT_PASET  , 4      , 	// 16: Row addr set, 4 args, no delay:
+  0x00, 0x02,				//     XSTART = 1
+  0x00, 0x81,				//     XEND = 160
+  ST7735_NORON  ,   TFT_CMD_DELAY,	// 17: Normal display on, no args, w/delay
+  10,						//     10 ms delay
+  TFT_DISPON ,   TFT_CMD_DELAY,  	// 18: Main screen turn on, no args, w/delay
+  255						//     255 = 500 ms delay
+};
+
+// Init for 7735R, part 1 (red or green tab)
+// --------------------------------------
+static const uint8_t  STP7735R_init[] = {
+#if PIN_NUM_RST
+  14,                       // 14 commands in list
+#else
+  15,						// 15 commands in list:
+  ST7735_SWRESET,   TFT_CMD_DELAY,	//  1: Software reset, 0 args, w/delay
+  150,						//     150 ms delay
+#endif
+  ST7735_SLPOUT ,   TFT_CMD_DELAY,	//  2: Out of sleep mode, 0 args, w/delay
+  255,						//     500 ms delay
+  ST7735_FRMCTR1, 3      ,	//  3: Frame rate ctrl - normal mode, 3 args:
+  0x01, 0x2C, 0x2D,			//     Rate = fosc/(1x2+40) * (LINE+2C+2D)
+  ST7735_FRMCTR2, 3      ,	//  4: Frame rate control - idle mode, 3 args:
+  0x01, 0x2C, 0x2D,			//     Rate = fosc/(1x2+40) * (LINE+2C+2D)
+  ST7735_FRMCTR3, 6      ,	//  5: Frame rate ctrl - partial mode, 6 args:
+  0x01, 0x2C, 0x2D,			//     Dot inversion mode
+  0x01, 0x2C, 0x2D,			//     Line inversion mode
+  ST7735_INVCTR , 1      ,	//  6: Display inversion ctrl, 1 arg, no delay:
+  0x07,						//     No inversion
+  ST7735_PWCTR1 , 3      ,	//  7: Power control, 3 args, no delay:
+  0xA2,
+  0x02,						//     -4.6V
+  0x84,						//     AUTO mode
+  ST7735_PWCTR2 , 1      ,	//  8: Power control, 1 arg, no delay:
+  0xC5,						//     VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
+  ST7735_PWCTR3 , 2      ,	//  9: Power control, 2 args, no delay:
+  0x0A,						//     Opamp current small
+  0x00,						//     Boost frequency
+  ST7735_PWCTR4 , 2      ,	// 10: Power control, 2 args, no delay:
+  0x8A,						//     BCLK/2, Opamp current small & Medium low
+  0x2A,
+  ST7735_PWCTR5 , 2      ,	// 11: Power control, 2 args, no delay:
+  0x8A, 0xEE,
+  ST7735_VMCTR1 , 1      ,	// 12: Power control, 1 arg, no delay:
+  0x0E,
+  TFT_INVOFF , 0      ,		// 13: Don't invert display, no args, no delay
+  TFT_MADCTL , 1      ,		// 14: Memory access control (directions), 1 arg:
+  0xC0,						//     row addr/col addr, bottom to top refresh, RGB order
+  TFT_CMD_PIXFMT , 1+TFT_CMD_DELAY,	//  15: Set color mode, 1 arg + delay:
+  0x06,								//      18-bit color 6-6-6 color format
+  10						//     10 ms delay
+};
+
+// Init for 7735R, part 2 (green tab only)
+// ---------------------------------------
+static const uint8_t Rcmd2green[] = {
+  2,						//  2 commands in list:
+  TFT_CASET  , 4      ,		//  1: Column addr set, 4 args, no delay:
+  0x00, 0x02,				//     XSTART = 0
+  0x00, 0x7F+0x02,			//     XEND = 129
+  TFT_PASET  , 4      ,	    //  2: Row addr set, 4 args, no delay:
+  0x00, 0x01,				//     XSTART = 0
+  0x00, 0x9F+0x01			//     XEND = 160
+};
+
+// Init for 7735R, part 2 (red tab only)
+// -------------------------------------
+static const uint8_t Rcmd2red[] = {
+  2,						//  2 commands in list:
+  TFT_CASET  , 4      ,	    //  1: Column addr set, 4 args, no delay:
+  0x00, 0x00,				//     XSTART = 0
+  0x00, 0x7F,				//     XEND = 127
+  TFT_PASET  , 4      ,	    //  2: Row addr set, 4 args, no delay:
+  0x00, 0x00,				//     XSTART = 0
+  0x00, 0x9F				//     XEND = 159
+};
+
+// Init for 7735R, part 3 (red or green tab)
+// -----------------------------------------
+static const uint8_t Rcmd3[] = {
+  4,						//  4 commands in list:
+  ST7735_GMCTRP1, 16      ,	//  1: Magical unicorn dust, 16 args, no delay:
+  0x02, 0x1c, 0x07, 0x12,
+  0x37, 0x32, 0x29, 0x2d,
+  0x29, 0x25, 0x2B, 0x39,
+  0x00, 0x01, 0x03, 0x10,
+  ST7735_GMCTRN1, 16      ,	//  2: Sparkles and rainbows, 16 args, no delay:
+  0x03, 0x1d, 0x07, 0x06,
+  0x2E, 0x2C, 0x29, 0x2D,
+  0x2E, 0x2E, 0x37, 0x3F,
+  0x00, 0x00, 0x02, 0x10,
+  ST7735_NORON  ,    TFT_CMD_DELAY,	//  3: Normal display on, no args, w/delay
+  10,						//     10 ms delay
+  TFT_DISPON ,    TFT_CMD_DELAY,	//  4: Main screen turn on, no args w/delay
+  100						//     100 ms delay
+};
+
+
+// ==== Public functions =========================================================
+
+// == Low level functions; usually not used directly ==
+esp_err_t wait_trans_finish(uint8_t free_line);
+void disp_spi_transfer_cmd(int8_t cmd);
+void disp_spi_transfer_cmd_data(int8_t cmd, uint8_t *data, uint32_t len);
+void drawPixel(int16_t x, int16_t y, color_t color, uint8_t sel);
+void send_data(int x1, int y1, int x2, int y2, uint32_t len, color_t *buf);
+void TFT_pushColorRep(int x1, int y1, int x2, int y2, color_t data, uint32_t len);
+int read_data(int x1, int y1, int x2, int y2, int len, uint8_t *buf, uint8_t set_sp);
+color_t readPixel(int16_t x, int16_t y);
+int touch_get_data(uint8_t type);
+
+
+// Deactivate display's CS line
+//========================
+esp_err_t disp_deselect();
+
+// Activate display's CS line and configure SPI interface if necessary
+//======================
+esp_err_t disp_select();
+
+
+// Find maximum spi clock for successful read from display RAM
+// ** Must be used AFTER the display is initialized **
+//======================
+uint32_t find_rd_speed();
+
+
+// Change the screen rotation.
+// Input: m new rotation value (0 to 3)
+//=================================
+void _tft_setRotation(uint8_t rot);
+
+// Initialize all pins used by display driver
+// ** MUST be executed before SPI interface initialization
+//=================
+void TFT_PinsInit();
+
+// Perform display initialization sequence
+// Sets orientation to landscape; clears the screen
+// * All pins must be configured
+// * SPI interface must already be setup
+// * 'tft_disp_type', 'COLOR_BITS', '_width', '_height' variables must be set
+//======================
+void TFT_display_init();
+
+//===================
+void stmpe610_Init();
+
+//============================================================
+int stmpe610_get_touch(uint16_t *x, uint16_t *y, uint16_t *z);
+
+//========================
+uint32_t stmpe610_getID();
+
+// ===============================================================================
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/tft/tooney32.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,331 @@
+// This comes with no warranty, implied or otherwise
+
+// This data structure was designed to support Proportional fonts
+// on Arduinos. It can however handle any ttf font that has been converted
+// using the conversion program. These could be fixed width or proportional 
+// fonts. Individual characters do not have to be multiples of 8 bits wide. 
+// Any width is fine and does not need to be fixed.
+
+// The data bits are packed to minimize data requirements, but the tradeoff
+// is that a header is required per character.
+
+// tooney32.c
+// Point Size   : 32
+// Memory usage : 5470 bytes
+// # characters : 95
+
+// Header Format (to make Arduino UTFT Compatible):
+// ------------------------------------------------
+// Character Width (Used as a marker to indicate use this format. i.e.: = 0x00)
+// Character Height
+// First Character (Reserved. 0x00)
+// Number Of Characters (Reserved. 0x00)
+
+unsigned char tft_tooney32[] =
+{
+0x00, 0x20, 0x00, 0x00,
+
+// Individual Character Format:
+// ----------------------------
+// Character Code
+// Adjusted Y Offset
+// Width
+// Height
+// xOffset
+// xDelta (the distance to move the cursor. Effective width of the character.)
+// Data[n]
+
+// NOTE: You can remove any of these characters if they are not needed in
+// your application. The first character number in each Glyph indicates
+// the ASCII character code. Therefore, these do not have to be sequential.
+// Just remove all the content for a particular character to save space.
+
+// ' '
+0x20,0x1E,0x00,0x00,0x00,0x09,
+
+// '!'
+0x21,0x09,0x0B,0x16,0x00,0x0B,
+0x3F,0xC8,0x07,0x81,0x70,0x27,0x00,0xE0,0x1C,0x21,0x84,0x30,0x86,0x10,0xC2,0x18,0x43,0xF0,0x61,0x0C,0x13,0x02,0x60,0x4E,0x09,0xE2,0x1F,0x81,0xE0,0x00,0x00,
+// '"'
+0x22,0x05,0x0E,0x0A,0xFF,0x0D,
+0x04,0x30,0x2D,0x61,0x8C,0x44,0x71,0x31,0x88,0xCE,0x42,0x72,0x18,0xC8,0x7B,0xC0,0xC6,0x00,
+// '#'
+0x23,0x07,0x18,0x16,0x00,0x18,
+0x00,0xFF,0xF8,0x01,0x83,0x08,0x01,0x82,0x08,0x01,0x82,0x08,0x0F,0x06,0x0F,0x10,0x00,0x01,0x30,0x00,0x01,0x30,0x00,0x00,0x20,0x00,0x02,0x7E,0x0C,0x1E,0x7E,0x0C,0x1E,0x60,0x00,0x02,0x60,0x00,0x00,0x40,0x00,0x04,0x40,0x00,0x04,0xC0,0x00,0x04,0xFC,0x10,0x78,0xFC,0x30,0x78,0x08,0x30,0x40,0x18,0x30,0x40,0x1F,0xFF,0x80,0x1F,0xFF,0x80,
+// '$'
+0x24,0x09,0x0F,0x14,0x00,0x0F,
+0x01,0x80,0x04,0xF8,0x18,0x08,0x20,0x10,0xC0,0x41,0x00,0x86,0x01,0x0C,0x12,0x18,0x38,0x30,0x60,0xA0,0x83,0x01,0x06,0x02,0x0C,0x04,0x18,0x10,0x30,0x20,0x6E,0x40,0xFF,0x81,0x9E,0x00,0x18,0x00,
+// '%'
+0x25,0x07,0x17,0x16,0x00,0x17,
+0x0F,0x81,0xF8,0x20,0x84,0x10,0x80,0x98,0x42,0x00,0x21,0x0C,0x01,0x82,0x18,0x43,0x08,0x30,0x04,0x10,0x70,0x18,0x40,0xE0,0x21,0x00,0xF1,0x82,0x00,0xFF,0x0F,0xC0,0xFC,0x10,0x40,0x18,0x40,0x40,0x21,0x80,0x40,0x82,0x00,0x83,0x0C,0x21,0x04,0x18,0x02,0x18,0x78,0x04,0x21,0x30,0x10,0xC2,0x78,0xC3,0xF8,0x7F,0x07,0xE0,0x7C,0x00,
+// '&'
+0x26,0x08,0x17,0x17,0x00,0x17,
+0x01,0xF6,0x00,0x04,0x1A,0x00,0x10,0x04,0x00,0x40,0x10,0x01,0x00,0x40,0x06,0x00,0x80,0x0C,0x1A,0x00,0x18,0x1F,0xC0,0x30,0x18,0xF8,0x60,0x08,0x09,0x80,0x00,0x33,0x00,0x00,0xCC,0x0C,0x01,0x18,0x3C,0x04,0x30,0x30,0x07,0x60,0x00,0x01,0xE0,0x00,0x07,0xC0,0x00,0x09,0xC0,0x0C,0x23,0xC0,0x38,0x03,0xE1,0xF8,0x03,0xFF,0x70,0x01,0xF8,0x60,0x00,
+// '''
+0x27,0x05,0x09,0x0A,0xFF,0x08,
+0x06,0x05,0x86,0x23,0x13,0x11,0x90,0x90,0xC8,0x78,0x18,0x00,
+// '('
+0x28,0x05,0x0D,0x1D,0x00,0x0D,
+0x03,0x00,0x34,0x01,0x90,0x08,0x40,0x81,0x88,0x1C,0xC1,0xC4,0x08,0x60,0x83,0x04,0x10,0x41,0x82,0x0C,0x10,0x60,0x83,0x04,0x18,0x20,0xC0,0x06,0x04,0x38,0x20,0xC0,0x87,0x06,0x38,0x1C,0xE0,0x67,0x86,0x1C,0x60,0x76,0x01,0xE0,0x0E,0x00,0x60,0x00,
+// ')'
+0x29,0x05,0x0D,0x1D,0x00,0x0D,
+0x01,0x00,0x10,0x01,0x20,0x11,0x03,0x04,0x30,0x11,0xE0,0x8F,0x82,0x1C,0x10,0x70,0x81,0x82,0x0E,0x10,0x30,0x81,0x84,0x0C,0x20,0x61,0x02,0x08,0x10,0x01,0x84,0x08,0x20,0x81,0x18,0x11,0x80,0x8E,0x08,0x78,0x80,0xE4,0x03,0xE0,0x0E,0x00,0x30,0x00,
+// '*'
+0x2A,0x09,0x0C,0x0D,0x01,0x0D,
+0x07,0x00,0x88,0x18,0xE4,0x11,0xC0,0x1F,0x8E,0xC0,0x1C,0x11,0xC8,0xBF,0x8E,0x7D,0xC1,0xE0,0x0C,0x00,
+// '+'
+0x2B,0x09,0x15,0x14,0x00,0x15,
+0x00,0x7C,0x00,0x04,0x10,0x00,0x60,0x80,0x07,0x04,0x00,0x38,0x20,0x01,0xC1,0x00,0xFE,0x0F,0xCC,0x00,0x01,0xE0,0x00,0x0F,0x00,0x00,0x78,0x00,0x03,0xFF,0x07,0xFF,0xF8,0x3F,0x7F,0xC1,0xF0,0x0E,0x08,0x00,0x70,0x40,0x03,0x82,0x00,0x1F,0xF0,0x00,0xFE,0x00,0x03,0xE0,0x00,
+// ','
+0x2C,0x17,0x09,0x0B,0x00,0x09,
+0x1E,0x10,0x98,0x38,0x1C,0x0F,0x0B,0xC4,0xE4,0x32,0x1E,0x0E,0x00,
+// '-'
+0x2D,0x11,0x09,0x06,0x00,0x09,
+0x1B,0x90,0x50,0x39,0x2F,0xE7,0xF0,
+// '.'
+0x2E,0x16,0x09,0x09,0x00,0x09,
+0x1E,0x10,0x90,0x38,0x1C,0x0F,0x07,0xCC,0xFC,0x3C,0x00,
+// '/'
+0x2F,0x09,0x11,0x19,0x00,0x11,
+0x00,0x3F,0x80,0x30,0x40,0x30,0x20,0x18,0x20,0x18,0x10,0x0C,0x08,0x06,0x08,0x06,0x04,0x03,0x04,0x03,0x02,0x01,0x81,0x00,0xC1,0x00,0xC0,0x80,0x60,0x00,0x60,0x40,0x30,0x20,0x18,0x20,0x18,0x10,0x0C,0x08,0x06,0x08,0x06,0x04,0x03,0x04,0x03,0xFE,0x01,0xFE,0x00,0xFE,0x00,0x00,
+// '0'
+0x30,0x08,0x17,0x17,0x00,0x17,
+0x00,0x7E,0x00,0x03,0x01,0x80,0x18,0x00,0x80,0x40,0x00,0x81,0x00,0x00,0x86,0x00,0x00,0x08,0x00,0x01,0x30,0x00,0x02,0x40,0x3C,0x03,0x80,0xFC,0x07,0x02,0x3C,0x0E,0x04,0x38,0x1C,0x08,0x30,0x3C,0x08,0x40,0x78,0x0F,0x01,0x30,0x00,0x02,0x70,0x00,0x08,0xF0,0x00,0x10,0xF0,0x00,0xC0,0xF0,0x03,0x00,0xFC,0x1C,0x00,0xFF,0xE0,0x00,0x3F,0x00,0x00,
+// '1'
+0x31,0x09,0x0D,0x16,0x00,0x0D,
+0x00,0x30,0x06,0x40,0xC4,0x18,0x43,0x02,0x20,0x13,0xC0,0x9E,0x04,0x30,0x21,0x81,0x0C,0x08,0x60,0x43,0x02,0x18,0x10,0xC0,0x86,0x04,0x30,0x21,0x81,0x98,0x03,0xFF,0xE7,0xFE,0x00,0x00,
+// '2'
+0x32,0x08,0x12,0x17,0x00,0x12,
+0x00,0xF0,0x00,0x81,0x00,0xC0,0x20,0xE0,0x04,0x40,0x01,0x38,0x00,0x2F,0x84,0x08,0xF9,0x82,0x1F,0xE0,0x81,0xF0,0x00,0x1C,0x10,0x06,0x04,0x01,0x03,0x80,0xC0,0xD0,0x20,0x04,0x18,0x01,0x0C,0x00,0x43,0x00,0x11,0x80,0x04,0x60,0x01,0x3F,0xFF,0x4F,0xFF,0xE0,0x00,0x30,
+// '3'
+0x33,0x08,0x12,0x17,0x00,0x12,
+0x0C,0x00,0x05,0xFF,0xE3,0x00,0x08,0xC0,0x02,0x30,0x01,0x0C,0x00,0x43,0x00,0x10,0xC0,0x0C,0x37,0x01,0x0F,0x80,0x23,0xE0,0x08,0x10,0x01,0x0F,0xE0,0x43,0xF8,0x10,0x4E,0x04,0x23,0x01,0x10,0x00,0x08,0x00,0x24,0x00,0x13,0x00,0x08,0xFC,0x0C,0x1F,0xFE,0x00,0xFE,0x00,
+// '4'
+0x34,0x09,0x12,0x16,0x00,0x12,
+0x00,0x0E,0x00,0x04,0x80,0x06,0x20,0x03,0x08,0x01,0x82,0x00,0xC0,0x80,0x40,0x20,0x20,0x08,0x10,0x03,0x88,0x20,0x94,0x00,0x07,0x00,0x01,0xC0,0x00,0x70,0x00,0x1C,0x00,0x07,0xFE,0x09,0xFF,0x83,0x80,0x60,0xC0,0x30,0x10,0x0F,0xF8,0x03,0xFC,0x00,0x00,0x00,
+// '5'
+0x35,0x08,0x11,0x17,0x00,0x11,
+0x00,0x03,0x00,0xFF,0x40,0x80,0x20,0xC0,0x10,0x60,0x08,0x30,0x04,0x30,0x02,0x18,0x1D,0x0C,0x07,0x06,0x01,0x82,0x00,0x43,0x00,0x11,0xFC,0x08,0xFF,0x04,0x17,0x82,0x18,0x01,0x18,0x00,0x08,0x00,0x8C,0x00,0x8C,0x00,0xC7,0xC1,0xC3,0xFF,0x80,0xBF,0x00,
+// '6'
+0x36,0x08,0x13,0x17,0x00,0x13,
+0x00,0x7F,0xC0,0x30,0x08,0x18,0x01,0x06,0x00,0x41,0x80,0x10,0x60,0x02,0x08,0x0C,0x83,0x00,0x30,0x60,0x01,0x18,0x00,0x13,0x00,0x01,0x60,0x00,0x0C,0x00,0x03,0x80,0xC0,0x78,0x3C,0x0F,0x03,0x01,0x60,0x00,0x0E,0x00,0x08,0xE0,0x02,0x1E,0x00,0x81,0xF0,0x60,0x1F,0xF8,0x00,0xFC,0x00,
+// '7'
+0x37,0x08,0x12,0x17,0x00,0x12,
+0x0C,0x00,0x0D,0xFF,0xF3,0x00,0x04,0xC0,0x02,0x30,0x00,0x8C,0x00,0x43,0x00,0x10,0xDC,0x08,0x3F,0x02,0x0E,0x81,0x00,0x60,0x40,0x10,0x20,0x0C,0x08,0x02,0x04,0x01,0x81,0x00,0x40,0x80,0x30,0x20,0x10,0x10,0x0F,0x04,0x03,0xF1,0x00,0x3F,0x40,0x03,0xE0,0x00,0x30,0x00,
+// '8'
+0x38,0x08,0x12,0x17,0x00,0x12,
+0x01,0xF0,0x00,0x83,0x00,0xC0,0x20,0x20,0x08,0x10,0x01,0x0C,0x18,0x43,0x06,0x10,0xC0,0x04,0x38,0x01,0x0C,0x00,0x42,0x00,0x09,0x80,0x01,0xC0,0x00,0x70,0x3C,0x1C,0x0F,0x07,0x00,0x01,0xE0,0x00,0x9C,0x00,0x27,0x80,0x30,0xF8,0x38,0x1F,0xFC,0x01,0xFC,0x00,0x00,0x00,
+// '9'
+0x39,0x08,0x13,0x17,0x00,0x13,
+0x01,0xF8,0x00,0xC0,0xC0,0x20,0x04,0x08,0x00,0x42,0x00,0x08,0xC0,0x00,0x90,0x18,0x16,0x07,0x81,0xC0,0x60,0x38,0x00,0x07,0x80,0x00,0xF0,0x00,0x17,0x00,0x02,0xF0,0x00,0x0F,0x80,0x10,0xE6,0x02,0x08,0x00,0x83,0x00,0x20,0x40,0x0C,0x10,0x03,0x06,0xC1,0xC0,0xFF,0xE0,0x1F,0xF0,0x00,
+// ':'
+0x3A,0x0E,0x09,0x11,0x00,0x09,
+0x0E,0x10,0x90,0x38,0x1C,0x0F,0x07,0xC4,0xFC,0x3C,0x19,0x98,0x38,0x1C,0x0F,0x07,0xCC,0xFC,0x3C,0x00,
+// ';'
+0x3B,0x0F,0x09,0x13,0x00,0x09,
+0x0E,0x10,0x98,0x38,0x1C,0x0F,0x07,0xC4,0xFC,0x3E,0x19,0x98,0x38,0x1C,0x0F,0x03,0xC4,0xE4,0x32,0x1E,0x0E,0x00,
+// '<'
+0x3C,0x0A,0x13,0x13,0x00,0x13,
+0x00,0x00,0xC0,0x00,0x64,0x00,0x60,0x80,0x30,0x10,0x38,0x02,0x18,0x03,0x8C,0x01,0xE3,0x01,0xF0,0xE0,0xF8,0x1C,0x06,0x03,0x80,0x18,0x7E,0x00,0xCF,0xF0,0x06,0x7F,0xC0,0x43,0xFE,0x08,0x0F,0xF9,0x00,0x7F,0xE0,0x01,0xF8,0x00,0x0C,0x00,
+// '='
+0x3D,0x0D,0x14,0x0E,0x00,0x14,
+0x3F,0xFF,0xE6,0x00,0x01,0xE0,0x00,0x1E,0x00,0x01,0xE0,0x00,0x1F,0xFF,0xFE,0xFF,0xFF,0xE6,0x00,0x01,0x60,0x00,0x1E,0x00,0x01,0xE0,0x00,0x1F,0xFF,0xFE,0xFF,0xFF,0xEF,0xFF,0xF8,
+// '>'
+0x3E,0x0A,0x13,0x13,0x00,0x13,
+0x38,0x00,0x0C,0xC0,0x03,0x87,0x00,0x70,0x18,0x0E,0x00,0xE1,0xF8,0x03,0x3F,0xC0,0x19,0xFF,0x01,0x0F,0xF8,0x20,0x3C,0x04,0x0C,0x00,0x86,0x00,0xE3,0x00,0x78,0xC0,0x7C,0x38,0x3E,0x07,0x3E,0x00,0xFF,0x00,0x1F,0x00,0x03,0x80,0x00,0x00,
+// '?'
+0x3F,0x08,0x11,0x16,0x00,0x11,
+0x00,0xF0,0x01,0x82,0x01,0x00,0x81,0x00,0x23,0x00,0x0B,0x00,0x05,0xC0,0x02,0xF8,0xC1,0x3F,0x40,0x87,0xE0,0x80,0xE0,0x40,0x30,0x40,0x18,0x20,0x0F,0xE0,0x07,0x30,0x03,0x04,0x03,0x02,0x01,0x81,0x00,0xE0,0x80,0x78,0x80,0x1F,0x80,0x07,0x80,
+// '@'
+0x40,0x09,0x16,0x16,0x00,0x16,
+0x00,0x3F,0x00,0x06,0x03,0x00,0x23,0xFB,0x01,0x3F,0xFC,0x09,0x81,0xF8,0x48,0x7F,0xE2,0x44,0x13,0xD9,0x23,0x8F,0x41,0x1E,0x1F,0x2C,0x51,0x7C,0xE0,0xC5,0xF3,0x8B,0x06,0xCE,0x28,0x3B,0x38,0xE2,0xEE,0x71,0x4D,0x19,0xE3,0x88,0x73,0xFF,0xD1,0xE7,0x9F,0xA3,0xC7,0xF1,0x07,0xE0,0x38,0x07,0xFF,0x80,0x07,0xF8,0x00,
+// 'A'
+0x41,0x08,0x19,0x17,0x00,0x19,
+0x00,0x0C,0x00,0x00,0x09,0x00,0x00,0x08,0x80,0x00,0x0C,0x20,0x00,0x04,0x08,0x00,0x06,0x04,0x00,0x02,0x01,0x00,0x03,0x00,0xC0,0x03,0x00,0x20,0x01,0x00,0x08,0x01,0x80,0x04,0x00,0x80,0x01,0x00,0xC0,0xC0,0x40,0xC0,0x60,0x20,0x40,0x00,0x08,0x60,0x00,0x06,0x40,0x00,0x00,0xF0,0x00,0x00,0xFE,0x1F,0xE1,0xCF,0xCF,0xF1,0x81,0xF4,0x1B,0x80,0x3E,0x0F,0x00,0x0E,0x06,0x00,
+// 'B'
+0x42,0x09,0x13,0x15,0x00,0x13,
+0x3F,0xFC,0x0C,0x00,0x61,0xC0,0x06,0x38,0x00,0x43,0x00,0x04,0x60,0x60,0x8C,0x0C,0x11,0x81,0x02,0x30,0x00,0x46,0x00,0x08,0xC0,0x00,0x98,0x18,0x13,0x03,0x02,0x60,0x60,0x4C,0x00,0x09,0x80,0x01,0x30,0x00,0x44,0x00,0x11,0x80,0x0E,0x3F,0xFF,0x07,0xFF,0x80,
+// 'C'
+0x43,0x08,0x16,0x17,0x00,0x16,
+0x00,0x7E,0x00,0x06,0x02,0x00,0x60,0x06,0x02,0x00,0x06,0x10,0x00,0x00,0xC0,0x00,0x22,0x00,0x01,0x18,0x0F,0x18,0x40,0x7E,0xC3,0x02,0x3E,0x0C,0x08,0xF0,0x30,0x21,0x80,0xC0,0x83,0x03,0x81,0x1A,0x0E,0x03,0xC4,0x18,0x00,0x08,0x70,0x00,0x19,0xE0,0x00,0x23,0xC0,0x01,0x87,0x80,0x1E,0x0F,0xC1,0xE0,0x0F,0xFE,0x00,0x0F,0xC0,0x00,
+// 'D'
+0x44,0x09,0x16,0x15,0xFF,0x15,
+0x1F,0xFE,0x00,0x80,0x06,0x07,0x00,0x06,0x1C,0x00,0x0C,0x30,0x00,0x10,0xC0,0x00,0x23,0x00,0x00,0x8C,0x0F,0x01,0x30,0x3E,0x04,0xC0,0xF8,0x13,0x03,0xE0,0x4C,0x0F,0x01,0x30,0x00,0x04,0xC0,0x00,0x23,0x00,0x00,0x8C,0x00,0x04,0x30,0x00,0x20,0x80,0x01,0x06,0x00,0x18,0x1F,0xFF,0x80,0x3F,0xF0,0x00,
+// 'E'
+0x45,0x08,0x11,0x17,0x00,0x11,
+0x00,0x01,0x0F,0xFF,0x48,0x00,0x2C,0x00,0x17,0x00,0x0B,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x1A,0x98,0x01,0x8C,0x00,0x86,0x00,0x43,0x00,0x31,0x80,0xD4,0xC0,0x02,0x60,0x01,0x30,0x00,0x98,0x00,0x4C,0x00,0x24,0x00,0x17,0xFF,0xEB,0xFF,0xF8,0x00,0x08,
+// 'F'
+0x46,0x08,0x11,0x16,0xFF,0x10,
+0x00,0x01,0x0F,0xFF,0x48,0x00,0x2E,0x00,0x17,0x00,0x09,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x3A,0x98,0x01,0x8C,0x00,0xC6,0x00,0x43,0x00,0x21,0x81,0xD0,0xC0,0xF0,0x60,0x70,0x30,0x20,0x18,0x10,0x08,0x02,0x0C,0x01,0x07,0xFF,0x03,0xFF,0x00,
+// 'G'
+0x47,0x08,0x16,0x17,0x00,0x16,
+0x00,0x7F,0x00,0x06,0x03,0x00,0x60,0x03,0x82,0x00,0x02,0x10,0x00,0x08,0xC0,0x00,0xC2,0x00,0x06,0x18,0x07,0x30,0x40,0x3C,0x83,0x01,0x3C,0x0C,0x04,0xFF,0xB0,0x14,0x02,0xC0,0x78,0x1B,0x80,0xE0,0x4E,0x01,0x81,0x18,0x00,0x04,0x70,0x00,0x11,0xE0,0x00,0x43,0xC0,0x01,0x07,0x80,0x18,0x0F,0x81,0xC0,0x1F,0xFC,0x00,0x0F,0xC0,0x00,
+// 'H'
+0x48,0x09,0x16,0x15,0x00,0x16,
+0x3F,0xC7,0xF9,0x00,0xE0,0x1E,0x03,0xC0,0x98,0x0B,0x02,0x60,0x2C,0x09,0x80,0xF0,0x26,0x00,0x00,0x98,0x00,0x02,0x60,0x00,0x09,0x80,0x00,0x26,0x00,0x00,0x98,0x00,0x02,0x60,0x3C,0x09,0x80,0xF0,0x26,0x02,0xC0,0x98,0x0B,0x02,0x60,0x2C,0x09,0x00,0x20,0x1F,0xFF,0xFF,0xFF,0xF7,0xFC,0x00,0x00,0x00,
+// 'I'
+0x49,0x09,0x0B,0x15,0x00,0x0B,
+0x3F,0xC8,0x07,0x81,0x70,0x26,0x04,0xC0,0x98,0x13,0x02,0x60,0x4C,0x09,0x81,0x30,0x26,0x04,0xC0,0x98,0x13,0x02,0x60,0x48,0x07,0x00,0xFF,0xEF,0xF8,
+// 'J'
+0x4A,0x09,0x0F,0x16,0x00,0x0F,
+0x03,0xFC,0x08,0x04,0x38,0x08,0x70,0x10,0x60,0x20,0xC0,0x41,0x80,0x83,0x01,0x06,0x02,0x0C,0x04,0x18,0x08,0x30,0x10,0xA0,0x23,0x00,0x44,0x00,0x98,0x01,0x20,0x00,0xC0,0x09,0x00,0x27,0xC1,0x8F,0xFE,0x0F,0xF0,0x00,
+// 'K'
+0x4B,0x08,0x17,0x18,0x00,0x17,
+0x00,0x01,0x80,0x7F,0xEE,0x81,0x80,0x38,0x87,0x81,0xE0,0x8F,0x03,0x80,0x86,0x06,0x00,0x8C,0x08,0x07,0x18,0x00,0x1C,0x30,0x00,0x70,0x60,0x01,0x00,0xC0,0x02,0x01,0x80,0x02,0x03,0x00,0x04,0x06,0x04,0x04,0x0C,0x0C,0x04,0x18,0x18,0x06,0x30,0x38,0x00,0x60,0x78,0x09,0x00,0x30,0x26,0x00,0x71,0x8F,0xFF,0xE4,0x0F,0xFC,0xD0,0x00,0x01,0xC0,0x00,0x00,0x00,
+// 'L'
+0x4C,0x09,0x11,0x16,0x00,0x11,
+0x3F,0xF0,0x30,0x04,0x1C,0x06,0x0E,0x02,0x03,0x01,0x01,0x80,0x80,0xC0,0x40,0x60,0x20,0x30,0x10,0x18,0x08,0x0C,0x04,0xC6,0x03,0xD3,0x00,0x09,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x00,0xB0,0x00,0x58,0x00,0x2F,0xFF,0xD7,0xFF,0xF0,0x00,0x30,
+// 'M'
+0x4D,0x09,0x20,0x16,0x00,0x1F,
+0x00,0xF8,0x1F,0x00,0x01,0x84,0x30,0x80,0x01,0x82,0x61,0x00,0x01,0x82,0x40,0x00,0x01,0x81,0xC0,0x80,0x01,0x01,0x80,0x80,0x03,0x00,0x80,0x40,0x02,0x00,0x00,0x40,0x06,0x00,0x00,0x20,0x06,0x00,0x00,0x20,0x04,0x00,0x00,0x10,0x0C,0x00,0x00,0x10,0x08,0x00,0x00,0x10,0x18,0x08,0x08,0x08,0x10,0x18,0x18,0x08,0x30,0x1C,0x1C,0x04,0x60,0x3C,0x3C,0x02,0x70,0x2E,0x2E,0x06,0x7C,0x27,0x4E,0x3C,0x3F,0x27,0xC6,0xF0,0x07,0xC3,0x8F,0x80,0x01,0x83,0x06,0x00,
+// 'N'
+0x4E,0x09,0x17,0x15,0x00,0x17,
+0x3F,0x07,0xFC,0xC1,0x10,0x07,0xC1,0x78,0x1F,0x81,0xF0,0x23,0x01,0xE0,0x46,0x01,0xC0,0x8C,0x01,0x81,0x18,0x01,0x02,0x30,0x00,0x04,0x60,0x00,0x08,0xC0,0x00,0x11,0x80,0x00,0x23,0x02,0x00,0x46,0x06,0x00,0x8C,0x0E,0x01,0x18,0x1E,0x02,0x30,0x3F,0x04,0x60,0x3F,0x0B,0x00,0x2F,0x17,0xFF,0x8F,0xEF,0xFE,0x0F,0x80,
+// 'O'
+0x4F,0x08,0x17,0x17,0x00,0x17,
+0x00,0x7E,0x00,0x03,0x01,0x80,0x18,0x00,0x80,0x40,0x00,0x81,0x00,0x00,0x86,0x00,0x00,0x08,0x00,0x01,0x30,0x00,0x02,0x40,0x3C,0x03,0x80,0xFC,0x07,0x02,0x3C,0x0E,0x04,0x38,0x1C,0x08,0x30,0x3C,0x08,0x40,0x78,0x0F,0x01,0x30,0x00,0x02,0x70,0x00,0x08,0xF0,0x00,0x10,0xF0,0x00,0xC0,0xF0,0x03,0x00,0xFC,0x1C,0x00,0xFF,0xE0,0x00,0x3F,0x00,0x00,
+// 'P'
+0x50,0x09,0x13,0x15,0x00,0x13,
+0x3F,0xFC,0x08,0x00,0x43,0x80,0x06,0x70,0x00,0x46,0x00,0x04,0xC0,0x00,0x18,0x00,0x0B,0x01,0x81,0x60,0x30,0x2C,0x04,0x05,0x80,0x00,0x30,0x00,0x26,0x00,0x08,0xC0,0x02,0x18,0x01,0x83,0x01,0xE0,0x60,0x30,0x08,0x04,0x03,0x00,0x80,0x7F,0xE0,0x07,0xF8,0x00,
+// 'Q'
+0x51,0x09,0x17,0x1C,0x00,0x17,
+0x00,0x7E,0x00,0x03,0x01,0x00,0x18,0x00,0x80,0x40,0x00,0x81,0x00,0x00,0x86,0x00,0x01,0x08,0x00,0x01,0x30,0x1E,0x02,0x40,0x7E,0x03,0x81,0x1E,0x07,0x02,0x1C,0x0E,0x04,0x18,0x1C,0x04,0x20,0x3C,0x07,0x80,0x78,0x00,0x01,0x30,0x00,0x02,0x70,0x00,0x04,0xF0,0x00,0x10,0xF0,0x00,0x70,0xF0,0x00,0x00,0xFC,0x00,0x40,0xFF,0x81,0x00,0x3F,0x84,0x00,0x0F,0x10,0x00,0x06,0x40,0x00,0x0F,0x00,0x00,0x1C,0x00,0x00,0x00,0x00,
+// 'R'
+0x52,0x09,0x18,0x17,0x00,0x18,
+0x3F,0xFE,0x00,0x60,0x01,0x80,0x70,0x00,0x40,0x70,0x00,0x20,0x30,0x00,0x20,0x30,0x00,0x10,0x30,0x10,0x10,0x30,0x18,0x10,0x30,0x10,0x10,0x30,0x00,0x10,0x30,0x00,0x20,0x30,0x00,0x20,0x30,0x00,0x10,0x30,0x00,0x1E,0x30,0x00,0x0A,0x30,0x18,0x06,0x30,0x1C,0x0C,0x20,0x0E,0x18,0x7F,0xFF,0x30,0x7F,0xE7,0x20,0x00,0x03,0xC0,0x00,0x03,0x80,0x00,0x03,0x00,
+// 'S'
+0x53,0x07,0x11,0x19,0x00,0x11,
+0x00,0x06,0x00,0x02,0x80,0x1E,0x40,0x10,0x20,0x30,0x10,0x30,0x08,0x10,0x04,0x18,0x01,0x08,0x00,0x8C,0x0F,0x46,0x07,0xA3,0x01,0xE1,0xC0,0xE1,0xE0,0x41,0x70,0x21,0x80,0x10,0xC0,0x08,0x70,0x08,0x38,0x04,0x0C,0x04,0x06,0x04,0x03,0x3C,0x01,0xFC,0x00,0xF0,0x00,0x30,0x00,0x00,
+// 'T'
+0x54,0x08,0x12,0x16,0x01,0x13,
+0x30,0x01,0x93,0xFF,0xDC,0x00,0x07,0x00,0x01,0xC0,0x00,0x70,0x00,0x1C,0x00,0x07,0x00,0x01,0xCC,0x07,0x7F,0x01,0xFF,0xC0,0x79,0xB0,0x1C,0x0C,0x04,0x03,0x01,0x00,0xC0,0x40,0x30,0x10,0x0C,0x04,0x03,0x01,0x00,0xC0,0x60,0x60,0x08,0x1F,0xFC,0x07,0xFE,0x00,
+// 'U'
+0x55,0x09,0x19,0x16,0xFF,0x18,
+0x1F,0xF1,0xFF,0x10,0x05,0x00,0x9C,0x07,0xC0,0xCE,0x02,0xE0,0x43,0x01,0x30,0x21,0x80,0x98,0x10,0xC0,0x4C,0x08,0x60,0x26,0x04,0x30,0x13,0x02,0x18,0x09,0x81,0x0C,0x04,0xC0,0x86,0x02,0x60,0x43,0x01,0x30,0x21,0xC0,0x70,0x10,0xE0,0x00,0x10,0x30,0x00,0x08,0x1C,0x00,0x08,0x0F,0x00,0x08,0x03,0xC0,0x08,0x00,0xFC,0x18,0x00,0x1F,0xF8,0x00,0x03,0xF0,0x00,
+// 'V'
+0x56,0x07,0x19,0x18,0x00,0x19,
+0x00,0xC1,0xC0,0x01,0x91,0x98,0x03,0x18,0xC3,0x06,0x04,0x60,0x64,0x02,0x20,0x1F,0x00,0xB0,0x1B,0xC0,0x70,0x0C,0xF0,0x10,0x08,0x38,0x00,0x08,0x0E,0x00,0x04,0x07,0x00,0x04,0x01,0xC0,0x06,0x00,0xE0,0x02,0x00,0x38,0x02,0x00,0x1C,0x01,0x00,0x07,0x01,0x00,0x03,0x81,0x80,0x00,0xE0,0x80,0x00,0x30,0x80,0x00,0x1C,0x40,0x00,0x06,0x40,0x00,0x03,0xE0,0x00,0x00,0xE0,0x00,0x00,0x60,0x00,
+// 'W'
+0x57,0x07,0x20,0x18,0x00,0x20,
+0x00,0x60,0x03,0x80,0x01,0x90,0x06,0x60,0x0E,0x10,0xC6,0x1C,0x38,0x11,0xA6,0x07,0x60,0x11,0x26,0x01,0x78,0x12,0x1C,0x06,0x78,0x0E,0x1C,0x04,0x3C,0x0C,0x08,0x08,0x1C,0x00,0x08,0x10,0x0C,0x00,0x00,0x10,0x0E,0x00,0x00,0x20,0x06,0x00,0x00,0x20,0x07,0x00,0x00,0x40,0x03,0x00,0x00,0x40,0x03,0x80,0x00,0x80,0x03,0x80,0x00,0x80,0x01,0xC0,0x81,0x00,0x01,0xC1,0xC0,0x00,0x00,0xE1,0xC2,0x00,0x00,0xE2,0xE4,0x00,0x00,0x74,0xE4,0x00,0x00,0x7C,0x78,0x00,0x00,0x38,0x78,0x00,0x00,0x30,0x30,0x00,
+// 'X'
+0x58,0x05,0x19,0x1D,0x00,0x18,
+0x00,0x01,0x80,0x00,0x01,0x20,0x00,0x61,0x8C,0x00,0x48,0xC3,0x00,0x44,0x40,0x60,0x43,0x60,0x10,0xC0,0xE0,0x18,0xC0,0x30,0x1C,0xC0,0x00,0x18,0x7C,0x00,0x08,0x1F,0x00,0x08,0x03,0x80,0x08,0x00,0xE0,0x04,0x00,0x38,0x02,0x00,0x0C,0x01,0x80,0x04,0x00,0x60,0x06,0x00,0x10,0x02,0x00,0x07,0x03,0x00,0x00,0x41,0x01,0x00,0x63,0x00,0xC0,0x62,0x00,0xF0,0x63,0x80,0x7C,0x61,0xF0,0x4E,0x60,0x7C,0x63,0xE0,0x0F,0x91,0xE0,0x01,0xF0,0x40,0x00,0x70,0x00,0x00,0x00,0x00,0x00,
+// 'Y'
+0x59,0x06,0x19,0x1B,0x00,0x19,
+0x00,0x00,0x60,0x00,0xC0,0x48,0x00,0x80,0x62,0x00,0x88,0x20,0xC0,0x82,0x30,0x11,0x80,0xB0,0x01,0x80,0x30,0x0D,0x80,0x00,0x0C,0xF8,0x00,0x08,0x3E,0x00,0x04,0x07,0x80,0x04,0x01,0xE0,0x04,0x00,0x78,0x04,0x00,0x1C,0x02,0x00,0x0C,0x02,0x00,0x04,0x02,0x00,0x04,0x02,0x00,0x06,0x01,0x00,0x0E,0x01,0x00,0x0C,0x01,0x00,0x07,0x01,0x00,0x03,0xC0,0x80,0x01,0xF0,0x80,0x00,0x3E,0x00,0x00,0x0F,0xC0,0x00,0x03,0xE0,0x00,0x00,0x40,0x00,0x00,
+// 'Z'
+0x5A,0x08,0x13,0x17,0xFF,0x12,
+0x0C,0x00,0x02,0xFF,0xFC,0xC0,0x01,0x98,0x00,0x23,0x00,0x04,0x60,0x01,0x0C,0x00,0x21,0x80,0x08,0x37,0x01,0x07,0xE0,0x60,0xF8,0x08,0x03,0x01,0x00,0x40,0x70,0x18,0x0D,0x02,0x00,0x20,0xC0,0x04,0x18,0x00,0x86,0x00,0x10,0xC0,0x02,0x10,0x00,0x47,0xFF,0xE8,0xFF,0xFE,0x00,0x01,0x80,
+// '['
+0x5B,0x04,0x0B,0x20,0x00,0x0B,
+0x00,0x40,0x19,0xFD,0x60,0x2C,0x05,0x80,0xB0,0x16,0x0E,0xC1,0xD8,0x33,0x04,0x60,0x8C,0x11,0x82,0x30,0x46,0x08,0xC1,0x18,0x23,0x04,0x60,0x8C,0x11,0x82,0xB0,0x76,0x02,0xC0,0x58,0x0B,0x01,0x60,0x2F,0xFD,0xFF,0x80,0x60,0x00,
+// '\'
+0x5C,0x09,0x11,0x18,0x00,0x11,
+0x3F,0x00,0x30,0x40,0x38,0x10,0x1E,0x08,0x0F,0x04,0x03,0x81,0x01,0xE0,0x80,0x70,0x20,0x3C,0x10,0x1E,0x08,0x07,0x02,0x03,0xC1,0x00,0xE0,0x40,0x78,0x20,0x3C,0x08,0x0F,0x04,0x07,0x82,0x01,0xC0,0x80,0xF0,0x40,0x38,0x10,0x1E,0x08,0x0F,0xFC,0x03,0xFC,0x01,0xFC,
+// ']'
+0x5D,0x04,0x0C,0x1F,0xFF,0x0B,
+0x30,0x02,0xFF,0x60,0x16,0x01,0x60,0x16,0x01,0x60,0x16,0xC1,0x7C,0x16,0xC1,0x6C,0x10,0xC1,0x0C,0x10,0xC1,0x0C,0x10,0xC1,0x0C,0x10,0xC1,0x0C,0x10,0xC1,0x0C,0x13,0xC1,0x6C,0x16,0x01,0x60,0x16,0x01,0x60,0x16,0x01,0x7F,0xE7,0xFE,0x60,0x00,
+// '^'
+0x5E,0x1E,0x00,0x00,0x00,0x09,
+
+// '_'
+0x5F,0x20,0x10,0x04,0x00,0x10,
+0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
+// '`'
+0x60,0x00,0x0A,0x09,0x00,0x0B,
+0x00,0x06,0x02,0x61,0x8C,0x60,0xBF,0x17,0xFC,0x3E,0x03,0x00,
+// 'a'
+0x61,0x08,0x19,0x17,0x00,0x19,
+0x00,0x0C,0x00,0x00,0x09,0x00,0x00,0x08,0x80,0x00,0x0C,0x20,0x00,0x04,0x08,0x00,0x06,0x04,0x00,0x02,0x01,0x00,0x03,0x00,0xC0,0x03,0x00,0x20,0x01,0x00,0x08,0x01,0x80,0x04,0x00,0x80,0x01,0x00,0xC0,0xC0,0x40,0xC0,0x60,0x20,0x40,0x00,0x08,0x60,0x00,0x06,0x40,0x00,0x00,0xF0,0x00,0x00,0xFE,0x1F,0xE1,0xCF,0xCF,0xF1,0x81,0xF4,0x1B,0x80,0x3E,0x0F,0x00,0x0E,0x06,0x00,
+// 'b'
+0x62,0x09,0x13,0x15,0x00,0x13,
+0x3F,0xFC,0x0C,0x00,0x61,0xC0,0x06,0x38,0x00,0x43,0x00,0x04,0x60,0x60,0x8C,0x0C,0x11,0x81,0x02,0x30,0x00,0x46,0x00,0x08,0xC0,0x00,0x98,0x18,0x13,0x03,0x02,0x60,0x60,0x4C,0x00,0x09,0x80,0x01,0x30,0x00,0x44,0x00,0x11,0x80,0x0E,0x3F,0xFF,0x07,0xFF,0x80,
+// 'c'
+0x63,0x08,0x16,0x17,0x00,0x16,
+0x00,0x7E,0x00,0x06,0x02,0x00,0x60,0x06,0x02,0x00,0x06,0x10,0x00,0x00,0xC0,0x00,0x22,0x00,0x01,0x18,0x0F,0x18,0x40,0x7E,0xC3,0x02,0x3E,0x0C,0x08,0xF0,0x30,0x21,0x80,0xC0,0x83,0x03,0x81,0x1A,0x0E,0x03,0xC4,0x18,0x00,0x08,0x70,0x00,0x19,0xE0,0x00,0x23,0xC0,0x01,0x87,0x80,0x1E,0x0F,0xC1,0xE0,0x0F,0xFE,0x00,0x0F,0xC0,0x00,
+// 'd'
+0x64,0x09,0x16,0x15,0xFF,0x15,
+0x1F,0xFE,0x00,0x80,0x06,0x07,0x00,0x06,0x1C,0x00,0x0C,0x30,0x00,0x10,0xC0,0x00,0x23,0x00,0x00,0x8C,0x0F,0x01,0x30,0x3E,0x04,0xC0,0xF8,0x13,0x03,0xE0,0x4C,0x0F,0x01,0x30,0x00,0x04,0xC0,0x00,0x23,0x00,0x00,0x8C,0x00,0x04,0x30,0x00,0x20,0x80,0x01,0x06,0x00,0x18,0x1F,0xFF,0x80,0x3F,0xF0,0x00,
+// 'e'
+0x65,0x08,0x11,0x17,0x00,0x11,
+0x00,0x01,0x0F,0xFF,0x48,0x00,0x2C,0x00,0x17,0x00,0x0B,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x1A,0x98,0x01,0x8C,0x00,0x86,0x00,0x43,0x00,0x31,0x80,0xD4,0xC0,0x02,0x60,0x01,0x30,0x00,0x98,0x00,0x4C,0x00,0x24,0x00,0x17,0xFF,0xEB,0xFF,0xF8,0x00,0x08,
+// 'f'
+0x66,0x08,0x11,0x16,0xFF,0x10,
+0x00,0x01,0x0F,0xFF,0x48,0x00,0x2E,0x00,0x17,0x00,0x09,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x3A,0x98,0x01,0x8C,0x00,0xC6,0x00,0x43,0x00,0x21,0x81,0xD0,0xC0,0xF0,0x60,0x70,0x30,0x20,0x18,0x10,0x08,0x02,0x0C,0x01,0x07,0xFF,0x03,0xFF,0x00,
+// 'g'
+0x67,0x08,0x16,0x17,0x00,0x16,
+0x00,0x7F,0x00,0x06,0x03,0x00,0x60,0x03,0x82,0x00,0x02,0x10,0x00,0x08,0xC0,0x00,0xC2,0x00,0x06,0x18,0x07,0x30,0x40,0x3C,0x83,0x01,0x3C,0x0C,0x04,0xFF,0xB0,0x14,0x02,0xC0,0x78,0x1B,0x80,0xE0,0x4E,0x01,0x81,0x18,0x00,0x04,0x70,0x00,0x11,0xE0,0x00,0x43,0xC0,0x01,0x07,0x80,0x18,0x0F,0x81,0xC0,0x1F,0xFC,0x00,0x0F,0xC0,0x00,
+// 'h'
+0x68,0x09,0x16,0x15,0x00,0x16,
+0x3F,0xC7,0xF9,0x00,0xE0,0x1E,0x03,0xC0,0x98,0x0B,0x02,0x60,0x2C,0x09,0x80,0xF0,0x26,0x00,0x00,0x98,0x00,0x02,0x60,0x00,0x09,0x80,0x00,0x26,0x00,0x00,0x98,0x00,0x02,0x60,0x3C,0x09,0x80,0xF0,0x26,0x02,0xC0,0x98,0x0B,0x02,0x60,0x2C,0x09,0x00,0x20,0x1F,0xFF,0xFF,0xFF,0xF7,0xFC,0x00,0x00,0x00,
+// 'i'
+0x69,0x09,0x0B,0x15,0x00,0x0B,
+0x3F,0xC8,0x07,0x81,0x70,0x26,0x04,0xC0,0x98,0x13,0x02,0x60,0x4C,0x09,0x81,0x30,0x26,0x04,0xC0,0x98,0x13,0x02,0x60,0x48,0x07,0x00,0xFF,0xEF,0xF8,
+// 'j'
+0x6A,0x09,0x0F,0x16,0x00,0x0F,
+0x03,0xFC,0x08,0x04,0x38,0x08,0x70,0x10,0x60,0x20,0xC0,0x41,0x80,0x83,0x01,0x06,0x02,0x0C,0x04,0x18,0x08,0x30,0x10,0xA0,0x23,0x00,0x44,0x00,0x98,0x01,0x20,0x00,0xC0,0x09,0x00,0x27,0xC1,0x8F,0xFE,0x0F,0xF0,0x00,
+// 'k'
+0x6B,0x08,0x17,0x18,0x00,0x17,
+0x00,0x01,0x80,0x7F,0xEE,0x81,0x80,0x38,0x87,0x81,0xE0,0x8F,0x03,0x80,0x86,0x06,0x00,0x8C,0x08,0x07,0x18,0x00,0x1C,0x30,0x00,0x70,0x60,0x01,0x00,0xC0,0x02,0x01,0x80,0x02,0x03,0x00,0x04,0x06,0x04,0x04,0x0C,0x0C,0x04,0x18,0x18,0x06,0x30,0x38,0x00,0x60,0x78,0x09,0x00,0x30,0x26,0x00,0x71,0x8F,0xFF,0xE4,0x0F,0xFC,0xD0,0x00,0x01,0xC0,0x00,0x00,0x00,
+// 'l'
+0x6C,0x09,0x11,0x16,0x00,0x11,
+0x3F,0xF0,0x30,0x04,0x1C,0x06,0x0E,0x02,0x03,0x01,0x01,0x80,0x80,0xC0,0x40,0x60,0x20,0x30,0x10,0x18,0x08,0x0C,0x04,0xC6,0x03,0xD3,0x00,0x09,0x80,0x04,0xC0,0x02,0x60,0x01,0x30,0x00,0xB0,0x00,0x58,0x00,0x2F,0xFF,0xD7,0xFF,0xF0,0x00,0x30,
+// 'm'
+0x6D,0x09,0x20,0x16,0x00,0x1F,
+0x00,0xF8,0x1F,0x00,0x01,0x84,0x30,0x80,0x01,0x82,0x61,0x00,0x01,0x82,0x40,0x00,0x01,0x81,0xC0,0x80,0x01,0x01,0x80,0x80,0x03,0x00,0x80,0x40,0x02,0x00,0x00,0x40,0x06,0x00,0x00,0x20,0x06,0x00,0x00,0x20,0x04,0x00,0x00,0x10,0x0C,0x00,0x00,0x10,0x08,0x00,0x00,0x10,0x18,0x08,0x08,0x08,0x10,0x18,0x18,0x08,0x30,0x1C,0x1C,0x04,0x60,0x3C,0x3C,0x02,0x70,0x2E,0x2E,0x06,0x7C,0x27,0x4E,0x3C,0x3F,0x27,0xC6,0xF0,0x07,0xC3,0x8F,0x80,0x01,0x83,0x06,0x00,
+// 'n'
+0x6E,0x09,0x17,0x15,0x00,0x17,
+0x3F,0x07,0xFC,0xC1,0x10,0x07,0xC1,0x78,0x1F,0x81,0xF0,0x23,0x01,0xE0,0x46,0x01,0xC0,0x8C,0x01,0x81,0x18,0x01,0x02,0x30,0x00,0x04,0x60,0x00,0x08,0xC0,0x00,0x11,0x80,0x00,0x23,0x02,0x00,0x46,0x06,0x00,0x8C,0x0E,0x01,0x18,0x1E,0x02,0x30,0x3F,0x04,0x60,0x3F,0x0B,0x00,0x2F,0x17,0xFF,0x8F,0xEF,0xFE,0x0F,0x80,
+// 'o'
+0x6F,0x08,0x17,0x17,0x00,0x17,
+0x00,0x7E,0x00,0x03,0x01,0x80,0x18,0x00,0x80,0x40,0x00,0x81,0x00,0x00,0x86,0x00,0x00,0x08,0x00,0x01,0x30,0x00,0x02,0x40,0x3C,0x03,0x80,0xFC,0x07,0x02,0x3C,0x0E,0x04,0x38,0x1C,0x08,0x30,0x3C,0x08,0x40,0x78,0x0F,0x01,0x30,0x00,0x02,0x70,0x00,0x08,0xF0,0x00,0x10,0xF0,0x00,0xC0,0xF0,0x03,0x00,0xFC,0x1C,0x00,0xFF,0xE0,0x00,0x3F,0x00,0x00,
+// 'p'
+0x70,0x09,0x13,0x15,0x00,0x13,
+0x3F,0xFC,0x08,0x00,0x43,0x80,0x06,0x70,0x00,0x46,0x00,0x04,0xC0,0x00,0x18,0x00,0x0B,0x01,0x81,0x60,0x30,0x2C,0x04,0x05,0x80,0x00,0x30,0x00,0x26,0x00,0x08,0xC0,0x02,0x18,0x01,0x83,0x01,0xE0,0x60,0x30,0x08,0x04,0x03,0x00,0x80,0x7F,0xE0,0x07,0xF8,0x00,
+// 'q'
+0x71,0x09,0x17,0x1C,0x00,0x17,
+0x00,0x7E,0x00,0x03,0x01,0x00,0x18,0x00,0x80,0x40,0x00,0x81,0x00,0x00,0x86,0x00,0x01,0x08,0x00,0x01,0x30,0x1E,0x02,0x40,0x7E,0x03,0x81,0x1E,0x07,0x02,0x1C,0x0E,0x04,0x18,0x1C,0x04,0x20,0x3C,0x07,0x80,0x78,0x00,0x01,0x30,0x00,0x02,0x70,0x00,0x04,0xF0,0x00,0x10,0xF0,0x00,0x70,0xF0,0x00,0x00,0xFC,0x00,0x40,0xFF,0x81,0x00,0x3F,0x84,0x00,0x0F,0x10,0x00,0x06,0x40,0x00,0x0F,0x00,0x00,0x1C,0x00,0x00,0x00,0x00,
+// 'r'
+0x72,0x09,0x18,0x17,0x00,0x18,
+0x3F,0xFE,0x00,0x60,0x01,0x80,0x70,0x00,0x40,0x70,0x00,0x20,0x30,0x00,0x20,0x30,0x00,0x10,0x30,0x10,0x10,0x30,0x18,0x10,0x30,0x10,0x10,0x30,0x00,0x10,0x30,0x00,0x20,0x30,0x00,0x20,0x30,0x00,0x10,0x30,0x00,0x1E,0x30,0x00,0x0A,0x30,0x18,0x06,0x30,0x1C,0x0C,0x20,0x0E,0x18,0x7F,0xFF,0x30,0x7F,0xE7,0x20,0x00,0x03,0xC0,0x00,0x03,0x80,0x00,0x03,0x00,
+// 's'
+0x73,0x07,0x11,0x19,0x00,0x11,
+0x00,0x06,0x00,0x02,0x80,0x1E,0x40,0x10,0x20,0x30,0x10,0x30,0x08,0x10,0x04,0x18,0x01,0x08,0x00,0x8C,0x0F,0x46,0x07,0xA3,0x01,0xE1,0xC0,0xE1,0xE0,0x41,0x70,0x21,0x80,0x10,0xC0,0x08,0x70,0x08,0x38,0x04,0x0C,0x04,0x06,0x04,0x03,0x3C,0x01,0xFC,0x00,0xF0,0x00,0x30,0x00,0x00,
+// 't'
+0x74,0x08,0x12,0x16,0x01,0x13,
+0x30,0x01,0x93,0xFF,0xDC,0x00,0x07,0x00,0x01,0xC0,0x00,0x70,0x00,0x1C,0x00,0x07,0x00,0x01,0xCC,0x07,0x7F,0x01,0xFF,0xC0,0x79,0xB0,0x1C,0x0C,0x04,0x03,0x01,0x00,0xC0,0x40,0x30,0x10,0x0C,0x04,0x03,0x01,0x00,0xC0,0x60,0x60,0x08,0x1F,0xFC,0x07,0xFE,0x00,
+// 'u'
+0x75,0x09,0x19,0x16,0xFF,0x18,
+0x1F,0xF1,0xFF,0x10,0x05,0x00,0x9C,0x07,0xC0,0xCE,0x02,0xE0,0x43,0x01,0x30,0x21,0x80,0x98,0x10,0xC0,0x4C,0x08,0x60,0x26,0x04,0x30,0x13,0x02,0x18,0x09,0x81,0x0C,0x04,0xC0,0x86,0x02,0x60,0x43,0x01,0x30,0x21,0xC0,0x70,0x10,0xE0,0x00,0x10,0x30,0x00,0x08,0x1C,0x00,0x08,0x0F,0x00,0x08,0x03,0xC0,0x08,0x00,0xFC,0x18,0x00,0x1F,0xF8,0x00,0x03,0xF0,0x00,
+// 'v'
+0x76,0x07,0x19,0x18,0x00,0x19,
+0x00,0xC1,0xC0,0x01,0x91,0x98,0x03,0x18,0xC3,0x06,0x04,0x60,0x64,0x02,0x20,0x1F,0x00,0xB0,0x1B,0xC0,0x70,0x0C,0xF0,0x10,0x08,0x38,0x00,0x08,0x0E,0x00,0x04,0x07,0x00,0x04,0x01,0xC0,0x06,0x00,0xE0,0x02,0x00,0x38,0x02,0x00,0x1C,0x01,0x00,0x07,0x01,0x00,0x03,0x81,0x80,0x00,0xE0,0x80,0x00,0x30,0x80,0x00,0x1C,0x40,0x00,0x06,0x40,0x00,0x03,0xE0,0x00,0x00,0xE0,0x00,0x00,0x60,0x00,
+// 'w'
+0x77,0x07,0x20,0x18,0x00,0x20,
+0x00,0x60,0x03,0x80,0x01,0x90,0x06,0x60,0x0E,0x10,0xC6,0x1C,0x38,0x11,0xA6,0x07,0x60,0x11,0x26,0x01,0x78,0x12,0x1C,0x06,0x78,0x0E,0x1C,0x04,0x3C,0x0C,0x08,0x08,0x1C,0x00,0x08,0x10,0x0C,0x00,0x00,0x10,0x0E,0x00,0x00,0x20,0x06,0x00,0x00,0x20,0x07,0x00,0x00,0x40,0x03,0x00,0x00,0x40,0x03,0x80,0x00,0x80,0x03,0x80,0x00,0x80,0x01,0xC0,0x81,0x00,0x01,0xC1,0xC0,0x00,0x00,0xE1,0xC2,0x00,0x00,0xE2,0xE4,0x00,0x00,0x74,0xE4,0x00,0x00,0x7C,0x78,0x00,0x00,0x38,0x78,0x00,0x00,0x30,0x30,0x00,
+// 'x'
+0x78,0x05,0x19,0x1D,0x00,0x18,
+0x00,0x01,0x80,0x00,0x01,0x20,0x00,0x61,0x8C,0x00,0x48,0xC3,0x00,0x44,0x40,0x60,0x43,0x60,0x10,0xC0,0xE0,0x18,0xC0,0x30,0x1C,0xC0,0x00,0x18,0x7C,0x00,0x08,0x1F,0x00,0x08,0x03,0x80,0x08,0x00,0xE0,0x04,0x00,0x38,0x02,0x00,0x0C,0x01,0x80,0x04,0x00,0x60,0x06,0x00,0x10,0x02,0x00,0x07,0x03,0x00,0x00,0x41,0x01,0x00,0x63,0x00,0xC0,0x62,0x00,0xF0,0x63,0x80,0x7C,0x61,0xF0,0x4E,0x60,0x7C,0x63,0xE0,0x0F,0x91,0xE0,0x01,0xF0,0x40,0x00,0x70,0x00,0x00,0x00,0x00,0x00,
+// 'y'
+0x79,0x06,0x19,0x1B,0x00,0x19,
+0x00,0x00,0x60,0x00,0xC0,0x48,0x00,0x80,0x62,0x00,0x88,0x20,0xC0,0x82,0x30,0x11,0x80,0xB0,0x01,0x80,0x30,0x0D,0x80,0x00,0x0C,0xF8,0x00,0x08,0x3E,0x00,0x04,0x07,0x80,0x04,0x01,0xE0,0x04,0x00,0x78,0x04,0x00,0x1C,0x02,0x00,0x0C,0x02,0x00,0x04,0x02,0x00,0x04,0x02,0x00,0x06,0x01,0x00,0x0E,0x01,0x00,0x0C,0x01,0x00,0x07,0x01,0x00,0x03,0xC0,0x80,0x01,0xF0,0x80,0x00,0x3E,0x00,0x00,0x0F,0xC0,0x00,0x03,0xE0,0x00,0x00,0x40,0x00,0x00,
+// 'z'
+0x7A,0x08,0x13,0x17,0xFF,0x12,
+0x0C,0x00,0x02,0xFF,0xFC,0xC0,0x01,0x98,0x00,0x23,0x00,0x04,0x60,0x01,0x0C,0x00,0x21,0x80,0x08,0x37,0x01,0x07,0xE0,0x60,0xF8,0x08,0x03,0x01,0x00,0x40,0x70,0x18,0x0D,0x02,0x00,0x20,0xC0,0x04,0x18,0x00,0x86,0x00,0x10,0xC0,0x02,0x10,0x00,0x47,0xFF,0xE8,0xFF,0xFE,0x00,0x01,0x80,
+// '{'
+0x7B,0x05,0x0D,0x1F,0x01,0x0E,
+0x00,0x10,0x0E,0x81,0x04,0x10,0x21,0x81,0x08,0x08,0xC0,0x46,0x0E,0x30,0x71,0x83,0x0C,0x10,0x60,0x84,0x04,0x60,0x23,0x02,0x18,0x08,0xC0,0x47,0x82,0x3C,0x10,0x60,0x83,0x05,0x18,0x38,0xC0,0x46,0x02,0x38,0x11,0xC0,0x87,0x84,0x1F,0xE0,0x7F,0x00,0x30,0x00,0x00,
+// '|'
+0x7C,0x1E,0x00,0x00,0x00,0x09,
+
+// '}'
+0x7D,0x04,0x0E,0x1F,0x00,0x0F,
+0x30,0x00,0xBC,0x06,0x0C,0x18,0x08,0x60,0x21,0x80,0x46,0x01,0x1F,0x04,0x7C,0x11,0xB0,0x44,0xC1,0x03,0x04,0x0C,0x0C,0x30,0x10,0xE0,0x43,0x81,0x04,0x04,0x30,0x70,0xC1,0x03,0x04,0x0C,0x10,0xF0,0x46,0xC1,0x18,0x04,0x60,0x11,0x80,0x86,0x04,0x18,0x60,0x7F,0x81,0xF8,0x06,0x00,0x00,
+// '~'
+0x7E,0x1E,0x00,0x00,0x00,0x09,
+
+
+// Terminator
+0xFF
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/vnc_server/Kconfig	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,27 @@
+
+menu "VNC Server"
+
+config VNC_SERVER_PORT
+	int "VNC server port number"
+	default 5900
+	help
+		Port VNC server will listen on for client connection.
+		This defaults to the standard port 5900, but may
+		be changed to any other port number if required.
+
+
+config VNC_SERVER_FRAME_WIDTH
+	int "Frame width in pixels"
+	default	320
+	help
+		This sets the width of the VNC server frame buffer.
+
+
+config VNC_SERVER_FRAME_HEIGHT
+	int "Frame heigth in pixels"
+	default 240
+	help
+		This sets the height of the VNC server frame buffer.
+
+
+endmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/vnc_server/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2 @@
+# Use defaults.
+COMPONENT_ADD_INCLUDEDIRS := .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/vnc_server/vnc-server.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1520 @@
+/**
+ * @file vnc-server.c
+ * @brief VNC server original written for eCos.
+ *
+ * The VNC server runs on the target platform and waits for a client 
+ * (vncviewer program running on a PC) to connect via an ethernet connection
+ * (port 5900). Once a client has connected, the target platform has a 
+ * virtual screen, keyboard and mouse.
+ *
+ * This port of the VNC server has been designed with some limitations in 
+ * order to keep memory and processor power requirements to a minimum.
+ *
+ * The VNC client must accept the settings that the VNC server suggests (bits-per-pixel, 
+ * number-of-colours and so on as specified at build time with the eCos configuration 
+ * tool) or the VNC server will just disconnect.
+ *
+ * The VNC server only supports CoRRE encoding and RAW encoding for sending 
+ * display data to the client.
+ *
+ * The VNC server does not support password authentication, the client is able 
+ * to connect without the user typing in a password.
+ *
+ * Only one VNC client may connect to the VNC server at a time.
+ *
+ * In reality these limitations are not a problem.
+ *
+ * @author Chris Garry <cgarry@sweeneydesign.co.uk>
+ * @author Michiel Broek.
+ */
+
+#include "vnc-server.h"
+#include "websocket_server.h"
+
+static const char *TAG = "vnc-server";
+
+#define BACKLOG             5       /* Number of pending connections queue will hold */
+#define MESSAGE_BUFFER_SIZE 60	/* Was 50, but I have seen 52 bytes length */
+#define TILE_SIZE           16
+#define TRUE_COLOUR_FLAG    1       /* True colour is set */
+#define BIG_ENDIAN_FLAG     1       /* Always send colour data big endian */
+
+/* Default display parameters */
+#define BITS_PER_PIXEL      8      /* Bits per pixel */
+#define PIXEL_DEPTH         8      /* Usefull bits per pixel */
+#define RED_MAX             7
+#define GREEN_MAX           7
+#define BLUE_MAX            3
+#define RED_SHIFT           5
+#define GREEN_SHIFT         2
+#define BLUE_SHIFT          0
+
+
+/* Initial default RGB332 */
+uint8_t			Bits_Per_Pixel = 8;
+
+uint16_t		Red_Max     = RED_MAX;
+uint16_t		Green_Max   = GREEN_MAX;
+uint16_t		Blue_Max    = BLUE_MAX;
+uint8_t			Red_Shift   = RED_SHIFT;
+uint8_t			Green_Shift = GREEN_SHIFT;
+uint8_t			Blue_Shift  = BLUE_SHIFT;
+bool			AltPixels   = false;
+
+/* Client to Server message types */
+#define SET_PIXEL_FORMAT         0
+#define FIX_COLOUR_MAP_ENTRIES   1
+#define SET_ENCODINGS            2
+#define FRAME_BUFFER_UPDATE_REQ  3
+#define KEY_EVENT                4
+#define POINTER_EVENT            5
+#define CLIENT_CUT_TEXT          6
+
+/* Macros to split colour to bytes */
+#define COLOUR2BYTE1(col) ((col>>8)&0xFF)
+#define COLOUR2BYTE0(col) (col&0xFF)
+
+
+/* Thread function prototypes */
+static TaskHandle_t	xTaskClientHandler = NULL;	///< Task for VNC clients
+static TaskHandle_t	xTaskFrameUpdate   = NULL;	///< Task for framebuffer updates
+static TaskHandle_t	xTaskWSclient      = NULL;	///< Task for websocket clients
+
+SemaphoreHandle_t	SoundBell_lock     = NULL;	///< Lock for the bell sound.
+
+static QueueHandle_t	message_queue;			///< Websockets message queue
+const static int	message_queue_size = 5;		///< Message queue size
+
+struct strMessage {
+    int			length;				///< Length of the message
+    uint8_t		message[MESSAGE_BUFFER_SIZE];	///< The message
+};
+
+
+uint8_t			VNC_pointer_button = 0;
+uint16_t		VNC_pointer_x = 0;
+uint16_t		VNC_pointer_y = 0;
+
+
+/* Define size of each thread's stack */
+#define MIN_STACK_SIZE	3072
+
+
+/* Messages */
+char			server_ProtocolVersion[] = "RFB 003.003\n";
+char			bad_protocol[] = "Unsupported ProtocolVersion";
+char			server_busy[] = "Server is Busy";
+char			sound_bell[] = "\2";
+char			desktop_name[] = "MBSE BrewBoard";	// Hardcoded, don't change or the VNC webclient breaks.
+
+/* Frame Buffer */
+vnc_color_t		frame_buffer[CONFIG_VNC_SERVER_FRAME_HEIGHT+1][CONFIG_VNC_SERVER_FRAME_WIDTH+1];
+
+/* Calculate the number of tiles in the X and Y directions */
+#if (CONFIG_VNC_SERVER_FRAME_HEIGHT % TILE_SIZE) != 0
+#define NUM_TILES_Y_AXIS (CONFIG_VNC_SERVER_FRAME_HEIGHT/TILE_SIZE + 1)
+#define LAST_TILE_HEIGHT (CONFIG_VNC_SERVER_FRAME_HEIGHT % TILE_SIZE)
+#else
+#define NUM_TILES_Y_AXIS (CONFIG_VNC_SERVER_FRAME_HEIGHT/TILE_SIZE)
+#define LAST_TILE_HEIGHT TILE_SIZE
+#endif
+
+#if (CONFIG_VNC_SERVER_FRAME_WIDTH % TILE_SIZE) != 0
+#define NUM_TILES_X_AXIS (CONFIG_VNC_SERVER_FRAME_WIDTH/TILE_SIZE + 1)
+#define LAST_TILE_WIDTH (CONFIG_VNC_SERVER_FRAME_WIDTH % TILE_SIZE)
+#else
+#define NUM_TILES_X_AXIS (CONFIG_VNC_SERVER_FRAME_WIDTH/TILE_SIZE)
+#define LAST_TILE_WIDTH TILE_SIZE
+#endif
+
+/* Array for marking tiles that have been updated */
+int			tile_updated[NUM_TILES_Y_AXIS+1][NUM_TILES_X_AXIS+1];
+
+/* Conditional variable to signal that a client is connected and initialised */
+EventGroupHandle_t	xEventGroupVNC;
+const int 		VNC_CLIENT_UPDATE_REQ = BIT0;
+int			vnc_client_sock = -1;
+bool			vnc_client_connected = false;
+
+/* Variable for sounding the client's bell */
+int			SoundBellCount;
+
+/* Variables for the Websocket client */
+bool			VNC_WS_run = false;
+int			VNC_WS_num = -1;
+
+
+
+/* Variable to hold the frame format details */
+vnc_frame_format_t	frame_format = {CONFIG_VNC_SERVER_FRAME_WIDTH, CONFIG_VNC_SERVER_FRAME_HEIGHT, frame_buffer,
+                                   1,			// RGB332 server native.
+                                   0,			// RGB555
+                                   0,			// RGB565
+                                   0,			// BGR233
+                                   0,			// TRUECOLOR0888
+};
+
+
+
+#if 0
+void dump_msg(char *buf, uint16_t buflen, char *mode)
+{   
+    int i, l = 0;
+
+    printf("%s %d", mode, buflen);
+    for (i = 0; i < buflen; i++) {
+	if ((i % 16) == 0) {
+	    printf("\n%02d: ", l);
+	    l++;
+	}
+	printf("%02x ", buf[i]);
+    }
+    printf("\n");
+}
+#endif
+
+
+/* Structure to hold the encoding type details */
+volatile struct encoding_type_struct
+{
+    uint8_t		raw;
+    uint8_t		copy_rectangle;
+    uint8_t		rre;
+    uint8_t		corre;
+    uint8_t		hextile;
+} encoding_type;
+
+
+static int GetMessageData(int, char *, int);
+static int GenTileUpdateData(uint8_t *);
+void task_VNCserver(void *);
+void task_frame(void *);
+void task_WS(void *);
+
+
+
+/**
+ * @brief Serve a new VNC client.
+ */
+void vnc_netconn_serve(void);
+
+
+/* VNC startup. */
+void VncStartup(void)
+{
+    /* Initialise mutex & cond vars */
+    xEventGroupVNC = xEventGroupCreate();
+    SoundBell_lock = xSemaphoreCreateMutex();
+
+    xTaskCreate(&task_VNCserver, "VNCserver",    MIN_STACK_SIZE, NULL, 5, &xTaskClientHandler);
+    xTaskCreate(&task_frame,     "frame_update", MIN_STACK_SIZE, NULL, 6, &xTaskFrameUpdate);
+}
+
+
+/** 
+ * @brief Client Handler Task.
+ *
+ * This task handles the client initialisation sequence. Once the client
+ * is initialised this task handles all received messages from the client,
+ * but does not send any data to the client.
+ */
+void task_VNCserver(void *pvParameter)
+{
+    int			server_sock; 
+    struct sockaddr_in	server_addr;
+    struct sockaddr_in	client_addr;
+    socklen_t		client_addr_size;
+
+    ESP_LOGI(TAG, "Starting VNC server");
+
+    /* Clear the encoding type structure */
+    encoding_type.raw = 0;
+    encoding_type.copy_rectangle = 0;
+    encoding_type.rre = 0;
+    encoding_type.corre = 0;
+    encoding_type.hextile = 0;
+
+    /* Clear the sound bell counter */
+    SoundBellCount = 0;
+    VNC_pointer_button = 0;
+    VNC_pointer_x = VNC_pointer_y = 0;
+
+    /* Create socket for incomming connections */
+    if ((server_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
+        ESP_LOGE(TAG, "socket() VNC function failed");
+        exit(1);
+    }
+
+    /* Construct the server address structure */
+    memset(&server_addr, 0, sizeof(server_addr));  /* Fill entire structure with 0's */
+    server_addr.sin_family      = AF_INET;         /* Internet address family */
+    server_addr.sin_addr.s_addr = INADDR_ANY;      /* Autofill with my IP address */
+    server_addr.sin_port        = htons(CONFIG_VNC_SERVER_PORT);
+
+    /* Bind socket to local address */
+    if (bind(server_sock, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) {
+        ESP_LOGE(TAG, "bind() VNC function failed");
+        exit(1);
+    }
+
+    /* Set the socket to listen for incoming connections */
+    if (listen(server_sock, BACKLOG) < 0) {
+        ESP_LOGE(TAG, "listen() VNC function failed");
+        exit(1);
+    }
+
+    do {
+	vnc_client_sock = (accept(server_sock, (struct sockaddr *) &client_addr, &client_addr_size));
+	if (vnc_client_sock >= 0) {
+	    vnc_client_connected = true;
+	    vnc_netconn_serve();
+	    vnc_client_connected = false;
+	}
+	vTaskDelay((TickType_t)10); /* allows the freeRTOS scheduler to take over if needed */
+    } while (vnc_client_sock >= 0);
+}
+
+
+
+/**
+ * @brief Serve a new VNC client and handle the whole session.
+ */
+void vnc_netconn_serve(void)
+{
+    long int			temp_long;
+    char 			protocol_ver[8], message_buffer[MESSAGE_BUFFER_SIZE];
+    int				i, j, message_len, ProtocolOkay;
+    uint32_t			*ptr_to_uint32;
+    uint16_t			*ptr_to_uint16;
+    struct sockaddr_storage	addr;
+    socklen_t			len = sizeof addr;
+    char			ipstr[INET6_ADDRSTRLEN];
+
+    getpeername(vnc_client_sock, (struct sockaddr*)&addr, &len);
+    if (addr.ss_family == AF_INET) {
+	struct sockaddr_in *s = (struct sockaddr_in *)&addr;
+	inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr);
+    }
+    ESP_LOGI(TAG, "VNC new client %s socket %d", ipstr, vnc_client_sock);
+
+    /* ProtocolVersion Handshake - begin */
+    /* Send ProtocolVersion we want to use to client */
+    message_len = sprintf(message_buffer, "RFB 003.003\n");
+    if (send(vnc_client_sock, message_buffer, message_len, 0) != message_len) {
+	goto close_connection;
+    }
+
+    /* Receive ProtocolVersion the client wants to use */
+    if (GetMessageData(vnc_client_sock, &(message_buffer[0]), 12) == 0) {
+	goto close_connection;
+    }
+
+    /* Check this is acceptable (RFB 003.xxx is okay) */
+    ProtocolOkay = 1;
+    for (i = 0; i < 8; i++) {
+	if (message_buffer[i] != server_ProtocolVersion[i]) {
+	    ProtocolOkay = 0;
+	}
+
+        /* Store the protocol version - ignoring thr 'RFB ' part */
+        protocol_ver[i] = message_buffer[i + 4];
+    }
+    protocol_ver[7] = 0;
+    /* ProtocolVersion Handshake - end */
+
+    /* Authentication - begin */
+    /* Send Authentication scheme to be used to client */
+    if (ProtocolOkay == 0) {
+	/* ProtocolVerion is not okay */
+
+	/* Generate the Bad ProtocolVerion message */
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+	*ptr_to_uint32 = htonl(0);
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[4]);
+	*ptr_to_uint32 = htonl(strlen(bad_protocol));
+	strcpy(&(message_buffer[8]), bad_protocol);
+
+	if (send(vnc_client_sock, message_buffer, 8 + strlen(bad_protocol), 0) != (8 + strlen(bad_protocol))) {
+	    printf("Call to send() 1 failed\n");
+	}
+	goto close_connection;
+
+    } else if (VNC_WS_num != -1) {
+	/* Busy with a websocket client */
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+	*ptr_to_uint32 = htonl(0);
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[4]);
+	*ptr_to_uint32 = htonl(strlen(server_busy));
+	strcpy(&(message_buffer[8]), server_busy);
+
+	if (send(vnc_client_sock, message_buffer, 8 + strlen(server_busy), 0) != (8 + strlen(server_busy))) {
+	    printf("Call to send() 1 failed\n");
+	}
+	goto close_connection;
+
+    } else {
+	/* ProtocolVerion is okay - connect with no authentication*/
+
+	/* Generate the No Authentication message */
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+	*ptr_to_uint32 = htonl((uint32_t)1);
+
+	if (send(vnc_client_sock, message_buffer, 4, 0) != 4) {
+	    goto close_connection;
+	}
+    }
+    /* Authentication - end */
+
+    /* ClientInitialisation - begin */
+    /* Receive initialisation message from client (1 byte) */
+    if (GetMessageData(vnc_client_sock, &(message_buffer[0]), 1) == 0) {
+	goto close_connection;
+    }
+    /* Do nothing with this as we only support 1 Client at a time */
+    /* ClientInitialisation - end */
+
+    /* ServerInitialisation - begin */
+
+    /* Initial default RGB332 */
+    Red_Max     = RED_MAX;
+    Green_Max   = GREEN_MAX;
+    Blue_Max    = BLUE_MAX;
+    Red_Shift   = RED_SHIFT;
+    Green_Shift = GREEN_SHIFT;
+    Blue_Shift  = BLUE_SHIFT;
+    AltPixels   = false;
+
+    /* Create Initialisation message for client */
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[0]);
+    *ptr_to_uint16 = htons((uint16_t)CONFIG_VNC_SERVER_FRAME_WIDTH);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[2]);
+    *ptr_to_uint16 = htons((uint16_t)CONFIG_VNC_SERVER_FRAME_HEIGHT);
+
+    message_buffer[4] = (uint8_t)BITS_PER_PIXEL;
+    message_buffer[5] = (uint8_t)PIXEL_DEPTH;
+    message_buffer[6] = (uint8_t)BIG_ENDIAN_FLAG;
+    message_buffer[7] = (uint8_t)TRUE_COLOUR_FLAG;
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[8]);
+    *ptr_to_uint16 = htons(Red_Max);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[10]);
+    *ptr_to_uint16 = htons(Green_Max);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[12]);
+    *ptr_to_uint16 = htons(Blue_Max);
+
+    message_buffer[14] = Red_Shift;
+    message_buffer[15] = Green_Shift;
+    message_buffer[16] = Blue_Shift;
+
+    ptr_to_uint32 = (uint32_t *) &(message_buffer[20]);
+    *ptr_to_uint32 = htonl(strlen(desktop_name));
+    strcpy(&(message_buffer[24]), desktop_name);
+
+    if (send(vnc_client_sock, message_buffer, 24 + strlen(desktop_name), 0) != (24 + strlen(desktop_name))) {
+	printf("Call to send() 3 failed\n");
+    }
+    /* ServerInitialisation - end */
+
+    /* Cancel any outstanding Sound Bell requests */
+    if (xSemaphoreTake(SoundBell_lock, 10) == pdTRUE) {
+	SoundBellCount = 0;
+	xSemaphoreGive(SoundBell_lock);
+    }
+
+    ESP_LOGI(TAG, "VNC client connected (RFB Protocol Ver: %s)", protocol_ver);
+
+    /* Main message handling loop */
+    while(1) {
+	int num_of_encodings;
+
+	/* Receive 1st byte of message from client */
+	if (GetMessageData(vnc_client_sock, &(message_buffer[0]), 1) == 0) {
+	    goto close_connection;	/* Connection lost */
+	}
+
+	switch(message_buffer[0]) {
+            case SET_PIXEL_FORMAT:
+
+                /* Get the remainder (19 bytes) of message */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 19) == 0) {
+                    goto close_connection;
+                }
+
+                /* Check pixel format is as expected */
+                i = 0;
+                if (message_buffer[4] != BITS_PER_PIXEL) {
+		    ESP_LOGI(TAG, "SetPixelFormat client wants %d bits-per-pixel", message_buffer[4]);
+                    i++;
+                }
+
+                if (message_buffer[5] != PIXEL_DEPTH) {
+		    ESP_LOGI(TAG, "SetPixelFormat client wants %d pixel-depth", message_buffer[5]);
+                    i++;
+                }
+
+                if ((message_buffer[7] & 0x01) != TRUE_COLOUR_FLAG) {
+		    ESP_LOGI(TAG, "SetPixelFormat client wants %d true-colour-flag", message_buffer[7]);
+                    i++;
+                }
+
+                ptr_to_uint16 = (uint16_t *)&(message_buffer[8]);
+                if (htons(*ptr_to_uint16) != Red_Max) {
+		    Red_Max = htons(*ptr_to_uint16);
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d red-max", Red_Max);
+                }
+
+                ptr_to_uint16 = (uint16_t *)&(message_buffer[10]);
+                if (htons(*ptr_to_uint16) != Green_Max) {
+		    Green_Max = htons(*ptr_to_uint16);
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d green-max", Green_Max);
+                }
+
+                ptr_to_uint16 = (uint16_t *)&(message_buffer[12]);
+                if (htons(*ptr_to_uint16) != Blue_Max) {
+		    Blue_Max = htons(*ptr_to_uint16);
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d blue-max", Blue_Max);
+                }
+
+                if (message_buffer[14] != Red_Shift) {
+		    Red_Shift = message_buffer[14];
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d red-shift", Red_Shift);
+                }
+
+                if (message_buffer[15] != Green_Shift) {
+		    Green_Shift = message_buffer[15];
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d green-shift", Green_Shift);
+                }
+
+                if (message_buffer[16] != Blue_Shift) {
+		    Blue_Shift = message_buffer[16];
+		    AltPixels = true;
+		    ESP_LOGI(TAG, "SetPixelFormat client granted %d blue-shift", Blue_Shift);
+                }
+
+                if (i) {
+		    ESP_LOGI(TAG, "SetPixelFormat %d errors, disconnect client", i);
+                     ESP_LOGI(TAG, "Ensure the 'Auto select' is not enabled in your vncviewer options,");
+                    goto close_connection_quietly;
+                }
+
+                break;
+
+            case FIX_COLOUR_MAP_ENTRIES:
+printf("fix colormap entries\n");
+                /* Not supported, just get the data from the buffer and ignore it */
+
+                /* Get the next 5 bytes of message */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 5) == 0) {
+                    goto close_connection;
+                }
+
+                /* Calculate how many colour entries are in the buffer */
+                i = message_buffer[4]*255 + message_buffer[5];
+                i *= 6;
+
+                /* Get this amount of data from the buffer */
+                for (j = 0; j < i; j++) {
+                    if (GetMessageData(vnc_client_sock, &(message_buffer[6]), 6) == 0) {
+                        goto close_connection;
+                    }
+                }
+
+                break;
+
+            case SET_ENCODINGS:
+
+                /* Get the next 3 bytes of message */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 3) == 0) {
+                    goto close_connection;
+                }
+
+                num_of_encodings = message_buffer[2]*255 + message_buffer[3];
+
+                /* Get the remainder of message */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[0]), 4 * num_of_encodings) == 0) {
+                    goto close_connection;
+                }
+
+                /* Clear the encoding type structure */
+                encoding_type.raw = 0;
+                encoding_type.copy_rectangle = 0;
+                encoding_type.rre = 0;
+                encoding_type.corre = 0;
+                encoding_type.hextile = 0;
+
+                for (i = 0; i < num_of_encodings; i++) {
+                    switch(message_buffer[3 + (i*4)]) {
+                    case 0:  /* Raw encoding */
+                        encoding_type.raw = i + 1;
+                        break;
+                    case 1:  /* Copy rectangle encoding */
+                        encoding_type.copy_rectangle = i + 1;
+                        break;
+                    case 2:  /* RRE encoding */
+                        encoding_type.rre = i + 1;
+                        break;
+                    case 4:  /* CoRRE encoding */
+                        encoding_type.corre = i + 1;
+                        break;
+                    case 5:  /* Hextile encoding */
+                        encoding_type.hextile = i + 1;
+                        break;
+                    default:  /* Unknown coding type - do nothing */
+                        break;
+                    }
+                }
+
+                if (encoding_type.corre) {
+		    ESP_LOGI(TAG, "SetEncodings use CORRE");
+		} else {
+		    ESP_LOGI(TAG, "SetEncodings use RAW");
+		}
+                break;
+
+            case FRAME_BUFFER_UPDATE_REQ:
+                /* Get the remainder of message (9 bytes) */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 9) == 0) {
+                    goto close_connection;
+                }
+
+                if (!message_buffer[1]) {
+                    /* Non-incremental mode - mark the squares that need to be updated */
+                    for (i = (message_buffer[2]*255 + message_buffer[3])/TILE_SIZE;
+                         i <= (message_buffer[2]*255 + message_buffer[3] + message_buffer[6]*255 + message_buffer[7])/TILE_SIZE;
+                         i++) {
+                        for (j = (message_buffer[4]*255 + message_buffer[5])/TILE_SIZE;
+                             j <= (message_buffer[4]*255 + message_buffer[5] + message_buffer[8]*255 + message_buffer[9])/TILE_SIZE;
+                             j++) {
+                            tile_updated[j][i] = 1;
+                        }
+                    }
+                }
+
+                /* Signal that there is now a pending update request */
+                xEventGroupSetBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ);
+		break;
+
+            case KEY_EVENT:
+                /* Handle the key event, ignored for brewboard */
+                /* Get the remainder of message (7 bytes) */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 7) == 0) {
+                    goto close_connection;
+                }
+                break;
+
+            case POINTER_EVENT:
+                /* Handle the pointer event, simulate the touch screen. */
+                /* Get the remainder of message (5 bytes) */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 5) == 0) {
+                    goto close_connection;
+                }
+		/* Set global variables that will be read by another task. */
+		VNC_pointer_button = message_buffer[1];
+		VNC_pointer_x = message_buffer[2]*255 + message_buffer[3];
+		VNC_pointer_y = message_buffer[4]*255 + message_buffer[5];
+                break;
+
+            case CLIENT_CUT_TEXT:
+                /* Handle the client has cut text event */
+                /* Current we just get and discard the data */
+                /* Get the next 7 bytes of the message */
+                if (GetMessageData(vnc_client_sock, &(message_buffer[1]), 7) == 0) {
+                    goto close_connection;
+                }
+
+                ptr_to_uint32 = (uint32_t *)&(message_buffer[4]);
+                temp_long = htonl(*ptr_to_uint32);
+
+                while (temp_long > 0) {
+                    /* Get the text in chunks MESSAGE_BUFFER_SIZE-1 characters */
+                    if (temp_long > MESSAGE_BUFFER_SIZE-2) {
+                        if (GetMessageData(vnc_client_sock, &(message_buffer[0]), MESSAGE_BUFFER_SIZE-1) == 0) {
+                            goto close_connection;
+                        }
+
+                        message_buffer[MESSAGE_BUFFER_SIZE-1] = 0;
+                        temp_long -= (MESSAGE_BUFFER_SIZE-1);
+                    } else {
+                        if (GetMessageData(vnc_client_sock, &(message_buffer[0]), temp_long) == 0) {
+                            goto close_connection;
+                        }
+
+                        message_buffer[temp_long] = 0;
+                        temp_long = 0;
+                    }
+                }
+
+                break;
+
+            default:
+		ESP_LOGI(TAG, "Unknown message %d from client", message_buffer[0]);
+        }
+
+	vTaskDelay( (TickType_t)1);
+    }
+
+close_connection:
+    ESP_LOGI(TAG, "VNC client disconnected");
+
+close_connection_quietly:
+
+    /* Cancel any outstanding update requests */
+    xEventGroupClearBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ);
+    close(vnc_client_sock);
+}
+
+
+
+/**
+ * @brief Frame update task. This thread handles the sending of all frame
+ *        update data to the client and sends the 'ring bell' message to
+ *        the client when required.
+ *        Works for VNC port connected clients and Websocket clients.
+ * @param pvParameter Ignored
+ */
+void task_frame(void *pvParameter)
+{
+    int			i, j, x_pos, y_pos, packet_length, num_updated_tiles;
+    uint16_t		*ptr_to_uint16;
+    /* These are declared static so they don't use thread stack memory */
+    static uint8_t	FramebufferUpdate_msg[4 + 12 + TILE_SIZE*TILE_SIZE*BITS_PER_PIXEL/8 + 1460];
+    static int		FrameBufferPtr;  /* Pointer to next space in buffer */
+    static int		tile_updated_local[NUM_TILES_Y_AXIS][NUM_TILES_X_AXIS];
+
+    ESP_LOGI(TAG, "Starting VNC frame updater");
+
+    while(1) {
+        /* Wait until client sends a frame update request */
+wait_for_client:
+	xEventGroupWaitBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ, pdFALSE, pdFALSE, portMAX_DELAY);
+
+        /* Copy tile_updated array to local version and clear copied tiles */
+	vTaskSuspendAll();
+        num_updated_tiles = 0;
+        for (i = 0; i < NUM_TILES_Y_AXIS; i++) {
+            for (j = 0; j < NUM_TILES_X_AXIS; j++) {
+                if (tile_updated[i][j]) {
+                    tile_updated_local[i][j] = 1;
+                    tile_updated[i][j] = 0;
+                    num_updated_tiles++;  /* Keep count of the updated tiles */
+                }
+            }
+        }
+	xTaskResumeAll();
+
+        if (num_updated_tiles) {
+	    /* Cancel update request */
+	    xEventGroupClearBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ);
+            /* Fill in constant parts of FramebufferUpdate Message */
+            FramebufferUpdate_msg[0] = 0;  /* Message-type */
+            FramebufferUpdate_msg[1] = 0;  /* Padding */
+            ptr_to_uint16 = (uint16_t *) &(FramebufferUpdate_msg[2]);
+            *ptr_to_uint16 = htons(num_updated_tiles);  /* Number-of-rectangles */
+            FrameBufferPtr = 4;
+
+            for (y_pos = 0; y_pos < NUM_TILES_Y_AXIS; y_pos++) {
+                for (x_pos = 0; x_pos < NUM_TILES_X_AXIS; x_pos++) {
+                    if (tile_updated_local[y_pos][x_pos]) {
+                        /* Send current square data to client */
+
+                        /* x-position */
+                        FramebufferUpdate_msg[FrameBufferPtr+0] = (x_pos * TILE_SIZE) / 256;
+                        FramebufferUpdate_msg[FrameBufferPtr+1] = (x_pos * TILE_SIZE) % 256;
+
+                        /* y-position */
+                        FramebufferUpdate_msg[FrameBufferPtr+2] = (y_pos * TILE_SIZE) / 256;
+                        FramebufferUpdate_msg[FrameBufferPtr+3] = (y_pos * TILE_SIZE) % 256;
+
+
+                        /* Set width of tile in packet */
+                        if (x_pos == (NUM_TILES_X_AXIS -1)) {
+                            /* Last tile in X-axis */
+                            FramebufferUpdate_msg[FrameBufferPtr+4] = LAST_TILE_WIDTH / 256;
+                            FramebufferUpdate_msg[FrameBufferPtr+5] = LAST_TILE_WIDTH % 256;
+                        } else {
+                            FramebufferUpdate_msg[FrameBufferPtr+4] = TILE_SIZE / 256;
+                            FramebufferUpdate_msg[FrameBufferPtr+5] = TILE_SIZE % 256;
+                        }
+
+                        if (y_pos == (NUM_TILES_Y_AXIS -1)) {
+                            /* Last tile in Y-axis */
+                            FramebufferUpdate_msg[FrameBufferPtr+6] = LAST_TILE_HEIGHT / 256;
+                            FramebufferUpdate_msg[FrameBufferPtr+7] = LAST_TILE_HEIGHT % 256;
+                        } else {
+                            FramebufferUpdate_msg[FrameBufferPtr+6] = TILE_SIZE / 256;
+                            FramebufferUpdate_msg[FrameBufferPtr+7] = TILE_SIZE % 256;
+                        }
+
+                        /* Generate the packet data for this tile */
+                        packet_length = GenTileUpdateData(&(FramebufferUpdate_msg[FrameBufferPtr]));
+
+                        /* Send the packet data for this tile to the client */
+                        FrameBufferPtr += packet_length;
+
+                        if (FrameBufferPtr > 1460) {
+                            /* Send the data to the client */
+			    if (VNC_WS_num != -1) {
+				ws_server_send_bin_client(VNC_WS_num, (char *)FramebufferUpdate_msg, FrameBufferPtr);
+			    }
+			    if (vnc_client_connected) {
+                            	if (send(vnc_client_sock, FramebufferUpdate_msg, FrameBufferPtr, 0) != FrameBufferPtr) {
+                            	    goto wait_for_client;
+                            	}
+			    }
+                            FrameBufferPtr = 0;
+                        }
+
+                        tile_updated_local[y_pos][x_pos] = 0;  /* Clear the update bit for this square */
+                    }
+                }
+            }
+
+            if (FrameBufferPtr > 0) {
+                /* Last data for this update, send it to the client */
+		if (VNC_WS_num != -1) {
+		    ws_server_send_bin_client(VNC_WS_num, (char *)FramebufferUpdate_msg, FrameBufferPtr);
+		}
+		if (vnc_client_connected) {
+                    if (send(vnc_client_sock, FramebufferUpdate_msg, FrameBufferPtr, 0) != FrameBufferPtr) {
+                    	goto wait_for_client;
+                    }
+		}
+
+                FrameBufferPtr = 0;
+            }
+
+        } else { /* if (num_updated_tiles) */
+            /* There was no new display data to send to the client */
+            /* Sleep for 1/20th second before checking again */
+	    vTaskDelay(20 / portTICK_PERIOD_MS);
+	}
+
+        /* Check for sound bell event */
+	if (xSemaphoreTake(SoundBell_lock, 10) == pdTRUE) {
+            if (SoundBellCount) {
+            	--SoundBellCount;
+		xSemaphoreGive(SoundBell_lock);
+
+		if (vnc_client_connected) {
+            	    if (send(vnc_client_sock, sound_bell, 1, 0) != 1) {
+                    	goto wait_for_client;
+            	    }
+		}
+		if (VNC_WS_num != -1) {
+		    ws_server_send_bin_client(VNC_WS_num, sound_bell, 1);
+		}
+            } else {
+	    	xSemaphoreGive(SoundBell_lock);
+	    }
+	}
+    }
+}
+
+
+
+/**
+ * Convert a framebuffer pixel to BGR233 if needed.
+ * @param pixel The 8 bit pixel value
+ * @return Then unchanged or changed pixel.
+ */
+vnc_color_t IRAM_ATTR PixelConvert(vnc_color_t pixel)
+{
+    if (!AltPixels)
+    	return pixel;
+
+    return (((pixel >> RED_SHIFT) & RED_MAX) << Red_Shift) | 
+	   (((pixel >> GREEN_SHIFT) & GREEN_MAX) << Green_Shift) | 
+	   (((pixel >> BLUE_SHIFT) & BLUE_MAX) << Blue_Shift);
+}
+
+
+
+/** 
+ * @brief Generate tile update data function
+ *
+ *        This function is called by the frame_update thread to generate the message
+ *        data for a tile to send to the client.  This function expects the
+ *        x-position, y-position, width and height fields of the buffer to be filled
+ *        in before it is called.
+ *
+ *        The format of the buffer is:
+ *        packet_buffer[0:1] - x-position of tile
+ *        packet_buffer[2:3] - y-position of tile
+ *        packet_buffer[4:5] - width of tile
+ *        packet_buffer[6:7] - height of tile
+ *        packet_buffer[8 onwards] - Pixel data for the tile
+ *
+ *        The pixel data will be encoded with CoRRE encoding (if the CDL option is
+ *        enabled and the client can handle it) or RAW encoding if that is smaller
+ *        than CoRRE encoding for that particular tile.
+ *
+ * @param  packet_buffer  Buffer to store tile data
+ * @return Length of generated data in bytes
+ */
+static int GenTileUpdateData(uint8_t *packet_buffer)
+{
+    uint16_t		x_pos, y_pos;
+    int			i, j;
+    int			tile_width, tile_height;
+    int			packet_length;
+    static vnc_color_t	tile_buffer[TILE_SIZE][TILE_SIZE];  /* Buffer to hold tile to be encoded */
+    vnc_color_t		pixel_colour;
+    vnc_color_t		bg_colour;
+    int			no_of_subrects, subrect_width, subrect_height;
+    int			k, l;
+
+    no_of_subrects = 0;  /* Set to no sub-rectangles to start with */
+    packet_length = 20-4+(Bits_Per_Pixel/8);  /* Set to minimum packet length to start with */
+
+    /* Get the X and Y positions of this tile from the packet buffer */
+    x_pos = packet_buffer[0] * 256 + packet_buffer[1];
+    y_pos = packet_buffer[2] * 256 + packet_buffer[3];
+
+    /* Get the tile width and height from the packet buffer */
+    tile_width = packet_buffer[4] * 256 + packet_buffer[5];
+    tile_height = packet_buffer[6] * 256 + packet_buffer[7];
+
+    /* Set the encoding type to RRE  */
+    if (!encoding_type.corre) {
+        /* CoRRE encoding is not supported - just use raw encoding */
+        goto use_raw_encoding;
+    }
+
+    /* Set encoding type to CoRRE encoding in packet buffer */
+    packet_buffer[8+0] = 0;
+    packet_buffer[8+1] = 0;
+    packet_buffer[8+2] = 0;
+    packet_buffer[8+3] = 4;
+
+    /* Copy tile from the main frame buffer to the local tile buffer */
+    for (i = 0; i < tile_height; i++) {
+        for (j = 0; j < tile_width; j++) {
+            tile_buffer[i][j] = frame_buffer[y_pos + i][x_pos + j];
+        }
+    }
+
+    /* Find the background colour */
+    /* We just assume the (0, 0) pixel in the tile is the bgcolour */
+    /* Its quick!!! */
+    bg_colour = frame_buffer[y_pos][x_pos];
+
+    /* Set the background colour in the packet buffer */
+    if (Bits_Per_Pixel == 8) {
+    	packet_buffer[16] = PixelConvert(bg_colour); /* (vnc_color_t) bg_colour; */
+    } else {
+	packet_buffer[16]   = COLOUR2BYTE0(bg_colour);
+	packet_buffer[16+1] = COLOUR2BYTE1(bg_colour);
+    }
+
+#ifdef CYGNUM_VNC_SERVER_CORRE_ENCODING_HACK
+    /* Add an initial sub-rectangle to paint the background the background colour */
+    /* This is required because of a known bug in the VNC viewer (x86 version) */
+//#if BITS_PER_PIXEL == 8
+    packet_buffer[packet_length] = (vnc_color_t) bg_colour;
+    packet_length++;
+//#endif
+//#if BITS_PER_PIXEL == 16
+//    packet_buffer[packet_length]   = packet_buffer[16];
+//    packet_buffer[packet_length+1] = packet_buffer[16+1];
+//    packet_length += 2;
+//#endif
+    packet_buffer[packet_length]   = (uint8_t) 0;  /* Sub-rect x-pos */
+    packet_buffer[packet_length+1] = (uint8_t) 0;  /* Sub-rect y-pos*/
+    packet_buffer[packet_length+2] = (uint8_t) tile_width;  /* Sub-rect width*/
+    packet_buffer[packet_length+3] = (uint8_t) tile_height;  /* Sub-rect height*/
+    packet_length += 4;
+    no_of_subrects++;  /* Increment sub-rectangle count */
+#endif
+
+    /* Scan trough tile and find sub-rectangles */
+    for (i = 0; i < tile_height; i++) {
+        for (j = 0; j < tile_width; j++) {
+            if (tile_buffer[i][j] != bg_colour) {
+                /* This is a non-background pixel */
+                subrect_width = 1;
+                pixel_colour = tile_buffer[i][j];
+
+                /* Extend the sub-rectangle to its maximum width */
+                for (subrect_width = 1; subrect_width <= tile_width-j-1; subrect_width++) {
+                    if (tile_buffer[i][j+subrect_width] != pixel_colour) {
+                        goto got_subrect_width;
+                    }
+                }
+
+got_subrect_width:
+
+                /* Extend the sub-rectangle to its maximum height */
+                for (subrect_height=1; subrect_height <= tile_height-i-1; subrect_height++) {
+                    for (k = j; k < j+subrect_width; k++) {
+                        if (tile_buffer[i+subrect_height][k] != pixel_colour) {
+                            goto got_subrect_height;
+                        }
+                    }
+                }
+
+got_subrect_height:
+
+                /* Delete the pixels for the sub-rectangle from the sub-rectangle */
+                for (k = i; k < i+subrect_height; k++) {
+                    for (l = j; l < j+subrect_width; l++) {
+                        tile_buffer[k][l] = bg_colour;
+                    }
+                }
+
+                /* Append new sub-rectangle data to the packet buffer */
+		if (Bits_Per_Pixel == 8) {
+                    packet_buffer[packet_length] = PixelConvert(pixel_colour); //  (vnc_color_t) pixel_colour;
+                    packet_length++;
+		} else {
+		    packet_buffer[packet_length]   = COLOUR2BYTE0(pixel_colour);
+		    packet_buffer[packet_length+1] = COLOUR2BYTE1(pixel_colour);
+		    packet_length += 2;
+		}
+
+                packet_buffer[packet_length] = (uint8_t) j;  /* Sub-rect x-pos */
+                packet_length++;
+
+                packet_buffer[packet_length] = (uint8_t) i;  /* Sub-rect y-pos*/
+                packet_length++;
+
+                packet_buffer[packet_length] = (uint8_t) subrect_width;  /* Sub-rect width*/
+                packet_length++;
+
+                packet_buffer[packet_length] = (uint8_t) subrect_height;  /* Sub-rect height*/
+                packet_length++;
+
+                no_of_subrects++;  /* Increment sub-rectangle count */
+
+                if (packet_length >= 12 + tile_height*tile_width*(BITS_PER_PIXEL/8) - 6) {
+                    /* The next sub-rectangle will make the packet size   */
+                    /* larger than a rew encoded packet - so just use raw */
+                    goto use_raw_encoding;
+                }
+            }
+        }
+    }
+
+    /* Fill in no_of_sub-rectangles field in packet buffer */
+    packet_buffer[12+0] = 0;
+    packet_buffer[12+1] = 0;
+    packet_buffer[12+2] = no_of_subrects / 256;
+    packet_buffer[12+3] = no_of_subrects % 256;
+
+    /* CoRRE data encoding for tile complete */
+    return packet_length;
+
+use_raw_encoding:
+
+    /* Create packet data using RAW encoding */
+    for (i = 0; i < tile_height; i++) {
+        for (j = 0; j < tile_width; j++) {
+	    if (Bits_Per_Pixel == 8) {
+             	packet_buffer[12 + tile_width * i + j] = PixelConvert(frame_buffer[y_pos + i][x_pos + j]);
+	    } else {
+		packet_buffer[12 + 2 * tile_width * i + 2*j]    = COLOUR2BYTE0(frame_buffer[y_pos + i][x_pos + j]);
+		packet_buffer[12 + 2 * tile_width * i + 2*j+ 1] = COLOUR2BYTE1(frame_buffer[y_pos + i][x_pos + j]);
+	    }
+        }
+    }
+
+    /* Set the encoding type to raw */
+    packet_buffer[8+0] = 0;
+    packet_buffer[8+1] = 0;
+    packet_buffer[8+2] = 0;
+    packet_buffer[8+3] = 0;
+
+    return (12 + tile_width*tile_height*(Bits_Per_Pixel/8));
+}
+
+
+
+/**
+ * @brief Get message data function
+ *        This function is called by the client_handler thread to get data
+ *        from the client's socket.
+ *
+ * @param socket_fd    File descriptor of the socket to get the data from.
+ * @param *buffer      Buffer to store received data in.
+ * @param num_bytes    Number of bytes to attempt to get.
+ *
+ * @return  1 on sucessfull completion - 0 on error.
+ */
+static int GetMessageData(int socket_fd, char *buffer, int num_bytes)
+{
+    int bytes_rxd;
+    int message_len = 0;
+
+    while (message_len < num_bytes) {
+        if ((bytes_rxd = recv(socket_fd, buffer, num_bytes, 0)) <= 0) {
+            return 0;
+        }
+        message_len += bytes_rxd;
+    }
+
+    return 1;
+}
+
+
+
+void VncCls(vnc_color_t color)
+{
+    /* Clear the frame buffer */
+    int i, j;
+
+    for (i = 0; i < CONFIG_VNC_SERVER_FRAME_HEIGHT; i++) {
+        for (j = 0; j < CONFIG_VNC_SERVER_FRAME_WIDTH; j++) {
+	    VncDrawPixel(j, i, color);
+        }
+    }
+}
+
+
+
+void IRAM_ATTR VncDrawPixel(uint16_t x, uint16_t y, vnc_color_t color)
+{
+    if (x >= CONFIG_VNC_SERVER_FRAME_WIDTH || y >= CONFIG_VNC_SERVER_FRAME_HEIGHT) {
+	printf("write_frame(%d, %d) tile %d/%d\n", x, y, x/TILE_SIZE, y/TILE_SIZE);
+	return;
+    }
+
+    /* Set that pixel to 'colour' */
+    frame_buffer[y][x] = color;
+
+    /* Mark the tile for update */
+    tile_updated[y/TILE_SIZE][x/TILE_SIZE] = 1;
+}
+
+
+
+void IRAM_ATTR VncDrawHorzLine(uint16_t x1, uint16_t x2, uint16_t y, vnc_color_t color)
+{
+    int i;
+
+    /* Draw the line */
+    for (i = x1; i <= x2; i++) {
+	VncDrawPixel(i, y, color);
+    }
+}
+
+
+
+void IRAM_ATTR VncDrawVertLine(uint16_t x, uint16_t y1, uint16_t y2, vnc_color_t color)
+{
+    int i;
+
+    /* Draw the line */
+    for (i = y1; i <= y2; i++) {
+	VncDrawPixel(x, i, color);
+    }
+}
+
+
+
+void IRAM_ATTR VncFillRect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, vnc_color_t color)
+{
+    /* Draw a solid rectangle */
+    int i, j;
+
+    for (i = y1; i <= y2; i++) {
+        for (j = x1; j <= x2; j++) {
+	    VncDrawPixel(j, i, color);
+        }
+    }
+}
+
+
+
+void VncSoundBell(void)
+{
+    if (xSemaphoreTake(SoundBell_lock, 10) == pdTRUE) {
+    	SoundBellCount++;
+	xSemaphoreGive(SoundBell_lock);
+    }
+}
+
+
+
+/**
+ * @brief Websocket task. Created on a websocket connection.
+ */
+void task_WS(void *pvParameter)
+{
+    int			i, j, num_of_encodings, rest_time;
+    uint16_t		*ptr_to_uint16;
+    struct strMessage   _msg;
+
+    // Do pin/pong.
+
+    VNC_WS_run = true;
+    rest_time = 0;
+
+    while (VNC_WS_run) {
+
+	/*
+	 * Wait until something is received from the client. Exit the queue receiver
+	 * each 5 milliseconds to allow to evaluate the VNC_WS_run flag and to allow
+	 * a clean exit of this task.
+	 */
+	if (xQueueReceive(message_queue, &_msg, 5 / portTICK_PERIOD_MS) == pdTRUE) {
+//	    dump_msg((char *)_msg.message, _msg.length, "pull");
+
+	    switch (_msg.message[0]) {
+
+	    	case SET_PIXEL_FORMAT:
+//		    printf("SET_PIXEL_FORMAT\n");
+		    /* Check total message length */
+		    if (_msg.length >= 20) {
+			/* Check pixel format is as expected */
+			i = 0;
+			if (_msg.message[4] != BITS_PER_PIXEL) {
+			    ESP_LOGI(TAG, "SetPixelFormat client wants %d bits-per-pixel", _msg.message[4]);
+			    i++;
+			}
+
+			if (_msg.message[5] != PIXEL_DEPTH) {
+			    ESP_LOGI(TAG, "SetPixelFormat client wants %d pixel-depth", _msg.message[5]);
+			    i++;
+			}
+
+			if ((_msg.message[7] & 0x01) != TRUE_COLOUR_FLAG) {
+			    ESP_LOGI(TAG, "SetPixelFormat client wants %d true-colour-flag", _msg.message[7]);
+			    i++;
+			}
+
+			ptr_to_uint16 = (uint16_t *)&(_msg.message[8]);
+			if (htons(*ptr_to_uint16) != Red_Max) {
+			    Red_Max = htons(*ptr_to_uint16);
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d red-max", Red_Max);
+			}
+
+			ptr_to_uint16 = (uint16_t *)&(_msg.message[10]);
+			if (htons(*ptr_to_uint16) != Green_Max) {
+			    Green_Max = htons(*ptr_to_uint16);
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d green-max", Green_Max);
+			}
+
+			ptr_to_uint16 = (uint16_t *)&(_msg.message[12]);
+			if (htons(*ptr_to_uint16) != Blue_Max) {
+			    Blue_Max = htons(*ptr_to_uint16);
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d blue-max", Blue_Max);
+			}
+
+			if (_msg.message[14] != Red_Shift) {
+			    Red_Shift = _msg.message[14];
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d red-shift", Red_Shift);
+			}
+
+			if (_msg.message[15] != Green_Shift) {
+			    Green_Shift = _msg.message[15];
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d green-shift", Green_Shift);
+			}
+
+			if (_msg.message[16] != Blue_Shift) {
+			    Blue_Shift = _msg.message[16];
+			    AltPixels = true;
+			    ESP_LOGI(TAG, "SetPixelFormat client granted %d blue-shift", Blue_Shift);
+			}
+
+			if (i) {
+			    ESP_LOGI(TAG, "SetPixelFormat %d errors, disconnect client", i);
+			    ESP_LOGI(TAG, "Ensure the 'Auto select' is not enabled in your vncviewer options,");
+			    _msg.length = 0;
+			    ws_server_remove_client(VNC_WS_num);
+			}
+		    }
+		    break;
+
+	    	case FIX_COLOUR_MAP_ENTRIES:
+		    printf("FIX_COLOUR_MAP_ENTRIES\n");
+		    break;
+
+	    	case SET_ENCODINGS:
+		    if (_msg.length >= 3) {
+			// Get the total encodings
+			num_of_encodings = _msg.message[2]*255 + _msg.message[3];
+
+			/* Clear the encoding type structure */
+			encoding_type.raw = 0;
+			encoding_type.copy_rectangle = 0;
+			encoding_type.rre = 0;
+			encoding_type.corre = 0;
+			encoding_type.hextile = 0;
+
+			for (i = 0; i < num_of_encodings; i++) {
+			    switch(_msg.message[3 + (i*4)]) {
+				case 0:  /* Raw encoding */
+					encoding_type.raw = i + 1;
+					break;
+				case 1:  /* Copy rectangle encoding */
+					encoding_type.copy_rectangle = i + 1;
+					break;
+				case 2:  /* RRE encoding */
+					encoding_type.rre = i + 1;
+					break;
+				case 4:  /* CoRRE encoding */
+					encoding_type.corre = i + 1;
+					break;
+				case 5:  /* Hextile encoding */
+					encoding_type.hextile = i + 1;
+					break;
+				default:  /* Unknown coding type - do nothing */
+					break;
+			    }
+			}
+
+			if (encoding_type.corre) {
+			    ESP_LOGI(TAG, "SetEncodings use CORRE");
+			} else {
+			    ESP_LOGI(TAG, "SetEncodings use RAW");
+			}
+		    }
+		    break;
+
+	    	case FRAME_BUFFER_UPDATE_REQ:
+		    if (_msg.length == 10) {
+			if (!_msg.message[1]) {
+			    /* Non-incremental mode - mark the squares that need to be updated */
+			    for (i = (_msg.message[2]*255 + _msg.message[3])/TILE_SIZE;
+				i <= (_msg.message[2]*255 + _msg.message[3] + _msg.message[6]*255 + _msg.message[7])/TILE_SIZE;
+				i++) {
+				for (j = (_msg.message[4]*255 + _msg.message[5])/TILE_SIZE;
+				    j <= (_msg.message[4]*255 + _msg.message[5] + _msg.message[8]*255 + _msg.message[9])/TILE_SIZE;
+				    j++) {
+					tile_updated[j][i] = 1;
+				}
+			    }
+			}
+
+			/* Signal that there is now a pending update request */
+			xEventGroupSetBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ);
+		    }
+		    break;
+
+	    	case KEY_EVENT:
+		    printf("KEY_EVENT\n");
+		    /* Handle the key event, ignored for brewboard */
+		    /* Get the remainder of message (7 bytes) */
+		    break;
+
+	    	case POINTER_EVENT:
+		    /* Handle the pointer event, simulate the touch screen. */
+		    if (_msg.length == 6) {
+			/* Set global variables that will be read by another task. */
+			VNC_pointer_button = _msg.message[1];
+			VNC_pointer_x = _msg.message[2]*255 + _msg.message[3];
+			VNC_pointer_y = _msg.message[4]*255 + _msg.message[5];
+//			printf("POINTER_EVENT button=%02x  x=%d y=%d\n", VNC_pointer_button, VNC_pointer_x, VNC_pointer_y);
+		    }
+		    _msg.length = 0;
+		    break;
+
+	    	case CLIENT_CUT_TEXT:
+		    printf("CLIENT_CUT_TEXT\n");
+		    break;
+
+	    	default:
+		    ESP_LOGI(TAG, "Unknown message %d from client", _msg.message[0]);
+	    }
+	    rest_time = 0;
+	} else {
+	    rest_time++;	// Each 5 milliseconds
+	    if (rest_time == 2000) {
+		// 10 seconds silence, send a PING
+		rest_time = 0;
+//		int rc = ws_server_ping(VNC_WS_num);
+//		ESP_LOGI(TAG, "Send PING to %d, rc=%d", VNC_WS_num, rc);
+	    }
+	}
+    }
+
+    VNC_WS_num = -1;
+    xEventGroupClearBits(xEventGroupVNC, VNC_CLIENT_UPDATE_REQ);
+    ESP_LOGI(TAG, "task_WS finished");
+    vTaskDelete(NULL);
+}
+
+
+
+int VncStartWS(int num)
+{
+    char		protocol_ver[8], message_buffer[MESSAGE_BUFFER_SIZE];
+    int			i, ProtocolOkay;
+    uint32_t		*ptr_to_uint32;
+    uint16_t		*ptr_to_uint16;
+    struct strMessage	_msg;
+
+    ESP_LOGI(TAG, "Start VNC WebSocket connection %d", num);
+    message_queue = xQueueCreate(message_queue_size, sizeof(struct strMessage));
+
+    /*
+     * Initial handshake
+     */
+    ws_server_send_bin_client(num, "RFB 003.003\n", 12);
+    ProtocolOkay = 1;
+    if (xQueueReceive(message_queue, &_msg, 5000 / portTICK_PERIOD_MS) == pdTRUE) {
+//	dump_msg((char *)_msg.message, _msg.length, "pull");
+
+	for (i = 0; i < 8; i++) {
+	    if (_msg.message[i] != server_ProtocolVersion[i]) {
+		ProtocolOkay = 0;
+	    }
+	    /* Store the protocol version - ignoring thr 'RFB ' part */
+	    protocol_ver[i] = _msg.message[i + 4];
+	}
+	protocol_ver[7] = 0;
+    } else {
+	ESP_LOGE(TAG, "Client timeout after initial message");
+	return -5;
+    }
+    
+    if (ProtocolOkay == 0) {
+	/* Generate the Bad ProtocolVerion message */
+	ESP_LOGI(TAG, "Start VNC WebSocket task failed, bad protocol.");
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+	*ptr_to_uint32 = htonl(0);
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[4]);
+	*ptr_to_uint32 = htonl(strlen(bad_protocol));
+	strcpy(&(message_buffer[8]), bad_protocol);
+	ws_server_send_bin_client(num, message_buffer, 8 + strlen(bad_protocol));
+	vTaskDelay(500 / portTICK_PERIOD_MS);
+	ws_server_remove_client(num);
+	return -2;
+    }
+    /* ProtocolVerion is okay - connect with no authentication*/
+
+    /* Server is busy with another client */
+    if (VNC_WS_run || vnc_client_connected) {
+	ESP_LOGI(TAG, "Start VNC WebSocket task failed, server busy.");
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+	*ptr_to_uint32 = htonl(0);
+	ptr_to_uint32 = (uint32_t *) &(message_buffer[4]);
+	*ptr_to_uint32 = htonl(strlen(server_busy));
+	strcpy(&(message_buffer[8]), server_busy);
+	ws_server_send_bin_client(num, message_buffer, 8 + strlen(server_busy));
+	vTaskDelay(500 / portTICK_PERIOD_MS);
+	ws_server_remove_client(num);
+	return -1;
+    }
+
+    /* Generate the No Authentication message */
+    ptr_to_uint32 = (uint32_t *) &(message_buffer[0]);
+    *ptr_to_uint32 = htonl((uint32_t)1);
+    ws_server_send_bin_client(num, message_buffer, 4);
+    /* Authentication - end */
+
+    /* ClientInitialisation - begin */
+    /* Receive initialisation message from client (1 byte) */
+    if (xQueueReceive(message_queue, &_msg, 5000 / portTICK_PERIOD_MS) == pdFALSE) {
+	ESP_LOGE(TAG, "Client timeout after auth message");
+	return -5;
+    }
+    /* Do nothing with this as we only support 1 Client at a time */
+    /* ClientInitialisation - end */
+
+    /* Initial default RGB332 */
+    Red_Max     = RED_MAX;
+    Green_Max   = GREEN_MAX;
+    Blue_Max    = BLUE_MAX;
+    Red_Shift   = RED_SHIFT;
+    Green_Shift = GREEN_SHIFT;
+    Blue_Shift  = BLUE_SHIFT;
+    AltPixels   = false;
+
+    /* ServerInitialisation - begin */
+    /* Create Initialisation message for client */
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[0]);
+    *ptr_to_uint16 = htons((uint16_t)CONFIG_VNC_SERVER_FRAME_WIDTH);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[2]);
+    *ptr_to_uint16 = htons((uint16_t)CONFIG_VNC_SERVER_FRAME_HEIGHT);
+
+    message_buffer[4] = (uint8_t)BITS_PER_PIXEL;
+    message_buffer[5] = (uint8_t)PIXEL_DEPTH;
+    message_buffer[6] = (uint8_t)BIG_ENDIAN_FLAG;
+    message_buffer[7] = (uint8_t)TRUE_COLOUR_FLAG;
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[8]);
+    *ptr_to_uint16 = htons(Red_Max);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[10]);
+    *ptr_to_uint16 = htons(Green_Max);
+
+    ptr_to_uint16 = (uint16_t *) &(message_buffer[12]);
+    *ptr_to_uint16 = htons(Blue_Max);
+
+    message_buffer[14] = Red_Shift;
+    message_buffer[15] = Green_Shift;
+    message_buffer[16] = Blue_Shift;
+
+    ptr_to_uint32 = (uint32_t *) &(message_buffer[20]);
+    *ptr_to_uint32 = htonl(strlen(desktop_name));
+    strcpy(&(message_buffer[24]), desktop_name);
+
+//  dump_msg(message_buffer, 24 + strlen(desktop_name), (char *)"send");
+    ws_server_send_bin_client(num, message_buffer, 24 + strlen(desktop_name));
+
+    /* ServerInitialisation - end */
+    ESP_LOGI(TAG, "VNC WebSocket client connected (RFB Protocol Ver: %s)", protocol_ver);
+
+    VNC_WS_num = num;
+    xTaskCreate(&task_WS, "WSclient",    MIN_STACK_SIZE, NULL, 5, &xTaskWSclient);
+    return 0;
+}
+
+
+
+void VncStopWS(int num)
+{
+    if (! VNC_WS_run) {
+	ESP_LOGI(TAG, "Stop VNC WebSocket, not running.");
+	return;
+    }
+
+    if (num != VNC_WS_num) {
+	ESP_LOGI(TAG, "Stop VNC WebSocket, %d is running, requested %d", VNC_WS_num, num);
+	return;
+    }
+
+    ESP_LOGI(TAG, "Stop VNC WebSocket task connection %d", num);
+    VNC_WS_run = false;
+    xQueueReset(message_queue);
+}
+
+
+
+void VncGetWSmessage(char *msg, uint16_t len)
+{
+    int			max;
+    struct strMessage	_msg;
+ 
+    if (len == 0)
+	return;
+
+    max = len;
+    if (max > MESSAGE_BUFFER_SIZE) {
+	    ESP_LOGE(TAG, "VncGetWSmessage need %d bytes in a %d length buffer", len, MESSAGE_BUFFER_SIZE);
+	    max = MESSAGE_BUFFER_SIZE;
+    }
+
+    for (int i = 0; i < max; i++) {
+	    _msg.message[i] = msg[i];
+    }
+    _msg.length = max;
+    if (xQueueSendToBack(message_queue, &_msg, 200 / portTICK_PERIOD_MS) == pdFALSE) {
+	ESP_LOGE(TAG, "VncGetWSmessage() Queue full, message lost");
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/vnc_server/vnc-server.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,143 @@
+/**
+ * @file vnc-server.h
+ * @brief VNC server original written for eCos.
+ *
+ * The VNC server runs on the target platform and waits for a client 
+ * (vncviewer program running on a PC) to connect via an ethernet connection
+ * (port 5900). Once a client has connected, the target platform has a 
+ * virtual screen, keyboard and mouse.
+ *
+ * This port of the VNC server has been designed with some limitations in 
+ * order to keep memory and processor power requirements to a minimum.
+ *
+ * The VNC client must accept the settings that the VNC server suggests (bits-per-pixel, 
+ * number-of-colours and so on as specified at build time with the eCos configuration 
+ * tool) or the VNC server will just disconnect.
+ *
+ * The VNC server only supports CoRRE encoding and RAW encoding for sending 
+ * display data to the client.
+ *
+ * The VNC server does not support password authentication, the client is able 
+ * to connect without the user typing in a password.
+ *
+ * Only one VNC client may connect to the VNC server at a time.
+ *
+ * In reality these limitations are not a problem.
+ *
+ * @author Chris Garry <cgarry@sweeneydesign.co.uk>
+ * @author Michiel Broek.
+ */
+
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <sys/unistd.h>
+#include "lwip/sockets.h"
+#include "lwip/netdb.h"
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "freertos/event_groups.h"
+
+
+
+/**
+ * @brief Type to hold the frame format details
+ */
+typedef struct
+{
+    uint16_t	frame_width;			///< Frame width
+    uint16_t	frame_height;			///< Frame height
+    void	*frame_buffer;			///< Frame buffer data
+    bool	rgb332;				///< 8 bits RGB format (only one is set).
+    bool	rgb555;				///< 15 bits RGB
+    bool	rgb565;				///< 16 bits RGB
+    bool	bgr233;				///< 8 bits BGR format
+    bool	truecolor0888;			///< 24 bits truecolor.
+} vnc_frame_format_t;
+
+
+
+typedef uint8_t vnc_color_t;
+
+
+
+/**
+ * @brief Start the VNC server. Two tasks are started, one to handle the
+ *        clients and one that serves the updates of the framebuffer.
+ */
+void VncStartup(void);
+
+/**
+ * @brief Clear the display and set a background color.
+ * @param color Then RGB color for the background.
+ */
+void  VncCls(vnc_color_t color);
+
+/**
+ * @brief Draw a pixel on the display.
+ * @param x The horizontal position of the pixel.
+ * @param y The vertical position of the pixel.
+ * @param color The RGB value of the pixel.
+ */
+void VncDrawPixel(uint16_t x, uint16_t y, vnc_color_t color);
+
+/**
+ * @brief Draw a horizontal line on the display.
+ * @param x1 Start horizontal position.
+ * @param x2 End of the horizontal position.
+ * @param y The vertical position of the line.
+ * @param colour The RGB color of the line.
+ */
+void VncDrawHorzLine(uint16_t x1, uint16_t x2, uint16_t y, vnc_color_t color);
+
+/**
+ * @brief Draw a vertical line on the display.
+ * @param x The jprizontal position of the line.
+ * @param y1 The vertical top position of the line.
+ * @param y2 The vertical bottom position of the line.
+ * @param color The RGB color of the line.
+ */
+void VncDrawVertLine(uint16_t x, uint16_t y1, uint16_t y2, vnc_color_t color);
+
+/**
+ * @brief Fill a rectangle with the given color.
+ * @param x1 The top-left horizontal start position.
+ * @param y1 The top-left vertical start position.
+ * @param x2 The bottom-right horizontal end position.
+ * @param y2 The bottom-right vertical end position.
+ * @param color The RGB color of the line.
+ */
+void VncFillRect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, vnc_color_t color);
+
+/**
+ * @brief Sound the bell on the client computer.
+ */
+void VncSoundBell(void);
+
+
+/* Macro to convert from RGB to colour values */
+#define VNC_RGB2COL(r,g,b) (vnc_color_t)(((((uint8_t)r)&0xE0) >> 0) | ((((uint8_t)g)&0xE0) >> 3) | ((((uint8_t)b)&0xC0) >> 6))
+
+
+/**
+ * @brief Start task for a websocket client.
+ */
+int VncStartWS(int num);
+
+/**
+ * @brief Stop task for a websocket client.
+ */
+void VncStopWS(int num);
+
+/**
+ * @brief Remote websocket message to the VNC server.
+ */
+void VncGetWSmessage(char *msg, uint16_t len);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/Kconfig	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,46 @@
+
+menu "WebSocket Server"
+
+config WEBSOCKET_SERVER_MAX_CLIENTS
+  int "Max clients"
+  range 1 1000
+  default 20
+  help
+    Maximum number of clients that the WebSocket
+    server can handle at a time.
+
+config WEBSOCKET_SERVER_QUEUE_SIZE
+  int "Queue read size"
+  range 1 100
+  default 10
+  help
+    Size of the queue to deal with incoming
+    WebSocket messages. The queue holds the
+    connection, not the actual message.
+
+config WEBSOCKET_SERVER_QUEUE_TIMEOUT
+  int "Queue timeout"
+  range 0 10000
+  default 30
+  help
+    Timeout for adding new connections to the
+    read queue.
+
+config WEBSOCKET_SERVER_TASK_STACK_DEPTH
+  int "Stack depth"
+  range 3000 20000
+  default 6000
+  help
+    Stack depth for the WebSocket server. The task
+    handles reads.
+
+config WEBSOCKET_SERVER_TASK_PRIORITY
+  int "Priority"
+  range 1 20
+  default 5
+  help
+    Priority for the WebSocket server. The task
+    handles reads.
+
+
+endmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/README.md	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,247 @@
+
+By Blake Felt - blake.w.felt@gmail.com
+
+ESP32 WebSocket
+==================
+
+A component for WebSockets on ESP-IDF using lwip netconn.
+For an example, see https://github.com/Molorius/ESP32-Examples.
+
+To add to a project, type:
+`git submodule add https://github.com/Molorius/esp32-websocket.git components/websocket`
+into the base directory of your project.
+
+Some configuration options for the Server can be found in menuconfig in:
+Component config ---> WebSocket Server
+
+This presently only has the WebSocket server code working, but client code will be added in the future (the groundwork is there).
+
+The code only allows one WebSocket server at a time, but this merely handles all incoming reads. New connections are added externally, so this can be used to hold various WebSocket connections.
+
+While this can theoretically handle very large messages, hardware constraints (RAM) limits the size of messages. I highly recommend not using more than 5000 bytes per message, but no constraint is in place for this. 
+
+Any suggestions or fixes are gladly appreciated.
+
+Table of Contents
+=================
+* [Enumerations](#enumerations)
+  * [WEBSOCKET_TYPE_t](#enum-websocket_type_t)
+* [Functions](#functions)
+  * [ws_server_start](#int-ws_server_start)
+  * [ws_server_stop](#int-ws_server_stop)
+  * [ws_server_add_client](#int-ws_server_add_clientstruct-netconn-connchar-msguint16_t-lenchar-urlvoid-callback)
+  * [ws_server_add_client_protocol](#int-ws_server_add_client_protocolstruct-netconn-connchar-msguint16_t-lenchar-urlchar-protocolvoid-callback)
+  * [ws_server_len_url](#int-ws_server_len_urlchar-url)
+  * [ws_server_len_all](#int-ws_server_len_all)
+  * [ws_server_remove_client](#int-ws_server_remove_clientint-num)
+  * [ws_server_remove_clients](#int-ws_server_remove_clientschar-url)
+  * [ws_server_remove_all](#int-ws_server_remove_all)
+  * [ws_server_send_text_client](#int-ws_server_send_text_clientint-numchar-msguint64_t-len)
+  * [ws_server_send_text_clients](#int-ws_server_send_text_clientschar-urlchar-msguint64_t-len)
+  * [ws_server_send_text_all](#int-ws_server_send_text_allchar-msguint64_t-len)
+  * [ws_server_send_text_client_from_callback](#int-ws_server_send_text_client_from_callbackint-numchar-msguint64_t-len)
+  * [ws_server_send_text_clients_from_callback](#int-ws_server_send_text_clients_from_callbackchar-urlchar-msguint64_t-len)
+  * [ws_server_send_text_all_from_callback](#int-ws_server_send_text_all_from_callbackchar-msguint64_t-len)
+
+Enumerations
+============
+
+enum WEBSOCKET_TYPE_t
+---------------------
+
+The different types of WebSocket events.
+
+*Values*
+  * `WEBSOCKET_CONNECT`: A new client has successfully connected.
+  * `WEBSOCKET_DISCONNECT_EXTERNAL`: The other side sent a disconnect message.
+  * `WEBSOCKET_DISCONNECT_INTERNAL`: The esp32 server sent a disconnect message.
+  * `WEBSOCKET_DISCONNECT_ERROR`: Disconnect due to a connection error.
+  * `WEBSOCKET_TEXT`: Incoming text.
+  * `WEBSOCKET_BIN`: Incoming binary.
+  * `WEBSOCKET_PING`: The other side sent a ping message.
+  * `WEBSOCKET_PONG`: The other side successfully replied to our ping.
+
+Functions
+=========
+
+int ws_server_start()
+---------------------
+
+Starts the WebSocket Server. Use this function before attempting any
+sort of transmission or adding a client.
+
+*Returns*
+  * 1: successful start
+  * 0: server already running
+
+int ws_server_stop()
+--------------------
+
+Stops the WebSocket Server. New clients can still be added and
+messages can be sent, but new messages will not be received.
+
+*Returns*
+  * 1: successful stop
+  * 0: server was not running before
+
+int ws_server_add_client(struct netconn* conn,char* msg,uint16_t len,char* url,void *callback)
+----------------------------------------------------------------------------------------------
+
+Adds a client to the WebSocket Server handler and performs the necessary handshake.
+
+*Parameters*
+  * `conn`: the lwip netconn connection.
+  * `msg`: the entire incoming request message to join the server. Necessary for the handshake.
+  * `len`: the length of `msg`.
+  * `url`: the NULL-terminated url. Used to keep track of clients, not required.
+  * `callback`: the callback that is used to run WebSocket events. This must be with parameters(uint8_t num,WEBSOCKET_TYPE_t type,char* msg,uint64_t len) where "num" is the client number, "type" is the event type, "msg" is the incoming message, and "len" is the message length. The callback itself is optional.
+
+*Returns*
+  * -2: not enough information in `msg` to perform handshake.
+  * -1: server full, or connection issue.
+  * 0 or greater: connection number
+
+int ws_server_add_client_protocol(struct netconn* conn,char* msg,uint16_t len,char* url,char* protocol,void *callback)
+----------------------------------------------------------------------------------------------------------------------
+
+Adds a client to the WebSocket Server handler and performs the necessary handshake. Will also send
+the specified protocol.
+
+*Parameters*
+  * `conn`: the lwip netconn connection.
+  * `msg`: the entire incoming request message to join the server. Necessary for the handshake.
+  * `len`: the length of `msg`.
+  * `url`: the NULL-terminated url. Used to keep track of clients, not required.
+  * `protocol`: the NULL-terminated protocol. This will be sent to the client in the header.
+  * `callback`: the callback that is used to run WebSocket events. This must be with parameters(uint8_t num,WEBSOCKET_TYPE_t type,char* msg,uint64_t len) where "num" is the client number, "type" is the event type, "msg" is the incoming message, and "len" is the message length. The callback itself is optional.
+
+*Returns*
+  * -2: not enough information in `msg` to perform handshake.
+  * -1: server full, or connection issue.
+  * 0 or greater: connection number
+
+int ws_server_len_url(char* url)
+--------------------------------
+
+Returns the number of clients connected to the specified URL.
+
+*Parameters*
+  * `url`: the NULL-terminated string of the desired URL.
+
+*Returns*
+  * The number of clients connected to the specified URL.
+
+int ws_server_len_all()
+-----------------------
+
+*Returns*
+  * The number of connected clients.
+
+int ws_server_remove_client(int num)
+------------------------------------
+
+Removes the desired client.
+
+*Parameters*
+  * `num`: the client number
+
+*Returns*
+  * 0: not a valid client number
+  * 1: client disconnected
+
+int ws_server_remove_clients(char* url)
+---------------------------------------
+
+Removes all clients connect to the desired URL.
+
+*Parameters*
+  * `url`: the NULL-terminated URL.
+
+*Returns*
+  * The number of clients that were disconnected.
+
+int ws_server_remove_all()
+--------------------------
+
+Removes all clients from server.
+
+*Returns*
+  * The number of clients that were disconnected.
+
+int ws_server_send_text_client(int num,char* msg,uint64_t len)
+--------------------------------------------------------------
+
+Sends the desired message to the client.
+
+*Parameters*
+  * `num`: the client's number.
+  * `msg`: the desired message.
+  * `len`: the length of the message.
+
+*Returns*
+  * 0: message not sent properly
+  * 1: message sent
+
+int ws_server_send_text_clients(char* url,char* msg,uint64_t len)
+-----------------------------------------------------------------
+
+Sends the message to clients connected to the desired URL.
+
+*Parameters*
+  * `url`: the NULL-terminated URL.
+  * `msg`: the desired message.
+  * `len`: the length of the message.
+
+*Returns*
+  * The number of clients that the message was sent to.
+
+int ws_server_send_text_all(char* msg,uint64_t len)
+---------------------------------------------------
+
+Sends the message to all connected clients.
+
+*Parameters*
+  * `msg`: the desired message
+  * `len`: the length of the message
+
+*Returns*
+  * The number of clients that the message was sent to.
+
+int ws_server_send_text_client_from_callback(int num,char* msg,uint64_t len)
+----------------------------------------------------------------------------
+
+Sends the desired message to the client. Only use this inside the callback function.
+
+*Parameters*
+  * `num`: the client's number.
+  * `msg`: the desired message.
+  * `len`: the length of the message.
+
+*Returns*
+  * 0: message not sent properly
+  * 1: message sent
+
+int ws_server_send_text_clients_from_callback(char* url,char* msg,uint64_t len)
+-------------------------------------------------------------------------------
+
+Sends the message to clients connected to the desired URL. Only use this inside the callback function.
+
+*Parameters*
+  * `url`: the NULL-terminated URL.
+  * `msg`: the desired message.
+  * `len`: the length of the message.
+
+*Returns*
+  * The number of clients that the message was sent to.
+
+int ws_server_send_text_all_from_callback(char* msg,uint64_t len)
+-----------------------------------------------------------------
+
+Sends the message to all connected clients. Only use this inside the callback function.
+
+*Parameters*
+  * `msg`: the desired message
+  * `len`: the length of the message
+
+*Returns*
+  * The number of clients that the message was sent to.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2 @@
+# websocket component makefile
+# all files are in default positions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/include/websocket.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * @file websocket.h
+ * @brief Websocket functions.
+ * @author Blake Felt - blake.w.felt@gmail.com`
+ */
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef WEBSOCKET_H
+#define WEBSOCKET_H
+
+#include "lwip/api.h"
+
+
+/**
+ * @brief the different codes for the callbacks
+ */
+typedef enum {
+  WEBSOCKET_CONNECT,			///< Connect
+  WEBSOCKET_DISCONNECT_EXTERNAL, 	///< the other side disconnected
+  WEBSOCKET_DISCONNECT_INTERNAL, 	///< the esp32 disconnected
+  WEBSOCKET_DISCONNECT_ERROR, 		///< disconnect due to error
+  WEBSOCKET_TEXT,			///< Text message
+  WEBSOCKET_BIN,			///< Binary message
+  WEBSOCKET_PING,			///< PING message
+  WEBSOCKET_PONG			///< PONG message
+} WEBSOCKET_TYPE_t;
+
+
+
+/**
+ * @brief websocket operation codes
+ */
+typedef enum {
+  WEBSOCKET_OPCODE_CONT  = 0x0,		///< Continue
+  WEBSOCKET_OPCODE_TEXT  = 0x1,		///< Text
+  WEBSOCKET_OPCODE_BIN   = 0x2,		///< Binary
+  WEBSOCKET_OPCODE_CLOSE = 0x8,		///< Close connection
+  WEBSOCKET_OPCODE_PING  = 0x9,		///< PING message
+  WEBSOCKET_OPCODE_PONG  = 0xA		///< PONG message
+} WEBSOCKET_OPCODES_t;
+
+
+/**
+ * @brief the header, useful for creating and quickly passing to functions
+ */
+typedef struct {
+  union {
+    struct {
+      uint16_t LEN:7;     		///< bits 0..  6
+      uint16_t MASK:1;    		///< bit  7
+      uint16_t OPCODE:4;  		///< bits 8..  11
+      uint16_t :3;        		///< bits 12.. 14 reserved
+      uint16_t FIN:1;     		///< bit  15
+    } bit;
+    struct {
+      uint16_t ONE:8;     		///< bits 0..  7
+      uint16_t ZERO:8;    		///< bits 8..  15
+    } pos;
+  } param; 				///< the initial parameters of the header
+  uint64_t length; 			///< actual message length
+  union {
+    char part[4]; 			///< the mask, array
+    uint32_t full; 			///< the mask, all 32 bits
+  } key; 				///< masking key
+  bool received; 			///< was a message successfully received?
+} ws_header_t;
+
+
+/**
+ * @brief A client, with space for a server callback or a client callback (depending on use)
+ */
+typedef struct {
+  struct netconn* conn; 		///< the connection
+  bool connected;			///< connection state
+  char* url;            		///< the associated url,  null terminated
+  char* protocol;			///< the associated protocol, null terminated
+  bool ping;            		///< did we send a ping?
+  WEBSOCKET_OPCODES_t last_opcode; 	///< the previous opcode
+  char* contin;         		///< any continuation piece
+  bool contin_text;     		///< is the continue a binary or text?
+  uint64_t len;         		///< length of continuation
+  uint32_t unfinished;      		///< sometimes netconn doesn't read a full frame, treated similarly to a continuation frame
+  void (*ccallback)(WEBSOCKET_TYPE_t type,char* msg,uint64_t len); 		///< client callback
+  void (*scallback)(uint8_t num,WEBSOCKET_TYPE_t type,char* msg,uint64_t len); 	///< server callback
+} ws_client_t;
+
+
+/**
+ * @brief returns the populated client struct
+ *        does not send any header, assumes the proper handshake has already occurred
+ * @param conn The network connection
+ * @param url The connection url.
+ * @param ccallback  callback for client (userspace)
+ * @param scallback  callback for server (userspace)
+ * @return The Websocket client structure.
+ */
+ws_client_t ws_connect_client(struct netconn* conn,
+                              char* url,
+                              void (*ccallback)(WEBSOCKET_TYPE_t type,char* msg,uint64_t len),
+                              void (*scallback)(uint8_t num,WEBSOCKET_TYPE_t type,char* msg,uint64_t len)
+                             );
+
+
+/**
+ * @brief Disconnect a websocket client.
+ * @param client The ws_client_t structure with the client information.
+ */
+void ws_disconnect_client(ws_client_t* client);
+
+/**
+ * @briek Test if the client is connected.
+ *        status updates after send/read/connect/disconnect.
+ * @param client The ws_client_t structure with the client information.
+ * @return True if connected, false if not.
+ */
+bool ws_is_connected(ws_client_t client);
+
+/**
+ * @brief Send data via websocjet to a client. This function performs the masking.
+ * @param client The ws_client_t structure with the client information.
+ * @param opcode The opcode to send the message.
+ * @param msg The message itself.
+ * @param len The length of the message.
+ * @param mask Encrypt??
+ * @return error code or ERR_OK
+ */
+err_t ws_send(ws_client_t* client,WEBSOCKET_OPCODES_t opcode,char* msg,uint64_t len,bool mask);
+
+/**
+ * @brief Receive a message from a websocket connection.
+ * @param client The ws_client_t structure with the client information.
+ * @param header unmasks and returns message. populates header.
+ * @return Unmasks and returns message. populates header.
+ */
+char* ws_read(ws_client_t* client,ws_header_t* header);
+
+/**
+ * @brief Create a handshake hashed string.
+ * @param key The key for the hash.
+ * @param len The length.
+ * @return The string of output.
+ */
+char* ws_hash_handshake(char* key,uint8_t len);
+
+#endif // ifndef WEBSOCKET_H
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/include/websocket_server.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,166 @@
+/**
+ * @file websocket_server.h
+ * @brief Websocket server functions.
+ * @author Blake Felt - blake.w.felt@gmail.com`
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef WEBSOCKET_SERVER_H
+#define WEBSOCKET_SERVER_H
+
+#include "websocket.h"
+
+#define WEBSOCKET_SERVER_MAX_CLIENTS CONFIG_WEBSOCKET_SERVER_MAX_CLIENTS
+#define WEBSOCKET_SERVER_QUEUE_SIZE CONFIG_WEBSOCKET_SERVER_QUEUE_SIZE
+#define WEBSOCKET_SERVER_QUEUE_TIMEOUT CONFIG_WEBSOCKET_SERVER_QUEUE_TIMEOUT
+#define WEBSOCKET_SERVER_TASK_STACK_DEPTH CONFIG_WEBSOCKET_SERVER_TASK_STACK_DEPTH
+#define WEBSOCKET_SERVER_TASK_PRIORITY CONFIG_WEBSOCKET_SERVER_TASK_PRIORITY
+#define WEBSOCKET_SERVER_PINNED CONFIG_WEBSOCKET_SERVER_PINNED
+#if WEBSOCKET_SERVER_PINNED
+#define WEBSOCKET_SERVER_PINNED_CORE CONFIG_WEBSOCKET_SERVER_PINNED_CORE
+#endif
+
+/**
+ * @brief Starts the server.
+ * @return 1 if started, 0 if the server was already started.
+ */
+int ws_server_start();
+
+/**
+ * @brief Stops the server.
+ * @return 1 if stopped, 0 if the server was not running.
+ */
+int ws_server_stop();
+
+/**
+ * @brief Adds a client, returns the client's number in the server.
+ * @param conn The network connection.
+ * @param msg The message received from the client.
+ * @param len The length of the message.
+ * @param url The url of the connection.
+ * @param callback The callback function.
+ * @return Negative if an error, else the connection number.
+ */
+int ws_server_add_client(struct netconn* conn,
+                         char* msg,
+                         uint16_t len,
+                         char* url,
+                         void (*callback)(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len));
+
+/**
+ * @brief Adds a client, returns the client's number in the server.
+ * @param conn The network connection.
+ * @param msg The message received from the client.
+ * @param len The length of the message.
+ * @param url The url of the connection.
+ * @param protocol The protocol requested by the client.
+ * @param callback The callback function.
+ * @return Negative if an error, else the connection number.
+ */
+int ws_server_add_client_protocol(struct netconn* conn,
+                                  char* msg,
+                                  uint16_t len,
+                                  char* url,
+                                  char* protocol,
+                                  void (*callback)(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len));
+
+/**
+ * @brief Returns the number of connected clients to url
+ * @param url The url to check.
+ * @return The number of connected clients.
+ */
+int ws_server_len_url(char* url);
+
+/**
+ * @brief Returns the total number of connected clients.
+ * @return The number of connected clients.
+ */
+int ws_server_len_all();
+
+/**
+ * @brief Removes the client with the set number
+ * @param num The client number.
+ * @return 1 if success, else 0.
+ */
+int ws_server_remove_client(int num);
+
+/**
+ * @brief Removes all clients connected to the specified url.
+ * @param url The connection url.
+ * @return The number of clients removed.
+ */
+int ws_server_remove_clients(char* url);
+
+/**
+ * @brief Removes all clients from the server.
+ * @return The number of clients removed.
+ */
+int ws_server_remove_all();
+
+/**
+ * @brief Send text to client with the set number.
+ * @param num The client connection number.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @retrun 1 if message send.
+ */
+int ws_server_send_text_client(int num,char* msg,uint64_t len);
+int ws_server_send_text_client_from_callback(int num,char* msg,uint64_t len);
+
+/**
+ * @brief Sends text to all clients with the set url.
+ * @param url The clients url.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @return The number of clients the message was send to.
+ */
+int ws_server_send_text_clients(char* url,char* msg,uint64_t len);
+int ws_server_send_text_clients_from_callback(char* url,char* msg,uint64_t len);
+
+/**
+ * @brief Sends text to all clients.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @return The number of clients the message was send to.
+ */
+int ws_server_send_text_all(char* msg,uint64_t len);
+int ws_server_send_text_all_from_callback(char* msg,uint64_t len);
+
+/**
+ * @brief Send binary message to client with the set number.
+ * @param num The client connection number.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @retrun 1 if message send.
+ */
+int ws_server_send_bin_client(int num,char* msg,uint64_t len);
+int ws_server_send_bin_client_from_callback(int num,char* msg,uint64_t len);
+
+/**
+ * @brief Sends binary message to all clients with the set url.
+ * @param url The clients url.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @return The number of clients the message was send to.
+ */
+int ws_server_send_bin_clients(char* url,char* msg,uint64_t len);
+
+/**
+ * @brief Sends binary message to all clients.
+ * @param msg The message to send.
+ * @param The length of the message to send.
+ * @return The number of clients the message was send to.
+ */
+int ws_server_send_bin_all(char* msg,uint64_t len);
+
+
+int ws_server_ping(int num); // sends a ping to client num
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/websocket.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,318 @@
+/**
+ * @file websocket.c
+ * @brief Websocket functions.
+ * @author Blake Felt - blake.w.felt@gmail.com`
+ */
+
+#include "websocket.h"
+#include "lwip/tcp.h" // for the netconn structure
+#include "esp_system.h" // for esp_random
+#include "esp_log.h"
+#include "mbedtls/base64.h"
+#include "mbedtls/sha1.h"
+#include <string.h>
+
+static const char       *TAG = "websoket";
+
+ws_client_t ws_connect_client(struct netconn* conn,
+                              char* url,
+                              void (*ccallback)(WEBSOCKET_TYPE_t type,char* msg,uint64_t len),
+                              void (*scallback)(uint8_t num,WEBSOCKET_TYPE_t type,char* msg,uint64_t len)
+                            ) {
+  ws_client_t client;
+  client.conn = conn;
+  client.connected = true;
+  client.url  = url;
+  client.ping = 0;
+  client.last_opcode = 0;
+  client.contin = NULL;
+  client.len = 0;
+  client.unfinished = 0;
+  client.ccallback = ccallback;
+  client.scallback = scallback;
+  return client;
+}
+
+void ws_disconnect_client(ws_client_t* client) {
+  ws_send(client,WEBSOCKET_OPCODE_CLOSE,NULL,0,1); // tell the client to close
+  if(client->conn) {
+    client->conn->callback = NULL; // shut off the callback
+    netconn_close(client->conn);
+    netconn_delete(client->conn);
+    client->conn = NULL;
+  }
+  client->connected = false;
+  client->url = NULL;
+  client->last_opcode = 0;
+  if(client->len) {
+    if(client->contin)
+      free(client->contin);
+    client->len = 0;
+  }
+  client->ccallback = NULL;
+  client->scallback = NULL;
+}
+
+bool ws_is_connected(ws_client_t client) {
+    if (client.conn && client.connected)
+	return 1;
+    return 0;
+}
+
+static void ws_generate_mask(ws_header_t* header) {
+  header->param.bit.MASK = 1;
+  header->key.full = esp_random(); // generate a random 32 bit number
+}
+
+static void ws_encrypt_decrypt(char* msg,ws_header_t header) {
+  if(header.param.bit.MASK) {
+    for(uint64_t i=0; i<header.length; i++) {
+      msg[i] ^= header.key.part[i%4];
+    }
+  }
+}
+
+
+
+err_t ws_send(ws_client_t* client,WEBSOCKET_OPCODES_t opcode,char* msg,uint64_t len,bool mask)
+{
+    static char		*out;
+    static char		*encrypt;
+    static uint64_t 	pos;
+    static uint64_t 	true_len;
+    ws_header_t		header;
+    static err_t	err;
+
+    header.param.pos.ZERO = 0; // reset the whole header
+    header.param.pos.ONE  = 0;
+
+    header.param.bit.FIN = 1; // all pieces are done (you don't need a huge message anyway...)
+    header.param.bit.OPCODE = opcode;
+    // populate LEN field
+    pos = 2;
+    header.length = len;
+    if (len<125) {
+    	header.param.bit.LEN = len;
+    } else if (len < 65536) {
+    	header.param.bit.LEN = 126;
+    	pos += 2;
+    } else {
+    	header.param.bit.LEN = 127;
+    	pos += 8;
+    }
+
+    if (mask) {
+    	ws_generate_mask(&header); // get a key
+    	encrypt = malloc(len); // allocate memory for the encryption
+    	memcpy(encrypt,msg,len);
+    	ws_encrypt_decrypt(encrypt,header); // encrypt it!
+    	pos += 4; // add the position
+    }
+
+    true_len = pos+len; // get the length of the entire message
+    pos = 2;
+    out = malloc(true_len); // allocate dat memory
+
+    if (out == NULL) {
+	ESP_LOGE(TAG, "ws_send malloc error for %llu bytes", true_len);
+	return ERR_MEM;
+    }
+
+    out[0] = header.param.pos.ZERO; // save header
+    out[1] = header.param.pos.ONE;
+
+    // put in the length, if necessary
+    if(header.param.bit.LEN == 126) {
+    	out[2] = (len >> 8) & 0xFF;
+    	out[3] = (len     ) & 0xFF;
+    	pos = 4;
+    }
+    if(header.param.bit.LEN == 127) {
+    	//memcpy(&out[2],&len,8);
+    	out[2] = (len >> 56) & 0xFF;
+    	out[3] = (len >> 48) & 0xFF;
+    	out[4] = (len >> 40) & 0xFF;
+    	out[5] = (len >> 32) & 0xFF;
+    	out[6] = (len >> 24) & 0xFF;
+    	out[7] = (len >> 16) & 0xFF;
+    	out[8] = (len >> 8)  & 0xFF;
+    	out[9] = (len)       & 0xFF;
+    	pos = 10;
+    }
+
+    if(mask) {
+    	out[pos] = header.key.part[0]; pos++;
+    	out[pos] = header.key.part[1]; pos++;
+    	out[pos] = header.key.part[2]; pos++;
+    	out[pos] = header.key.part[3]; pos++;
+    	memcpy(&out[pos],encrypt,len); // put in the encrypted message
+    	free(encrypt);
+    } else {
+    	memcpy(&out[pos],msg,len);
+    }
+
+    err = netconn_write(client->conn,out,true_len,NETCONN_COPY); // finally! send it.
+    if (err != ERR_OK) {
+	client->connected = false;
+	ESP_LOGE(TAG, "ws_send netconn_write error %d", err);
+    }
+    free(out); // free the entire message
+    return err;
+}
+
+
+
+char* ws_read(ws_client_t* client,ws_header_t* header) 
+{
+    char		*ret, *append;
+    err_t		err;
+    struct netbuf	*inbuf, *inbuf2;
+    char		*buf, *buf2;
+    uint16_t		len, len2;
+    uint64_t		pos, cont_len, cont_pos;
+
+    // if we read from this previously (not cont frames), stop reading
+    if(client->unfinished) {
+    	client->unfinished--;
+    	return NULL;
+    }
+
+    err = netconn_recv(client->conn,&inbuf);
+    if (err != ERR_OK) {
+	client->connected = false;
+	ESP_LOGE(TAG, "ws_read netconn_recv error %d", err);
+	return NULL;
+    }
+    netbuf_data(inbuf,(void**)&buf, &len);
+    if(!buf) return NULL;
+
+    // get the header
+    header->param.pos.ZERO = buf[0];
+    header->param.pos.ONE  = buf[1];
+
+  // get the message length
+    pos = 2;
+    if(header->param.bit.LEN < 125) {
+    	header->length = header->param.bit.LEN;
+    } else if(header->param.bit.LEN == 126) {
+    	header->length = buf[2] << 8 | buf[3];
+    	pos = 4;
+    } else { // LEN = 127
+    	header->length = (uint64_t)buf[2] << 56 | (uint64_t)buf[3] << 48
+                       | (uint64_t)buf[4] << 40 | (uint64_t)buf[5] << 32
+                       | (uint64_t)buf[6] << 24 | (uint64_t)buf[7] << 16
+                       | (uint64_t)buf[8] << 8  | (uint64_t)buf[9];
+    	pos = 10;
+    }
+
+    if(header->param.bit.MASK) {
+    	memcpy(&(header->key.full),&buf[pos],4); // extract the key
+    	pos += 4;
+    }
+
+    ret = malloc(header->length+1); // allocate memory, plus a byte
+    if(!ret) {
+	ESP_LOGE(TAG, "ws_read malloc error for %llu bytes", header->length+1);
+    	netbuf_delete(inbuf);
+    	header->received = 0;
+    	return NULL;
+    }
+
+    cont_len = len-pos; // get the actual length
+    memcpy(ret,&buf[pos],header->length); // allocate the total memory
+    cont_pos = cont_len; // get the initial position
+  
+    // netconn gives messages in pieces, so we need to get those (different than OPCODE_CONT)
+    while(cont_len < header->length) { // while the actual length is less than the header stated
+    	err = netconn_recv(client->conn,&inbuf2);
+    	if (err != ERR_OK) {
+	    ESP_LOGE(TAG, "ws_read netconn_recv error %d", err);
+      	    netbuf_delete(inbuf2);
+      	    free(ret);
+      	    client->unfinished = 0;
+      	    client->connected = false;
+      	    header->received = 0;
+      	    return NULL;
+    	}
+    
+	netbuf_data(inbuf2,(void**)&buf2, &len2);
+    	memcpy(&ret[cont_pos],buf2,len2);
+    	cont_pos += len2;
+    	if (!buf2) {
+      	    client->unfinished = 0;
+      	    header->received = 0;
+    	}
+    	netbuf_delete(inbuf2);
+    	client->unfinished++;
+    	cont_len += len2;
+    }
+
+    ret[header->length] = '\0'; // end string
+    ws_encrypt_decrypt(ret,*header); // unencrypt, if necessary
+
+  if(header->param.bit.FIN == 0) { // if the message isn't done
+    if((header->param.bit.OPCODE == WEBSOCKET_OPCODE_CONT) &&
+       ((client->last_opcode==WEBSOCKET_OPCODE_BIN) || (client->last_opcode==WEBSOCKET_OPCODE_TEXT))) {
+         cont_len = header->length + client->len;
+         append = malloc(cont_len);
+         memcpy(append,client->contin,client->len);
+         memcpy(&append[client->len],ret,header->length);
+         free(client->contin);
+         client->contin = malloc(cont_len);
+         client->len = cont_len;
+
+         free(append);
+         free(ret);
+         netbuf_delete(inbuf);
+         //free(buf);
+         return NULL;
+    }
+    else if((header->param.bit.OPCODE==WEBSOCKET_OPCODE_BIN) || (header->param.bit.OPCODE==WEBSOCKET_OPCODE_TEXT)) {
+      if(client->len) {
+        free(client->contin);
+      }
+      client->contin = malloc(header->length);
+      memcpy(client->contin,ret,header->length);
+      client->len = header->length;
+      client->last_opcode = header->param.bit.OPCODE;
+
+      free(ret);
+      netbuf_delete(inbuf);
+      //free(buf);
+      return NULL;
+    }
+    else { // there shouldn't be another FIN code....
+      free(ret);
+      netbuf_delete(inbuf);
+      //free(buf);
+      return NULL;
+    }
+  }
+  client->last_opcode = header->param.bit.OPCODE;
+  if(inbuf) netbuf_delete(inbuf);
+  header->received = 1;
+  return ret;
+}
+
+char* ws_hash_handshake(char* handshake,uint8_t len) {
+  const char hash[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+  const uint8_t hash_len = sizeof(hash);
+  char* ret;
+  char key[64];
+  unsigned char sha1sum[20];
+  unsigned int ret_len;
+
+  if(!len) return NULL;
+  ret = malloc(32);
+
+  memcpy(key,handshake,len);
+  strlcpy(&key[len],hash,sizeof(key));
+  mbedtls_sha1((unsigned char*)key,len+hash_len-1,sha1sum);
+  mbedtls_base64_encode(NULL, 0, &ret_len, sha1sum, 20);
+  if(!mbedtls_base64_encode((unsigned char*)ret,32,&ret_len,sha1sum,20)) {
+    ret[ret_len] = '\0';
+    return ret;
+  }
+  free(ret);
+  return NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/websocket/websocket_server.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,489 @@
+/**
+ * @file websocket_server.c
+ * @brief Websocket server functions.
+ * @author Blake Felt - blake.w.felt@gmail.com`
+ */
+
+#include "websocket_server.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+#include "freertos/task.h"
+#include "freertos/queue.h"
+#include "lwip/tcp.h"
+#include <string.h>
+
+static SemaphoreHandle_t xwebsocket_mutex; // to lock the client array
+static QueueHandle_t xwebsocket_queue; // to hold the clients that send messages
+static ws_client_t clients[WEBSOCKET_SERVER_MAX_CLIENTS]; // holds list of clients
+static TaskHandle_t xtask; // the task itself
+
+/**
+ * @brief Add a connection to the callback queue.
+ * @param conn The network connection.
+ * @param evt The event.
+ * @param len Not used.
+ */
+static void background_callback(struct netconn* conn, enum netconn_evt evt,u16_t len) {
+  switch(evt) {
+    case NETCONN_EVT_RCVPLUS:
+      xQueueSendToBack(xwebsocket_queue,&conn,WEBSOCKET_SERVER_QUEUE_TIMEOUT);
+      break;
+    default:
+      break;
+  }
+}
+
+/**
+ * @brief Handle client read.
+ * @param num The client connection number.
+ */
+static void handle_read(uint8_t num) 
+{
+    ws_header_t header;
+    char* msg;
+
+    header.received = 0;
+    msg = ws_read(&clients[num],&header);
+
+    if (!header.received) {
+    	if (msg) 
+	    free(msg);
+    	return NULL;
+    }
+
+    switch(clients[num].last_opcode) {
+    	case WEBSOCKET_OPCODE_CONT:
+      		break;
+    	case WEBSOCKET_OPCODE_BIN:
+      		clients[num].scallback(num,WEBSOCKET_BIN,msg,header.length);
+      		break;
+    	case WEBSOCKET_OPCODE_TEXT:
+      		clients[num].scallback(num,WEBSOCKET_TEXT,msg,header.length);
+      		break;
+    	case WEBSOCKET_OPCODE_PING:
+      		ws_send(&clients[num],WEBSOCKET_OPCODE_PONG,msg,header.length,0);
+      		clients[num].scallback(num,WEBSOCKET_PING,msg,header.length);
+      		break;
+    	case WEBSOCKET_OPCODE_PONG:
+      		if (clients[num].ping) {
+        	    clients[num].scallback(num,WEBSOCKET_PONG,NULL,0);
+        	    clients[num].ping = 0;
+      		}
+      		break;
+    	case WEBSOCKET_OPCODE_CLOSE:
+      		clients[num].scallback(num,WEBSOCKET_DISCONNECT_EXTERNAL,NULL,0);
+      		ws_disconnect_client(&clients[num]);
+      		break;
+    	default:
+      		break;
+    }
+
+    if(msg) 
+	free(msg);
+}
+
+/**
+ * @brief The webserver task.
+ */
+static void ws_server_task(void* pvParameters) 
+{
+    struct netconn* conn;
+
+    xwebsocket_mutex = xSemaphoreCreateMutex();
+    xwebsocket_queue = xQueueCreate(WEBSOCKET_SERVER_QUEUE_SIZE, sizeof(struct netconn*));
+
+    // initialize all clients
+    for (int i = 0; i < WEBSOCKET_SERVER_MAX_CLIENTS; i++) {
+    	clients[i].conn = NULL;
+	clients[i].connected = false;
+    	clients[i].url  = NULL;
+    	clients[i].ping = 0;
+    	clients[i].last_opcode = 0;
+    	clients[i].contin = NULL;
+    	clients[i].len = 0;
+    	clients[i].ccallback = NULL;
+    	clients[i].scallback = NULL;
+    }
+
+    for(;;) {
+    	xQueueReceive(xwebsocket_queue, &conn, portMAX_DELAY);
+    	if(!conn) continue; // if the connection was NULL, ignore it
+
+    	if (xSemaphoreTake(xwebsocket_mutex, 25) == pdTRUE) { // take access
+    	    for(int i = 0; i < WEBSOCKET_SERVER_MAX_CLIENTS; i++) {
+      	    	if(clients[i].conn == conn) {
+        	    handle_read(i);
+        	    break;
+      	    	}
+    	    }
+    	    xSemaphoreGive(xwebsocket_mutex); // return access
+    	}
+    }
+    vTaskDelete(NULL);
+}
+
+
+
+int ws_server_start() 
+{
+    if(xtask)
+	return 0;
+  
+    xTaskCreate(&ws_server_task, "ws_server_task", WEBSOCKET_SERVER_TASK_STACK_DEPTH, NULL, WEBSOCKET_SERVER_TASK_PRIORITY, &xtask);
+    return 1;
+}
+
+
+
+int ws_server_stop() {
+    if(!xtask) 
+	return 0;
+  
+    vTaskDelete(xtask);
+    return 1;
+}
+
+
+
+/**
+ * @brief Prepare a response for a new websocket connection.
+ * @param buf The client message buffer.
+ * @param buflen The length of the buffer.
+ * @param handshake The jandshake hash.
+ * @param protocol The requested protocol or NULL.
+ * @return True iif success, else false.
+ */
+static bool prepare_response(char* buf,uint32_t buflen,char* handshake,char* protocol) {
+  const char WS_HEADER[] = "Upgrade: websocket\r\n";
+  const char WS_KEY[] = "Sec-WebSocket-Key: ";
+  const char WS_RSP[] = "HTTP/1.1 101 Switching Protocols\r\n" \
+                        "Upgrade: websocket\r\n" \
+                        "Connection: Upgrade\r\n" \
+                        "Sec-WebSocket-Accept: %s\r\n" \
+                        "%s\r\n";
+
+  char* key_start;
+  char* key_end;
+  char* hashed_key;
+
+  if(!strstr(buf,WS_HEADER)) return 0;
+  if(!buflen) return 0;
+  key_start = strstr(buf,WS_KEY);
+  if(!key_start) return 0;
+  key_start += 19;
+  key_end = strstr(key_start,"\r\n");
+  if(!key_end) return 0;
+
+  hashed_key = ws_hash_handshake(key_start,key_end-key_start);
+  if(!hashed_key) return 0;
+  if(protocol) {
+    char tmp[256];
+    sprintf(tmp,WS_RSP,hashed_key,"Sec-WebSocket-Protocol: %s\r\n");
+    sprintf(handshake,tmp,protocol);
+  }
+  else {
+    sprintf(handshake,WS_RSP,hashed_key,"");
+  }
+  free(hashed_key);
+  return 1;
+}
+
+int ws_server_add_client_protocol(struct netconn* conn,
+                         char* msg,
+                         uint16_t len,
+                         char* url,
+                         char* protocol,
+                         void (*callback)(uint8_t num,
+                                          WEBSOCKET_TYPE_t type,
+                                          char* msg,
+                                          uint64_t len)) {
+  int ret;
+  char handshake[256];
+
+    if(!prepare_response(msg,len,handshake,protocol)) {
+    	netconn_close(conn);
+    	netconn_delete(conn);
+    	return -2;
+    }
+
+    ret = -1;
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	conn->callback = background_callback;
+  	netconn_write(conn,handshake,strlen(handshake),NETCONN_COPY);
+
+    	for(int i = 0; i < WEBSOCKET_SERVER_MAX_CLIENTS; i++) {
+    	    if(clients[i].conn) continue;
+    	    callback(i,WEBSOCKET_CONNECT,NULL,0);
+    	    clients[i] = ws_connect_client(conn,url,NULL,callback);
+    	    if(!ws_is_connected(clients[i])) {
+      	    	callback(i,WEBSOCKET_DISCONNECT_ERROR,NULL,0);
+      	    	ws_disconnect_client(&clients[i]);
+      	    	break;
+    	    }
+    	    ret = i;
+    	    break;
+    	}
+    	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+int ws_server_len_url(char* url) 
+{
+    int ret;
+    ret = 0;
+  
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	for(int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	    if(clients[i].url && !strcmp(url,clients[i].url)) 
+		ret++;
+  	}
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+int ws_server_add_client(struct netconn* conn,
+                         char* msg,
+                         uint16_t len,
+                         char* url,
+                         void (*callback)(uint8_t num,
+                                          WEBSOCKET_TYPE_t type,
+                                          char* msg,
+                                          uint64_t len)) {
+
+  return ws_server_add_client_protocol(conn,msg,len,url,NULL,callback);
+
+}
+
+int ws_server_len_all() 
+{
+    int ret;
+    ret = 0;
+  
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	for(int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	    if(clients[i].conn) 
+		ret++;
+  	}
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+int ws_server_remove_client(int num) 
+{
+    int ret = 0;
+  
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	if(ws_is_connected(clients[num])) {
+    	    clients[num].scallback(num,WEBSOCKET_DISCONNECT_INTERNAL,NULL,0);
+    	    ws_disconnect_client(&clients[num]);
+    	    ret = 1;
+  	}
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+int ws_server_remove_clients(char* url) 
+{
+    int ret = 0;
+  
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	for(int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	    if(ws_is_connected(clients[i]) && !strcmp(url,clients[i].url)) {
+      		clients[i].scallback(i,WEBSOCKET_DISCONNECT_INTERNAL,NULL,0);
+      		ws_disconnect_client(&clients[i]);
+      		ret += 1;
+    	    }
+  	}
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+int ws_server_remove_all() 
+{
+    int ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	for(int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	    if(ws_is_connected(clients[i])) {
+      		clients[i].scallback(i,WEBSOCKET_DISCONNECT_INTERNAL,NULL,0);
+      		ws_disconnect_client(&clients[i]);
+      		ret += 1;
+    	    }
+  	}
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+// The following functions are already written below, but without the mutex.
+
+int ws_server_send_text_client(int num,char* msg,uint64_t len)
+{
+    int ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	ret = ws_server_send_text_client_from_callback(num, msg, len);
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+int ws_server_send_text_clients(char* url,char* msg,uint64_t len) 
+{
+    int ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	ret = ws_server_send_text_clients_from_callback(url, msg, len);
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+int ws_server_send_text_all(char* msg,uint64_t len) 
+{
+    int ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+  	ret = ws_server_send_text_all_from_callback(msg, len);
+  	xSemaphoreGive(xwebsocket_mutex);
+    }
+  
+    return ret;
+}
+
+
+
+// the following functions should be used inside of the callback. The regular versions
+// grab the mutex, but it is already grabbed from inside the callback so it will hang.
+
+int ws_server_send_text_client_from_callback(int num,char* msg,uint64_t len) 
+{
+    int ret = 0;
+  
+    if (ws_is_connected(clients[num])) {
+    	if ((ws_send(&clients[num],WEBSOCKET_OPCODE_TEXT,msg,len,0) == ERR_OK)) {
+    	    ret = 1;
+	} else {
+      	    clients[num].scallback(num,WEBSOCKET_DISCONNECT_ERROR,NULL,0);
+            ws_disconnect_client(&clients[num]);
+      	    ret = 0;
+    	}
+    }
+    return ret;
+}
+
+
+
+int ws_server_send_text_clients_from_callback(char* url,char* msg,uint64_t len) 
+{
+    int ret = 0;
+    
+    for (int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	if (ws_is_connected(clients[i]) && !strcmp(clients[i].url,url)) {
+      	    if ((ws_send(&clients[i],WEBSOCKET_OPCODE_TEXT,msg,len,0) == ERR_OK)) {
+		ret++;
+	    } else {
+        	clients[i].scallback(i,WEBSOCKET_DISCONNECT_ERROR,NULL,0);
+        	ws_disconnect_client(&clients[i]);
+      	    }
+    	}
+    }
+    return ret;
+}
+
+
+
+int ws_server_send_text_all_from_callback(char* msg,uint64_t len) 
+{
+    int ret = 0;
+  
+    for (int i=0;i<WEBSOCKET_SERVER_MAX_CLIENTS;i++) {
+    	if (ws_is_connected(clients[i])) {
+      	    if ((ws_send(&clients[i],WEBSOCKET_OPCODE_TEXT,msg,len,0) == ERR_OK)) {
+		ret++;
+	    } else {
+        	clients[i].scallback(i,WEBSOCKET_DISCONNECT_ERROR,NULL,0);
+        	ws_disconnect_client(&clients[i]);
+      	    }
+    	}
+    }
+    return ret;
+}
+
+
+
+int ws_server_send_bin_client_from_callback(int num,char* msg,uint64_t len)
+{
+    int ret = 0;
+
+    if (ws_is_connected(clients[num])) {
+        if ((ws_send(&clients[num],WEBSOCKET_OPCODE_BIN,msg,len,0) == ERR_OK)) {
+	    ret = 1;
+	} else {
+	    clients[num].scallback(num,WEBSOCKET_DISCONNECT_ERROR,NULL,0);
+	    ws_disconnect_client(&clients[num]);
+	    ret = 0;
+	}
+    }
+
+    return ret;
+}
+
+
+
+int ws_server_send_bin_client(int num,char* msg,uint64_t len) 
+{
+    int	ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+    	ret = ws_server_send_bin_client_from_callback(num, msg, len);
+    	xSemaphoreGive(xwebsocket_mutex);
+    }
+    return ret;
+}
+
+
+
+int ws_server_ping(int num) 
+{
+    int ret = 0;
+
+    if (xSemaphoreTake(xwebsocket_mutex, 10) == pdTRUE) {
+    	if (ws_is_connected(clients[num])) {
+	    if ((ws_send(&clients[num], WEBSOCKET_OPCODE_PING, (char *)"BrewPing", 8, 0) == ERR_OK)) {
+	    	ret = 1;
+	    } else {
+	    	clients[num].scallback(num, WEBSOCKET_DISCONNECT_ERROR, NULL, 0);
+	    	ws_disconnect_client(&clients[num]);
+	    	ret = 0;
+	    }
+    	}
+    	xSemaphoreGive(xwebsocket_mutex);
+    }
+    
+    return ret;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/Doxyfile	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2431 @@
+# Doxyfile 1.8.9.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
+# for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "ESP32 BrewBoard"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         = 
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = 
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           = 
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = 
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        = 
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    = 
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines.
+
+ALIASES                = 
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              = 
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      = 
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       = 
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    = 
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            = 
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = YES
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           = doxygen-warning-log.txt
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces.
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = ../main/ \
+			 ../components/esp32-ds18b20/ \
+                         ../components/esp32-ds18b20/include/ \
+                         ../components/esp32-owb/ \
+                         ../components/esp32-owb/include/ \
+                         ../components/PID/ \
+			 ../components/spidriver/ \
+			 ../components/tft/ \
+			 ../components/vnc_server/ \
+			 ../components/websocket \
+			 ../components/websocket/include/
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank the
+# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii,
+# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp,
+# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown,
+# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf,
+# *.qsf, *.as and *.js.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.py \
+                         *.f90 \
+                         *.f \
+                         *.for \
+                         *.tcl \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf \
+                         *.as \
+                         *.js
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                = 
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       = 
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        = 
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           = 
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             = 
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+
+INPUT_FILTER           = 
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+
+FILTER_PATTERNS        = 
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS = 
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see http://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            = 
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            = 
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        = 
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  = 
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       = 
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               = 
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           = 
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     = 
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               = 
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   = 
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  = 
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  = 
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           = 
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     = 
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       = 
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = NO
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       = 
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     = 
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. To get the times font for
+# instance you can specify
+# EXTRA_PACKAGES=times
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         = 
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           = 
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           = 
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET = 
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      = 
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    = 
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    = 
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             = 
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sf.net) file that captures the
+# structure of the code including all documentation. Note that this feature is
+# still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = YES
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           = 
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  = 
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             = __attribute__(x)= \
+                         IRAM_ATTR= \
+                         configSUPPORT_DYNAMIC_ALLOCATION=1 \
+                         configSUPPORT_STATIC_ALLOCATION=1 \
+                         configQUEUE_REGISTRY_SIZE=1 \
+                         configUSE_RECURSIVE_MUTEXES=1 \
+                         configTHREAD_LOCAL_STORAGE_DELETE_CALLBACKS=1 \
+                         configNUM_THREAD_LOCAL_STORAGE_POINTERS=1 \
+                         configUSE_APPLICATION_TASK_TAG=1
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      = 
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               = 
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       = 
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            = 
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               = 
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           = 
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot.
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif and svg.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               = 
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           = 
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           = 
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           = 
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      = 
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  = 
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
Binary file image/fonts/BigFont.fon has changed
Binary file image/fonts/DejaVuSans12.fon has changed
Binary file image/fonts/DejaVuSans18.fon has changed
Binary file image/fonts/DejaVuSans24.fon has changed
Binary file image/fonts/DotMatrix_M.fon has changed
Binary file image/fonts/Grotesk24x48.fon has changed
Binary file image/fonts/SmallFont.fon has changed
Binary file image/fonts/Ubuntu.fon has changed
Binary file image/fonts/arial_bold.fon has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/fonts/ocrfont.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,119 @@
+// OCR_A_Extended_M.c
+// Font type    : Full (95 characters)
+// Font size    : 16x24 pixels
+// Memory usage : 4564 bytes
+
+#if defined(__AVR__)
+	#include <avr/pgmspace.h>
+	#define fontdatatype const uint8_t
+#elif defined(__PIC32MX__)
+	#define PROGMEM
+	#define fontdatatype const unsigned char
+#elif defined(__arm__)
+	#define PROGMEM
+	#define fontdatatype const unsigned char
+#endif
+
+fontdatatype OCR_A_Extended_M[4564] PROGMEM={
+0x10,0x18,0x20,0x5F,
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // <space>
+0x00,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x03,0x80,0x03,0x80,0x03,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // !
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x1E,0x78,0x1E,0x78,0x0E,0x70,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // "
+0x00,0x00,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x1F,0xF8,0x1F,0xF8,0x06,0x60,0x06,0x60,0x06,0x60,0x1F,0xF8,0x1F,0xF8,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // #
+0x00,0x00,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x1F,0xF8,0x1F,0xF8,0x18,0x00,0x18,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x18,0x00,0x18,0x1F,0xF8,0x1F,0xF8,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // $
+0x00,0x00,0x1C,0x00,0x1C,0x18,0x1C,0x18,0x00,0x30,0x00,0x30,0x00,0x60,0x00,0xE0,0x00,0xC0,0x01,0x80,0x01,0x80,0x03,0x00,0x07,0x00,0x06,0x00,0x0C,0x00,0x0C,0x00,0x18,0x38,0x18,0x38,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // %
+0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0xC0,0x0F,0xC0,0x18,0x60,0x18,0x60,0x18,0x60,0x18,0x60,0x0C,0xC0,0x07,0x80,0x07,0x00,0x0F,0x80,0x1D,0xC8,0x18,0xF8,0x18,0x70,0x18,0xF0,0x0F,0xD8,0x0F,0x98,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // &
+0x00,0x00,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0x80,0x03,0x80,0x03,0x80,0x03,0x80,0x03,0x00,0x03,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // '
+0x00,0x00,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x03,0x80,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x80,0x01,0x80,0x00,0xC0,0x00,0x60,0x00,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // (
+0x00,0x00,0x0C,0x00,0x06,0x00,0x03,0x00,0x01,0x80,0x01,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x01,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // )
+0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x80,0x01,0x80,0x19,0x88,0x19,0x98,0x0F,0xF0,0x07,0xE0,0x03,0xC0,0x03,0xC0,0x07,0xE0,0x0F,0xF0,0x1D,0xB8,0x11,0x98,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // *
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x1F,0xF8,0x1F,0xF8,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // +
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x07,0xE0,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ,
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // -
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // .
+0x00,0x00,0x00,0x18,0x00,0x18,0x00,0x30,0x00,0x30,0x00,0x60,0x00,0x60,0x00,0xC0,0x00,0xC0,0x01,0x80,0x01,0x80,0x03,0x00,0x03,0x00,0x06,0x00,0x06,0x00,0x0C,0x00,0x0C,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // /
+
+0x00,0x00,0x0F,0xF0,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 0
+0x00,0x00,0x1F,0x80,0x1F,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x98,0x01,0x98,0x01,0x98,0x01,0x98,0x01,0x98,0x01,0x98,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 1
+0x00,0x00,0x1F,0xF0,0x1F,0xF8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x0F,0xF8,0x1F,0xF0,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 2
+0x00,0x00,0x1F,0xF8,0x1F,0xFC,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x07,0xF8,0x07,0xF8,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x1F,0xFC,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 3
+0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x1F,0xF8,0x1F,0xF8,0x00,0x30,0x00,0x30,0x00,0x30,0x00,0x30,0x00,0x30,0x00,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 4
+0x00,0x00,0x07,0xF8,0x07,0xF8,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x07,0xF0,0x07,0xF8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x18,0x18,0x1F,0xF8,0x07,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 5
+0x00,0x00,0x1C,0x00,0x1C,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xF8,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 6
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x18,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 7
+0x00,0x00,0x07,0xE0,0x07,0xE0,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x0F,0xF0,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 8
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x1F,0xF8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x38,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // 9
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // :
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x07,0xE0,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ;
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x78,0x00,0xE0,0x01,0xC0,0x07,0x00,0x0E,0x00,0x1C,0x00,0x0E,0x00,0x07,0x00,0x01,0xC0,0x00,0xE0,0x00,0x78,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // <
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // =
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x1E,0x00,0x07,0x00,0x03,0x80,0x00,0xE0,0x00,0x70,0x00,0x38,0x00,0x70,0x00,0xE0,0x03,0x80,0x07,0x00,0x1E,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // >
+0x00,0x00,0x00,0x20,0x00,0x60,0x01,0xF0,0x03,0x98,0x07,0x18,0x0E,0x18,0x18,0x70,0x10,0xE0,0x01,0xC0,0x03,0x80,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ?
+
+0x00,0x00,0x07,0xE0,0x0F,0xF0,0x18,0x18,0x18,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x0F,0x98,0x0F,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x0F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // @
+0x00,0x00,0x01,0x80,0x01,0x80,0x01,0x80,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x06,0x60,0x06,0x60,0x06,0x60,0x06,0x60,0x0C,0x30,0x0F,0xF0,0x0F,0xF0,0x0C,0x30,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // A
+0x00,0x00,0x1F,0xF0,0x1F,0xF8,0x18,0x1C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x1C,0x1F,0xF8,0x1F,0xF8,0x18,0x1C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x1C,0x1F,0xF8,0x1F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // B
+0x00,0x00,0x01,0xFC,0x03,0xFC,0x03,0x00,0x06,0x00,0x06,0x00,0x0C,0x00,0x0C,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1C,0x00,0x0C,0x00,0x0E,0x00,0x06,0x00,0x07,0x00,0x03,0xFC,0x01,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // C
+0x00,0x00,0x1F,0xC0,0x1F,0xE0,0x06,0x70,0x06,0x30,0x06,0x30,0x06,0x18,0x06,0x18,0x06,0x0C,0x06,0x0C,0x06,0x0C,0x06,0x0C,0x06,0x1C,0x06,0x18,0x06,0x38,0x06,0x30,0x06,0x70,0x1F,0xE0,0x1F,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // D
+0x00,0x00,0x1F,0xFC,0x1F,0xFC,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xC0,0x1F,0xC0,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xFC,0x1F,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // E
+0x00,0x00,0x1F,0xFC,0x1F,0xFC,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xF0,0x1F,0xF0,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // F
+0x00,0x00,0x01,0xF8,0x03,0xF8,0x03,0x00,0x06,0x00,0x0E,0x00,0x0C,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0xF8,0x18,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0F,0xF8,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // G
+0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // H
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // I
+0x00,0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x0C,0x18,0x07,0xF0,0x03,0xE0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // J
+0x00,0x00,0x18,0x0C,0x18,0x18,0x18,0x30,0x18,0x60,0x18,0xC0,0x19,0x80,0x1B,0x00,0x1E,0x00,0x1C,0x00,0x1E,0x00,0x1F,0x00,0x1B,0x80,0x19,0xC0,0x18,0xE0,0x18,0x70,0x18,0x30,0x18,0x18,0x18,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // K
+0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // L
+0x00,0x00,0x1C,0x38,0x1C,0x38,0x1E,0x78,0x1E,0xF8,0x1B,0xD8,0x19,0x98,0x19,0x98,0x19,0x98,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // M
+0x00,0x00,0x1C,0x18,0x1C,0x18,0x1E,0x18,0x1E,0x18,0x1E,0x18,0x1B,0x18,0x1B,0x18,0x1B,0x18,0x19,0x98,0x19,0x98,0x18,0xD8,0x18,0xD8,0x18,0xD8,0x18,0x78,0x18,0x78,0x18,0x78,0x18,0x38,0x18,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // N
+0x00,0x00,0x03,0xC0,0x03,0xC0,0x06,0x60,0x06,0x60,0x0E,0x70,0x0C,0x30,0x0C,0x30,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0C,0x30,0x0C,0x30,0x0E,0x70,0x06,0x60,0x06,0x60,0x03,0xC0,0x01,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // O
+
+0x00,0x00,0x1F,0xF0,0x1F,0xF0,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF0,0x1F,0xF0,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // P
+0x00,0x00,0x00,0x70,0x00,0xF8,0x01,0xD8,0x03,0x98,0x07,0x18,0x0C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x19,0x98,0x19,0x98,0x19,0xF0,0x18,0xE0,0x19,0xC0,0x1B,0xE0,0x1F,0x78,0x0E,0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // Q
+0x00,0x00,0x1F,0xF0,0x1F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x1F,0xF0,0x19,0x80,0x19,0x80,0x18,0xC0,0x18,0xC0,0x18,0x60,0x18,0x60,0x18,0x30,0x18,0x30,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // R
+0x00,0x00,0x0F,0xF0,0x1F,0xF8,0x18,0x18,0x18,0x18,0x0C,0x00,0x06,0x00,0x06,0x00,0x03,0x00,0x01,0x80,0x01,0x80,0x00,0xC0,0x00,0x60,0x00,0x60,0x00,0x30,0x18,0x18,0x18,0x18,0x1F,0xF8,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // S
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x19,0x98,0x19,0x98,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // T
+0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // U
+0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0C,0x30,0x0C,0x30,0x0E,0x70,0x06,0x60,0x06,0x60,0x07,0xE0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // V
+0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x19,0x98,0x0F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // W
+0x00,0x00,0x18,0x18,0x18,0x18,0x0C,0x30,0x0C,0x30,0x06,0x60,0x06,0x60,0x03,0xC0,0x03,0xC0,0x01,0x80,0x01,0x80,0x03,0xC0,0x03,0xC0,0x06,0x60,0x06,0x60,0x0C,0x30,0x0C,0x30,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // X
+0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0C,0x30,0x0E,0x60,0x07,0xE0,0x03,0xC0,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // Y
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x30,0x00,0x30,0x00,0x60,0x00,0x60,0x00,0xC0,0x00,0xC0,0x01,0x80,0x01,0x80,0x03,0x00,0x03,0x00,0x06,0x00,0x06,0x00,0x0C,0x00,0x0C,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // Z
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // [
+0x00,0x00,0x18,0x00,0x18,0x00,0x0C,0x00,0x0C,0x00,0x06,0x00,0x07,0x00,0x03,0x00,0x01,0x80,0x01,0x80,0x00,0xC0,0x00,0xC0,0x00,0x60,0x00,0x70,0x00,0x30,0x00,0x18,0x00,0x18,0x00,0x0C,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // <backslash>
+0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x00,0xD8,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ]
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x80,0x01,0x80,0x03,0xC0,0x03,0xC0,0x07,0xE0,0x07,0xE0,0x0E,0x70,0x0E,0x70,0x1C,0x30,0x1C,0x38,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ^
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,  // _
+
+0x00,0x00,0x06,0x00,0x07,0xC0,0x03,0xE0,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // `
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xF0,0x07,0xF0,0x00,0x18,0x00,0x18,0x00,0x18,0x0F,0xF8,0x0F,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x38,0x1F,0xF8,0x0F,0xD8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // a
+0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1B,0xE0,0x1F,0xF0,0x1E,0x38,0x1C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1C,0x18,0x1E,0x38,0x1F,0xF0,0x1B,0xE0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // b
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xFC,0x07,0xFC,0x0E,0x00,0x1C,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x1C,0x00,0x0E,0x00,0x07,0xFC,0x03,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // c
+0x00,0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x07,0xD8,0x0F,0xF8,0x1C,0x78,0x18,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x38,0x1C,0x78,0x0F,0xF8,0x07,0xD8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // d
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x0F,0xF0,0x1C,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0xF8,0x1F,0xF8,0x18,0x00,0x18,0x00,0x1C,0x00,0x0F,0xF8,0x07,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // e
+0x00,0x00,0x00,0xFC,0x01,0xFC,0x03,0x80,0x03,0x00,0x03,0x00,0x0F,0xF0,0x0F,0xF0,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // f
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xD8,0x0F,0xF8,0x1C,0x78,0x18,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x38,0x1C,0x78,0x0F,0xF8,0x07,0xD8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x38,0x0F,0xF0,0x0F,0xE0,  // g
+0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x19,0xF0,0x1B,0xF0,0x1F,0x18,0x1C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // h
+0x03,0x80,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0x80,0x0F,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x0F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // i
+0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xF8,0x03,0xF8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x0C,0x18,0x0C,0x18,0x07,0xF0,0x03,0xE0,  // j
+0x00,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x30,0x18,0x60,0x18,0xC0,0x19,0x80,0x1B,0x80,0x1F,0x00,0x1E,0x00,0x1F,0x00,0x19,0x80,0x18,0xC0,0x18,0x60,0x18,0x30,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // k
+0x00,0x00,0x0F,0x80,0x0F,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x0F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // l
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x78,0x3F,0xFC,0x39,0xCC,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // m
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x19,0xF0,0x1B,0xF0,0x1F,0x18,0x1C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // n
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x0F,0xF0,0x1C,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1C,0x38,0x0F,0xF0,0x07,0xE0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // o
+
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1B,0xC0,0x1F,0xE0,0x1E,0x70,0x1C,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x1C,0x38,0x1E,0x70,0x1F,0xE0,0x1B,0xC0,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,  // p
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xD8,0x0F,0xF8,0x1C,0x78,0x18,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x38,0x1C,0x78,0x0F,0xF8,0x07,0xD8,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,  // q
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x19,0xF0,0x1B,0xF8,0x1F,0x0C,0x1C,0x0C,0x18,0x0C,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // r
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0xF0,0x1F,0xF8,0x18,0x18,0x18,0x00,0x1E,0x00,0x07,0x80,0x01,0xF0,0x00,0x70,0x00,0x18,0x00,0x18,0x18,0x18,0x1F,0xF0,0x0F,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // s
+0x00,0x00,0x00,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x1F,0xF8,0x1F,0xF8,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x0C,0x06,0x1C,0x03,0xF8,0x01,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // t
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x38,0x18,0xF8,0x0F,0xD8,0x07,0x98,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // u
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x38,0x38,0x18,0x30,0x18,0x30,0x0C,0x60,0x0C,0x60,0x06,0xC0,0x06,0xC0,0x03,0x80,0x03,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // v
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x30,0x0C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x31,0x8C,0x33,0xCC,0x1F,0xF8,0x1E,0x78,0x1C,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // w
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x1C,0x38,0x0C,0x30,0x06,0x60,0x03,0xC0,0x03,0xC0,0x01,0x80,0x03,0xC0,0x06,0xE0,0x0E,0x70,0x0C,0x30,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // x
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x18,0x30,0x18,0x30,0x18,0x30,0x18,0x38,0x38,0x18,0x30,0x18,0x30,0x0C,0x60,0x0C,0x60,0x06,0xC0,0x07,0xC0,0x07,0x80,0x01,0x80,0x03,0x80,0x03,0x00,0x03,0x00,0x3E,0x00,0x3E,0x00,  // y
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x38,0x00,0x30,0x00,0x60,0x00,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x1C,0x00,0x1F,0xF8,0x1F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // z
+0x00,0x00,0x00,0xF8,0x01,0xF8,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x1F,0x00,0x1F,0x00,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0xF8,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // {
+0x00,0x00,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // |
+0x00,0x00,0x1F,0x00,0x1F,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x00,0xF8,0x00,0xF8,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x1F,0x80,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // }
+0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x8C,0x1F,0xFC,0x18,0xF0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,  // ~
+};
Binary file image/fonts/swiss721_outline.fon has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/log/br1806301750.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,134 @@
+{
+ "brew": [{
+  "Recipe":"The Irish Rover  Test",
+  "Date":"Sat Jun 30 17:50:46 2018",
+  "brewdata":[
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"58.062","MLT_pwm":"66","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.250","HLT_pwm":"23","Label":"00:00"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"58.062","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.500","HLT_pwm":"0","Label":"00:01"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"60.375","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.750","HLT_pwm":"0","Label":"00:02"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"64.688","MLT_pwm":"77","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"74.688","HLT_pwm":"13","Label":"00:03"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"67.188","MLT_pwm":"0","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"80.812","HLT_pwm":"99","Label":"00:05"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.938","MLT_pwm":"0","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"84.125","HLT_pwm":"99","Label":"00:06"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.688","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"31","Label":"00:07"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"8","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:08"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:09"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:10"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:11"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"7","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:12"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"3","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"8","Label":"00:13"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:14"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"5","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:15"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:16"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"5","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:17"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"8","Label":"00:18"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:19"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:20"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"8","Label":"00:21"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:22"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"8","Label":"00:23"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"3","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:24"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.312","MLT_pwm":"10","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:25"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"30","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:26"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.625","MLT_pwm":"19","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"3","Label":"00:27"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.375","MLT_pwm":"13","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:28"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"27","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"3","Label":"00:29"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"22","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:30"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"12","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:31"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"19","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"3","Label":"00:32"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"16","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:33"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"16","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:34"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"22","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:35"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.625","MLT_pwm":"14","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:36"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"15","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:37"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"24","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:38"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"19","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:39"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"11","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:40"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"22","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:41"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"20","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"3","Label":"00:42"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.562","MLT_pwm":"11","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:43"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"18","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:44"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.500","MLT_pwm":"22","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:45"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.625","MLT_pwm":"14","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:46"},
+   {"Screen":"100","MLT_sp":"66.500","MLT_pv":"66.438","MLT_pwm":"14","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:47"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"66.250","MLT_pwm":"8","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:48"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.938","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:49"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.625","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:50"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.312","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:51"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"4","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:52"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"64.938","MLT_pwm":"21","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:53"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.125","MLT_pwm":"15","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:54"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"8","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:55"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"7","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:56"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"12","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:57"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"6","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:58"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"64.938","MLT_pwm":"14","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:59"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"11","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"01:00"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"3","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"01:01"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"64.938","MLT_pwm":"15","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"01:02"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"13","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"01:03"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.125","MLT_pwm":"2","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"01:04"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"2","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:05"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"3","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"01:06"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"4","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:07"},
+   {"Screen":"101","MLT_sp":"65.000","MLT_pv":"65.000","MLT_pwm":"3","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:08"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"65.250","MLT_pwm":"39","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"84.938","HLT_pwm":"6","Label":"01:09"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"67.750","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"0","Label":"01:10"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"72.500","MLT_pwm":"98","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"0","Label":"01:11"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"77.062","MLT_pwm":"32","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"01:12"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"78.438","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:14"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"78.188","MLT_pwm":"1","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"01:15"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"2","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:16"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"2","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"01:17"},
+   {"Screen":"107","MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"2","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"8","Label":"01:18"},
+   {"Screen":"108","MLT_sp":"99.000","MLT_pv":"77.875","MLT_pwm":"27","MLT_tr":"0","Pump":"0","Label":"01:19"},
+   {"Screen":"108","MLT_sp":"99.000","MLT_pv":"80.250","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"01:20"},
+   {"Screen":"108","MLT_sp":"99.000","MLT_pv":"85.938","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"01:21"},
+   {"Screen":"108","MLT_sp":"99.000","MLT_pv":"94.438","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"01:22"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.125","MLT_pwm":"78","MLT_tr":"1","Pump":"0","Label":"01:23"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:24"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:25"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:26"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:27"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:28"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:29"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:30"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:31"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:32"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:33"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:34"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:35"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:36"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:37"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:38"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:39"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:40"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:41"},
+   {"Screen":"109","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"01:42"},
+   {"Screen":"110","MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"0","MLT_tr":"1","Pump":"0","Label":"01:43"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"98.812","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:44"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"89.625","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:45"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"81.375","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:46"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"74.000","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:47"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"67.375","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:48"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"61.438","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:49"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"55.875","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:50"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"50.875","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:51"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"46.500","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:52"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"42.562","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:53"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"39.062","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:54"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"36.000","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:55"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"33.250","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:56"},
+   {"Screen":"115","MLT_sp":"30.000","MLT_pv":"30.812","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"01:57"}  ],
+  "annotations":[
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:03","borderColor":"#42f445","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Mout storten","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:47","borderColor":"#8942f4","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Maisch stap 1 begin","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:06","borderColor":"#42f445","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Jodium test","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:08","borderColor":"#8942f4","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Maisch stap 7 begin","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:17","borderColor":"#42f445","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Mout verwijderen","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:18","borderColor":"#8942f4","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Begin koken","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:22","borderColor":"#8942f4","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Koken","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:26","borderColor":"#42f445","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Hopgift 1 Golding East Well","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:31","borderColor":"#42f445","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Hopgift 2 Golding East Well, Vertmont Esdoornsiroop, Iers mos","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"01:43","borderColor":"#8942f4","borderWidth":2,"label":{"backgroundColor":"#00215b","content":"Begin koelen","enabled":true}}  ]
+ }]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/log/br1806302023.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,79 @@
+{
+ "brew": [{
+  "Recipe":"The Irish Rover  Test",
+  "Date":"Sat Jun 30 20:23:36 2018",
+  "brewdata":[
+   {"MLT_sp":"66.500","MLT_pv":"58.062","MLT_pwm":"54","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.312","HLT_pwm":"35","Label":"00:00"},
+   {"MLT_sp":"66.500","MLT_pv":"58.000","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.562","HLT_pwm":"0","Label":"00:01"},
+   {"MLT_sp":"66.500","MLT_pv":"60.250","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"73.812","HLT_pwm":"0","Label":"00:02"},
+   {"MLT_sp":"66.500","MLT_pv":"64.500","MLT_pwm":"81","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"74.500","HLT_pwm":"9","Label":"00:03"},
+   {"MLT_sp":"65.000","MLT_pv":"67.312","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"81.500","HLT_pwm":"98","Label":"00:05"},
+   {"MLT_sp":"65.000","MLT_pv":"67.000","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"84.750","HLT_pwm":"100","Label":"00:06"},
+   {"MLT_sp":"65.000","MLT_pv":"66.688","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"11","Label":"00:07"},
+   {"MLT_sp":"65.000","MLT_pv":"66.375","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:08"},
+   {"MLT_sp":"65.000","MLT_pv":"66.125","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:09"},
+   {"MLT_sp":"65.000","MLT_pv":"65.812","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:10"},
+   {"MLT_sp":"65.000","MLT_pv":"65.500","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:11"},
+   {"MLT_sp":"65.000","MLT_pv":"65.188","MLT_pwm":"1","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:12"},
+   {"MLT_sp":"65.000","MLT_pv":"64.938","MLT_pwm":"11","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:13"},
+   {"MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"13","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:14"},
+   {"MLT_sp":"65.000","MLT_pv":"65.062","MLT_pwm":"3","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:15"},
+   {"MLT_sp":"65.000","MLT_pv":"64.938","MLT_pwm":"8","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:16"},
+   {"MLT_sp":"78.000","MLT_pv":"65.250","MLT_pwm":"48","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"2","Label":"00:17"},
+   {"MLT_sp":"78.000","MLT_pv":"67.812","MLT_pwm":"99","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"84.938","HLT_pwm":"0","Label":"00:18"},
+   {"MLT_sp":"78.000","MLT_pv":"72.625","MLT_pwm":"98","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"0","Label":"00:19"},
+   {"MLT_sp":"78.000","MLT_pv":"77.062","MLT_pwm":"28","MLT_tr":"0","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"4","Label":"00:20"},
+   {"MLT_sp":"78.000","MLT_pv":"78.312","MLT_pwm":"0","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:22"},
+   {"MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"6","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"6","Label":"00:23"},
+   {"MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"8","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"7","Label":"00:24"},
+   {"MLT_sp":"78.000","MLT_pv":"78.062","MLT_pwm":"4","MLT_tr":"1","Pump":"1","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:25"},
+   {"MLT_sp":"78.000","MLT_pv":"77.938","MLT_pwm":"11","MLT_tr":"1","Pump":"0","HLT_sp":"85.000","HLT_pv":"85.000","HLT_pwm":"5","Label":"00:26"},
+   {"MLT_sp":"99.000","MLT_pv":"78.688","MLT_pwm":"70","MLT_tr":"0","Pump":"0","Label":"00:27"},
+   {"MLT_sp":"99.000","MLT_pv":"82.750","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"00:28"},
+   {"MLT_sp":"99.000","MLT_pv":"89.875","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"00:29"},
+   {"MLT_sp":"99.000","MLT_pv":"98.750","MLT_pwm":"100","MLT_tr":"0","Pump":"0","Label":"00:30"},
+   {"MLT_sp":"99.000","MLT_pv":"100.125","MLT_pwm":"70","MLT_tr":"1","Pump":"0","Label":"00:31"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:32"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:33"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:34"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:35"},
+   {"MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:36"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:37"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:38"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:39"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:40"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:41"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:42"},
+   {"MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:43"},
+   {"MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:44"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:45"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:46"},
+   {"MLT_sp":"99.000","MLT_pv":"100.062","MLT_pwm":"64","MLT_tr":"1","Pump":"0","Label":"00:47"},
+   {"MLT_sp":"99.000","MLT_pv":"100.000","MLT_pwm":"0","MLT_tr":"1","Pump":"0","Label":"00:48"},
+   {"MLT_sp":"30.000","MLT_pv":"98.812","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"00:49"},
+   {"MLT_sp":"30.000","MLT_pv":"89.562","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"00:50"},
+   {"MLT_sp":"30.000","MLT_pv":"81.312","MLT_pwm":"0","MLT_tr":"0","Pump":"0","Label":"00:51"},
+   {"MLT_sp":"30.000","MLT_pv":"73.875","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:52"},
+   {"MLT_sp":"30.000","MLT_pv":"67.188","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:53"},
+   {"MLT_sp":"30.000","MLT_pv":"61.188","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:54"},
+   {"MLT_sp":"30.000","MLT_pv":"55.688","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:55"},
+   {"MLT_sp":"30.000","MLT_pv":"50.688","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:56"},
+   {"MLT_sp":"30.000","MLT_pv":"46.250","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:57"},
+   {"MLT_sp":"30.000","MLT_pv":"42.312","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:58"},
+   {"MLT_sp":"30.000","MLT_pv":"38.812","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"00:59"},
+   {"MLT_sp":"30.000","MLT_pv":"35.750","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"01:00"},
+   {"MLT_sp":"30.000","MLT_pv":"33.000","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"01:01"},
+   {"MLT_sp":"30.000","MLT_pv":"30.562","MLT_pwm":"0","MLT_tr":"0","Pump":"1","Label":"01:02"}  ],
+  "annotations":[
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:03","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Mout storten","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:03","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Maisch: 1","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:15","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Jodium test","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:16","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Uitmaischen","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:25","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Mout verwijderen","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:30","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Koken","position":"bottom","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:33","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Hopgift 1 Golding East Well","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:36","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Hopgift 2 Golding East Well, Vertmont Esdoornsiroop, Iers mos","position":"top","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:47","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Vlamuit","position":"bottom","enabled":true}},
+{"type":"line","mode":"vertical","scaleID":"x-axis-0","value":"00:48","borderColor":"#8942f","borderWidth":2,"label":{"backgroundColor":"#00215","content":"Koelen","position":"bottom","enabled":true}}  ]
+ }]
+}
Binary file image/www/app/error-handler.js.gz has changed
Binary file image/www/app/images/clipboard.svg.gz has changed
Binary file image/www/app/images/connect.svg.gz has changed
Binary file image/www/app/images/disconnect.svg.gz has changed
Binary file image/www/app/images/drag.svg.gz has changed
Binary file image/www/app/images/error.svg.gz has changed
Binary file image/www/app/images/expander.svg.gz has changed
Binary file image/www/app/images/fullscreen.svg.gz has changed
Binary file image/www/app/images/handle.svg.gz has changed
Binary file image/www/app/images/handle_bg.svg.gz has changed
Binary file image/www/app/images/info.svg.gz has changed
Binary file image/www/app/images/mouse_left.svg.gz has changed
Binary file image/www/app/images/mouse_middle.svg.gz has changed
Binary file image/www/app/images/mouse_none.svg.gz has changed
Binary file image/www/app/images/mouse_right.svg.gz has changed
Binary file image/www/app/images/settings.svg.gz has changed
Binary file image/www/app/images/warning.svg.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/de.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Verbinden...",
+    "Disconnecting...": "Verbindung trennen...",
+    "Reconnecting...": "Verbindung wiederherstellen...",
+    "Internal error": "Interner Fehler",
+    "Must set host": "Richten Sie den Server ein",
+    "Connected (encrypted) to ": "Verbunden mit (verschlüsselt) ",
+    "Connected (unencrypted) to ": "Verbunden mit (unverschlüsselt) ",
+    "Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt",
+    "Disconnected": "Verbindung zum Server getrennt",
+    "New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ",
+    "New connection has been rejected": "Verbindung wurde abgelehnt",
+    "Password is required": "Passwort ist erforderlich",
+    "noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
+    "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
+    "Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen",
+    "viewport drag": "Ansichtsfenster ziehen",
+    "Active Mouse Button": "Aktive Maustaste",
+    "No mousebutton": "Keine Maustaste",
+    "Left mousebutton": "Linke Maustaste",
+    "Middle mousebutton": "Mittlere Maustaste",
+    "Right mousebutton": "Rechte Maustaste",
+    "Keyboard": "Tastatur",
+    "Show Keyboard": "Tastatur anzeigen",
+    "Extra keys": "Zusatztasten",
+    "Show Extra Keys": "Zusatztasten anzeigen",
+    "Ctrl": "Strg",
+    "Toggle Ctrl": "Strg umschalten",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt umschalten",
+    "Send Tab": "Tab senden",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Escape senden",
+    "Ctrl+Alt+Del": "Strg+Alt+Entf",
+    "Send Ctrl-Alt-Del": "Strg+Alt+Entf senden",
+    "Shutdown/Reboot": "Herunterfahren/Neustarten",
+    "Shutdown/Reboot...": "Herunterfahren/Neustarten...",
+    "Power": "Energie",
+    "Shutdown": "Herunterfahren",
+    "Reboot": "Neustarten",
+    "Reset": "Zurücksetzen",
+    "Clipboard": "Zwischenablage",
+    "Clear": "Löschen",
+    "Fullscreen": "Vollbild",
+    "Settings": "Einstellungen",
+    "Shared Mode": "Geteilter Modus",
+    "View Only": "Nur betrachten",
+    "Clip to Window": "Auf Fenster begrenzen",
+    "Scaling Mode:": "Skalierungsmodus:",
+    "None": "Keiner",
+    "Local Scaling": "Lokales skalieren",
+    "Remote Resizing": "Serverseitiges skalieren",
+    "Advanced": "Erweitert",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Verschlüsselt",
+    "Host:": "Server:",
+    "Port:": "Port:",
+    "Path:": "Pfad:",
+    "Automatic Reconnect": "Automatisch wiederverbinden",
+    "Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):",
+    "Logging:": "Protokollierung:",
+    "Disconnect": "Verbindung trennen",
+    "Connect": "Verbinden",
+    "Password:": "Passwort:",
+    "Cancel": "Abbrechen",
+    "Canvas not supported.": "Canvas nicht unterstützt."
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/el.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Συνδέεται...",
+    "Disconnecting...": "Aποσυνδέεται...",
+    "Reconnecting...": "Επανασυνδέεται...",
+    "Internal error": "Εσωτερικό σφάλμα",
+    "Must set host": "Πρέπει να οριστεί ο διακομιστής",
+    "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
+    "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
+    "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
+    "Disconnected": "Αποσυνδέθηκε",
+    "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
+    "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
+    "Password is required": "Απαιτείται ο κωδικός πρόσβασης",
+    "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
+    "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
+    "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
+    "viewport drag": "σύρσιμο θεατού πεδίου",
+    "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
+    "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
+    "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
+    "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
+    "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
+    "Keyboard": "Πληκτρολόγιο",
+    "Show Keyboard": "Εμφάνιση Πληκτρολογίου",
+    "Extra keys": "Επιπλέον πλήκτρα",
+    "Show Extra Keys": "Εμφάνιση Επιπλέον Πλήκτρων",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Εναλλαγή Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Εναλλαγή Alt",
+    "Send Tab": "Αποστολή Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Αποστολή Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Αποστολή Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Κλείσιμο/Επανεκκίνηση",
+    "Shutdown/Reboot...": "Κλείσιμο/Επανεκκίνηση...",
+    "Power": "Απενεργοποίηση",
+    "Shutdown": "Κλείσιμο",
+    "Reboot": "Επανεκκίνηση",
+    "Reset": "Επαναφορά",
+    "Clipboard": "Πρόχειρο",
+    "Clear": "Καθάρισμα",
+    "Fullscreen": "Πλήρης Οθόνη",
+    "Settings": "Ρυθμίσεις",
+    "Shared Mode": "Κοινόχρηστη Λειτουργία",
+    "View Only": "Μόνο Θέαση",
+    "Clip to Window": "Αποκοπή στο όριο του Παράθυρου",
+    "Scaling Mode:": "Λειτουργία Κλιμάκωσης:",
+    "None": "Καμία",
+    "Local Scaling": "Τοπική Κλιμάκωση",
+    "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
+    "Advanced": "Για προχωρημένους",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Κρυπτογράφηση",
+    "Host:": "Όνομα διακομιστή:",
+    "Port:": "Πόρτα διακομιστή:",
+    "Path:": "Διαδρομή:",
+    "Automatic Reconnect": "Αυτόματη επανασύνδεση",
+    "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
+    "Logging:": "Καταγραφή:",
+    "Disconnect": "Αποσύνδεση",
+    "Connect": "Σύνδεση",
+    "Password:": "Κωδικός Πρόσβασης:",
+    "Cancel": "Ακύρωση",
+    "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas"
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/es.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,68 @@
+{
+    "Connecting...": "Conectando...",
+    "Connected (encrypted) to ": "Conectado (con encriptación) a",
+    "Connected (unencrypted) to ": "Conectado (sin encriptación) a",
+    "Disconnecting...": "Desconectando...",
+    "Disconnected": "Desconectado",
+    "Must set host": "Debes configurar el host",
+    "Reconnecting...": "Reconectando...",
+    "Password is required": "Contraseña es obligatoria",
+    "Disconnect timeout": "Tiempo de desconexión agotado",
+    "noVNC encountered an error:": "noVNC ha encontrado un error:",
+    "Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
+    "Move/Drag Viewport": "Mover/Arrastrar la ventana",
+    "viewport drag": "Arrastrar la ventana",
+    "Active Mouse Button": "Botón activo del ratón",
+    "No mousebutton": "Ningún botón del ratón",
+    "Left mousebutton": "Botón izquierdo del ratón",
+    "Middle mousebutton": "Botón central del ratón",
+    "Right mousebutton": "Botón derecho del ratón",
+    "Keyboard": "Teclado",
+    "Show Keyboard": "Mostrar teclado",
+    "Extra keys": "Teclas adicionales",
+    "Show Extra Keys": "Mostrar Teclas Adicionales",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Pulsar/Soltar Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Pulsar/Soltar Alt",
+    "Send Tab": "Enviar Tabulación",
+    "Tab": "Tabulación",
+    "Esc": "Esc",
+    "Send Escape": "Enviar Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del",
+    "Shutdown/Reboot": "Apagar/Reiniciar",
+    "Shutdown/Reboot...": "Apagar/Reiniciar...",
+    "Power": "Encender",
+    "Shutdown": "Apagar",
+    "Reboot": "Reiniciar",
+    "Reset": "Restablecer",
+    "Clipboard": "Portapapeles",
+    "Clear": "Vaciar",
+    "Fullscreen": "Pantalla Completa",
+    "Settings": "Configuraciones",
+    "Shared Mode": "Modo Compartido",
+    "View Only": "Solo visualización",
+    "Clip to Window": "Recortar al tamaño de la ventana",
+    "Scaling Mode:": "Modo de escalado:",
+    "None": "Ninguno",
+    "Local Scaling": "Escalado Local",
+    "Local Downscaling": "Reducción de escala local",
+    "Remote Resizing": "Cambio de tamaño remoto",
+    "Advanced": "Avanzado",
+    "Local Cursor": "Cursor Local",
+    "Repeater ID:": "ID del Repetidor",
+    "WebSocket": "WebSocket",
+    "Encrypt": "",
+    "Host:": "Host",
+    "Port:": "Puesto",
+    "Path:": "Ruta",
+    "Automatic Reconnect": "Reconexión automática",
+    "Reconnect Delay (ms):": "Retraso en la reconexión (ms)",
+    "Logging:": "Logging",
+    "Disconnect": "Desconectar",
+    "Connect": "Conectar",
+    "Password:": "Contraseña",
+    "Cancel": "Cancelar",
+    "Canvas not supported.": "Canvas no está soportado"
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/nl.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,68 @@
+{
+    "Connecting...": "Verbinden...",
+    "Connected (encrypted) to ": "Verbonden (versleuteld) met ",
+    "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
+    "Disconnecting...": "Verbinding verbreken...",
+    "Disconnected": "Verbinding verbroken",
+    "Must set host": "Host moeten worden ingesteld",
+    "Reconnecting...": "Opnieuw verbinding maken...",
+    "Password is required": "Wachtwoord is vereist",
+    "Disconnect timeout": "Timeout tijdens verbreken van verbinding",
+    "noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
+    "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
+    "Move/Drag Viewport": "Verplaats/Versleep Kijkvenster",
+    "viewport drag": "kijkvenster slepen",
+    "Active Mouse Button": "Actieve Muisknop",
+    "No mousebutton": "Geen muisknop",
+    "Left mousebutton": "Linker muisknop",
+    "Middle mousebutton": "Middelste muisknop",
+    "Right mousebutton": "Rechter muisknop",
+    "Keyboard": "Toetsenbord",
+    "Show Keyboard": "Toon Toetsenbord",
+    "Extra keys": "Extra toetsen",
+    "Show Extra Keys": "Toon Extra Toetsen",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl aan/uitzetten",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt aan/uitzetten",
+    "Send Tab": "Tab Sturen",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Escape Sturen",
+    "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+    "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen",
+    "Shutdown/Reboot": "Uitschakelen/Herstarten",
+    "Shutdown/Reboot...": "Uitschakelen/Herstarten...",
+    "Power": "Systeem",
+    "Shutdown": "Uitschakelen",
+    "Reboot": "Herstarten",
+    "Reset": "Resetten",
+    "Clipboard": "Klembord",
+    "Clear": "Wissen",
+    "Fullscreen": "Volledig Scherm",
+    "Settings": "Instellingen",
+    "Shared Mode": "Gedeelde Modus",
+    "View Only": "Alleen Kijken",
+    "Clip to Window": "Randen buiten venster afsnijden",
+    "Scaling Mode:": "Schaalmodus:",
+    "None": "Geen",
+    "Local Scaling": "Lokaal Schalen",
+    "Local Downscaling": "Lokaal Neerschalen",
+    "Remote Resizing": "Op Afstand Formaat Wijzigen",
+    "Advanced": "Geavanceerd",
+    "Local Cursor": "Lokale Cursor",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Versleutelen",
+    "Host:": "Host:",
+    "Port:": "Poort:",
+    "Path:": "Pad:",
+    "Automatic Reconnect": "Automatisch Opnieuw Verbinden",
+    "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
+    "Logging:": "Logmeldingen:",
+    "Disconnect": "Verbinding verbreken",
+    "Connect": "Verbinden",
+    "Password:": "Wachtwoord:",
+    "Cancel": "Annuleren",
+    "Canvas not supported.": "Canvas wordt niet ondersteund."
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/pl.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Łączenie...",
+    "Disconnecting...": "Rozłączanie...",
+    "Reconnecting...": "Łączenie...",
+    "Internal error": "Błąd wewnętrzny",
+    "Must set host": "Host i port są wymagane",
+    "Connected (encrypted) to ": "Połączenie (szyfrowane) z ",
+    "Connected (unencrypted) to ": "Połączenie (nieszyfrowane) z ",
+    "Something went wrong, connection is closed": "Coś poszło źle, połączenie zostało zamknięte",
+    "Disconnected": "Rozłączony",
+    "New connection has been rejected with reason: ": "Nowe połączenie zostało odrzucone z powodu: ",
+    "New connection has been rejected": "Nowe połączenie zostało odrzucone",
+    "Password is required": "Hasło jest wymagane",
+    "noVNC encountered an error:": "noVNC napotkało błąd:",
+    "Hide/Show the control bar": "Pokaż/Ukryj pasek ustawień",
+    "Move/Drag Viewport": "Ruszaj/Przeciągaj Viewport",
+    "viewport drag": "przeciągnij viewport",
+    "Active Mouse Button": "Aktywny Przycisk Myszy",
+    "No mousebutton": "Brak przycisku myszy",
+    "Left mousebutton": "Lewy przycisk myszy",
+    "Middle mousebutton": "Środkowy przycisk myszy",
+    "Right mousebutton": "Prawy przycisk myszy",
+    "Keyboard": "Klawiatura",
+    "Show Keyboard": "Pokaż klawiaturę",
+    "Extra keys": "Przyciski dodatkowe",
+    "Show Extra Keys": "Pokaż przyciski dodatkowe",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Przełącz Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Przełącz Alt",
+    "Send Tab": "Wyślij Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Wyślij Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Wyślij Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Wyłącz/Uruchom ponownie",
+    "Shutdown/Reboot...": "Wyłącz/Uruchom ponownie...",
+    "Power": "Włączony",
+    "Shutdown": "Wyłącz",
+    "Reboot": "Uruchom ponownie",
+    "Reset": "Resetuj",
+    "Clipboard": "Schowek",
+    "Clear": "Wyczyść",
+    "Fullscreen": "Pełny ekran",
+    "Settings": "Ustawienia",
+    "Shared Mode": "Tryb Współdzielenia",
+    "View Only": "Tylko Podgląd",
+    "Clip to Window": "Przytnij do Okna",
+    "Scaling Mode:": "Tryb Skalowania:",
+    "None": "Brak",
+    "Local Scaling": "Skalowanie lokalne",
+    "Remote Resizing": "Skalowanie zdalne",
+    "Advanced": "Zaawansowane",
+    "Repeater ID:": "ID Repeatera:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Szyfrowanie",
+    "Host:": "Host:",
+    "Port:": "Port:",
+    "Path:": "Ścieżka:",
+    "Automatic Reconnect": "Automatycznie wznawiaj połączenie",
+    "Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):",
+    "Logging:": "Poziom logowania:",
+    "Disconnect": "Rozłącz",
+    "Connect": "Połącz",
+    "Password:": "Hasło:",
+    "Cancel": "Anuluj",
+    "Canvas not supported.": "Element Canvas nie jest wspierany."
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/sv.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,68 @@
+{
+    "Connecting...": "Ansluter...",
+    "Connected (encrypted) to ": "Ansluten (krypterat) till ",
+    "Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
+    "Disconnecting...": "Kopplar ner...",
+    "Disconnected": "Frånkopplad",
+    "Must set host": "Du måste specifiera en värd",
+    "Reconnecting...": "Återansluter...",
+    "Password is required": "Lösenord krävs",
+    "Disconnect timeout": "Det tog för lång tid att koppla ner",
+    "noVNC encountered an error:": "noVNC stötte på ett problem:",
+    "Hide/Show the control bar": "Göm/Visa kontrollbaren",
+    "Move/Drag Viewport": "Flytta/Dra Vyn",
+    "viewport drag": "dra vy",
+    "Active Mouse Button": "Aktiv musknapp",
+    "No mousebutton": "Ingen musknapp",
+    "Left mousebutton": "Vänster musknapp",
+    "Middle mousebutton": "Mitten-musknapp",
+    "Right mousebutton": "Höger musknapp",
+    "Keyboard": "Tangentbord",
+    "Show Keyboard": "Visa Tangentbord",
+    "Extra keys": "Extraknappar",
+    "Show Extra Keys": "Visa Extraknappar",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Växla Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Växla Alt",
+    "Send Tab": "Skicka Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Skicka Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Stäng av/Boota om",
+    "Shutdown/Reboot...": "Stäng av/Boota om...",
+    "Power": "Ström",
+    "Shutdown": "Stäng av",
+    "Reboot": "Boota om",
+    "Reset": "Återställ",
+    "Clipboard": "Urklipp",
+    "Clear": "Rensa",
+    "Fullscreen": "Fullskärm",
+    "Settings": "Inställningar",
+    "Shared Mode": "Delat Läge",
+    "View Only": "Endast Visning",
+    "Clip to Window": "Begränsa till Fönster",
+    "Scaling Mode:": "Skalningsläge:",
+    "None": "Ingen",
+    "Local Scaling": "Lokal Skalning",
+    "Local Downscaling": "Lokal Nedskalning",
+    "Remote Resizing": "Ändra Storlek",
+    "Advanced": "Avancerat",
+    "Local Cursor": "Lokal Muspekare",
+    "Repeater ID:": "Repeater-ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Kryptera",
+    "Host:": "Värd:",
+    "Port:": "Port:",
+    "Path:": "Sökväg:",
+    "Automatic Reconnect": "Automatisk Återanslutning",
+    "Reconnect Delay (ms):": "Fördröjning (ms):",
+    "Logging:": "Loggning:",
+    "Disconnect": "Koppla från",
+    "Connect": "Anslut",
+    "Password:": "Lösenord:",
+    "Cancel": "Avbryt",
+    "Canvas not supported.": "Canvas stöds ej"
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/tr.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Bağlanıyor...",
+    "Disconnecting...": "Bağlantı kesiliyor...",
+    "Reconnecting...": "Yeniden bağlantı kuruluyor...",
+    "Internal error": "İç hata",
+    "Must set host": "Sunucuyu kur",
+    "Connected (encrypted) to ": "Bağlı (şifrelenmiş)",
+    "Connected (unencrypted) to ": "Bağlandı (şifrelenmemiş)",
+    "Something went wrong, connection is closed": "Bir şeyler ters gitti, bağlantı kesildi",
+    "Disconnected": "Bağlantı kesildi",
+    "New connection has been rejected with reason: ": "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: ",
+    "New connection has been rejected": "Bağlantı reddedildi",
+    "Password is required": "Şifre gerekli",
+    "noVNC encountered an error:": "Bir hata oluştu:",
+    "Hide/Show the control bar": "Denetim masasını Gizle/Göster",
+    "Move/Drag Viewport": "Görünümü Taşı/Sürükle",
+    "viewport drag": "Görüntü penceresini sürükle",
+    "Active Mouse Button": "Aktif Fare Düğmesi",
+    "No mousebutton": "Fare düğmesi yok",
+    "Left mousebutton": "Farenin sol düğmesi",
+    "Middle mousebutton": "Farenin orta düğmesi",
+    "Right mousebutton": "Farenin sağ düğmesi",
+    "Keyboard": "Klavye",
+    "Show Keyboard": "Klavye Düzenini Göster",
+    "Extra keys": "Ekstra tuşlar",
+    "Show Extra Keys": "Ekstra tuşları göster",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl Değiştir ",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt Değiştir",
+    "Send Tab": "Sekme Gönder",
+    "Tab": "Sekme",
+    "Esc": "Esc",
+    "Send Escape": "Boşluk Gönder",
+    "Ctrl+Alt+Del": "Ctrl + Alt + Del",
+    "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gönder",
+    "Shutdown/Reboot": "Kapat/Yeniden Başlat",
+    "Shutdown/Reboot...": "Kapat/Yeniden Başlat...",
+    "Power": "Güç",
+    "Shutdown": "Kapat",
+    "Reboot": "Yeniden Başlat",
+    "Reset": "Sıfırla",
+    "Clipboard": "Pano",
+    "Clear": "Temizle",
+    "Fullscreen": "Tam Ekran",
+    "Settings": "Ayarlar",
+    "Shared Mode": "Paylaşım Modu",
+    "View Only": "Sadece Görüntüle",
+    "Clip to Window": "Pencereye Tıkla",
+    "Scaling Mode:": "Ölçekleme Modu:",
+    "None": "Bilinmeyen",
+    "Local Scaling": "Yerel Ölçeklendirme",
+    "Remote Resizing": "Uzaktan Yeniden Boyutlandırma",
+    "Advanced": "Gelişmiş",
+    "Repeater ID:": "Tekralayıcı ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Şifrele",
+    "Host:": "Ana makine:",
+    "Port:": "Port:",
+    "Path:": "Yol:",
+    "Automatic Reconnect": "Otomatik Yeniden Bağlan",
+    "Reconnect Delay (ms):": "Yeniden Bağlanma Süreci (ms):",
+    "Logging:": "Giriş yapılıyor:",
+    "Disconnect": "Bağlantıyı Kes",
+    "Connect": "Bağlan",
+    "Password:": "Parola:",
+    "Cancel": "Vazgeç",
+    "Canvas not supported.": "Tuval desteklenmiyor."
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/locale/zh.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "連線中...",
+    "Disconnecting...": "正在中斷連線...",
+    "Reconnecting...": "重新連線中...",
+    "Internal error": "內部錯誤",
+    "Must set host": "請提供主機資訊",
+    "Connected (encrypted) to ": "已加密連線到",
+    "Connected (unencrypted) to ": "未加密連線到",
+    "Something went wrong, connection is closed": "發生錯誤,連線已關閉",
+    "Failed to connect to server": "無法連線到伺服器",
+    "Disconnected": "連線已中斷",
+    "New connection has been rejected with reason: ": "連線被拒絕,原因:",
+    "New connection has been rejected": "連線被拒絕",
+    "Password is required": "請提供密碼",
+    "noVNC encountered an error:": "noVNC 遇到一個錯誤:",
+    "Hide/Show the control bar": "顯示/隱藏控制列",
+    "Move/Drag Viewport": "拖放顯示範圍",
+    "viewport drag": "顯示範圍拖放",
+    "Active Mouse Button": "啟用滑鼠按鍵",
+    "No mousebutton": "無滑鼠按鍵",
+    "Left mousebutton": "滑鼠左鍵",
+    "Middle mousebutton": "滑鼠中鍵",
+    "Right mousebutton": "滑鼠右鍵",
+    "Keyboard": "鍵盤",
+    "Show Keyboard": "顯示鍵盤",
+    "Extra keys": "額外按鍵",
+    "Show Extra Keys": "顯示額外按鍵",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "切換 Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "切換 Alt",
+    "Send Tab": "送出 Tab 鍵",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "送出 Escape 鍵",
+    "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+    "Send Ctrl-Alt-Del": "送出 Ctrl-Alt-Del 快捷鍵",
+    "Shutdown/Reboot": "關機/重新啟動",
+    "Shutdown/Reboot...": "關機/重新啟動...",
+    "Power": "電源",
+    "Shutdown": "關機",
+    "Reboot": "重新啟動",
+    "Reset": "重設",
+    "Clipboard": "剪貼簿",
+    "Clear": "清除",
+    "Fullscreen": "全螢幕",
+    "Settings": "設定",
+    "Shared Mode": "分享模式",
+    "View Only": "僅檢視",
+    "Clip to Window": "限制/裁切視窗大小",
+    "Scaling Mode:": "縮放模式:",
+    "None": "無",
+    "Local Scaling": "本機縮放",
+    "Remote Resizing": "遠端調整大小",
+    "Advanced": "進階",
+    "Repeater ID:": "中繼站 ID",
+    "WebSocket": "WebSocket",
+    "Encrypt": "加密",
+    "Host:": "主機:",
+    "Port:": "連接埠:",
+    "Path:": "路徑:",
+    "Automatic Reconnect": "自動重新連線",
+    "Reconnect Delay (ms):": "重新連線間隔 (ms):",
+    "Logging:": "日誌級別:",
+    "Disconnect": "中斷連線",
+    "Connect": "連線",
+    "Password:": "密碼:",
+    "Cancel": "取消"
+}
\ No newline at end of file
Binary file image/www/app/localization.js.gz has changed
Binary file image/www/app/sounds/bell.mp3 has changed
Binary file image/www/app/sounds/bell.oga has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/styles/base.css	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,893 @@
+/*
+ * noVNC base CSS
+ * Copyright (C) 2012 Joel Martin
+ * Copyright (C) 2016 Samuel Mannehed for Cendio AB
+ * Copyright (C) 2016 Pierre Ossman for Cendio AB
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/*
+ * Z index layers:
+ *
+ * 0: Main screen
+ * 10: Control bar
+ * 50: Transition blocker
+ * 60: Connection popups
+ * 100: Status bar
+ * ...
+ * 1000: Javascript crash
+ * ...
+ * 10000: Max (used for polyfills)
+ */
+
+body {
+  margin:0;
+  padding:0;
+  font-family: Helvetica;
+  /*Background image with light grey curve.*/
+  background-color:#494949;
+  background-repeat:no-repeat;
+  background-position:right bottom;
+  height:100%;
+  touch-action: none;
+}
+
+html {
+  height:100%;
+}
+
+.noVNC_only_touch.noVNC_hidden {
+  display: none;
+}
+
+.noVNC_disabled {
+  color: rgb(128, 128, 128);
+}
+
+/* ----------------------------------------
+ * Spinner
+ * ----------------------------------------
+ */
+
+.noVNC_spinner {
+  position: relative;
+}
+.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {
+  width: 10px;
+  height: 10px;
+  border-radius: 2px;
+  box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);
+  animation: noVNC_spinner 1.0s linear infinite;
+}
+.noVNC_spinner::before {
+  content: "";
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  animation-delay: -0.1s;
+}
+.noVNC_spinner::after {
+  content: "";
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  animation-delay: 0.1s;
+}
+@keyframes noVNC_spinner {
+  0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }
+  25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }
+  50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
+}
+
+/* ----------------------------------------
+ * Input Elements
+ * ----------------------------------------
+ */
+
+input[type=input], input[type=password], input[type=number],
+input:not([type]), textarea {
+  /* Disable default rendering */
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  background: none;
+
+  margin: 2px;
+  padding: 2px;
+  border: 1px solid rgb(192, 192, 192);
+  border-radius: 5px;
+  color: black;
+  background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
+}
+
+input[type=button], input[type=submit], select {
+  /* Disable default rendering */
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  background: none;
+
+  margin: 2px;
+  padding: 2px;
+  border: 1px solid rgb(192, 192, 192);
+  border-bottom-width: 2px;
+  border-radius: 5px;
+  color: black;
+  background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240));
+
+  /* This avoids it jumping around when :active */
+  vertical-align: middle;
+}
+
+input[type=button], input[type=submit] {
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+option {
+  color: black;
+  background: white;
+}
+
+input[type=input]:focus, input[type=password]:focus,
+input:not([type]):focus, input[type=button]:focus,
+input[type=submit]:focus,
+textarea:focus, select:focus {
+  box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
+  border-color: rgb(74, 144, 217);
+  outline: none;
+}
+
+input[type=button]::-moz-focus-inner,
+input[type=submit]::-moz-focus-inner {
+  border: none;
+}
+
+input[type=input]:disabled, input[type=password]:disabled,
+input:not([type]):disabled, input[type=button]:disabled,
+input[type=submit]:disabled, input[type=number]:disabled,
+textarea:disabled, select:disabled {
+  color: rgb(128, 128, 128);
+  background: rgb(240, 240, 240);
+}
+
+input[type=button]:active, input[type=submit]:active,
+select:active {
+  border-bottom-width: 1px;
+  margin-top: 3px;
+}
+
+:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
+:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
+:root:not(.noVNC_touch) select:hover:not(:disabled) {
+  background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
+}
+
+/* ----------------------------------------
+ * WebKit centering hacks
+ * ----------------------------------------
+ */
+
+.noVNC_center {
+  /*
+   * This is a workaround because webkit misrenders transforms and
+   * uses non-integer coordinates, resulting in blurry content.
+   * Ideally we'd use "top: 50%; transform: translateY(-50%);" on
+   * the objects instead.
+   */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+}
+.noVNC_center > * {
+  pointer-events: auto;
+}
+.noVNC_vcenter {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  position: fixed;
+  top: 0;
+  left: 0;
+  height: 100%;
+  pointer-events: none;
+}
+.noVNC_vcenter > * {
+  pointer-events: auto;
+}
+
+/* ----------------------------------------
+ * Layering
+ * ----------------------------------------
+ */
+
+.noVNC_connect_layer {
+  z-index: 60;
+}
+
+/* ----------------------------------------
+ * Fallback error
+ * ----------------------------------------
+ */
+
+#noVNC_fallback_error {
+  z-index: 1000;
+  visibility: hidden;
+}
+#noVNC_fallback_error.noVNC_open {
+  visibility: visible;
+}
+
+#noVNC_fallback_error > div {
+  max-width: 90%;
+  padding: 15px;
+
+  transition: 0.5s ease-in-out;
+
+  transform: translateY(-50px);
+  opacity: 0;
+
+  text-align: center;
+  font-weight: bold;
+  color: #fff;
+
+  border-radius: 10px;
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+  background: rgba(200,55,55,0.8);
+}
+#noVNC_fallback_error.noVNC_open > div {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+#noVNC_fallback_errormsg {
+  font-weight: normal;
+}
+
+#noVNC_fallback_errormsg .noVNC_message {
+  display: inline-block;
+  text-align: left;
+  font-family: monospace;
+  white-space: pre-wrap;
+}
+
+#noVNC_fallback_error .noVNC_location {
+  font-style: italic;
+  font-size: 0.8em;
+  color: rgba(255, 255, 255, 0.8);
+}
+
+#noVNC_fallback_error .noVNC_stack {
+  max-height: 50vh;
+  padding: 10px;
+  margin: 10px;
+  font-size: 0.8em;
+  text-align: left;
+  font-family: monospace;
+  white-space: pre;
+  border: 1px solid rgba(0, 0, 0, 0.5);
+  background: rgba(0, 0, 0, 0.2);
+  overflow: auto;
+}
+
+/* ----------------------------------------
+ * Control Bar
+ * ----------------------------------------
+ */
+
+#noVNC_control_bar_anchor {
+  /* The anchor is needed to get z-stacking to work */
+  position: fixed;
+  z-index: 10;
+
+  transition: 0.5s ease-in-out;
+
+  /* Edge misrenders animations wihthout this */
+  transform: translateX(0);
+}
+:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
+  opacity: 0.8;
+}
+#noVNC_control_bar_anchor.noVNC_right {
+  left: auto;
+  right: 0;
+}
+
+#noVNC_control_bar {
+  position: relative;
+  left: -100%;
+
+  transition: 0.5s ease-in-out;
+
+  background-color: rgb(110, 132, 163);
+  border-radius: 0 10px 10px 0;
+
+}
+#noVNC_control_bar.noVNC_open {
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+  left: 0;
+}
+#noVNC_control_bar::before {
+  /* This extra element is to get a proper shadow */
+  content: "";
+  position: absolute;
+  z-index: -1;
+  height: 100%;
+  width: 30px;
+  left: -30px;
+  transition: box-shadow 0.5s ease-in-out;
+}
+#noVNC_control_bar.noVNC_open::before {
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_right #noVNC_control_bar {
+  left: 100%;
+  border-radius: 10px 0 0 10px;
+}
+.noVNC_right #noVNC_control_bar.noVNC_open {
+  left: 0;
+}
+.noVNC_right #noVNC_control_bar::before {
+  visibility: hidden;
+}
+
+#noVNC_control_bar_handle {
+  position: absolute;
+  left: -15px;
+  top: 0;
+  transform: translateY(35px);
+  width: calc(100% + 30px);
+  height: 50px;
+  z-index: -1;
+  cursor: pointer;
+  border-radius: 5px;
+  background-color: rgb(83, 99, 122);
+  background-image: url("../images/handle_bg.svg");
+  background-repeat: no-repeat;
+  background-position: right;
+  box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_control_bar_handle:after {
+  content: "";
+  transition: transform 0.5s ease-in-out;
+  background: url("../images/handle.svg");
+  position: absolute;
+  top: 22px; /* (50px-6px)/2 */
+  right: 5px;
+  width: 5px;
+  height: 6px;
+}
+#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+  transform: translateX(1px) rotate(180deg);
+}
+:root:not(.noVNC_connected) #noVNC_control_bar_handle {
+  display: none;
+}
+.noVNC_right #noVNC_control_bar_handle {
+  background-position: left;
+}
+.noVNC_right #noVNC_control_bar_handle:after {
+  left: 5px;
+  right: 0;
+  transform: translateX(1px) rotate(180deg);
+}
+.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+  transform: none;
+}
+#noVNC_control_bar_handle div {
+  position: absolute;
+  right: -35px;
+  top: 0;
+  width: 50px;
+  height: 50px;
+}
+:root:not(.noVNC_touch) #noVNC_control_bar_handle div {
+  display: none;
+}
+.noVNC_right #noVNC_control_bar_handle div {
+  left: -35px;
+  right: auto;
+}
+
+#noVNC_control_bar .noVNC_scroll {
+  max-height: 100vh; /* Chrome is buggy with 100% */
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 0 10px 0 5px;
+}
+.noVNC_right #noVNC_control_bar .noVNC_scroll {
+  padding: 0 5px 0 10px;
+}
+
+/* Control bar hint */
+#noVNC_control_bar_hint {
+  position: fixed;
+  left: calc(100vw - 50px);
+  right: auto;
+  top: 50%;
+  transform: translateY(-50%) scale(0);
+  width: 100px;
+  height: 50%;
+  max-height: 600px;
+
+  visibility: hidden;
+  opacity: 0;
+  transition: 0.2s ease-in-out;
+  background: transparent;
+  box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8);
+  border-radius: 10px;
+  transition-delay: 0s;
+}
+#noVNC_control_bar_anchor.noVNC_right #noVNC_control_bar_hint{
+  left: auto;
+  right: calc(100vw - 50px);
+}
+#noVNC_control_bar_hint.noVNC_active {
+  visibility: visible;
+  opacity: 1;
+  transition-delay: 0.2s;
+  transform: translateY(-50%) scale(1);
+}
+
+/* General button style */
+.noVNC_button {
+  display: block;
+  padding: 4px 4px;
+  margin: 10px 0;
+  vertical-align: middle;
+  border:1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+}
+.noVNC_button.noVNC_selected {
+  border-color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.5);
+}
+.noVNC_button:disabled {
+  opacity: 0.4;
+}
+.noVNC_button:focus {
+  outline: none;
+}
+.noVNC_button:active {
+  padding-top: 5px;
+  padding-bottom: 3px;
+}
+/* Android browsers don't properly update hover state if touch events
+ * are intercepted, but focus should be safe to display */
+:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover,
+.noVNC_button.noVNC_selected:focus {
+  border-color: rgba(0, 0, 0, 0.4);
+  background: rgba(0, 0, 0, 0.2);
+}
+:root:not(.noVNC_touch) .noVNC_button:hover,
+.noVNC_button:focus {
+  background: rgba(255, 255, 255, 0.2);
+}
+.noVNC_button.noVNC_hidden {
+  display: none;
+}
+
+/* Panels */
+.noVNC_panel {
+  transform: translateX(25px);
+
+  transition: 0.5s ease-in-out;
+
+  max-height: 100vh; /* Chrome is buggy with 100% */
+  overflow-x: hidden;
+  overflow-y: auto;
+
+  visibility: hidden;
+  opacity: 0;
+
+  padding: 15px;
+
+  background: #fff;
+  border-radius: 10px;
+  color: #000;
+  border: 2px solid #E0E0E0;
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_panel.noVNC_open {
+  visibility: visible;
+  opacity: 1;
+  transform: translateX(75px);
+}
+.noVNC_right .noVNC_vcenter {
+  left: auto;
+  right: 0;
+}
+.noVNC_right .noVNC_panel {
+  transform: translateX(-25px);
+}
+.noVNC_right .noVNC_panel.noVNC_open {
+  transform: translateX(-75px);
+}
+
+.noVNC_panel hr {
+  border: none;
+  border-top: 1px solid rgb(192, 192, 192);
+}
+
+.noVNC_panel label {
+  display: block;
+  white-space: nowrap;
+}
+
+.noVNC_panel .noVNC_heading {
+  background-color: rgb(110, 132, 163);
+  border-radius: 5px;
+  padding: 5px;
+  /* Compensate for padding in image */
+  padding-right: 8px;
+  color: white;
+  font-size: 20px;
+  margin-bottom: 10px;
+  white-space: nowrap;
+}
+.noVNC_panel .noVNC_heading img {
+  vertical-align: bottom;
+}
+
+.noVNC_submit {
+  float: right;
+}
+
+/* Expanders */
+.noVNC_expander {
+  cursor: pointer;
+}
+.noVNC_expander::before {
+  content: url("../images/expander.svg");
+  display: inline-block;
+  margin-right: 5px;
+  transition: 0.2s ease-in-out;
+}
+.noVNC_expander.noVNC_open::before {
+  transform: rotateZ(90deg);
+}
+.noVNC_expander ~ * {
+  margin: 5px;
+  margin-left: 10px;
+  padding: 5px;
+  background: rgba(0, 0, 0, 0.05);
+  border-radius: 5px;
+}
+.noVNC_expander:not(.noVNC_open) ~ * {
+  display: none;
+}
+
+/* Control bar content */
+
+#noVNC_control_bar .noVNC_logo {
+  font-size: 13px;
+}
+
+:root:not(.noVNC_connected) #noVNC_view_drag_button {
+  display: none;
+}
+
+/* noVNC Touch Device only buttons */
+:root:not(.noVNC_connected) #noVNC_mobile_buttons {
+  display: none;
+}
+:root:not(.noVNC_touch) #noVNC_mobile_buttons {
+  display: none;
+}
+
+/* Extra manual keys */
+:root:not(.noVNC_connected) #noVNC_extra_keys {
+  display: none;
+}
+
+#noVNC_modifiers {
+  background-color: rgb(92, 92, 92);
+  border: none;
+  padding: 0 10px;
+}
+
+/* Shutdown/Reboot */
+:root:not(.noVNC_connected) #noVNC_power_button {
+  display: none;
+}
+#noVNC_power {
+}
+#noVNC_power_buttons {
+  display: none;
+}
+
+#noVNC_power input[type=button] {
+  width: 100%;
+}
+
+/* Clipboard */
+:root:not(.noVNC_connected) #noVNC_clipboard_button {
+  display: none;
+}
+#noVNC_clipboard {
+  /* Full screen, minus padding and left and right margins */
+  max-width: calc(100vw - 2*15px - 75px - 25px);
+}
+#noVNC_clipboard_text {
+  width: 500px;
+  max-width: 100%;
+}
+
+/* Settings */
+#noVNC_settings {
+}
+#noVNC_settings ul {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+}
+#noVNC_setting_port {
+  width: 80px;
+}
+#noVNC_setting_path {
+  width: 100px;
+}
+
+/* Connection Controls */
+:root:not(.noVNC_connected) #noVNC_disconnect_button {
+  display: none;
+}
+
+/* ----------------------------------------
+ * Status Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_status {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  z-index: 100;
+  transform: translateY(-100%);
+
+  cursor: pointer;
+
+  transition: 0.5s ease-in-out;
+
+  visibility: hidden;
+  opacity: 0;
+
+  padding: 5px;
+
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-content: center;
+
+  line-height: 25px;
+  word-wrap: break-word;
+  color: #fff;
+
+  border-bottom: 1px solid rgba(0, 0, 0, 0.9);
+}
+#noVNC_status.noVNC_open {
+  transform: translateY(0);
+  visibility: visible;
+  opacity: 1;
+}
+
+#noVNC_status::before {
+  content: "";
+  display: inline-block;
+  width: 25px;
+  height: 25px;
+  margin-right: 5px;
+}
+
+#noVNC_status.noVNC_status_normal {
+  background: rgba(128,128,128,0.9);
+}
+#noVNC_status.noVNC_status_normal::before {
+  content: url("../images/info.svg") " ";
+}
+#noVNC_status.noVNC_status_error {
+  background: rgba(200,55,55,0.9);
+}
+#noVNC_status.noVNC_status_error::before {
+  content: url("../images/error.svg") " ";
+}
+#noVNC_status.noVNC_status_warn {
+  background: rgba(180,180,30,0.9);
+}
+#noVNC_status.noVNC_status_warn::before {
+  content: url("../images/warning.svg") " ";
+}
+
+/* ----------------------------------------
+ * Connect Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_connect_dlg {
+  transition: 0.5s ease-in-out;
+
+  transform: scale(0, 0);
+  visibility: hidden;
+  opacity: 0;
+}
+#noVNC_connect_dlg.noVNC_open {
+  transform: scale(1, 1);
+  visibility: visible;
+  opacity: 1;
+}
+#noVNC_connect_dlg .noVNC_logo {
+  transition: 0.5s ease-in-out;
+  padding: 10px;
+  margin-bottom: 10px;
+
+  font-size: 80px;
+  text-align: center;
+
+  border-radius: 5px;
+}
+@media (max-width: 440px) {
+  #noVNC_connect_dlg {
+    max-width: calc(100vw - 100px);
+  }
+  #noVNC_connect_dlg .noVNC_logo {
+    font-size: calc(25vw - 30px);
+  }
+}
+#noVNC_connect_button {
+  cursor: pointer;
+
+  padding: 10px;
+
+  color: white;
+  background-color: rgb(110, 132, 163);
+  border-radius: 12px;
+
+  text-align: center;
+  font-size: 20px;
+
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_connect_button div {
+  margin: 2px;
+  padding: 5px 30px;
+  border: 1px solid rgb(83, 99, 122);
+  border-bottom-width: 2px;
+  border-radius: 5px;
+  background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147));
+
+  /* This avoids it jumping around when :active */
+  vertical-align: middle;
+}
+#noVNC_connect_button div:active {
+  border-bottom-width: 1px;
+  margin-top: 3px;
+}
+:root:not(.noVNC_touch) #noVNC_connect_button div:hover {
+  background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155));
+}
+
+#noVNC_connect_button img {
+  vertical-align: bottom;
+  height: 1.3em;
+}
+
+/* ----------------------------------------
+ * Password Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_password_dlg {
+  position: relative;
+
+  transform: translateY(-50px);
+}
+#noVNC_password_dlg.noVNC_open {
+  transform: translateY(0);
+}
+#noVNC_password_dlg ul {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+}
+
+/* ----------------------------------------
+ * Main Area
+ * ----------------------------------------
+ */
+
+/* Transition screen */
+#noVNC_transition {
+  display: none;
+
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+
+  color: white;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 50;
+
+  /*display: flex;*/
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+}
+:root.noVNC_loading #noVNC_transition,
+:root.noVNC_connecting #noVNC_transition,
+:root.noVNC_disconnecting #noVNC_transition,
+:root.noVNC_reconnecting #noVNC_transition {
+  display: flex;
+}
+:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
+  display: none;
+}
+#noVNC_transition_text {
+  font-size: 1.5em;
+}
+
+/* Main container */
+#noVNC_container {
+  width: 100%;
+  height: 100%;
+  background-color: #313131;
+  border-bottom-right-radius: 800px 600px;
+  /*border-top-left-radius: 800px 600px;*/
+}
+
+#noVNC_keyboardinput {
+  width: 1px;
+  height: 1px;
+  background-color: #fff;
+  color: #fff;
+  border: 0;
+  position: absolute;
+  left: -40px;
+  z-index: -1;
+  ime-mode: disabled;
+}
+
+/*Default noVNC logo.*/
+.noVNC_logo {
+  color:yellow;
+  font-family: sans-serif;
+  line-height:90%;
+  text-shadow: 0.1em 0.1em 0 black;
+}
+.noVNC_logo span{
+  color:green;
+}
+
+#noVNC_bell {
+  display: none;
+}
+
+/* ----------------------------------------
+ * Media sizing
+ * ----------------------------------------
+ */
+
+@media screen and (max-width: 640px){
+  #noVNC_logo {
+    font-size: 150px;
+  }
+}
+
+@media screen and (min-width: 321px) and (max-width: 480px) {
+  #noVNC_logo {
+    font-size: 110px;
+  }
+}
+
+@media screen and (max-width: 320px) {
+  #noVNC_logo {
+    font-size: 90px;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/app/styles/web.css	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,86 @@
+
+body {
+  margin:0;
+  padding:0;
+  font-family: Helvetica;
+  /*Background image with light grey curve.*/
+  background-color:#494949;
+  background-repeat:no-repeat;
+  background-position:right bottom;
+  height:100%;
+/*  touch-action: none; */
+}
+
+html {
+  height:100%;
+}
+
+
+/* ----------------------------------------
+ * Main Area
+ * ----------------------------------------
+ */
+
+
+.menu {
+  width: 270px;
+  margin: auto;
+}
+
+.menu input[type=none] {
+  background-color: #0077bb;
+  border: none;
+  color: yellow;
+  font-weight: bold;
+  width: 250px;
+  padding: 10px 0px;
+  text-decoration: none;
+  font-size: 16px;
+  margin: 8px;
+  text-align: center;
+  border-radius: 10px;
+}
+
+.menu input[type=submit] {
+  background-color: #33b5e5;
+  border: none;
+  color: white;
+  font-weight: bold;
+  width: 250px;
+  padding: 15px 32px;
+  text-decoration: none;
+  font-size: 16px;
+  cursor: pointer;
+  margin: 8px;
+  text-align: center;
+  border-radius: 10px;
+}
+
+.menu input[type=submit]:hover {
+  background-color: #0099cc;
+}
+
+
+.menu ul {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+table.directory {
+  width:  90%;
+  border-collapse : collapse;
+  margin          : auto;
+  padding         : 0px;
+  background: yellow;
+}
+
+tr.directory:nth-child(odd) {
+  background      : #FFFFFF;
+}
+
+tr.directory:nth-child(even) {
+  background      : #CCFFFF;
+}
+
+
Binary file image/www/app/ui.js.gz has changed
Binary file image/www/app/webutil.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/chart.html	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Brew Log</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 
+  <script src="js/jquery-1.12.4.min.js"></script>
+  <script src="js/Chart.min.js"></script>
+  <script src="js/ch-plug-anno.min.js"></script>
+  <link rel="stylesheet" href="app/styles/web.css">
+  <style>
+   canvas{
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	-ms-user-select: none;
+	background: white;
+   }
+  </style>
+</head>
+<body>
+
+  <div id="tmap"></div>
+ <div style="width:95%; margin:0 auto; margin-top: 20px;">
+  <canvas id="myChart"></canvas>
+ </div>
+ <br>
+ <form action="index.html"><input type="submit" class="button" value="Hoofdmenu"></form>
+
+ <script>
+
+var tokens = [];
+var query = location.search;
+query = query.slice(1);
+query = query.split('&');
+$.each(query, function(i,value){    
+  var token = value.split('=');   
+  var key = decodeURIComponent(token[0]);     
+  var data = decodeURIComponent(token[1]);
+  tokens[key] = data;
+});
+
+
+var logfile = tokens["show"];
+$('#tmap').append(logfile);
+
+
+$.getJSON(logfile, function(data) {
+  var recipe = data.brew.map(function(item) {
+    return item.Recipe;
+  });
+  var brewdate = data.brew.map(function(item) {
+    return item.Date;
+  });
+  var labels = data.brew[0].brewdata.map(function(item) {
+    return item.Label;
+  });
+  var mlt_sp = data.brew[0].brewdata.map(function(item) {
+    return item.MLT_sp;
+  });
+  var mlt_pv = data.brew[0].brewdata.map(function(item) {
+    return item.MLT_pv;
+  });
+  var mlt_pwm = data.brew[0].brewdata.map(function(item) {
+    return item.MLT_pwm;
+  });
+  var hlt_sp = data.brew[0].brewdata.map(function(item) {
+    return item.HLT_sp;
+  });
+  var hlt_pv = data.brew[0].brewdata.map(function(item) {
+    return item.HLT_pv;
+  });
+  var hlt_pwm = data.brew[0].brewdata.map(function(item) {
+    return item.HLT_pwm;
+  });
+  var annos = data.brew[0].annotations.map(function(item) {
+    return item;
+  });
+
+  var ctx = document.getElementById("myChart");
+  var myChart = new Chart(ctx, {
+    type: 'line',
+    data: {
+      labels: labels,
+      datasets: [{
+	label: 'MLT sp',
+	backgroundColor: 'rgba(250, 0, 0, 0.4)',
+	borderColor: 'rgba(250, 0, 0, 0.4)',
+	borderDash: [8, 4],
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	pointStyle: 'dash',
+	lineTension: 0,
+	data: mlt_sp,
+	fill: false
+      },{
+	label: 'MLT pv',
+	backgroundColor: 'rgba(54, 162, 235, 0.9)',
+	borderColor: 'rgba(54, 162, 235, 0.9)',
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	lineTension: 0,
+	data: mlt_pv,
+	fill: false
+      },{
+	label: 'MLT pwm',
+	backgroundColor: 'rgba(63, 131, 191, 0.4)',
+	borderColor: 'rgba(63, 131, 191, 0.4)',
+	borderDash: [8, 4],
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	pointStyle: 'dash',
+	lineTension: 0,
+	data: mlt_pwm,
+	fill: false
+      },{
+	label: 'HLT sp',
+	backgroundColor: 'rgba(255, 99, 132, 0.4)',
+	borderColor: 'rgba(255, 99, 132, 0.4)',
+	borderDash: [8, 4],
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	pointStyle: 'dash',
+	lineTension: 0,
+	data: hlt_sp,
+	fill: false
+      },{
+	label: 'HLT pv',
+	backgroundColor: 'rgba(255, 206, 86, 0.9)',
+	borderColor: 'rgba(255, 206, 86, 0.9)',
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	lineTension: 0,
+	data: hlt_pv,
+	fill: false
+      },{
+	label: 'HLT pwm',
+	backgroundColor: 'rgba(75, 192, 192, 0.4)',
+	borderColor: 'rgba(75, 192, 192, 0.4)',
+	borderDash: [8, 4],
+	pointBorderWidth: 1,
+	pointRadius: 1,
+	pointStyle: 'dash',
+	lineTension: 0,
+	data: hlt_pwm,
+	fill: false
+      }]
+    },
+    options: {
+      title: {
+        display: true,
+	fontSize: 18,
+        text: recipe
+      },
+      responsive: true,
+      legend: {
+	position: 'bottom'
+      },
+      animation: {
+	duration: 0
+      },
+      scales: {
+	xAxes: [{
+	  display: true,
+	  scaleLabel: {
+	    display: true,
+	    labelString: brewdate
+	  }
+	}],
+        yAxes: [{
+          display: true,
+          scaleLabel: {
+            display: true,
+            labelString: 'Temperature'
+          }
+        }]
+      },
+      annotation: {
+	drawTime: "afterDraw",
+	annotations: annos
+      }
+    }
+  });
+  /* myChart.toBase64Image(); return pmg image string */
+});
+ </script>
+
+</body>
+</html>
+
Binary file image/www/core/base64.js.gz has changed
Binary file image/www/core/des.js.gz has changed
Binary file image/www/core/display.js.gz has changed
Binary file image/www/core/encodings.js.gz has changed
Binary file image/www/core/inflator.js.gz has changed
Binary file image/www/core/input/mouse.js.gz has changed
Binary file image/www/core/rfb.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/core/rfb.js.orig	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,2554 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2012 Joel Martin
+ * Copyright (C) 2017 Samuel Mannehed for Cendio AB
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ * TIGHT decoder portion:
+ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
+ */
+
+import * as Log from './util/logging.js';
+import { decodeUTF8 } from './util/strings.js';
+import { supportsCursorURIs, isTouchDevice } from './util/browser.js';
+import EventTargetMixin from './util/eventtarget.js';
+import Display from "./display.js";
+import Keyboard from "./input/keyboard.js";
+import Mouse from "./input/mouse.js";
+import Websock from "./websock.js";
+import DES from "./des.js";
+import KeyTable from "./input/keysym.js";
+import XtScancode from "./input/xtscancodes.js";
+import Inflator from "./inflator.js";
+import { encodings, encodingName } from "./encodings.js";
+import "./util/polyfill.js";
+
+/*jslint white: false, browser: true */
+/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode */
+
+// How many seconds to wait for a disconnect to finish
+var DISCONNECT_TIMEOUT = 3;
+
+export default function RFB(target, url, options) {
+    if (!target) {
+        throw Error("Must specify target");
+    }
+    if (!url) {
+        throw Error("Must specify URL");
+    }
+
+    this._target = target;
+    this._url = url;
+
+    // Connection details
+    options = options || {};
+    this._rfb_credentials = options.credentials || {};
+    this._shared = 'shared' in options ? !!options.shared : true;
+    this._repeaterID = options.repeaterID || '';
+
+    // Internal state
+    this._rfb_connection_state = '';
+    this._rfb_init_state = '';
+    this._rfb_auth_scheme = '';
+    this._rfb_clean_disconnect = true;
+
+    // Server capabilities
+    this._rfb_version = 0;
+    this._rfb_max_version = 3.8;
+    this._rfb_tightvnc = false;
+    this._rfb_xvp_ver = 0;
+
+    this._fb_width = 0;
+    this._fb_height = 0;
+
+    this._fb_name = "";
+
+    this._capabilities = { power: false };
+
+    this._supportsFence = false;
+
+    this._supportsContinuousUpdates = false;
+    this._enabledContinuousUpdates = false;
+
+    this._supportsSetDesktopSize = false;
+    this._screen_id = 0;
+    this._screen_flags = 0;
+
+    this._qemuExtKeyEventSupported = false;
+
+    // Internal objects
+    this._sock = null;              // Websock object
+    this._display = null;           // Display object
+    this._flushing = false;         // Display flushing state
+    this._keyboard = null;          // Keyboard input handler object
+    this._mouse = null;             // Mouse input handler object
+
+    // Timers
+    this._disconnTimer = null;      // disconnection timer
+    this._resizeTimeout = null;     // resize rate limiting
+
+    // Decoder states and stats
+    this._encHandlers = {};
+    this._encStats = {};
+
+    this._FBU = {
+        rects: 0,
+        subrects: 0,            // RRE and HEXTILE
+        lines: 0,               // RAW
+        tiles: 0,               // HEXTILE
+        bytes: 0,
+        x: 0,
+        y: 0,
+        width: 0,
+        height: 0,
+        encoding: 0,
+        subencoding: -1,
+        background: null,
+        zlibs: []               // TIGHT zlib streams
+    };
+    for (var i = 0; i < 4; i++) {
+        this._FBU.zlibs[i] = new Inflator();
+    }
+
+    this._destBuff = null;
+    this._paletteBuff = new Uint8Array(1024);  // 256 * 4 (max palette size * max bytes-per-pixel)
+
+    this._rre_chunk_sz = 100;
+
+    this._timing = {
+        last_fbu: 0,
+        fbu_total: 0,
+        fbu_total_cnt: 0,
+        full_fbu_total: 0,
+        full_fbu_cnt: 0,
+
+        fbu_rt_start: 0,
+        fbu_rt_total: 0,
+        fbu_rt_cnt: 0,
+        pixels: 0
+    };
+
+    // Mouse state
+    this._mouse_buttonMask = 0;
+    this._mouse_arr = [];
+    this._viewportDragging = false;
+    this._viewportDragPos = {};
+    this._viewportHasMoved = false;
+
+    // Bound event handlers
+    this._eventHandlers = {
+        focusCanvas: this._focusCanvas.bind(this),
+        windowResize: this._windowResize.bind(this),
+    };
+
+    // main setup
+    Log.Debug(">> RFB.constructor");
+
+    // Create DOM elements
+    this._screen = document.createElement('div');
+    this._screen.style.display = 'flex';
+    this._screen.style.width = '100%';
+    this._screen.style.height = '100%';
+    this._screen.style.overflow = 'auto';
+    this._screen.style.backgroundColor = 'rgb(40, 40, 40)';
+    this._canvas = document.createElement('canvas');
+    this._canvas.style.margin = 'auto';
+    // Some browsers add an outline on focus
+    this._canvas.style.outline = 'none';
+    // IE miscalculates width without this :(
+    this._canvas.style.flexShrink = '0';
+    this._canvas.width = 0;
+    this._canvas.height = 0;
+    this._canvas.tabIndex = -1;
+    this._screen.appendChild(this._canvas);
+
+    // populate encHandlers with bound versions
+    this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this);
+    this._encHandlers[encodings.encodingCopyRect] = RFB.encodingHandlers.COPYRECT.bind(this);
+    this._encHandlers[encodings.encodingRRE] = RFB.encodingHandlers.RRE.bind(this);
+    this._encHandlers[encodings.encodingHextile] = RFB.encodingHandlers.HEXTILE.bind(this);
+    this._encHandlers[encodings.encodingTight] = RFB.encodingHandlers.TIGHT.bind(this);
+
+    this._encHandlers[encodings.pseudoEncodingDesktopSize] = RFB.encodingHandlers.DesktopSize.bind(this);
+    this._encHandlers[encodings.pseudoEncodingLastRect] = RFB.encodingHandlers.last_rect.bind(this);
+    this._encHandlers[encodings.pseudoEncodingCursor] = RFB.encodingHandlers.Cursor.bind(this);
+    this._encHandlers[encodings.pseudoEncodingQEMUExtendedKeyEvent] = RFB.encodingHandlers.QEMUExtendedKeyEvent.bind(this);
+    this._encHandlers[encodings.pseudoEncodingExtendedDesktopSize] = RFB.encodingHandlers.ExtendedDesktopSize.bind(this);
+
+    // NB: nothing that needs explicit teardown should be done
+    // before this point, since this can throw an exception
+    try {
+        this._display = new Display(this._canvas);
+    } catch (exc) {
+        Log.Error("Display exception: " + exc);
+        throw exc;
+    }
+    this._display.onflush = this._onFlush.bind(this);
+    this._display.clear();
+
+    this._keyboard = new Keyboard(this._canvas);
+    this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
+
+    this._mouse = new Mouse(this._canvas);
+    this._mouse.onmousebutton = this._handleMouseButton.bind(this);
+    this._mouse.onmousemove = this._handleMouseMove.bind(this);
+
+    this._sock = new Websock();
+    this._sock.on('message', this._handle_message.bind(this));
+    this._sock.on('open', function () {
+        if ((this._rfb_connection_state === 'connecting') &&
+            (this._rfb_init_state === '')) {
+            this._rfb_init_state = 'ProtocolVersion';
+            Log.Debug("Starting VNC handshake");
+        } else {
+            this._fail("Unexpected server connection while " +
+                       this._rfb_connection_state);
+        }
+    }.bind(this));
+    this._sock.on('close', function (e) {
+        Log.Debug("WebSocket on-close event");
+        var msg = "";
+        if (e.code) {
+            msg = "(code: " + e.code;
+            if (e.reason) {
+                msg += ", reason: " + e.reason;
+            }
+            msg += ")";
+        }
+        switch (this._rfb_connection_state) {
+            case 'connecting':
+                this._fail("Connection closed " + msg);
+                break;
+            case 'connected':
+                // Handle disconnects that were initiated server-side
+                this._updateConnectionState('disconnecting');
+                this._updateConnectionState('disconnected');
+                break;
+            case 'disconnecting':
+                // Normal disconnection path
+                this._updateConnectionState('disconnected');
+                break;
+            case 'disconnected':
+                this._fail("Unexpected server disconnect " +
+                           "when already disconnected " + msg);
+                break;
+            default:
+                this._fail("Unexpected server disconnect before connecting " +
+                           msg);
+                break;
+        }
+        this._sock.off('close');
+    }.bind(this));
+    this._sock.on('error', function (e) {
+        Log.Warn("WebSocket on-error event");
+    });
+
+    // Slight delay of the actual connection so that the caller has
+    // time to set up callbacks
+    setTimeout(this._updateConnectionState.bind(this, 'connecting'));
+
+    Log.Debug("<< RFB.constructor");
+};
+
+RFB.prototype = {
+    // ===== PROPERTIES =====
+
+    dragViewport: false,
+    focusOnClick: true,
+
+    _viewOnly: false,
+    get viewOnly() { return this._viewOnly; },
+    set viewOnly(viewOnly) {
+        this._viewOnly = viewOnly;
+
+        if (this._rfb_connection_state === "connecting" ||
+            this._rfb_connection_state === "connected") {
+            if (viewOnly) {
+                this._keyboard.ungrab();
+                this._mouse.ungrab();
+            } else {
+                this._keyboard.grab();
+                this._mouse.grab();
+            }
+        }
+    },
+
+    get capabilities() { return this._capabilities; },
+
+    get touchButton() { return this._mouse.touchButton; },
+    set touchButton(button) { this._mouse.touchButton = button; },
+
+    _clipViewport: false,
+    get clipViewport() { return this._clipViewport; },
+    set clipViewport(viewport) {
+        this._clipViewport = viewport;
+        this._updateClip();
+    },
+
+    _scaleViewport: false,
+    get scaleViewport() { return this._scaleViewport; },
+    set scaleViewport(scale) {
+        this._scaleViewport = scale;
+        // Scaling trumps clipping, so we may need to adjust
+        // clipping when enabling or disabling scaling
+        if (scale && this._clipViewport) {
+            this._updateClip();
+        }
+        this._updateScale();
+        if (!scale && this._clipViewport) {
+            this._updateClip();
+        }
+    },
+
+    _resizeSession: false,
+    get resizeSession() { return this._resizeSession; },
+    set resizeSession(resize) {
+        this._resizeSession = resize;
+        if (resize) {
+            this._requestRemoteResize();
+        }
+    },
+
+    // ===== PUBLIC METHODS =====
+
+    disconnect: function () {
+        this._updateConnectionState('disconnecting');
+        this._sock.off('error');
+        this._sock.off('message');
+        this._sock.off('open');
+    },
+
+    sendCredentials: function (creds) {
+        this._rfb_credentials = creds;
+        setTimeout(this._init_msg.bind(this), 0);
+    },
+
+    sendCtrlAltDel: function () {
+        if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
+        Log.Info("Sending Ctrl-Alt-Del");
+
+        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+        this.sendKey(KeyTable.XK_Delete, "Delete", true);
+        this.sendKey(KeyTable.XK_Delete, "Delete", false);
+        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+    },
+
+    machineShutdown: function () {
+        this._xvpOp(1, 2);
+    },
+
+    machineReboot: function () {
+        this._xvpOp(1, 3);
+    },
+
+    machineReset: function () {
+        this._xvpOp(1, 4);
+    },
+
+    // Send a key press. If 'down' is not specified then send a down key
+    // followed by an up key.
+    sendKey: function (keysym, code, down) {
+        if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
+
+        if (down === undefined) {
+            this.sendKey(keysym, code, true);
+            this.sendKey(keysym, code, false);
+            return;
+        }
+
+        var scancode = XtScancode[code];
+
+        if (this._qemuExtKeyEventSupported && scancode) {
+            // 0 is NoSymbol
+            keysym = keysym || 0;
+
+            Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
+
+            RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
+        } else {
+            if (!keysym) {
+                return;
+            }
+            Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
+            RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
+        }
+    },
+
+    focus: function () {
+        this._canvas.focus();
+    },
+
+    blur: function () {
+        this._canvas.blur();
+    },
+
+    clipboardPasteFrom: function (text) {
+        if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
+        RFB.messages.clientCutText(this._sock, text);
+    },
+
+    // ===== PRIVATE METHODS =====
+
+    _connect: function () {
+        Log.Debug(">> RFB.connect");
+
+        Log.Info("connecting to " + this._url);
+
+        try {
+            // WebSocket.onopen transitions to the RFB init states
+            this._sock.open(this._url, ['binary']);
+        } catch (e) {
+            if (e.name === 'SyntaxError') {
+                this._fail("Invalid host or port (" + e + ")");
+            } else {
+                this._fail("Error when opening socket (" + e + ")");
+            }
+        }
+
+        // Make our elements part of the page
+        this._target.appendChild(this._screen);
+
+        // Monitor size changes of the screen
+        // FIXME: Use ResizeObserver, or hidden overflow
+        window.addEventListener('resize', this._eventHandlers.windowResize);
+
+        // Always grab focus on some kind of click event
+        this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
+        this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
+
+        Log.Debug("<< RFB.connect");
+    },
+
+    _disconnect: function () {
+        Log.Debug(">> RFB.disconnect");
+        this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
+        this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
+        window.removeEventListener('resize', this._eventHandlers.windowResize);
+        this._keyboard.ungrab();
+        this._mouse.ungrab();
+        this._sock.close();
+        this._print_stats();
+        try {
+            this._target.removeChild(this._screen);
+        } catch (e) {
+            if (e.name === 'NotFoundError') {
+                // Some cases where the initial connection fails
+                // can disconnect before the _screen is created
+            } else {
+                throw e;
+            }
+        }
+        clearTimeout(this._resizeTimeout);
+        Log.Debug("<< RFB.disconnect");
+    },
+
+    _print_stats: function () {
+        var stats = this._encStats;
+
+        Log.Info("Encoding stats for this connection:");
+        Object.keys(stats).forEach(function (key) {
+            var s = stats[key];
+            if (s[0] + s[1] > 0) {
+                Log.Info("    " + encodingName(key) + ": " + s[0] + " rects");
+            }
+        });
+
+        Log.Info("Encoding stats since page load:");
+        Object.keys(stats).forEach(function (key) {
+            var s = stats[key];
+            Log.Info("    " + encodingName(key) + ": " + s[1] + " rects");
+        });
+    },
+
+    _focusCanvas: function(event) {
+        // Respect earlier handlers' request to not do side-effects
+        if (event.defaultPrevented) {
+            return;
+        }
+
+        if (!this.focusOnClick) {
+            return;
+        }
+
+        this.focus();
+    },
+
+    _windowResize: function (event) {
+        // If the window resized then our screen element might have
+        // as well. Update the viewport dimensions.
+        window.requestAnimationFrame(function () {
+            this._updateClip();
+            this._updateScale();
+        }.bind(this));
+
+        if (this._resizeSession) {
+            // Request changing the resolution of the remote display to
+            // the size of the local browser viewport.
+
+            // In order to not send multiple requests before the browser-resize
+            // is finished we wait 0.5 seconds before sending the request.
+            clearTimeout(this._resizeTimeout);
+            this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
+        }
+    },
+
+    // Update state of clipping in Display object, and make sure the
+    // configured viewport matches the current screen size
+    _updateClip: function () {
+        var cur_clip = this._display.clipViewport;
+        var new_clip = this._clipViewport;
+
+        if (this._scaleViewport) {
+            // Disable viewport clipping if we are scaling
+            new_clip = false;
+        }
+
+        if (cur_clip !== new_clip) {
+            this._display.clipViewport = new_clip;
+        }
+
+        if (new_clip) {
+            // When clipping is enabled, the screen is limited to
+            // the size of the container.
+            let size = this._screenSize();
+            this._display.viewportChangeSize(size.w, size.h);
+            this._fixScrollbars();
+        }
+    },
+
+    _updateScale: function () {
+        if (!this._scaleViewport) {
+            this._display.scale = 1.0;
+        } else {
+            let size = this._screenSize();
+            this._display.autoscale(size.w, size.h);
+        }
+        this._fixScrollbars();
+    },
+
+    // Requests a change of remote desktop size. This message is an extension
+    // and may only be sent if we have received an ExtendedDesktopSize message
+    _requestRemoteResize: function () {
+        clearTimeout(this._resizeTimeout);
+        this._resizeTimeout = null;
+
+        if (!this._resizeSession || this._viewOnly ||
+            !this._supportsSetDesktopSize) {
+            return;
+        }
+
+        let size = this._screenSize();
+        RFB.messages.setDesktopSize(this._sock, size.w, size.h,
+                                    this._screen_id, this._screen_flags);
+
+        Log.Debug('Requested new desktop size: ' +
+                   size.w + 'x' + size.h);
+    },
+
+    // Gets the the size of the available screen
+    _screenSize: function () {
+        return { w: this._screen.offsetWidth,
+                 h: this._screen.offsetHeight };
+    },
+
+    _fixScrollbars: function () {
+        // This is a hack because Chrome screws up the calculation
+        // for when scrollbars are needed. So to fix it we temporarily
+        // toggle them off and on.
+        var orig = this._screen.style.overflow;
+        this._screen.style.overflow = 'hidden';
+        // Force Chrome to recalculate the layout by asking for
+        // an element's dimensions
+        this._screen.getBoundingClientRect();
+        this._screen.style.overflow = orig;
+    },
+
+    /*
+     * Connection states:
+     *   connecting
+     *   connected
+     *   disconnecting
+     *   disconnected - permanent state
+     */
+    _updateConnectionState: function (state) {
+        var oldstate = this._rfb_connection_state;
+
+        if (state === oldstate) {
+            Log.Debug("Already in state '" + state + "', ignoring");
+            return;
+        }
+
+        // The 'disconnected' state is permanent for each RFB object
+        if (oldstate === 'disconnected') {
+            Log.Error("Tried changing state of a disconnected RFB object");
+            return;
+        }
+
+        // Ensure proper transitions before doing anything
+        switch (state) {
+            case 'connected':
+                if (oldstate !== 'connecting') {
+                    Log.Error("Bad transition to connected state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'disconnected':
+                if (oldstate !== 'disconnecting') {
+                    Log.Error("Bad transition to disconnected state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'connecting':
+                if (oldstate !== '') {
+                    Log.Error("Bad transition to connecting state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'disconnecting':
+                if (oldstate !== 'connected' && oldstate !== 'connecting') {
+                    Log.Error("Bad transition to disconnecting state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            default:
+                Log.Error("Unknown connection state: " + state);
+                return;
+        }
+
+        // State change actions
+
+        this._rfb_connection_state = state;
+
+        var smsg = "New state '" + state + "', was '" + oldstate + "'.";
+        Log.Debug(smsg);
+
+        if (this._disconnTimer && state !== 'disconnecting') {
+            Log.Debug("Clearing disconnect timer");
+            clearTimeout(this._disconnTimer);
+            this._disconnTimer = null;
+
+            // make sure we don't get a double event
+            this._sock.off('close');
+        }
+
+        switch (state) {
+            case 'connecting':
+                this._connect();
+                break;
+
+            case 'connected':
+                var event = new CustomEvent("connect", { detail: {} });
+                this.dispatchEvent(event);
+                break;
+
+            case 'disconnecting':
+                this._disconnect();
+
+                this._disconnTimer = setTimeout(function () {
+                    Log.Error("Disconnection timed out.");
+                    this._updateConnectionState('disconnected');
+                }.bind(this), DISCONNECT_TIMEOUT * 1000);
+                break;
+
+            case 'disconnected':
+                event = new CustomEvent(
+                    "disconnect", { detail:
+                                    { clean: this._rfb_clean_disconnect } });
+                this.dispatchEvent(event);
+                break;
+        }
+    },
+
+    /* Print errors and disconnect
+     *
+     * The parameter 'details' is used for information that
+     * should be logged but not sent to the user interface.
+     */
+    _fail: function (details) {
+        switch (this._rfb_connection_state) {
+            case 'disconnecting':
+                Log.Error("Failed when disconnecting: " + details);
+                break;
+            case 'connected':
+                Log.Error("Failed while connected: " + details);
+                break;
+            case 'connecting':
+                Log.Error("Failed when connecting: " + details);
+                break;
+            default:
+                Log.Error("RFB failure: " + details);
+                break;
+        }
+        this._rfb_clean_disconnect = false; //This is sent to the UI
+
+        // Transition to disconnected without waiting for socket to close
+        this._updateConnectionState('disconnecting');
+        this._updateConnectionState('disconnected');
+
+        return false;
+    },
+
+    _setCapability: function (cap, val) {
+        this._capabilities[cap] = val;
+        var event = new CustomEvent("capabilities",
+                                    { detail: { capabilities: this._capabilities } });
+        this.dispatchEvent(event);
+    },
+
+    _handle_message: function () {
+        if (this._sock.rQlen() === 0) {
+            Log.Warn("handle_message called on an empty receive queue");
+            return;
+        }
+
+        switch (this._rfb_connection_state) {
+            case 'disconnected':
+                Log.Error("Got data while disconnected");
+                break;
+            case 'connected':
+                while (true) {
+                    if (this._flushing) {
+                        break;
+                    }
+                    if (!this._normal_msg()) {
+                        break;
+                    }
+                    if (this._sock.rQlen() === 0) {
+                        break;
+                    }
+                }
+                break;
+            default:
+                this._init_msg();
+                break;
+        }
+    },
+
+    _handleKeyEvent: function (keysym, code, down) {
+        this.sendKey(keysym, code, down);
+    },
+
+    _handleMouseButton: function (x, y, down, bmask) {
+        if (down) {
+            this._mouse_buttonMask |= bmask;
+        } else {
+            this._mouse_buttonMask &= ~bmask;
+        }
+
+        if (this.dragViewport) {
+            if (down && !this._viewportDragging) {
+                this._viewportDragging = true;
+                this._viewportDragPos = {'x': x, 'y': y};
+                this._viewportHasMoved = false;
+
+                // Skip sending mouse events
+                return;
+            } else {
+                this._viewportDragging = false;
+
+                // If we actually performed a drag then we are done
+                // here and should not send any mouse events
+                if (this._viewportHasMoved) {
+                    return;
+                }
+
+                // Otherwise we treat this as a mouse click event.
+                // Send the button down event here, as the button up
+                // event is sent at the end of this function.
+                RFB.messages.pointerEvent(this._sock,
+                                          this._display.absX(x),
+                                          this._display.absY(y),
+                                          bmask);
+            }
+        }
+
+        if (this._viewOnly) { return; } // View only, skip mouse events
+
+        if (this._rfb_connection_state !== 'connected') { return; }
+        RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
+    },
+
+    _handleMouseMove: function (x, y) {
+        if (this._viewportDragging) {
+            var deltaX = this._viewportDragPos.x - x;
+            var deltaY = this._viewportDragPos.y - y;
+
+            // The goal is to trigger on a certain physical width, the
+            // devicePixelRatio brings us a bit closer but is not optimal.
+            var dragThreshold = 10 * (window.devicePixelRatio || 1);
+
+            if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
+                                           Math.abs(deltaY) > dragThreshold)) {
+                this._viewportHasMoved = true;
+
+                this._viewportDragPos = {'x': x, 'y': y};
+                this._display.viewportChangePos(deltaX, deltaY);
+            }
+
+            // Skip sending mouse events
+            return;
+        }
+
+        if (this._viewOnly) { return; } // View only, skip mouse events
+
+        if (this._rfb_connection_state !== 'connected') { return; }
+        RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
+    },
+
+    // Message Handlers
+
+    _negotiate_protocol_version: function () {
+        if (this._sock.rQlen() < 12) {
+            return this._fail("Received incomplete protocol version.");
+        }
+
+        var sversion = this._sock.rQshiftStr(12).substr(4, 7);
+        Log.Info("Server ProtocolVersion: " + sversion);
+        var is_repeater = 0;
+        switch (sversion) {
+            case "000.000":  // UltraVNC repeater
+                is_repeater = 1;
+                break;
+            case "003.003":
+            case "003.006":  // UltraVNC
+            case "003.889":  // Apple Remote Desktop
+                this._rfb_version = 3.3;
+                break;
+            case "003.007":
+                this._rfb_version = 3.7;
+                break;
+            case "003.008":
+            case "004.000":  // Intel AMT KVM
+            case "004.001":  // RealVNC 4.6
+            case "005.000":  // RealVNC 5.3
+                this._rfb_version = 3.8;
+                break;
+            default:
+                return this._fail("Invalid server version " + sversion);
+        }
+
+        if (is_repeater) {
+            var repeaterID = "ID:" + this._repeaterID;
+            while (repeaterID.length < 250) {
+                repeaterID += "\0";
+            }
+            this._sock.send_string(repeaterID);
+            return true;
+        }
+
+        if (this._rfb_version > this._rfb_max_version) {
+            this._rfb_version = this._rfb_max_version;
+        }
+
+        var cversion = "00" + parseInt(this._rfb_version, 10) +
+                       ".00" + ((this._rfb_version * 10) % 10);
+        this._sock.send_string("RFB " + cversion + "\n");
+        Log.Debug('Sent ProtocolVersion: ' + cversion);
+
+        this._rfb_init_state = 'Security';
+    },
+
+    _negotiate_security: function () {
+        // Polyfill since IE and PhantomJS doesn't have
+        // TypedArray.includes()
+        function includes(item, array) {
+            for (var i = 0; i < array.length; i++) {
+                if (array[i] === item) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        if (this._rfb_version >= 3.7) {
+            // Server sends supported list, client decides
+            var num_types = this._sock.rQshift8();
+            if (this._sock.rQwait("security type", num_types, 1)) { return false; }
+
+            if (num_types === 0) {
+                return this._handle_security_failure("no security types");
+            }
+
+            var types = this._sock.rQshiftBytes(num_types);
+            Log.Debug("Server security types: " + types);
+
+            // Look for each auth in preferred order
+            this._rfb_auth_scheme = 0;
+            if (includes(1, types)) {
+                this._rfb_auth_scheme = 1; // None
+            } else if (includes(22, types)) {
+                this._rfb_auth_scheme = 22; // XVP
+            } else if (includes(16, types)) {
+                this._rfb_auth_scheme = 16; // Tight
+            } else if (includes(2, types)) {
+                this._rfb_auth_scheme = 2; // VNC Auth
+            } else {
+                return this._fail("Unsupported security types (types: " + types + ")");
+            }
+
+            this._sock.send([this._rfb_auth_scheme]);
+        } else {
+            // Server decides
+            if (this._sock.rQwait("security scheme", 4)) { return false; }
+            this._rfb_auth_scheme = this._sock.rQshift32();
+        }
+
+        this._rfb_init_state = 'Authentication';
+        Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme);
+
+        return this._init_msg(); // jump to authentication
+    },
+
+    /*
+     * Get the security failure reason if sent from the server and
+     * send the 'securityfailure' event.
+     *
+     * - The optional parameter context can be used to add some extra
+     *   context to the log output.
+     *
+     * - The optional parameter security_result_status can be used to
+     *   add a custom status code to the event.
+     */
+    _handle_security_failure: function (context, security_result_status) {
+
+        if (typeof context === 'undefined') {
+            context = "";
+        } else {
+            context = " on " + context;
+        }
+
+        if (typeof security_result_status === 'undefined') {
+            security_result_status = 1; // fail
+        }
+
+        if (this._sock.rQwait("reason length", 4)) {
+            return false;
+        }
+        let strlen = this._sock.rQshift32();
+        let reason = "";
+
+        if (strlen > 0) {
+            if (this._sock.rQwait("reason", strlen, 8)) { return false; }
+            reason = this._sock.rQshiftStr(strlen);
+        }
+
+        if (reason !== "") {
+
+            let event = new CustomEvent(
+                "securityfailure",
+                { detail: { status: security_result_status, reason: reason } });
+            this.dispatchEvent(event);
+
+            return this._fail("Security negotiation failed" + context +
+                              " (reason: " + reason + ")");
+        } else {
+
+            let event = new CustomEvent(
+                "securityfailure",
+                { detail: { status: security_result_status } });
+            this.dispatchEvent(event);
+
+            return this._fail("Security negotiation failed" + context);
+        }
+    },
+
+    // authentication
+    _negotiate_xvp_auth: function () {
+        if (!this._rfb_credentials.username ||
+            !this._rfb_credentials.password ||
+            !this._rfb_credentials.target) {
+            var event = new CustomEvent("credentialsrequired",
+                                        { detail: { types: ["username", "password", "target"] } });
+            this.dispatchEvent(event);
+            return false;
+        }
+
+        var xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
+                           String.fromCharCode(this._rfb_credentials.target.length) +
+                           this._rfb_credentials.username +
+                           this._rfb_credentials.target;
+        this._sock.send_string(xvp_auth_str);
+        this._rfb_auth_scheme = 2;
+        return this._negotiate_authentication();
+    },
+
+    _negotiate_std_vnc_auth: function () {
+        if (this._sock.rQwait("auth challenge", 16)) { return false; }
+
+        if (!this._rfb_credentials.password) {
+            var event = new CustomEvent("credentialsrequired",
+                                        { detail: { types: ["password"] } });
+            this.dispatchEvent(event);
+            return false;
+        }
+
+        // TODO(directxman12): make genDES not require an Array
+        var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
+        var response = RFB.genDES(this._rfb_credentials.password, challenge);
+        this._sock.send(response);
+        this._rfb_init_state = "SecurityResult";
+        return true;
+    },
+
+    _negotiate_tight_tunnels: function (numTunnels) {
+        var clientSupportedTunnelTypes = {
+            0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
+        };
+        var serverSupportedTunnelTypes = {};
+        // receive tunnel capabilities
+        for (var i = 0; i < numTunnels; i++) {
+            var cap_code = this._sock.rQshift32();
+            var cap_vendor = this._sock.rQshiftStr(4);
+            var cap_signature = this._sock.rQshiftStr(8);
+            serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
+        }
+
+        // choose the notunnel type
+        if (serverSupportedTunnelTypes[0]) {
+            if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
+                serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
+                return this._fail("Client's tunnel type had the incorrect " +
+                                  "vendor or signature");
+            }
+            this._sock.send([0, 0, 0, 0]);  // use NOTUNNEL
+            return false; // wait until we receive the sub auth count to continue
+        } else {
+            return this._fail("Server wanted tunnels, but doesn't support " +
+                              "the notunnel type");
+        }
+    },
+
+    _negotiate_tight_auth: function () {
+        if (!this._rfb_tightvnc) {  // first pass, do the tunnel negotiation
+            if (this._sock.rQwait("num tunnels", 4)) { return false; }
+            var numTunnels = this._sock.rQshift32();
+            if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
+
+            this._rfb_tightvnc = true;
+
+            if (numTunnels > 0) {
+                this._negotiate_tight_tunnels(numTunnels);
+                return false;  // wait until we receive the sub auth to continue
+            }
+        }
+
+        // second pass, do the sub-auth negotiation
+        if (this._sock.rQwait("sub auth count", 4)) { return false; }
+        var subAuthCount = this._sock.rQshift32();
+        if (subAuthCount === 0) {  // empty sub-auth list received means 'no auth' subtype selected
+            this._rfb_init_state = 'SecurityResult';
+            return true;
+        }
+
+        if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
+
+        var clientSupportedTypes = {
+            'STDVNOAUTH__': 1,
+            'STDVVNCAUTH_': 2
+        };
+
+        var serverSupportedTypes = [];
+
+        for (var i = 0; i < subAuthCount; i++) {
+            var capNum = this._sock.rQshift32();
+            var capabilities = this._sock.rQshiftStr(12);
+            serverSupportedTypes.push(capabilities);
+        }
+
+        for (var authType in clientSupportedTypes) {
+            if (serverSupportedTypes.indexOf(authType) != -1) {
+                this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
+
+                switch (authType) {
+                    case 'STDVNOAUTH__':  // no auth
+                        this._rfb_init_state = 'SecurityResult';
+                        return true;
+                    case 'STDVVNCAUTH_': // VNC auth
+                        this._rfb_auth_scheme = 2;
+                        return this._init_msg();
+                    default:
+                        return this._fail("Unsupported tiny auth scheme " +
+                                          "(scheme: " + authType + ")");
+                }
+            }
+        }
+
+        return this._fail("No supported sub-auth types!");
+    },
+
+    _negotiate_authentication: function () {
+        switch (this._rfb_auth_scheme) {
+            case 0:  // connection failed
+                return this._handle_security_failure("authentication scheme");
+
+            case 1:  // no auth
+                if (this._rfb_version >= 3.8) {
+                    this._rfb_init_state = 'SecurityResult';
+                    return true;
+                }
+                this._rfb_init_state = 'ClientInitialisation';
+                return this._init_msg();
+
+            case 22:  // XVP auth
+                return this._negotiate_xvp_auth();
+
+            case 2:  // VNC authentication
+                return this._negotiate_std_vnc_auth();
+
+            case 16:  // TightVNC Security Type
+                return this._negotiate_tight_auth();
+
+            default:
+                return this._fail("Unsupported auth scheme (scheme: " +
+                                  this._rfb_auth_scheme + ")");
+        }
+    },
+
+    _handle_security_result: function () {
+        if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
+
+        let status = this._sock.rQshift32();
+
+        if (status === 0) { // OK
+            this._rfb_init_state = 'ClientInitialisation';
+            Log.Debug('Authentication OK');
+            return this._init_msg();
+        } else {
+            if (this._rfb_version >= 3.8) {
+                return this._handle_security_failure("security result", status);
+            } else {
+                let event = new CustomEvent("securityfailure",
+                                            { detail: { status: status } });
+                this.dispatchEvent(event);
+
+                return this._fail("Security handshake failed");
+            }
+        }
+    },
+
+    _negotiate_server_init: function () {
+        if (this._sock.rQwait("server initialization", 24)) { return false; }
+
+        /* Screen size */
+        var width = this._sock.rQshift16();
+        var height = this._sock.rQshift16();
+
+        /* PIXEL_FORMAT */
+        var bpp         = this._sock.rQshift8();
+        var depth       = this._sock.rQshift8();
+        var big_endian  = this._sock.rQshift8();
+        var true_color  = this._sock.rQshift8();
+
+        var red_max     = this._sock.rQshift16();
+        var green_max   = this._sock.rQshift16();
+        var blue_max    = this._sock.rQshift16();
+        var red_shift   = this._sock.rQshift8();
+        var green_shift = this._sock.rQshift8();
+        var blue_shift  = this._sock.rQshift8();
+        this._sock.rQskipBytes(3);  // padding
+
+        // NB(directxman12): we don't want to call any callbacks or print messages until
+        //                   *after* we're past the point where we could backtrack
+
+        /* Connection name/title */
+        var name_length = this._sock.rQshift32();
+        if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
+        this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length));
+
+        if (this._rfb_tightvnc) {
+            if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
+            // In TightVNC mode, ServerInit message is extended
+            var numServerMessages = this._sock.rQshift16();
+            var numClientMessages = this._sock.rQshift16();
+            var numEncodings = this._sock.rQshift16();
+            this._sock.rQskipBytes(2);  // padding
+
+            var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
+            if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
+
+            // we don't actually do anything with the capability information that TIGHT sends,
+            // so we just skip the all of this.
+
+            // TIGHT server message capabilities
+            this._sock.rQskipBytes(16 * numServerMessages);
+
+            // TIGHT client message capabilities
+            this._sock.rQskipBytes(16 * numClientMessages);
+
+            // TIGHT encoding capabilities
+            this._sock.rQskipBytes(16 * numEncodings);
+        }
+
+        // NB(directxman12): these are down here so that we don't run them multiple times
+        //                   if we backtrack
+        Log.Info("Screen: " + width + "x" + height +
+                  ", bpp: " + bpp + ", depth: " + depth +
+                  ", big_endian: " + big_endian +
+                  ", true_color: " + true_color +
+                  ", red_max: " + red_max +
+                  ", green_max: " + green_max +
+                  ", blue_max: " + blue_max +
+                  ", red_shift: " + red_shift +
+                  ", green_shift: " + green_shift +
+                  ", blue_shift: " + blue_shift);
+
+        if (big_endian !== 0) {
+            Log.Warn("Server native endian is not little endian");
+        }
+
+        if (red_shift !== 16) {
+            Log.Warn("Server native red-shift is not 16");
+        }
+
+        if (blue_shift !== 0) {
+            Log.Warn("Server native blue-shift is not 0");
+        }
+
+        // we're past the point where we could backtrack, so it's safe to call this
+        var event = new CustomEvent("desktopname",
+                                    { detail: { name: this._fb_name } });
+        this.dispatchEvent(event);
+
+        this._resize(width, height);
+
+        if (!this._viewOnly) { this._keyboard.grab(); }
+        if (!this._viewOnly) { this._mouse.grab(); }
+
+        this._fb_depth = 24;
+
+        if (this._fb_name === "Intel(r) AMT KVM") {
+            Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
+            this._fb_depth = 8;
+        } else if (this._fb_name === "MBSE BrewBoard") {
+	    Log.Warn("MBSE BrewBoard only supports 8 bit depths. Using low color mode.");
+	    this._fb_depth = 8;
+	}
+
+        RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
+        this._sendEncodings();
+        RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
+
+        this._timing.fbu_rt_start = (new Date()).getTime();
+        this._timing.pixels = 0;
+
+        // Cursor will be server side until the server decides to honor
+        // our request and send over the cursor image
+        this._display.disableLocalCursor();
+
+        this._updateConnectionState('connected');
+        return true;
+    },
+
+    _sendEncodings: function () {
+        var encs = [];
+
+        // In preference order
+        encs.push(encodings.encodingCopyRect);
+        // Only supported with full depth support
+        if (this._fb_depth == 24) {
+            encs.push(encodings.encodingTight);
+            encs.push(encodings.encodingHextile);
+            encs.push(encodings.encodingRRE);
+        }
+        encs.push(encodings.encodingRaw);
+
+        // Psuedo-encoding settings
+        encs.push(encodings.pseudoEncodingTightPNG);
+        encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
+        encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
+
+        encs.push(encodings.pseudoEncodingDesktopSize);
+        encs.push(encodings.pseudoEncodingLastRect);
+        encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
+        encs.push(encodings.pseudoEncodingExtendedDesktopSize);
+        encs.push(encodings.pseudoEncodingXvp);
+        encs.push(encodings.pseudoEncodingFence);
+        encs.push(encodings.pseudoEncodingContinuousUpdates);
+
+        if (supportsCursorURIs() &&
+            !isTouchDevice && this._fb_depth == 24) {
+            encs.push(encodings.pseudoEncodingCursor);
+        }
+
+        RFB.messages.clientEncodings(this._sock, encs);
+    },
+
+    /* RFB protocol initialization states:
+     *   ProtocolVersion
+     *   Security
+     *   Authentication
+     *   SecurityResult
+     *   ClientInitialization - not triggered by server message
+     *   ServerInitialization
+     */
+    _init_msg: function () {
+        switch (this._rfb_init_state) {
+            case 'ProtocolVersion':
+                return this._negotiate_protocol_version();
+
+            case 'Security':
+                return this._negotiate_security();
+
+            case 'Authentication':
+                return this._negotiate_authentication();
+
+            case 'SecurityResult':
+                return this._handle_security_result();
+
+            case 'ClientInitialisation':
+                this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
+                this._rfb_init_state = 'ServerInitialisation';
+                return true;
+
+            case 'ServerInitialisation':
+                return this._negotiate_server_init();
+
+            default:
+                return this._fail("Unknown init state (state: " +
+                                  this._rfb_init_state + ")");
+        }
+    },
+
+    _handle_set_colour_map_msg: function () {
+        Log.Debug("SetColorMapEntries");
+
+        return this._fail("Unexpected SetColorMapEntries message");
+    },
+
+    _handle_server_cut_text: function () {
+        Log.Debug("ServerCutText");
+
+        if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
+        this._sock.rQskipBytes(3);  // Padding
+        var length = this._sock.rQshift32();
+        if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
+
+        var text = this._sock.rQshiftStr(length);
+
+        if (this._viewOnly) { return true; }
+
+        var event = new CustomEvent("clipboard",
+                                    { detail: { text: text } });
+        this.dispatchEvent(event);
+
+        return true;
+    },
+
+    _handle_server_fence_msg: function() {
+        if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
+        this._sock.rQskipBytes(3); // Padding
+        var flags = this._sock.rQshift32();
+        var length = this._sock.rQshift8();
+
+        if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
+
+        if (length > 64) {
+            Log.Warn("Bad payload length (" + length + ") in fence response");
+            length = 64;
+        }
+
+        var payload = this._sock.rQshiftStr(length);
+
+        this._supportsFence = true;
+
+        /*
+         * Fence flags
+         *
+         *  (1<<0)  - BlockBefore
+         *  (1<<1)  - BlockAfter
+         *  (1<<2)  - SyncNext
+         *  (1<<31) - Request
+         */
+
+        if (!(flags & (1<<31))) {
+            return this._fail("Unexpected fence response");
+        }
+
+        // Filter out unsupported flags
+        // FIXME: support syncNext
+        flags &= (1<<0) | (1<<1);
+
+        // BlockBefore and BlockAfter are automatically handled by
+        // the fact that we process each incoming message
+        // synchronuosly.
+        RFB.messages.clientFence(this._sock, flags, payload);
+
+        return true;
+    },
+
+    _handle_xvp_msg: function () {
+        if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
+        this._sock.rQskip8();  // Padding
+        var xvp_ver = this._sock.rQshift8();
+        var xvp_msg = this._sock.rQshift8();
+
+        switch (xvp_msg) {
+            case 0:  // XVP_FAIL
+                Log.Error("XVP Operation Failed");
+                break;
+            case 1:  // XVP_INIT
+                this._rfb_xvp_ver = xvp_ver;
+                Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
+                this._setCapability("power", true);
+                break;
+            default:
+                this._fail("Illegal server XVP message (msg: " + xvp_msg + ")");
+                break;
+        }
+
+        return true;
+    },
+
+    _normal_msg: function () {
+        var msg_type;
+
+        if (this._FBU.rects > 0) {
+            msg_type = 0;
+        } else {
+            msg_type = this._sock.rQshift8();
+        }
+
+        switch (msg_type) {
+            case 0:  // FramebufferUpdate
+                var ret = this._framebufferUpdate();
+                if (ret && !this._enabledContinuousUpdates) {
+                    RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
+                                                 this._fb_width, this._fb_height);
+                }
+                return ret;
+
+            case 1:  // SetColorMapEntries
+                return this._handle_set_colour_map_msg();
+
+            case 2:  // Bell
+                Log.Debug("Bell");
+                var event = new CustomEvent("bell", { detail: {} });
+                this.dispatchEvent(event);
+                return true;
+
+            case 3:  // ServerCutText
+                return this._handle_server_cut_text();
+
+            case 150: // EndOfContinuousUpdates
+                var first = !(this._supportsContinuousUpdates);
+                this._supportsContinuousUpdates = true;
+                this._enabledContinuousUpdates = false;
+                if (first) {
+                    this._enabledContinuousUpdates = true;
+                    this._updateContinuousUpdates();
+                    Log.Info("Enabling continuous updates.");
+                } else {
+                    // FIXME: We need to send a framebufferupdaterequest here
+                    // if we add support for turning off continuous updates
+                }
+                return true;
+
+            case 248: // ServerFence
+                return this._handle_server_fence_msg();
+
+            case 250:  // XVP
+                return this._handle_xvp_msg();
+
+            default:
+                this._fail("Unexpected server message (type " + msg_type + ")");
+                Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
+                return true;
+        }
+    },
+
+    _onFlush: function() {
+        this._flushing = false;
+        // Resume processing
+        if (this._sock.rQlen() > 0) {
+            this._handle_message();
+        }
+    },
+
+    _framebufferUpdate: function () {
+        var ret = true;
+        var now;
+
+        if (this._FBU.rects === 0) {
+            if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
+            this._sock.rQskip8();  // Padding
+            this._FBU.rects = this._sock.rQshift16();
+            this._FBU.bytes = 0;
+            this._timing.cur_fbu = 0;
+            if (this._timing.fbu_rt_start > 0) {
+                now = (new Date()).getTime();
+                Log.Info("First FBU latency: " + (now - this._timing.fbu_rt_start));
+            }
+
+            // Make sure the previous frame is fully rendered first
+            // to avoid building up an excessive queue
+            if (this._display.pending()) {
+                this._flushing = true;
+                this._display.flush();
+                return false;
+            }
+        }
+
+        while (this._FBU.rects > 0) {
+            if (this._rfb_connection_state !== 'connected') { return false; }
+
+            if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; }
+            if (this._FBU.bytes === 0) {
+                if (this._sock.rQwait("rect header", 12)) { return false; }
+                /* New FramebufferUpdate */
+
+                var hdr = this._sock.rQshiftBytes(12);
+                this._FBU.x        = (hdr[0] << 8) + hdr[1];
+                this._FBU.y        = (hdr[2] << 8) + hdr[3];
+                this._FBU.width    = (hdr[4] << 8) + hdr[5];
+                this._FBU.height   = (hdr[6] << 8) + hdr[7];
+                this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
+                                              (hdr[10] << 8) + hdr[11], 10);
+
+                if (!this._encHandlers[this._FBU.encoding]) {
+                    this._fail("Unsupported encoding (encoding: " +
+                               this._FBU.encoding + ")");
+                    return false;
+                }
+            }
+
+            this._timing.last_fbu = (new Date()).getTime();
+
+            ret = this._encHandlers[this._FBU.encoding]();
+
+            now = (new Date()).getTime();
+            this._timing.cur_fbu += (now - this._timing.last_fbu);
+
+            if (ret) {
+                if (!(this._FBU.encoding in this._encStats)) {
+                    this._encStats[this._FBU.encoding] = [0, 0];
+                }
+                this._encStats[this._FBU.encoding][0]++;
+                this._encStats[this._FBU.encoding][1]++;
+                this._timing.pixels += this._FBU.width * this._FBU.height;
+            }
+
+            if (this._timing.pixels >= (this._fb_width * this._fb_height)) {
+                if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) ||
+                    this._timing.fbu_rt_start > 0) {
+                    this._timing.full_fbu_total += this._timing.cur_fbu;
+                    this._timing.full_fbu_cnt++;
+                    Log.Info("Timing of full FBU, curr: " +
+                              this._timing.cur_fbu + ", total: " +
+                              this._timing.full_fbu_total + ", cnt: " +
+                              this._timing.full_fbu_cnt + ", avg: " +
+                              (this._timing.full_fbu_total / this._timing.full_fbu_cnt));
+                }
+
+                if (this._timing.fbu_rt_start > 0) {
+                    var fbu_rt_diff = now - this._timing.fbu_rt_start;
+                    this._timing.fbu_rt_total += fbu_rt_diff;
+                    this._timing.fbu_rt_cnt++;
+                    Log.Info("full FBU round-trip, cur: " +
+                              fbu_rt_diff + ", total: " +
+                              this._timing.fbu_rt_total + ", cnt: " +
+                              this._timing.fbu_rt_cnt + ", avg: " +
+                              (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt));
+                    this._timing.fbu_rt_start = 0;
+                }
+            }
+
+            if (!ret) { return ret; }  // need more data
+        }
+
+        this._display.flip();
+
+        return true;  // We finished this FBU
+    },
+
+    _updateContinuousUpdates: function() {
+        if (!this._enabledContinuousUpdates) { return; }
+
+        RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
+                                             this._fb_width, this._fb_height);
+    },
+
+    _resize: function(width, height) {
+        this._fb_width = width;
+        this._fb_height = height;
+
+        this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4);
+
+        this._display.resize(this._fb_width, this._fb_height);
+
+        // Adjust the visible viewport based on the new dimensions
+        this._updateClip();
+        this._updateScale();
+
+        this._timing.fbu_rt_start = (new Date()).getTime();
+        this._updateContinuousUpdates();
+    },
+
+    _xvpOp: function (ver, op) {
+        if (this._rfb_xvp_ver < ver) { return; }
+        Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
+        RFB.messages.xvpOp(this._sock, ver, op);
+    },
+};
+
+Object.assign(RFB.prototype, EventTargetMixin);
+
+// Class Methods
+RFB.messages = {
+    keyEvent: function (sock, keysym, down) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 4;  // msg-type
+        buff[offset + 1] = down;
+
+        buff[offset + 2] = 0;
+        buff[offset + 3] = 0;
+
+        buff[offset + 4] = (keysym >> 24);
+        buff[offset + 5] = (keysym >> 16);
+        buff[offset + 6] = (keysym >> 8);
+        buff[offset + 7] = keysym;
+
+        sock._sQlen += 8;
+        sock.flush();
+    },
+
+    QEMUExtendedKeyEvent: function (sock, keysym, down, keycode) {
+        function getRFBkeycode(xt_scancode) {
+            var upperByte = (keycode >> 8);
+            var lowerByte = (keycode & 0x00ff);
+            if (upperByte === 0xe0 && lowerByte < 0x7f) {
+                lowerByte = lowerByte | 0x80;
+                return lowerByte;
+            }
+            return xt_scancode;
+        }
+
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 255; // msg-type
+        buff[offset + 1] = 0; // sub msg-type
+
+        buff[offset + 2] = (down >> 8);
+        buff[offset + 3] = down;
+
+        buff[offset + 4] = (keysym >> 24);
+        buff[offset + 5] = (keysym >> 16);
+        buff[offset + 6] = (keysym >> 8);
+        buff[offset + 7] = keysym;
+
+        var RFBkeycode = getRFBkeycode(keycode);
+
+        buff[offset + 8] = (RFBkeycode >> 24);
+        buff[offset + 9] = (RFBkeycode >> 16);
+        buff[offset + 10] = (RFBkeycode >> 8);
+        buff[offset + 11] = RFBkeycode;
+
+        sock._sQlen += 12;
+        sock.flush();
+    },
+
+    pointerEvent: function (sock, x, y, mask) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 5; // msg-type
+
+        buff[offset + 1] = mask;
+
+        buff[offset + 2] = x >> 8;
+        buff[offset + 3] = x;
+
+        buff[offset + 4] = y >> 8;
+        buff[offset + 5] = y;
+
+        sock._sQlen += 6;
+        sock.flush();
+    },
+
+    // TODO(directxman12): make this unicode compatible?
+    clientCutText: function (sock, text) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 6; // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        var n = text.length;
+
+        buff[offset + 4] = n >> 24;
+        buff[offset + 5] = n >> 16;
+        buff[offset + 6] = n >> 8;
+        buff[offset + 7] = n;
+
+        for (var i = 0; i < n; i++) {
+            buff[offset + 8 + i] =  text.charCodeAt(i);
+        }
+
+        sock._sQlen += 8 + n;
+        sock.flush();
+    },
+
+    setDesktopSize: function (sock, width, height, id, flags) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 251;              // msg-type
+        buff[offset + 1] = 0;            // padding
+        buff[offset + 2] = width >> 8;   // width
+        buff[offset + 3] = width;
+        buff[offset + 4] = height >> 8;  // height
+        buff[offset + 5] = height;
+
+        buff[offset + 6] = 1;            // number-of-screens
+        buff[offset + 7] = 0;            // padding
+
+        // screen array
+        buff[offset + 8] = id >> 24;     // id
+        buff[offset + 9] = id >> 16;
+        buff[offset + 10] = id >> 8;
+        buff[offset + 11] = id;
+        buff[offset + 12] = 0;           // x-position
+        buff[offset + 13] = 0;
+        buff[offset + 14] = 0;           // y-position
+        buff[offset + 15] = 0;
+        buff[offset + 16] = width >> 8;  // width
+        buff[offset + 17] = width;
+        buff[offset + 18] = height >> 8; // height
+        buff[offset + 19] = height;
+        buff[offset + 20] = flags >> 24; // flags
+        buff[offset + 21] = flags >> 16;
+        buff[offset + 22] = flags >> 8;
+        buff[offset + 23] = flags;
+
+        sock._sQlen += 24;
+        sock.flush();
+    },
+
+    clientFence: function (sock, flags, payload) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 248; // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        buff[offset + 4] = flags >> 24; // flags
+        buff[offset + 5] = flags >> 16;
+        buff[offset + 6] = flags >> 8;
+        buff[offset + 7] = flags;
+
+        var n = payload.length;
+
+        buff[offset + 8] = n; // length
+
+        for (var i = 0; i < n; i++) {
+            buff[offset + 9 + i] = payload.charCodeAt(i);
+        }
+
+        sock._sQlen += 9 + n;
+        sock.flush();
+    },
+
+    enableContinuousUpdates: function (sock, enable, x, y, width, height) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 150;             // msg-type
+        buff[offset + 1] = enable;      // enable-flag
+
+        buff[offset + 2] = x >> 8;      // x
+        buff[offset + 3] = x;
+        buff[offset + 4] = y >> 8;      // y
+        buff[offset + 5] = y;
+        buff[offset + 6] = width >> 8;  // width
+        buff[offset + 7] = width;
+        buff[offset + 8] = height >> 8; // height
+        buff[offset + 9] = height;
+
+        sock._sQlen += 10;
+        sock.flush();
+    },
+
+    pixelFormat: function (sock, depth, true_color) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        var bpp, bits;
+
+        if (depth > 16) {
+            bpp = 32;
+        } else if (depth > 8) {
+            bpp = 16;
+        } else {
+            bpp = 8;
+        }
+
+        bits = Math.floor(depth/3);
+
+        buff[offset] = 0;  // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        buff[offset + 4] = bpp;                 // bits-per-pixel
+        buff[offset + 5] = depth;               // depth
+        buff[offset + 6] = 0;                   // little-endian
+        buff[offset + 7] = true_color ? 1 : 0;  // true-color
+
+        buff[offset + 8] = 0;    // red-max
+        buff[offset + 10] = 0;   // green-max
+        buff[offset + 12] = 0;   // blue-max
+
+	if (depth == 8) {
+	    buff[offset + 9] = 7;	// red-max
+	    buff[offset + 11] = 7;	// green-max
+	    buff[offset + 13] = 3;	// blue-max
+	    buff[offset + 14] = 5;	// red-shift
+	    buff[offset + 15] = 2;	// green-shift
+	    buff[offset + 16] = 0;	// blue-shift
+	} else {
+	    buff[offset + 9] = (1 << bits) - 1;  // red-max
+	    buff[offset + 11] = (1 << bits) - 1; // green-max
+	    buff[offset + 13] = (1 << bits) - 1; // blue-max
+            buff[offset + 14] = bits * 2; // red-shift
+            buff[offset + 15] = bits * 1; // green-shift
+            buff[offset + 16] = bits * 0; // blue-shift
+	}
+
+        buff[offset + 17] = 0;   // padding
+        buff[offset + 18] = 0;   // padding
+        buff[offset + 19] = 0;   // padding
+
+        sock._sQlen += 20;
+        sock.flush();
+    },
+
+    clientEncodings: function (sock, encodings) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 2; // msg-type
+        buff[offset + 1] = 0; // padding
+
+        buff[offset + 2] = encodings.length >> 8;
+        buff[offset + 3] = encodings.length;
+
+        var i, j = offset + 4;
+        for (i = 0; i < encodings.length; i++) {
+            var enc = encodings[i];
+            buff[j] = enc >> 24;
+            buff[j + 1] = enc >> 16;
+            buff[j + 2] = enc >> 8;
+            buff[j + 3] = enc;
+
+            j += 4;
+        }
+
+        sock._sQlen += j - offset;
+        sock.flush();
+    },
+
+    fbUpdateRequest: function (sock, incremental, x, y, w, h) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        if (typeof(x) === "undefined") { x = 0; }
+        if (typeof(y) === "undefined") { y = 0; }
+
+        buff[offset] = 3;  // msg-type
+        buff[offset + 1] = incremental ? 1 : 0;
+
+        buff[offset + 2] = (x >> 8) & 0xFF;
+        buff[offset + 3] = x & 0xFF;
+
+        buff[offset + 4] = (y >> 8) & 0xFF;
+        buff[offset + 5] = y & 0xFF;
+
+        buff[offset + 6] = (w >> 8) & 0xFF;
+        buff[offset + 7] = w & 0xFF;
+
+        buff[offset + 8] = (h >> 8) & 0xFF;
+        buff[offset + 9] = h & 0xFF;
+
+        sock._sQlen += 10;
+        sock.flush();
+    },
+
+    xvpOp: function (sock, ver, op) {
+        var buff = sock._sQ;
+        var offset = sock._sQlen;
+
+        buff[offset] = 250; // msg-type
+        buff[offset + 1] = 0; // padding
+
+        buff[offset + 2] = ver;
+        buff[offset + 3] = op;
+
+        sock._sQlen += 4;
+        sock.flush();
+    },
+};
+
+RFB.genDES = function (password, challenge) {
+    var passwd = [];
+    for (var i = 0; i < password.length; i++) {
+        passwd.push(password.charCodeAt(i));
+    }
+    return (new DES(passwd)).encrypt(challenge);
+};
+
+RFB.encodingHandlers = {
+    RAW: function () {
+        if (this._FBU.lines === 0) {
+            this._FBU.lines = this._FBU.height;
+        }
+
+        var pixelSize = this._fb_depth == 8 ? 1 : 4;
+        this._FBU.bytes = this._FBU.width * pixelSize;  // at least a line
+        if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; }
+        var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines);
+        var curr_height = Math.min(this._FBU.lines,
+                                   Math.floor(this._sock.rQlen() / (this._FBU.width * pixelSize)));
+        var data = this._sock.get_rQ();
+        var index = this._sock.get_rQi();
+        if (this._fb_depth == 8) {
+            var pixels = this._FBU.width * curr_height
+            var newdata = new Uint8Array(pixels * 4);
+            var i;
+            for (i = 0;i < pixels;i++) {
+                //newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
+                //newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
+                //newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
+		// Convert 8 bit RGB332 color to internal RGB888.
+		newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;	// blue
+		newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x7) * 255 / 7;	// green
+		newdata[i * 4 + 2] = ((data[index + i] >> 5) & 0x7) * 255 / 7;	// red
+                newdata[i * 4 + 4] = 0;
+            }
+            data = newdata;
+            index = 0;
+        }
+        this._display.blitImage(this._FBU.x, cur_y, this._FBU.width,
+                                curr_height, data, index);
+        this._sock.rQskipBytes(this._FBU.width * curr_height * pixelSize);
+        this._FBU.lines -= curr_height;
+
+        if (this._FBU.lines > 0) {
+            this._FBU.bytes = this._FBU.width * pixelSize;  // At least another line
+        } else {
+            this._FBU.rects--;
+            this._FBU.bytes = 0;
+        }
+
+        return true;
+    },
+
+    COPYRECT: function () {
+        this._FBU.bytes = 4;
+        if (this._sock.rQwait("COPYRECT", 4)) { return false; }
+        this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(),
+                                this._FBU.x, this._FBU.y, this._FBU.width,
+                                this._FBU.height);
+
+        this._FBU.rects--;
+        this._FBU.bytes = 0;
+        return true;
+    },
+
+    RRE: function () {
+        var color;
+        if (this._FBU.subrects === 0) {
+            this._FBU.bytes = 4 + 4;
+            if (this._sock.rQwait("RRE", 4 + 4)) { return false; }
+            this._FBU.subrects = this._sock.rQshift32();
+            color = this._sock.rQshiftBytes(4);  // Background
+            this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color);
+        }
+
+        while (this._FBU.subrects > 0 && this._sock.rQlen() >= (4 + 8)) {
+            color = this._sock.rQshiftBytes(4);
+            var x = this._sock.rQshift16();
+            var y = this._sock.rQshift16();
+            var width = this._sock.rQshift16();
+            var height = this._sock.rQshift16();
+            this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color);
+            this._FBU.subrects--;
+        }
+
+        if (this._FBU.subrects > 0) {
+            var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects);
+            this._FBU.bytes = (4 + 8) * chunk;
+        } else {
+            this._FBU.rects--;
+            this._FBU.bytes = 0;
+        }
+
+        return true;
+    },
+
+    HEXTILE: function () {
+        var rQ = this._sock.get_rQ();
+        var rQi = this._sock.get_rQi();
+
+        if (this._FBU.tiles === 0) {
+            this._FBU.tiles_x = Math.ceil(this._FBU.width / 16);
+            this._FBU.tiles_y = Math.ceil(this._FBU.height / 16);
+            this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y;
+            this._FBU.tiles = this._FBU.total_tiles;
+        }
+
+        while (this._FBU.tiles > 0) {
+            this._FBU.bytes = 1;
+            if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
+            var subencoding = rQ[rQi];  // Peek
+            if (subencoding > 30) {  // Raw
+                this._fail("Illegal hextile subencoding (subencoding: " +
+                           subencoding + ")");
+                return false;
+            }
+
+            var subrects = 0;
+            var curr_tile = this._FBU.total_tiles - this._FBU.tiles;
+            var tile_x = curr_tile % this._FBU.tiles_x;
+            var tile_y = Math.floor(curr_tile / this._FBU.tiles_x);
+            var x = this._FBU.x + tile_x * 16;
+            var y = this._FBU.y + tile_y * 16;
+            var w = Math.min(16, (this._FBU.x + this._FBU.width) - x);
+            var h = Math.min(16, (this._FBU.y + this._FBU.height) - y);
+
+            // Figure out how much we are expecting
+            if (subencoding & 0x01) {  // Raw
+                this._FBU.bytes += w * h * 4;
+            } else {
+                if (subencoding & 0x02) {  // Background
+                    this._FBU.bytes += 4;
+                }
+                if (subencoding & 0x04) {  // Foreground
+                    this._FBU.bytes += 4;
+                }
+                if (subencoding & 0x08) {  // AnySubrects
+                    this._FBU.bytes++;  // Since we aren't shifting it off
+                    if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; }
+                    subrects = rQ[rQi + this._FBU.bytes - 1];  // Peek
+                    if (subencoding & 0x10) {  // SubrectsColoured
+                        this._FBU.bytes += subrects * (4 + 2);
+                    } else {
+                        this._FBU.bytes += subrects * 2;
+                    }
+                }
+            }
+
+            if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; }
+
+            // We know the encoding and have a whole tile
+            this._FBU.subencoding = rQ[rQi];
+            rQi++;
+            if (this._FBU.subencoding === 0) {
+                if (this._FBU.lastsubencoding & 0x01) {
+                    // Weird: ignore blanks are RAW
+                    Log.Debug("     Ignoring blank after RAW");
+                } else {
+                    this._display.fillRect(x, y, w, h, this._FBU.background);
+                }
+            } else if (this._FBU.subencoding & 0x01) {  // Raw
+                this._display.blitImage(x, y, w, h, rQ, rQi);
+                rQi += this._FBU.bytes - 1;
+            } else {
+                if (this._FBU.subencoding & 0x02) {  // Background
+                    this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
+                }
+                if (this._FBU.subencoding & 0x04) {  // Foreground
+                    this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
+                }
+
+                this._display.startTile(x, y, w, h, this._FBU.background);
+                if (this._FBU.subencoding & 0x08) {  // AnySubrects
+                    subrects = rQ[rQi];
+                    rQi++;
+
+                    for (var s = 0; s < subrects; s++) {
+                        var color;
+                        if (this._FBU.subencoding & 0x10) {  // SubrectsColoured
+                            color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                            rQi += 4;
+                        } else {
+                            color = this._FBU.foreground;
+                        }
+                        var xy = rQ[rQi];
+                        rQi++;
+                        var sx = (xy >> 4);
+                        var sy = (xy & 0x0f);
+
+                        var wh = rQ[rQi];
+                        rQi++;
+                        var sw = (wh >> 4) + 1;
+                        var sh = (wh & 0x0f) + 1;
+
+                        this._display.subTile(sx, sy, sw, sh, color);
+                    }
+                }
+                this._display.finishTile();
+            }
+            this._sock.set_rQi(rQi);
+            this._FBU.lastsubencoding = this._FBU.subencoding;
+            this._FBU.bytes = 0;
+            this._FBU.tiles--;
+        }
+
+        if (this._FBU.tiles === 0) {
+            this._FBU.rects--;
+        }
+
+        return true;
+    },
+
+    TIGHT: function () {
+        this._FBU.bytes = 1;  // compression-control byte
+        if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; }
+
+        var checksum = function (data) {
+            var sum = 0;
+            for (var i = 0; i < data.length; i++) {
+                sum += data[i];
+                if (sum > 65536) sum -= 65536;
+            }
+            return sum;
+        };
+
+        var resetStreams = 0;
+        var streamId = -1;
+        var decompress = function (data, expected) {
+            for (var i = 0; i < 4; i++) {
+                if ((resetStreams >> i) & 1) {
+                    this._FBU.zlibs[i].reset();
+                    Log.Info("Reset zlib stream " + i);
+                }
+            }
+
+            //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0);
+            var uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected);
+            /*if (uncompressed.status !== 0) {
+                Log.Error("Invalid data in zlib stream");
+            }*/
+
+            //return uncompressed.data;
+            return uncompressed;
+        }.bind(this);
+
+        var indexedToRGBX2Color = function (data, palette, width, height) {
+            // Convert indexed (palette based) image data to RGB
+            // TODO: reduce number of calculations inside loop
+            var dest = this._destBuff;
+            var w = Math.floor((width + 7) / 8);
+            var w1 = Math.floor(width / 8);
+
+            /*for (var y = 0; y < height; y++) {
+                var b, x, dp, sp;
+                var yoffset = y * width;
+                var ybitoffset = y * w;
+                var xoffset, targetbyte;
+                for (x = 0; x < w1; x++) {
+                    xoffset = yoffset + x * 8;
+                    targetbyte = data[ybitoffset + x];
+                    for (b = 7; b >= 0; b--) {
+                        dp = (xoffset + 7 - b) * 3;
+                        sp = (targetbyte >> b & 1) * 3;
+                        dest[dp] = palette[sp];
+                        dest[dp + 1] = palette[sp + 1];
+                        dest[dp + 2] = palette[sp + 2];
+                    }
+                }
+
+                xoffset = yoffset + x * 8;
+                targetbyte = data[ybitoffset + x];
+                for (b = 7; b >= 8 - width % 8; b--) {
+                    dp = (xoffset + 7 - b) * 3;
+                    sp = (targetbyte >> b & 1) * 3;
+                    dest[dp] = palette[sp];
+                    dest[dp + 1] = palette[sp + 1];
+                    dest[dp + 2] = palette[sp + 2];
+                }
+            }*/
+
+            for (var y = 0; y < height; y++) {
+                var b, x, dp, sp;
+                for (x = 0; x < w1; x++) {
+                    for (b = 7; b >= 0; b--) {
+                        dp = (y * width + x * 8 + 7 - b) * 4;
+                        sp = (data[y * w + x] >> b & 1) * 3;
+                        dest[dp] = palette[sp];
+                        dest[dp + 1] = palette[sp + 1];
+                        dest[dp + 2] = palette[sp + 2];
+                        dest[dp + 3] = 255;
+                    }
+                }
+
+                for (b = 7; b >= 8 - width % 8; b--) {
+                    dp = (y * width + x * 8 + 7 - b) * 4;
+                    sp = (data[y * w + x] >> b & 1) * 3;
+                    dest[dp] = palette[sp];
+                    dest[dp + 1] = palette[sp + 1];
+                    dest[dp + 2] = palette[sp + 2];
+                    dest[dp + 3] = 255;
+                }
+            }
+
+            return dest;
+        }.bind(this);
+
+        var indexedToRGBX = function (data, palette, width, height) {
+            // Convert indexed (palette based) image data to RGB
+            var dest = this._destBuff;
+            var total = width * height * 4;
+            for (var i = 0, j = 0; i < total; i += 4, j++) {
+                var sp = data[j] * 3;
+                dest[i] = palette[sp];
+                dest[i + 1] = palette[sp + 1];
+                dest[i + 2] = palette[sp + 2];
+                dest[i + 3] = 255;
+            }
+
+            return dest;
+        }.bind(this);
+
+        var rQi = this._sock.get_rQi();
+        var rQ = this._sock.rQwhole();
+        var cmode, data;
+        var cl_header, cl_data;
+
+        var handlePalette = function () {
+            var numColors = rQ[rQi + 2] + 1;
+            var paletteSize = numColors * 3;
+            this._FBU.bytes += paletteSize;
+            if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; }
+
+            var bpp = (numColors <= 2) ? 1 : 8;
+            var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8);
+            var raw = false;
+            if (rowSize * this._FBU.height < 12) {
+                raw = true;
+                cl_header = 0;
+                cl_data = rowSize * this._FBU.height;
+                //clength = [0, rowSize * this._FBU.height];
+            } else {
+                // begin inline getTightCLength (returning two-item arrays is bad for performance with GC)
+                var cl_offset = rQi + 3 + paletteSize;
+                cl_header = 1;
+                cl_data = 0;
+                cl_data += rQ[cl_offset] & 0x7f;
+                if (rQ[cl_offset] & 0x80) {
+                    cl_header++;
+                    cl_data += (rQ[cl_offset + 1] & 0x7f) << 7;
+                    if (rQ[cl_offset + 1] & 0x80) {
+                        cl_header++;
+                        cl_data += rQ[cl_offset + 2] << 14;
+                    }
+                }
+                // end inline getTightCLength
+            }
+
+            this._FBU.bytes += cl_header + cl_data;
+            if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+            // Shift ctl, filter id, num colors, palette entries, and clength off
+            this._sock.rQskipBytes(3);
+            //var palette = this._sock.rQshiftBytes(paletteSize);
+            this._sock.rQshiftTo(this._paletteBuff, paletteSize);
+            this._sock.rQskipBytes(cl_header);
+
+            if (raw) {
+                data = this._sock.rQshiftBytes(cl_data);
+            } else {
+                data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height);
+            }
+
+            // Convert indexed (palette based) image data to RGB
+            var rgbx;
+            if (numColors == 2) {
+                rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height);
+                this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false);
+            } else {
+                rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height);
+                this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false);
+            }
+
+
+            return true;
+        }.bind(this);
+
+        var handleCopy = function () {
+            var raw = false;
+            var uncompressedSize = this._FBU.width * this._FBU.height * 3;
+            if (uncompressedSize < 12) {
+                raw = true;
+                cl_header = 0;
+                cl_data = uncompressedSize;
+            } else {
+                // begin inline getTightCLength (returning two-item arrays is for peformance with GC)
+                var cl_offset = rQi + 1;
+                cl_header = 1;
+                cl_data = 0;
+                cl_data += rQ[cl_offset] & 0x7f;
+                if (rQ[cl_offset] & 0x80) {
+                    cl_header++;
+                    cl_data += (rQ[cl_offset + 1] & 0x7f) << 7;
+                    if (rQ[cl_offset + 1] & 0x80) {
+                        cl_header++;
+                        cl_data += rQ[cl_offset + 2] << 14;
+                    }
+                }
+                // end inline getTightCLength
+            }
+            this._FBU.bytes = 1 + cl_header + cl_data;
+            if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+            // Shift ctl, clength off
+            this._sock.rQshiftBytes(1 + cl_header);
+
+            if (raw) {
+                data = this._sock.rQshiftBytes(cl_data);
+            } else {
+                data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize);
+            }
+
+            this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false);
+
+            return true;
+        }.bind(this);
+
+        var ctl = this._sock.rQpeek8();
+
+        // Keep tight reset bits
+        resetStreams = ctl & 0xF;
+
+        // Figure out filter
+        ctl = ctl >> 4;
+        streamId = ctl & 0x3;
+
+        if (ctl === 0x08)       cmode = "fill";
+        else if (ctl === 0x09)  cmode = "jpeg";
+        else if (ctl === 0x0A)  cmode = "png";
+        else if (ctl & 0x04)    cmode = "filter";
+        else if (ctl < 0x04)    cmode = "copy";
+        else return this._fail("Illegal tight compression received (ctl: " +
+                               ctl + ")");
+
+        switch (cmode) {
+            // fill use depth because TPIXELs drop the padding byte
+            case "fill":  // TPIXEL
+                this._FBU.bytes += 3;
+                break;
+            case "jpeg":  // max clength
+                this._FBU.bytes += 3;
+                break;
+            case "png":  // max clength
+                this._FBU.bytes += 3;
+                break;
+            case "filter":  // filter id + num colors if palette
+                this._FBU.bytes += 2;
+                break;
+            case "copy":
+                break;
+        }
+
+        if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+        // Determine FBU.bytes
+        switch (cmode) {
+            case "fill":
+                // skip ctl byte
+                this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false);
+                this._sock.rQskipBytes(4);
+                break;
+            case "png":
+            case "jpeg":
+                // begin inline getTightCLength (returning two-item arrays is for peformance with GC)
+                var cl_offset = rQi + 1;
+                cl_header = 1;
+                cl_data = 0;
+                cl_data += rQ[cl_offset] & 0x7f;
+                if (rQ[cl_offset] & 0x80) {
+                    cl_header++;
+                    cl_data += (rQ[cl_offset + 1] & 0x7f) << 7;
+                    if (rQ[cl_offset + 1] & 0x80) {
+                        cl_header++;
+                        cl_data += rQ[cl_offset + 2] << 14;
+                    }
+                }
+                // end inline getTightCLength
+                this._FBU.bytes = 1 + cl_header + cl_data;  // ctl + clength size + jpeg-data
+                if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+                // We have everything, render it
+                this._sock.rQskipBytes(1 + cl_header);  // shift off clt + compact length
+                data = this._sock.rQshiftBytes(cl_data);
+                this._display.imageRect(this._FBU.x, this._FBU.y, "image/" + cmode, data);
+                break;
+            case "filter":
+                var filterId = rQ[rQi + 1];
+                if (filterId === 1) {
+                    if (!handlePalette()) { return false; }
+                } else {
+                    // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
+                    // Filter 2, Gradient is valid but not use if jpeg is enabled
+                    this._fail("Unsupported tight subencoding received " +
+                               "(filter: " + filterId + ")");
+                }
+                break;
+            case "copy":
+                if (!handleCopy()) { return false; }
+                break;
+        }
+
+
+        this._FBU.bytes = 0;
+        this._FBU.rects--;
+
+        return true;
+    },
+
+    last_rect: function () {
+        this._FBU.rects = 0;
+        return true;
+    },
+
+    ExtendedDesktopSize: function () {
+        this._FBU.bytes = 1;
+        if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; }
+
+        var firstUpdate = !this._supportsSetDesktopSize;
+        this._supportsSetDesktopSize = true;
+
+        // Normally we only apply the current resize mode after a
+        // window resize event. However there is no such trigger on the
+        // initial connect. And we don't know if the server supports
+        // resizing until we've gotten here.
+        if (firstUpdate) {
+            this._requestRemoteResize();
+        }
+
+        var number_of_screens = this._sock.rQpeek8();
+
+        this._FBU.bytes = 4 + (number_of_screens * 16);
+        if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; }
+
+        this._sock.rQskipBytes(1);  // number-of-screens
+        this._sock.rQskipBytes(3);  // padding
+
+        for (var i = 0; i < number_of_screens; i += 1) {
+            // Save the id and flags of the first screen
+            if (i === 0) {
+                this._screen_id = this._sock.rQshiftBytes(4);    // id
+                this._sock.rQskipBytes(2);                       // x-position
+                this._sock.rQskipBytes(2);                       // y-position
+                this._sock.rQskipBytes(2);                       // width
+                this._sock.rQskipBytes(2);                       // height
+                this._screen_flags = this._sock.rQshiftBytes(4); // flags
+            } else {
+                this._sock.rQskipBytes(16);
+            }
+        }
+
+        /*
+         * The x-position indicates the reason for the change:
+         *
+         *  0 - server resized on its own
+         *  1 - this client requested the resize
+         *  2 - another client requested the resize
+         */
+
+        // We need to handle errors when we requested the resize.
+        if (this._FBU.x === 1 && this._FBU.y !== 0) {
+            var msg = "";
+            // The y-position indicates the status code from the server
+            switch (this._FBU.y) {
+            case 1:
+                msg = "Resize is administratively prohibited";
+                break;
+            case 2:
+                msg = "Out of resources";
+                break;
+            case 3:
+                msg = "Invalid screen layout";
+                break;
+            default:
+                msg = "Unknown reason";
+                break;
+            }
+            Log.Warn("Server did not accept the resize request: "
+                     + msg);
+        } else {
+            this._resize(this._FBU.width, this._FBU.height);
+        }
+
+        this._FBU.bytes = 0;
+        this._FBU.rects -= 1;
+        return true;
+    },
+
+    DesktopSize: function () {
+        this._resize(this._FBU.width, this._FBU.height);
+        this._FBU.bytes = 0;
+        this._FBU.rects -= 1;
+        return true;
+    },
+
+    Cursor: function () {
+        Log.Debug(">> set_cursor");
+        var x = this._FBU.x;  // hotspot-x
+        var y = this._FBU.y;  // hotspot-y
+        var w = this._FBU.width;
+        var h = this._FBU.height;
+
+        var pixelslength = w * h * 4;
+        var masklength = Math.floor((w + 7) / 8) * h;
+
+        this._FBU.bytes = pixelslength + masklength;
+        if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; }
+
+        this._display.changeCursor(this._sock.rQshiftBytes(pixelslength),
+                                   this._sock.rQshiftBytes(masklength),
+                                   x, y, w, h);
+
+        this._FBU.bytes = 0;
+        this._FBU.rects--;
+
+        Log.Debug("<< set_cursor");
+        return true;
+    },
+
+    QEMUExtendedKeyEvent: function () {
+        this._FBU.rects--;
+
+        // Old Safari doesn't support creating keyboard events
+        try {
+            var keyboardEvent = document.createEvent("keyboardEvent");
+            if (keyboardEvent.code !== undefined) {
+                this._qemuExtKeyEventSupported = true;
+            }
+        } catch (err) {
+        }
+    },
+};
Binary file image/www/core/util/browser.js.gz has changed
Binary file image/www/core/util/events.js.gz has changed
Binary file image/www/core/util/eventtarget.js.gz has changed
Binary file image/www/core/util/logging.js.gz has changed
Binary file image/www/core/util/polyfill.js.gz has changed
Binary file image/www/core/util/strings.js.gz has changed
Binary file image/www/core/websock.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/css/style.css	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,72 @@
+
+
+body {
+    background: #ccc;
+    font-family: Verdana, Arial, sans-serif;
+    margin: 0px;
+}
+
+
+#webui_table {
+    width: 360px;
+    height: 270px;
+    background: #353536;
+    border: 2px solid;
+    border-color: #59b4d4;
+}
+
+
+#mlt_table {
+    width: 178px;
+    height: 90px;
+    margin: auto;
+    background: #303030;
+    border: 1px solid;
+    border-collapse: collapse;
+    color: white;
+    border-color: white;
+}
+
+#hlt_table {
+    width: 178px;
+    height: 90px;
+    margin: auto;
+    background: #3f3f40;
+    border: 1px solid;
+    border-collapse: collapse;
+    color: yellow;
+    border-color: yellow;
+}
+
+.appbutton {
+    width: 60px;
+    height: 36px;
+    border-radius: 4px;
+    background-color: #e7e7e7; color: black;
+}
+
+#timer {
+    color: white;
+    text-align: center;
+    font-family: "Courier New", "Lucida Console";
+    font-size: 34px;
+}
+
+/* LED */
+.LEDred {
+    margin: 5px auto;
+    width: 12px;
+    height: 12px;
+    background-color: #820;	/* On = #F40 */
+    border-radius: 50%;
+}
+
+.LEDgreen {
+    margin: 5px auto;
+    width: 12px;
+    height: 12px;
+    background-color: #270;	/* On = #5E0 */
+    border-radius: 50%;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/index.html	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<HTML>
+ <HEAD>
+  <title>BrewBoard</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+  <link rel="stylesheet" href="app/styles/web.css" />
+ </HEAD>
+ <BODY>
+
+  <div class="columns menu">
+   <ul>
+    <li><input type="none" class="button" value="BrewBoard menu"></li>
+    <li><form action="vnc.html"><input type="submit" class="button" value="Touch Scherm"></form></li>
+    <li><form action="logs.html"><input type="submit" class="button" value="Brouw logs"></form></li>
+    <li><form action="webui.html"><input type="submit" class="button" value="Web User Interface"></form></li>
+    <li><form action="#"><input type="submit" class="button" value="Menu 3"></form></li>
+   </ul>
+  </div>
+
+ </BODY>
+</HTML>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/js/Chart.min.js	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,10 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 2.7.2
+ *
+ * Copyright 2018 Chart.js Contributors
+ * Released under the MIT license
+ * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md
+ */
+!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Chart=t()}}(function(){return function t(e,i,n){function a(r,s){if(!i[r]){if(!e[r]){var l="function"==typeof require&&require;if(!s&&l)return l(r,!0);if(o)return o(r,!0);var u=new Error("Cannot find module '"+r+"'");throw u.code="MODULE_NOT_FOUND",u}var d=i[r]={exports:{}};e[r][0].call(d.exports,function(t){var i=e[r][1][t];return a(i||t)},d,d.exports,t,e,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;r<n.length;r++)a(n[r]);return a}({1:[function(t,e,i){},{}],2:[function(t,e,i){var n=t(6);function a(t){if(t){var e=[0,0,0],i=1,a=t.match(/^#([a-fA-F0-9]{3})$/i);if(a){a=a[1];for(var o=0;o<e.length;o++)e[o]=parseInt(a[o]+a[o],16)}else if(a=t.match(/^#([a-fA-F0-9]{6})$/i)){a=a[1];for(o=0;o<e.length;o++)e[o]=parseInt(a.slice(2*o,2*o+2),16)}else if(a=t.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(o=0;o<e.length;o++)e[o]=parseInt(a[o+1]);i=parseFloat(a[4])}else if(a=t.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(o=0;o<e.length;o++)e[o]=Math.round(2.55*parseFloat(a[o+1]));i=parseFloat(a[4])}else if(a=t.match(/(\w+)/)){if("transparent"==a[1])return[0,0,0,0];if(!(e=n[a[1]]))return}for(o=0;o<e.length;o++)e[o]=d(e[o],0,255);return i=i||0==i?d(i,0,1):1,e[3]=i,e}}function o(t){if(t){var e=t.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var i=parseFloat(e[4]);return[d(parseInt(e[1]),0,360),d(parseFloat(e[2]),0,100),d(parseFloat(e[3]),0,100),d(isNaN(i)?1:i,0,1)]}}}function r(t){if(t){var e=t.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var i=parseFloat(e[4]);return[d(parseInt(e[1]),0,360),d(parseFloat(e[2]),0,100),d(parseFloat(e[3]),0,100),d(isNaN(i)?1:i,0,1)]}}}function s(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function l(t,e){return"rgba("+Math.round(t[0]/255*100)+"%, "+Math.round(t[1]/255*100)+"%, "+Math.round(t[2]/255*100)+"%, "+(e||t[3]||1)+")"}function u(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function d(t,e,i){return Math.min(Math.max(e,t),i)}function c(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}e.exports={getRgba:a,getHsla:o,getRgb:function(t){var e=a(t);return e&&e.slice(0,3)},getHsl:function(t){var e=o(t);return e&&e.slice(0,3)},getHwb:r,getAlpha:function(t){var e=a(t);{if(e)return e[3];if(e=o(t))return e[3];if(e=r(t))return e[3]}},hexString:function(t){return"#"+c(t[0])+c(t[1])+c(t[2])},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return s(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:s,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return l(t,e);var i=Math.round(t[0]/255*100),n=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+i+"%, "+n+"%, "+a+"%)"},percentaString:l,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return u(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:u,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return h[t.slice(0,3)]}};var h={};for(var f in n)h[n[f]]=f},{6:6}],3:[function(t,e,i){var n=t(5),a=t(2),o=function(t){return t instanceof o?t:this instanceof o?(this.valid=!1,this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1},void("string"==typeof t?(e=a.getRgba(t))?this.setValues("rgb",e):(e=a.getHsla(t))?this.setValues("hsl",e):(e=a.getHwb(t))&&this.setValues("hwb",e):"object"==typeof t&&(void 0!==(e=t).r||void 0!==e.red?this.setValues("rgb",e):void 0!==e.l||void 0!==e.lightness?this.setValues("hsl",e):void 0!==e.v||void 0!==e.value?this.setValues("hsv",e):void 0!==e.w||void 0!==e.whiteness?this.setValues("hwb",e):void 0===e.c&&void 0===e.cyan||this.setValues("cmyk",e)))):new o(t);var e};o.prototype={isValid:function(){return this.valid},rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var t=this.values;return 1!==t.alpha?t.hwb.concat([t.alpha]):t.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var t=this.values;return t.rgb.concat([t.alpha])},hslaArray:function(){var t=this.values;return t.hsl.concat([t.alpha])},alpha:function(t){return void 0===t?this.values.alpha:(this.setValues("alpha",t),this)},red:function(t){return this.setChannel("rgb",0,t)},green:function(t){return this.setChannel("rgb",1,t)},blue:function(t){return this.setChannel("rgb",2,t)},hue:function(t){return t&&(t=(t%=360)<0?360+t:t),this.setChannel("hsl",0,t)},saturation:function(t){return this.setChannel("hsl",1,t)},lightness:function(t){return this.setChannel("hsl",2,t)},saturationv:function(t){return this.setChannel("hsv",1,t)},whiteness:function(t){return this.setChannel("hwb",1,t)},blackness:function(t){return this.setChannel("hwb",2,t)},value:function(t){return this.setChannel("hsv",2,t)},cyan:function(t){return this.setChannel("cmyk",0,t)},magenta:function(t){return this.setChannel("cmyk",1,t)},yellow:function(t){return this.setChannel("cmyk",2,t)},black:function(t){return this.setChannel("cmyk",3,t)},hexString:function(){return a.hexString(this.values.rgb)},rgbString:function(){return a.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return a.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return a.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return a.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return a.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return a.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return a.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var t=this.values.rgb;return t[0]<<16|t[1]<<8|t[2]},luminosity:function(){for(var t=this.values.rgb,e=[],i=0;i<t.length;i++){var n=t[i]/255;e[i]=n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4)}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast:function(t){var e=this.luminosity(),i=t.luminosity();return e>i?(e+.05)/(i+.05):(i+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,i=(e[0]+t)%360;return e[0]=i<0?360+i:i,this.setValues("hsl",e),this},mix:function(t,e){var i=this,n=t,a=void 0===e?.5:e,o=2*a-1,r=i.alpha()-n.alpha(),s=((o*r==-1?o:(o+r)/(1+o*r))+1)/2,l=1-s;return this.rgb(s*i.red()+l*n.red(),s*i.green()+l*n.green(),s*i.blue()+l*n.blue()).alpha(i.alpha()*a+n.alpha()*(1-a))},toJSON:function(){return this.rgb()},clone:function(){var t,e,i=new o,n=this.values,a=i.values;for(var r in n)n.hasOwnProperty(r)&&(t=n[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return i}},o.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},o.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},o.prototype.getValues=function(t){for(var e=this.values,i={},n=0;n<t.length;n++)i[t.charAt(n)]=e[t][n];return 1!==e.alpha&&(i.a=e.alpha),i},o.prototype.setValues=function(t,e){var i,a,o=this.values,r=this.spaces,s=this.maxes,l=1;if(this.valid=!0,"alpha"===t)l=e;else if(e.length)o[t]=e.slice(0,t.length),l=e[t.length];else if(void 0!==e[t.charAt(0)]){for(i=0;i<t.length;i++)o[t][i]=e[t.charAt(i)];l=e.a}else if(void 0!==e[r[t][0]]){var u=r[t];for(i=0;i<t.length;i++)o[t][i]=e[u[i]];l=e.alpha}if(o.alpha=Math.max(0,Math.min(1,void 0===l?o.alpha:l)),"alpha"===t)return!1;for(i=0;i<t.length;i++)a=Math.max(0,Math.min(s[t][i],o[t][i])),o[t][i]=Math.round(a);for(var d in r)d!==t&&(o[d]=n[t][d](o[t]));return!0},o.prototype.setSpace=function(t,e){var i=e[0];return void 0===i?this.getValues(t):("number"==typeof i&&(i=Array.prototype.slice.call(e)),this.setValues(t,i),this)},o.prototype.setChannel=function(t,e,i){var n=this.values[t];return void 0===i?n[e]:i===n[e]?this:(n[e]=i,this.setValues(t,n),this)},"undefined"!=typeof window&&(window.Color=o),e.exports=o},{2:2,5:5}],4:[function(t,e,i){function n(t){var e,i,n=t[0]/255,a=t[1]/255,o=t[2]/255,r=Math.min(n,a,o),s=Math.max(n,a,o),l=s-r;return s==r?e=0:n==s?e=(a-o)/l:a==s?e=2+(o-n)/l:o==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),i=(r+s)/2,[e,100*(s==r?0:i<=.5?l/(s+r):l/(2-s-r)),100*i]}function a(t){var e,i,n=t[0],a=t[1],o=t[2],r=Math.min(n,a,o),s=Math.max(n,a,o),l=s-r;return i=0==s?0:l/s*1e3/10,s==r?e=0:n==s?e=(a-o)/l:a==s?e=2+(o-n)/l:o==s&&(e=4+(n-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),[e,i,s/255*1e3/10]}function o(t){var e=t[0],i=t[1],a=t[2];return[n(t)[0],100*(1/255*Math.min(e,Math.min(i,a))),100*(a=1-1/255*Math.max(e,Math.max(i,a)))]}function s(t){var e,i=t[0]/255,n=t[1]/255,a=t[2]/255;return[100*((1-i-(e=Math.min(1-i,1-n,1-a)))/(1-e)||0),100*((1-n-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]}function l(t){return C[JSON.stringify(t)]}function u(t){var e=t[0]/255,i=t[1]/255,n=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)+.1805*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)),100*(.2126*e+.7152*i+.0722*n),100*(.0193*e+.1192*i+.9505*n)]}function d(t){var e=u(t),i=e[0],n=e[1],a=e[2];return n/=100,a/=108.883,i=(i/=95.047)>.008856?Math.pow(i,1/3):7.787*i+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(i-n),200*(n-(a=a>.008856?Math.pow(a,1/3):7.787*a+16/116))]}function c(t){var e,i,n,a,o,r=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return[o=255*l,o,o];e=2*l-(i=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(n=r+1/3*-(u-1))<0&&n++,n>1&&n--,o=6*n<1?e+6*(i-e)*n:2*n<1?i:3*n<2?e+(i-e)*(2/3-n)*6:e,a[u]=255*o;return a}function h(t){var e=t[0]/60,i=t[1]/100,n=t[2]/100,a=Math.floor(e)%6,o=e-Math.floor(e),r=255*n*(1-i),s=255*n*(1-i*o),l=255*n*(1-i*(1-o));n*=255;switch(a){case 0:return[n,l,r];case 1:return[s,n,r];case 2:return[r,n,l];case 3:return[r,s,n];case 4:return[l,r,n];case 5:return[n,r,s]}}function f(t){var e,i,n,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,u=s+l;switch(u>1&&(s/=u,l/=u),n=6*o-(e=Math.floor(6*o)),0!=(1&e)&&(n=1-n),a=s+n*((i=1-l)-s),e){default:case 6:case 0:r=i,g=a,b=s;break;case 1:r=a,g=i,b=s;break;case 2:r=s,g=i,b=a;break;case 3:r=s,g=a,b=i;break;case 4:r=a,g=s,b=i;break;case 5:r=i,g=s,b=a}return[255*r,255*g,255*b]}function p(t){var e=t[0]/100,i=t[1]/100,n=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a))]}function m(t){var e,i,n,a=t[0]/100,o=t[1]/100,r=t[2]/100;return i=-.9689*a+1.8758*o+.0415*r,n=.0557*a+-.204*o+1.057*r,e=(e=3.2406*a+-1.5372*o+-.4986*r)>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,[255*(e=Math.min(Math.max(0,e),1)),255*(i=Math.min(Math.max(0,i),1)),255*(n=Math.min(Math.max(0,n),1))]}function v(t){var e=t[0],i=t[1],n=t[2];return i/=100,n/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(e-i),200*(i-(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116))]}function x(t){var e,i,n,a,o=t[0],r=t[1],s=t[2];return o<=8?a=(i=100*o/903.3)/100*7.787+16/116:(i=100*Math.pow((o+16)/116,3),a=Math.pow(i/100,1/3)),[e=e/95.047<=.008856?e=95.047*(r/500+a-16/116)/7.787:95.047*Math.pow(r/500+a,3),i,n=n/108.883<=.008859?n=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3)]}function y(t){var e,i=t[0],n=t[1],a=t[2];return(e=360*Math.atan2(a,n)/2/Math.PI)<0&&(e+=360),[i,Math.sqrt(n*n+a*a),e]}function k(t){return m(x(t))}function M(t){var e,i=t[0],n=t[1];return e=t[2]/360*2*Math.PI,[i,n*Math.cos(e),n*Math.sin(e)]}function w(t){return S[t]}e.exports={rgb2hsl:n,rgb2hsv:a,rgb2hwb:o,rgb2cmyk:s,rgb2keyword:l,rgb2xyz:u,rgb2lab:d,rgb2lch:function(t){return y(d(t))},hsl2rgb:c,hsl2hsv:function(t){var e=t[0],i=t[1]/100,n=t[2]/100;if(0===n)return[0,0,0];return[e,100*(2*(i*=(n*=2)<=1?n:2-n)/(n+i)),100*((n+i)/2)]},hsl2hwb:function(t){return o(c(t))},hsl2cmyk:function(t){return s(c(t))},hsl2keyword:function(t){return l(c(t))},hsv2rgb:h,hsv2hsl:function(t){var e,i,n=t[0],a=t[1]/100,o=t[2]/100;return e=a*o,[n,100*(e=(e/=(i=(2-a)*o)<=1?i:2-i)||0),100*(i/=2)]},hsv2hwb:function(t){return o(h(t))},hsv2cmyk:function(t){return s(h(t))},hsv2keyword:function(t){return l(h(t))},hwb2rgb:f,hwb2hsl:function(t){return n(f(t))},hwb2hsv:function(t){return a(f(t))},hwb2cmyk:function(t){return s(f(t))},hwb2keyword:function(t){return l(f(t))},cmyk2rgb:p,cmyk2hsl:function(t){return n(p(t))},cmyk2hsv:function(t){return a(p(t))},cmyk2hwb:function(t){return o(p(t))},cmyk2keyword:function(t){return l(p(t))},keyword2rgb:w,keyword2hsl:function(t){return n(w(t))},keyword2hsv:function(t){return a(w(t))},keyword2hwb:function(t){return o(w(t))},keyword2cmyk:function(t){return s(w(t))},keyword2lab:function(t){return d(w(t))},keyword2xyz:function(t){return u(w(t))},xyz2rgb:m,xyz2lab:v,xyz2lch:function(t){return y(v(t))},lab2xyz:x,lab2rgb:k,lab2lch:y,lch2lab:M,lch2xyz:function(t){return x(M(t))},lch2rgb:function(t){return k(M(t))}};var S={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},C={};for(var _ in S)C[JSON.stringify(S[_])]=_},{}],5:[function(t,e,i){var n=t(4),a=function(){return new u};for(var o in n){a[o+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),n[t](e)}}(o);var r=/(\w+)2(\w+)/.exec(o),s=r[1],l=r[2];(a[s]=a[s]||{})[l]=a[o]=function(t){return function(e){"number"==typeof e&&(e=Array.prototype.slice.call(arguments));var i=n[t](e);if("string"==typeof i||void 0===i)return i;for(var a=0;a<i.length;a++)i[a]=Math.round(i[a]);return i}}(o)}var u=function(){this.convs={}};u.prototype.routeSpace=function(t,e){var i=e[0];return void 0===i?this.getValues(t):("number"==typeof i&&(i=Array.prototype.slice.call(e)),this.setValues(t,i))},u.prototype.setValues=function(t,e){return this.space=t,this.convs={},this.convs[t]=e,this},u.prototype.getValues=function(t){var e=this.convs[t];if(!e){var i=this.space,n=this.convs[i];e=a[i][t](n),this.convs[t]=e}return e},["rgb","hsl","hsv","cmyk","keyword"].forEach(function(t){u.prototype[t]=function(e){return this.routeSpace(t,arguments)}}),e.exports=a},{4:4}],6:[function(t,e,i){"use strict";e.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}},{}],7:[function(t,e,i){var n=t(29)();n.helpers=t(45),t(27)(n),n.defaults=t(25),n.Element=t(26),n.elements=t(40),n.Interaction=t(28),n.layouts=t(30),n.platform=t(48),n.plugins=t(31),n.Ticks=t(34),t(22)(n),t(23)(n),t(24)(n),t(33)(n),t(32)(n),t(35)(n),t(55)(n),t(53)(n),t(54)(n),t(56)(n),t(57)(n),t(58)(n),t(15)(n),t(16)(n),t(17)(n),t(18)(n),t(19)(n),t(20)(n),t(21)(n),t(8)(n),t(9)(n),t(10)(n),t(11)(n),t(12)(n),t(13)(n),t(14)(n);var a=t(49);for(var o in a)a.hasOwnProperty(o)&&n.plugins.register(a[o]);n.platform.initialize(),e.exports=n,"undefined"!=typeof window&&(window.Chart=n),n.Legend=a.legend._element,n.Title=a.title._element,n.pluginService=n.plugins,n.PluginBase=n.Element.extend({}),n.canvasHelpers=n.helpers.canvas,n.layoutService=n.layouts},{10:10,11:11,12:12,13:13,14:14,15:15,16:16,17:17,18:18,19:19,20:20,21:21,22:22,23:23,24:24,25:25,26:26,27:27,28:28,29:29,30:30,31:31,32:32,33:33,34:34,35:35,40:40,45:45,48:48,49:49,53:53,54:54,55:55,56:56,57:57,58:58,8:8,9:9}],8:[function(t,e,i){"use strict";e.exports=function(t){t.Bar=function(e,i){return i.type="bar",new t(e,i)}}},{}],9:[function(t,e,i){"use strict";e.exports=function(t){t.Bubble=function(e,i){return i.type="bubble",new t(e,i)}}},{}],10:[function(t,e,i){"use strict";e.exports=function(t){t.Doughnut=function(e,i){return i.type="doughnut",new t(e,i)}}},{}],11:[function(t,e,i){"use strict";e.exports=function(t){t.Line=function(e,i){return i.type="line",new t(e,i)}}},{}],12:[function(t,e,i){"use strict";e.exports=function(t){t.PolarArea=function(e,i){return i.type="polarArea",new t(e,i)}}},{}],13:[function(t,e,i){"use strict";e.exports=function(t){t.Radar=function(e,i){return i.type="radar",new t(e,i)}}},{}],14:[function(t,e,i){"use strict";e.exports=function(t){t.Scatter=function(e,i){return i.type="scatter",new t(e,i)}}},{}],15:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),n._set("horizontalBar",{hover:{mode:"index",axis:"y"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{position:"left",type:"category",categoryPercentage:.8,barPercentage:.9,offset:!0,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{callbacks:{title:function(t,e){var i="";return t.length>0&&(t[0].yLabel?i=t[0].yLabel:e.labels.length>0&&t[0].index<e.labels.length&&(i=e.labels[t[0].index])),i},label:function(t,e){return(e.datasets[t.datasetIndex].label||"")+": "+t.xLabel}},mode:"index",axis:"y"}}),e.exports=function(t){t.controllers.bar=t.DatasetController.extend({dataElementType:a.Rectangle,initialize:function(){var e;t.DatasetController.prototype.initialize.apply(this,arguments),(e=this.getMeta()).stack=this.getDataset().stack,e.bar=!0},update:function(t){var e,i,n=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,i=n.length;e<i;++e)this.updateElement(n[e],e,t)},updateElement:function(t,e,i){var n=this,a=n.chart,r=n.getMeta(),s=n.getDataset(),l=t.custom||{},u=a.options.elements.rectangle;t._xScale=n.getScaleForId(r.xAxisID),t._yScale=n.getScaleForId(r.yAxisID),t._datasetIndex=n.index,t._index=e,t._model={datasetLabel:s.label,label:a.data.labels[e],borderSkipped:l.borderSkipped?l.borderSkipped:u.borderSkipped,backgroundColor:l.backgroundColor?l.backgroundColor:o.valueAtIndexOrDefault(s.backgroundColor,e,u.backgroundColor),borderColor:l.borderColor?l.borderColor:o.valueAtIndexOrDefault(s.borderColor,e,u.borderColor),borderWidth:l.borderWidth?l.borderWidth:o.valueAtIndexOrDefault(s.borderWidth,e,u.borderWidth)},n.updateElementGeometry(t,e,i),t.pivot()},updateElementGeometry:function(t,e,i){var n=this,a=t._model,o=n.getValueScale(),r=o.getBasePixel(),s=o.isHorizontal(),l=n._ruler||n.getRuler(),u=n.calculateBarValuePixels(n.index,e),d=n.calculateBarIndexPixels(n.index,e,l);a.horizontal=s,a.base=i?r:u.base,a.x=s?i?r:u.head:d.center,a.y=s?d.center:i?r:u.head,a.height=s?d.size:void 0,a.width=s?void 0:d.size},getValueScaleId:function(){return this.getMeta().yAxisID},getIndexScaleId:function(){return this.getMeta().xAxisID},getValueScale:function(){return this.getScaleForId(this.getValueScaleId())},getIndexScale:function(){return this.getScaleForId(this.getIndexScaleId())},_getStacks:function(t){var e,i,n=this.chart,a=this.getIndexScale().options.stacked,o=void 0===t?n.data.datasets.length:t+1,r=[];for(e=0;e<o;++e)(i=n.getDatasetMeta(e)).bar&&n.isDatasetVisible(e)&&(!1===a||!0===a&&-1===r.indexOf(i.stack)||void 0===a&&(void 0===i.stack||-1===r.indexOf(i.stack)))&&r.push(i.stack);return r},getStackCount:function(){return this._getStacks().length},getStackIndex:function(t,e){var i=this._getStacks(t),n=void 0!==e?i.indexOf(e):-1;return-1===n?i.length-1:n},getRuler:function(){var t,e,i=this.getIndexScale(),n=this.getStackCount(),a=this.index,r=i.isHorizontal(),s=r?i.left:i.top,l=s+(r?i.width:i.height),u=[];for(t=0,e=this.getMeta().data.length;t<e;++t)u.push(i.getPixelForValue(null,t,a));return{min:o.isNullOrUndef(i.options.barThickness)?function(t,e){var i,n,a,o,r=t.isHorizontal()?t.width:t.height,s=t.getTicks();for(a=1,o=e.length;a<o;++a)r=Math.min(r,e[a]-e[a-1]);for(a=0,o=s.length;a<o;++a)n=t.getPixelForTick(a),r=a>0?Math.min(r,n-i):r,i=n;return r}(i,u):-1,pixels:u,start:s,end:l,stackCount:n,scale:i}},calculateBarValuePixels:function(t,e){var i,n,a,o,r,s,l=this.chart,u=this.getMeta(),d=this.getValueScale(),c=l.data.datasets,h=d.getRightValue(c[t].data[e]),f=d.options.stacked,g=u.stack,p=0;if(f||void 0===f&&void 0!==g)for(i=0;i<t;++i)(n=l.getDatasetMeta(i)).bar&&n.stack===g&&n.controller.getValueScaleId()===d.id&&l.isDatasetVisible(i)&&(a=d.getRightValue(c[i].data[e]),(h<0&&a<0||h>=0&&a>0)&&(p+=a));return o=d.getPixelForValue(p),{size:s=((r=d.getPixelForValue(p+h))-o)/2,base:o,head:r,center:r+s/2}},calculateBarIndexPixels:function(t,e,i){var n,a,r,s,l,u,d,c,h,f,g,p,m,v,b,x,y,k=i.scale.options,M="flex"===k.barThickness?(h=e,g=k,m=(f=i).pixels,v=m[h],b=h>0?m[h-1]:null,x=h<m.length-1?m[h+1]:null,y=g.categoryPercentage,null===b&&(b=v-(null===x?f.end-v:x-v)),null===x&&(x=v+v-b),p=v-(v-b)/2*y,{chunk:(x-b)/2*y/f.stackCount,ratio:g.barPercentage,start:p}):(n=e,a=i,u=(r=k).barThickness,d=a.stackCount,c=a.pixels[n],o.isNullOrUndef(u)?(s=a.min*r.categoryPercentage,l=r.barPercentage):(s=u*d,l=1),{chunk:s/d,ratio:l,start:c-s/2}),w=this.getStackIndex(t,this.getMeta().stack),S=M.start+M.chunk*w+M.chunk/2,C=Math.min(o.valueOrDefault(k.maxBarThickness,1/0),M.chunk*M.ratio);return{base:S-C/2,head:S+C/2,center:S,size:C}},draw:function(){var t=this.chart,e=this.getValueScale(),i=this.getMeta().data,n=this.getDataset(),a=i.length,r=0;for(o.canvas.clipArea(t.ctx,t.chartArea);r<a;++r)isNaN(e.getRightValue(n.data[r]))||i[r].draw();o.canvas.unclipArea(t.ctx)},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,n=t.custom||{},a=t._model;a.backgroundColor=n.hoverBackgroundColor?n.hoverBackgroundColor:o.valueAtIndexOrDefault(e.hoverBackgroundColor,i,o.getHoverColor(a.backgroundColor)),a.borderColor=n.hoverBorderColor?n.hoverBorderColor:o.valueAtIndexOrDefault(e.hoverBorderColor,i,o.getHoverColor(a.borderColor)),a.borderWidth=n.hoverBorderWidth?n.hoverBorderWidth:o.valueAtIndexOrDefault(e.hoverBorderWidth,i,a.borderWidth)},removeHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,n=t.custom||{},a=t._model,r=this.chart.options.elements.rectangle;a.backgroundColor=n.backgroundColor?n.backgroundColor:o.valueAtIndexOrDefault(e.backgroundColor,i,r.backgroundColor),a.borderColor=n.borderColor?n.borderColor:o.valueAtIndexOrDefault(e.borderColor,i,r.borderColor),a.borderWidth=n.borderWidth?n.borderWidth:o.valueAtIndexOrDefault(e.borderWidth,i,r.borderWidth)}}),t.controllers.horizontalBar=t.controllers.bar.extend({getValueScaleId:function(){return this.getMeta().xAxisID},getIndexScaleId:function(){return this.getMeta().yAxisID}})}},{25:25,40:40,45:45}],16:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("bubble",{hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(t,e){var i=e.datasets[t.datasetIndex].label||"",n=e.datasets[t.datasetIndex].data[t.index];return i+": ("+t.xLabel+", "+t.yLabel+", "+n.r+")"}}}}),e.exports=function(t){t.controllers.bubble=t.DatasetController.extend({dataElementType:a.Point,update:function(t){var e=this,i=e.getMeta().data;o.each(i,function(i,n){e.updateElement(i,n,t)})},updateElement:function(t,e,i){var n=this,a=n.getMeta(),o=t.custom||{},r=n.getScaleForId(a.xAxisID),s=n.getScaleForId(a.yAxisID),l=n._resolveElementOptions(t,e),u=n.getDataset().data[e],d=n.index,c=i?r.getPixelForDecimal(.5):r.getPixelForValue("object"==typeof u?u:NaN,e,d),h=i?s.getBasePixel():s.getPixelForValue(u,e,d);t._xScale=r,t._yScale=s,t._options=l,t._datasetIndex=d,t._index=e,t._model={backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,hitRadius:l.hitRadius,pointStyle:l.pointStyle,radius:i?0:l.radius,skip:o.skip||isNaN(c)||isNaN(h),x:c,y:h},t.pivot()},setHoverStyle:function(t){var e=t._model,i=t._options;e.backgroundColor=o.valueOrDefault(i.hoverBackgroundColor,o.getHoverColor(i.backgroundColor)),e.borderColor=o.valueOrDefault(i.hoverBorderColor,o.getHoverColor(i.borderColor)),e.borderWidth=o.valueOrDefault(i.hoverBorderWidth,i.borderWidth),e.radius=i.radius+i.hoverRadius},removeHoverStyle:function(t){var e=t._model,i=t._options;e.backgroundColor=i.backgroundColor,e.borderColor=i.borderColor,e.borderWidth=i.borderWidth,e.radius=i.radius},_resolveElementOptions:function(t,e){var i,n,a,r=this.chart,s=r.data.datasets[this.index],l=t.custom||{},u=r.options.elements.point,d=o.options.resolve,c=s.data[e],h={},f={chart:r,dataIndex:e,dataset:s,datasetIndex:this.index},g=["backgroundColor","borderColor","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","pointStyle"];for(i=0,n=g.length;i<n;++i)h[a=g[i]]=d([l[a],s[a],u[a]],f,e);return h.radius=d([l.radius,c?c.r:void 0,s.radius,u.radius],f,e),h}})}},{25:25,40:40,45:45}],17:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("doughnut",{animation:{animateRotate:!0,animateScale:!1},hover:{mode:"single"},legendCallback:function(t){var e=[];e.push('<ul class="'+t.id+'-legend">');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var o=0;o<n[0].data.length;++o)e.push('<li><span style="background-color:'+n[0].backgroundColor[o]+'"></span>'),a[o]&&e.push(a[o]),e.push("</li>");return e.push("</ul>"),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],s=a.data[n],l=s&&s.custom||{},u=o.valueAtIndexOrDefault,d=t.options.elements.arc;return{text:i,fillStyle:l.backgroundColor?l.backgroundColor:u(r.backgroundColor,n,d.backgroundColor),strokeStyle:l.borderColor?l.borderColor:u(r.borderColor,n,d.borderColor),lineWidth:l.borderWidth?l.borderWidth:u(r.borderWidth,n,d.borderWidth),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,o=e.index,r=this.chart;for(i=0,n=(r.data.datasets||[]).length;i<n;++i)(a=r.getDatasetMeta(i)).data[o]&&(a.data[o].hidden=!a.data[o].hidden);r.update()}},cutoutPercentage:50,rotation:-.5*Math.PI,circumference:2*Math.PI,tooltips:{callbacks:{title:function(){return""},label:function(t,e){var i=e.labels[t.index],n=": "+e.datasets[t.datasetIndex].data[t.index];return o.isArray(i)?(i=i.slice())[0]+=n:i+=n,i}}}}),n._set("pie",o.clone(n.doughnut)),n._set("pie",{cutoutPercentage:0}),e.exports=function(t){t.controllers.doughnut=t.controllers.pie=t.DatasetController.extend({dataElementType:a.Arc,linkScales:o.noop,getRingIndex:function(t){for(var e=0,i=0;i<t;++i)this.chart.isDatasetVisible(i)&&++e;return e},update:function(t){var e=this,i=e.chart,n=i.chartArea,a=i.options,r=a.elements.arc,s=n.right-n.left-r.borderWidth,l=n.bottom-n.top-r.borderWidth,u=Math.min(s,l),d={x:0,y:0},c=e.getMeta(),h=a.cutoutPercentage,f=a.circumference;if(f<2*Math.PI){var g=a.rotation%(2*Math.PI),p=(g+=2*Math.PI*(g>=Math.PI?-1:g<-Math.PI?1:0))+f,m=Math.cos(g),v=Math.sin(g),b=Math.cos(p),x=Math.sin(p),y=g<=0&&p>=0||g<=2*Math.PI&&2*Math.PI<=p,k=g<=.5*Math.PI&&.5*Math.PI<=p||g<=2.5*Math.PI&&2.5*Math.PI<=p,M=g<=-Math.PI&&-Math.PI<=p||g<=Math.PI&&Math.PI<=p,w=g<=.5*-Math.PI&&.5*-Math.PI<=p||g<=1.5*Math.PI&&1.5*Math.PI<=p,S=h/100,C=M?-1:Math.min(m*(m<0?1:S),b*(b<0?1:S)),_=w?-1:Math.min(v*(v<0?1:S),x*(x<0?1:S)),D=y?1:Math.max(m*(m>0?1:S),b*(b>0?1:S)),I=k?1:Math.max(v*(v>0?1:S),x*(x>0?1:S)),P=.5*(D-C),A=.5*(I-_);u=Math.min(s/P,l/A),d={x:-.5*(D+C),y:-.5*(I+_)}}i.borderWidth=e.getMaxBorderWidth(c.data),i.outerRadius=Math.max((u-i.borderWidth)/2,0),i.innerRadius=Math.max(h?i.outerRadius/100*h:0,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),i.offsetX=d.x*i.outerRadius,i.offsetY=d.y*i.outerRadius,c.total=e.calculateTotal(),e.outerRadius=i.outerRadius-i.radiusLength*e.getRingIndex(e.index),e.innerRadius=Math.max(e.outerRadius-i.radiusLength,0),o.each(c.data,function(i,n){e.updateElement(i,n,t)})},updateElement:function(t,e,i){var n=this,a=n.chart,r=a.chartArea,s=a.options,l=s.animation,u=(r.left+r.right)/2,d=(r.top+r.bottom)/2,c=s.rotation,h=s.rotation,f=n.getDataset(),g=i&&l.animateRotate?0:t.hidden?0:n.calculateCircumference(f.data[e])*(s.circumference/(2*Math.PI)),p=i&&l.animateScale?0:n.innerRadius,m=i&&l.animateScale?0:n.outerRadius,v=o.valueAtIndexOrDefault;o.extend(t,{_datasetIndex:n.index,_index:e,_model:{x:u+a.offsetX,y:d+a.offsetY,startAngle:c,endAngle:h,circumference:g,outerRadius:m,innerRadius:p,label:v(f.label,e,a.data.labels[e])}});var b=t._model;this.removeHoverStyle(t),i&&l.animateRotate||(b.startAngle=0===e?s.rotation:n.getMeta().data[e-1]._model.endAngle,b.endAngle=b.startAngle+b.circumference),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},calculateTotal:function(){var t,e=this.getDataset(),i=this.getMeta(),n=0;return o.each(i.data,function(i,a){t=e.data[a],isNaN(t)||i.hidden||(n+=Math.abs(t))}),n},calculateCircumference:function(t){var e=this.getMeta().total;return e>0&&!isNaN(t)?2*Math.PI*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){for(var e,i,n=0,a=this.index,o=t.length,r=0;r<o;r++)e=t[r]._model?t[r]._model.borderWidth:0,n=(i=t[r]._chart?t[r]._chart.config.data.datasets[a].hoverBorderWidth:0)>(n=e>n?e:n)?i:n;return n}})}},{25:25,40:40,45:45}],18:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("line",{showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}}),e.exports=function(t){function e(t,e){return o.valueOrDefault(t.showLine,e.showLines)}t.controllers.line=t.DatasetController.extend({datasetElementType:a.Line,dataElementType:a.Point,update:function(t){var i,n,a,r=this,s=r.getMeta(),l=s.dataset,u=s.data||[],d=r.chart.options,c=d.elements.line,h=r.getScaleForId(s.yAxisID),f=r.getDataset(),g=e(f,d);for(g&&(a=l.custom||{},void 0!==f.tension&&void 0===f.lineTension&&(f.lineTension=f.tension),l._scale=h,l._datasetIndex=r.index,l._children=u,l._model={spanGaps:f.spanGaps?f.spanGaps:d.spanGaps,tension:a.tension?a.tension:o.valueOrDefault(f.lineTension,c.tension),backgroundColor:a.backgroundColor?a.backgroundColor:f.backgroundColor||c.backgroundColor,borderWidth:a.borderWidth?a.borderWidth:f.borderWidth||c.borderWidth,borderColor:a.borderColor?a.borderColor:f.borderColor||c.borderColor,borderCapStyle:a.borderCapStyle?a.borderCapStyle:f.borderCapStyle||c.borderCapStyle,borderDash:a.borderDash?a.borderDash:f.borderDash||c.borderDash,borderDashOffset:a.borderDashOffset?a.borderDashOffset:f.borderDashOffset||c.borderDashOffset,borderJoinStyle:a.borderJoinStyle?a.borderJoinStyle:f.borderJoinStyle||c.borderJoinStyle,fill:a.fill?a.fill:void 0!==f.fill?f.fill:c.fill,steppedLine:a.steppedLine?a.steppedLine:o.valueOrDefault(f.steppedLine,c.stepped),cubicInterpolationMode:a.cubicInterpolationMode?a.cubicInterpolationMode:o.valueOrDefault(f.cubicInterpolationMode,c.cubicInterpolationMode)},l.pivot()),i=0,n=u.length;i<n;++i)r.updateElement(u[i],i,t);for(g&&0!==l._model.tension&&r.updateBezierControlPoints(),i=0,n=u.length;i<n;++i)u[i].pivot()},getPointBackgroundColor:function(t,e){var i=this.chart.options.elements.point.backgroundColor,n=this.getDataset(),a=t.custom||{};return a.backgroundColor?i=a.backgroundColor:n.pointBackgroundColor?i=o.valueAtIndexOrDefault(n.pointBackgroundColor,e,i):n.backgroundColor&&(i=n.backgroundColor),i},getPointBorderColor:function(t,e){var i=this.chart.options.elements.point.borderColor,n=this.getDataset(),a=t.custom||{};return a.borderColor?i=a.borderColor:n.pointBorderColor?i=o.valueAtIndexOrDefault(n.pointBorderColor,e,i):n.borderColor&&(i=n.borderColor),i},getPointBorderWidth:function(t,e){var i=this.chart.options.elements.point.borderWidth,n=this.getDataset(),a=t.custom||{};return isNaN(a.borderWidth)?!isNaN(n.pointBorderWidth)||o.isArray(n.pointBorderWidth)?i=o.valueAtIndexOrDefault(n.pointBorderWidth,e,i):isNaN(n.borderWidth)||(i=n.borderWidth):i=a.borderWidth,i},updateElement:function(t,e,i){var n,a,r=this,s=r.getMeta(),l=t.custom||{},u=r.getDataset(),d=r.index,c=u.data[e],h=r.getScaleForId(s.yAxisID),f=r.getScaleForId(s.xAxisID),g=r.chart.options.elements.point;void 0!==u.radius&&void 0===u.pointRadius&&(u.pointRadius=u.radius),void 0!==u.hitRadius&&void 0===u.pointHitRadius&&(u.pointHitRadius=u.hitRadius),n=f.getPixelForValue("object"==typeof c?c:NaN,e,d),a=i?h.getBasePixel():r.calculatePointY(c,e,d),t._xScale=f,t._yScale=h,t._datasetIndex=d,t._index=e,t._model={x:n,y:a,skip:l.skip||isNaN(n)||isNaN(a),radius:l.radius||o.valueAtIndexOrDefault(u.pointRadius,e,g.radius),pointStyle:l.pointStyle||o.valueAtIndexOrDefault(u.pointStyle,e,g.pointStyle),backgroundColor:r.getPointBackgroundColor(t,e),borderColor:r.getPointBorderColor(t,e),borderWidth:r.getPointBorderWidth(t,e),tension:s.dataset._model?s.dataset._model.tension:0,steppedLine:!!s.dataset._model&&s.dataset._model.steppedLine,hitRadius:l.hitRadius||o.valueAtIndexOrDefault(u.pointHitRadius,e,g.hitRadius)}},calculatePointY:function(t,e,i){var n,a,o,r=this.chart,s=this.getMeta(),l=this.getScaleForId(s.yAxisID),u=0,d=0;if(l.options.stacked){for(n=0;n<i;n++)if(a=r.data.datasets[n],"line"===(o=r.getDatasetMeta(n)).type&&o.yAxisID===l.id&&r.isDatasetVisible(n)){var c=Number(l.getRightValue(a.data[e]));c<0?d+=c||0:u+=c||0}var h=Number(l.getRightValue(t));return h<0?l.getPixelForValue(d+h):l.getPixelForValue(u+h)}return l.getPixelForValue(t)},updateBezierControlPoints:function(){var t,e,i,n,a=this.getMeta(),r=this.chart.chartArea,s=a.data||[];function l(t,e,i){return Math.max(Math.min(t,i),e)}if(a.dataset._model.spanGaps&&(s=s.filter(function(t){return!t._model.skip})),"monotone"===a.dataset._model.cubicInterpolationMode)o.splineCurveMonotone(s);else for(t=0,e=s.length;t<e;++t)i=s[t]._model,n=o.splineCurve(o.previousItem(s,t)._model,i,o.nextItem(s,t)._model,a.dataset._model.tension),i.controlPointPreviousX=n.previous.x,i.controlPointPreviousY=n.previous.y,i.controlPointNextX=n.next.x,i.controlPointNextY=n.next.y;if(this.chart.options.elements.line.capBezierPoints)for(t=0,e=s.length;t<e;++t)(i=s[t]._model).controlPointPreviousX=l(i.controlPointPreviousX,r.left,r.right),i.controlPointPreviousY=l(i.controlPointPreviousY,r.top,r.bottom),i.controlPointNextX=l(i.controlPointNextX,r.left,r.right),i.controlPointNextY=l(i.controlPointNextY,r.top,r.bottom)},draw:function(){var t=this.chart,i=this.getMeta(),n=i.data||[],a=t.chartArea,r=n.length,s=0;for(o.canvas.clipArea(t.ctx,a),e(this.getDataset(),t.options)&&i.dataset.draw(),o.canvas.unclipArea(t.ctx);s<r;++s)n[s].draw(a)},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,n=t.custom||{},a=t._model;a.radius=n.hoverRadius||o.valueAtIndexOrDefault(e.pointHoverRadius,i,this.chart.options.elements.point.hoverRadius),a.backgroundColor=n.hoverBackgroundColor||o.valueAtIndexOrDefault(e.pointHoverBackgroundColor,i,o.getHoverColor(a.backgroundColor)),a.borderColor=n.hoverBorderColor||o.valueAtIndexOrDefault(e.pointHoverBorderColor,i,o.getHoverColor(a.borderColor)),a.borderWidth=n.hoverBorderWidth||o.valueAtIndexOrDefault(e.pointHoverBorderWidth,i,a.borderWidth)},removeHoverStyle:function(t){var e=this,i=e.chart.data.datasets[t._datasetIndex],n=t._index,a=t.custom||{},r=t._model;void 0!==i.radius&&void 0===i.pointRadius&&(i.pointRadius=i.radius),r.radius=a.radius||o.valueAtIndexOrDefault(i.pointRadius,n,e.chart.options.elements.point.radius),r.backgroundColor=e.getPointBackgroundColor(t,n),r.borderColor=e.getPointBorderColor(t,n),r.borderWidth=e.getPointBorderWidth(t,n)}})}},{25:25,40:40,45:45}],19:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("polarArea",{scale:{type:"radialLinear",angleLines:{display:!1},gridLines:{circular:!0},pointLabels:{display:!1},ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,legendCallback:function(t){var e=[];e.push('<ul class="'+t.id+'-legend">');var i=t.data,n=i.datasets,a=i.labels;if(n.length)for(var o=0;o<n[0].data.length;++o)e.push('<li><span style="background-color:'+n[0].backgroundColor[o]+'"></span>'),a[o]&&e.push(a[o]),e.push("</li>");return e.push("</ul>"),e.join("")},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(function(i,n){var a=t.getDatasetMeta(0),r=e.datasets[0],s=a.data[n].custom||{},l=o.valueAtIndexOrDefault,u=t.options.elements.arc;return{text:i,fillStyle:s.backgroundColor?s.backgroundColor:l(r.backgroundColor,n,u.backgroundColor),strokeStyle:s.borderColor?s.borderColor:l(r.borderColor,n,u.borderColor),lineWidth:s.borderWidth?s.borderWidth:l(r.borderWidth,n,u.borderWidth),hidden:isNaN(r.data[n])||a.data[n].hidden,index:n}}):[]}},onClick:function(t,e){var i,n,a,o=e.index,r=this.chart;for(i=0,n=(r.data.datasets||[]).length;i<n;++i)(a=r.getDatasetMeta(i)).data[o].hidden=!a.data[o].hidden;r.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}}),e.exports=function(t){t.controllers.polarArea=t.DatasetController.extend({dataElementType:a.Arc,linkScales:o.noop,update:function(t){var e=this,i=e.chart,n=i.chartArea,a=e.getMeta(),r=i.options,s=r.elements.arc,l=Math.min(n.right-n.left,n.bottom-n.top);i.outerRadius=Math.max((l-s.borderWidth/2)/2,0),i.innerRadius=Math.max(r.cutoutPercentage?i.outerRadius/100*r.cutoutPercentage:1,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),e.outerRadius=i.outerRadius-i.radiusLength*e.index,e.innerRadius=e.outerRadius-i.radiusLength,a.count=e.countVisibleElements(),o.each(a.data,function(i,n){e.updateElement(i,n,t)})},updateElement:function(t,e,i){for(var n=this,a=n.chart,r=n.getDataset(),s=a.options,l=s.animation,u=a.scale,d=a.data.labels,c=n.calculateCircumference(r.data[e]),h=u.xCenter,f=u.yCenter,g=0,p=n.getMeta(),m=0;m<e;++m)isNaN(r.data[m])||p.data[m].hidden||++g;var v=s.startAngle,b=t.hidden?0:u.getDistanceFromCenterForValue(r.data[e]),x=v+c*g,y=x+(t.hidden?0:c),k=l.animateScale?0:u.getDistanceFromCenterForValue(r.data[e]);o.extend(t,{_datasetIndex:n.index,_index:e,_scale:u,_model:{x:h,y:f,innerRadius:0,outerRadius:i?k:b,startAngle:i&&l.animateRotate?v:x,endAngle:i&&l.animateRotate?v:y,label:o.valueAtIndexOrDefault(d,e,d[e])}}),n.removeHoverStyle(t),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},countVisibleElements:function(){var t=this.getDataset(),e=this.getMeta(),i=0;return o.each(e.data,function(e,n){isNaN(t.data[n])||e.hidden||i++}),i},calculateCircumference:function(t){var e=this.getMeta().count;return e>0&&!isNaN(t)?2*Math.PI/e:0}})}},{25:25,40:40,45:45}],20:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("radar",{scale:{type:"radialLinear"},elements:{line:{tension:0}}}),e.exports=function(t){t.controllers.radar=t.DatasetController.extend({datasetElementType:a.Line,dataElementType:a.Point,linkScales:o.noop,update:function(t){var e=this,i=e.getMeta(),n=i.dataset,a=i.data,r=n.custom||{},s=e.getDataset(),l=e.chart.options.elements.line,u=e.chart.scale;void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),o.extend(i.dataset,{_datasetIndex:e.index,_scale:u,_children:a,_loop:!0,_model:{tension:r.tension?r.tension:o.valueOrDefault(s.lineTension,l.tension),backgroundColor:r.backgroundColor?r.backgroundColor:s.backgroundColor||l.backgroundColor,borderWidth:r.borderWidth?r.borderWidth:s.borderWidth||l.borderWidth,borderColor:r.borderColor?r.borderColor:s.borderColor||l.borderColor,fill:r.fill?r.fill:void 0!==s.fill?s.fill:l.fill,borderCapStyle:r.borderCapStyle?r.borderCapStyle:s.borderCapStyle||l.borderCapStyle,borderDash:r.borderDash?r.borderDash:s.borderDash||l.borderDash,borderDashOffset:r.borderDashOffset?r.borderDashOffset:s.borderDashOffset||l.borderDashOffset,borderJoinStyle:r.borderJoinStyle?r.borderJoinStyle:s.borderJoinStyle||l.borderJoinStyle}}),i.dataset.pivot(),o.each(a,function(i,n){e.updateElement(i,n,t)},e),e.updateBezierControlPoints()},updateElement:function(t,e,i){var n=this,a=t.custom||{},r=n.getDataset(),s=n.chart.scale,l=n.chart.options.elements.point,u=s.getPointPositionForValue(e,r.data[e]);void 0!==r.radius&&void 0===r.pointRadius&&(r.pointRadius=r.radius),void 0!==r.hitRadius&&void 0===r.pointHitRadius&&(r.pointHitRadius=r.hitRadius),o.extend(t,{_datasetIndex:n.index,_index:e,_scale:s,_model:{x:i?s.xCenter:u.x,y:i?s.yCenter:u.y,tension:a.tension?a.tension:o.valueOrDefault(r.lineTension,n.chart.options.elements.line.tension),radius:a.radius?a.radius:o.valueAtIndexOrDefault(r.pointRadius,e,l.radius),backgroundColor:a.backgroundColor?a.backgroundColor:o.valueAtIndexOrDefault(r.pointBackgroundColor,e,l.backgroundColor),borderColor:a.borderColor?a.borderColor:o.valueAtIndexOrDefault(r.pointBorderColor,e,l.borderColor),borderWidth:a.borderWidth?a.borderWidth:o.valueAtIndexOrDefault(r.pointBorderWidth,e,l.borderWidth),pointStyle:a.pointStyle?a.pointStyle:o.valueAtIndexOrDefault(r.pointStyle,e,l.pointStyle),hitRadius:a.hitRadius?a.hitRadius:o.valueAtIndexOrDefault(r.pointHitRadius,e,l.hitRadius)}}),t._model.skip=a.skip?a.skip:isNaN(t._model.x)||isNaN(t._model.y)},updateBezierControlPoints:function(){var t=this.chart.chartArea,e=this.getMeta();o.each(e.data,function(i,n){var a=i._model,r=o.splineCurve(o.previousItem(e.data,n,!0)._model,a,o.nextItem(e.data,n,!0)._model,a.tension);a.controlPointPreviousX=Math.max(Math.min(r.previous.x,t.right),t.left),a.controlPointPreviousY=Math.max(Math.min(r.previous.y,t.bottom),t.top),a.controlPointNextX=Math.max(Math.min(r.next.x,t.right),t.left),a.controlPointNextY=Math.max(Math.min(r.next.y,t.bottom),t.top),i.pivot()})},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},n=t._index,a=t._model;a.radius=i.hoverRadius?i.hoverRadius:o.valueAtIndexOrDefault(e.pointHoverRadius,n,this.chart.options.elements.point.hoverRadius),a.backgroundColor=i.hoverBackgroundColor?i.hoverBackgroundColor:o.valueAtIndexOrDefault(e.pointHoverBackgroundColor,n,o.getHoverColor(a.backgroundColor)),a.borderColor=i.hoverBorderColor?i.hoverBorderColor:o.valueAtIndexOrDefault(e.pointHoverBorderColor,n,o.getHoverColor(a.borderColor)),a.borderWidth=i.hoverBorderWidth?i.hoverBorderWidth:o.valueAtIndexOrDefault(e.pointHoverBorderWidth,n,a.borderWidth)},removeHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},n=t._index,a=t._model,r=this.chart.options.elements.point;a.radius=i.radius?i.radius:o.valueAtIndexOrDefault(e.pointRadius,n,r.radius),a.backgroundColor=i.backgroundColor?i.backgroundColor:o.valueAtIndexOrDefault(e.pointBackgroundColor,n,r.backgroundColor),a.borderColor=i.borderColor?i.borderColor:o.valueAtIndexOrDefault(e.pointBorderColor,n,r.borderColor),a.borderWidth=i.borderWidth?i.borderWidth:o.valueAtIndexOrDefault(e.pointBorderWidth,n,r.borderWidth)}})}},{25:25,40:40,45:45}],21:[function(t,e,i){"use strict";t(25)._set("scatter",{hover:{mode:"single"},scales:{xAxes:[{id:"x-axis-1",type:"linear",position:"bottom"}],yAxes:[{id:"y-axis-1",type:"linear",position:"left"}]},showLines:!1,tooltips:{callbacks:{title:function(){return""},label:function(t){return"("+t.xLabel+", "+t.yLabel+")"}}}}),e.exports=function(t){t.controllers.scatter=t.controllers.line}},{25:25}],22:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45);n._set("global",{animation:{duration:1e3,easing:"easeOutQuart",onProgress:o.noop,onComplete:o.noop}}),e.exports=function(t){t.Animation=a.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),t.animationService={frameDuration:17,animations:[],dropFrames:0,request:null,addAnimation:function(t,e,i,n){var a,o,r=this.animations;for(e.chart=t,n||(t.animating=!0),a=0,o=r.length;a<o;++a)if(r[a].chart===t)return void(r[a]=e);r.push(e),1===r.length&&this.requestAnimationFrame()},cancelAnimation:function(t){var e=o.findIndex(this.animations,function(e){return e.chart===t});-1!==e&&(this.animations.splice(e,1),t.animating=!1)},requestAnimationFrame:function(){var t=this;null===t.request&&(t.request=o.requestAnimFrame.call(window,function(){t.request=null,t.startDigest()}))},startDigest:function(){var t=this,e=Date.now(),i=0;t.dropFrames>1&&(i=Math.floor(t.dropFrames),t.dropFrames=t.dropFrames%1),t.advance(1+i);var n=Date.now();t.dropFrames+=(n-e)/t.frameDuration,t.animations.length>0&&t.requestAnimationFrame()},advance:function(t){for(var e,i,n=this.animations,a=0;a<n.length;)i=(e=n[a]).chart,e.currentStep=(e.currentStep||0)+t,e.currentStep=Math.min(e.currentStep,e.numSteps),o.callback(e.render,[i,e],i),o.callback(e.onAnimationProgress,[e],i),e.currentStep>=e.numSteps?(o.callback(e.onAnimationComplete,[e],i),i.animating=!1,n.splice(a,1)):++a}},Object.defineProperty(t.Animation.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(t.Animation.prototype,"chartInstance",{get:function(){return this.chart},set:function(t){this.chart=t}})}},{25:25,26:26,45:45}],23:[function(t,e,i){"use strict";var n=t(25),a=t(45),o=t(28),r=t(30),s=t(48),l=t(31);e.exports=function(t){function e(t){return"top"===t||"bottom"===t}t.types={},t.instances={},t.controllers={},a.extend(t.prototype,{construct:function(e,i){var o,r,l=this;(r=(o=(o=i)||{}).data=o.data||{}).datasets=r.datasets||[],r.labels=r.labels||[],o.options=a.configMerge(n.global,n[o.type],o.options||{}),i=o;var u=s.acquireContext(e,i),d=u&&u.canvas,c=d&&d.height,h=d&&d.width;l.id=a.uid(),l.ctx=u,l.canvas=d,l.config=i,l.width=h,l.height=c,l.aspectRatio=c?h/c:null,l.options=i.options,l._bufferedRender=!1,l.chart=l,l.controller=l,t.instances[l.id]=l,Object.defineProperty(l,"data",{get:function(){return l.config.data},set:function(t){l.config.data=t}}),u&&d?(l.initialize(),l.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return l.notify(t,"beforeInit"),a.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.initToolTip(),l.notify(t,"afterInit"),t},clear:function(){return a.canvas.clear(this),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(t){var e=this,i=e.options,n=e.canvas,o=i.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(a.getMaximumWidth(n))),s=Math.max(0,Math.floor(o?r/o:a.getMaximumHeight(n)));if((e.width!==r||e.height!==s)&&(n.width=e.width=r,n.height=e.height=s,n.style.width=r+"px",n.style.height=s+"px",a.retinaScale(e,i.devicePixelRatio),!t)){var u={width:r,height:s};l.notify(e,"resize",[u]),e.options.onResize&&e.options.onResize(e,u),e.stop(),e.update(e.options.responsiveAnimationDuration)}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},i=t.scale;a.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),a.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildOrUpdateScales:function(){var i=this,n=i.options,o=i.scales||{},r=[],s=Object.keys(o).reduce(function(t,e){return t[e]=!1,t},{});n.scales&&(r=r.concat((n.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category",dposition:"bottom"}}),(n.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear",dposition:"left"}}))),n.scale&&r.push({options:n.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),a.each(r,function(n){var r=n.options,l=r.id,u=a.valueOrDefault(r.type,n.dtype);e(r.position)!==e(n.dposition)&&(r.position=n.dposition),s[l]=!0;var d=null;if(l in o&&o[l].type===u)(d=o[l]).options=r,d.ctx=i.ctx,d.chart=i;else{var c=t.scaleService.getScaleConstructor(u);if(!c)return;d=new c({id:l,type:u,options:r,ctx:i.ctx,chart:i}),o[d.id]=d}d.mergeTicksOptions(),n.isDefault&&(i.scale=d)}),a.each(s,function(t,e){t||delete o[e]}),i.scales=o,t.scaleService.addScalesToLayout(this)},buildOrUpdateControllers:function(){var e=this,i=[],n=[];return a.each(e.data.datasets,function(a,o){var r=e.getDatasetMeta(o),s=a.type||e.config.type;if(r.type&&r.type!==s&&(e.destroyDatasetMeta(o),r=e.getDatasetMeta(o)),r.type=s,i.push(r.type),r.controller)r.controller.updateIndex(o),r.controller.linkScales();else{var l=t.controllers[r.type];if(void 0===l)throw new Error('"'+r.type+'" is not a chart type.');r.controller=new l(e,o),n.push(r.controller)}},e),n},resetElements:function(){var t=this;a.each(t.data.datasets,function(e,i){t.getDatasetMeta(i).controller.reset()},t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(e){var i,n,o=this;if(e&&"object"==typeof e||(e={duration:e,lazy:arguments[1]}),n=(i=o).options,a.each(i.scales,function(t){r.removeBox(i,t)}),n=a.configMerge(t.defaults.global,t.defaults[i.config.type],n),i.options=i.config.options=n,i.ensureScalesHaveIDs(),i.buildOrUpdateScales(),i.tooltip._options=n.tooltips,i.tooltip.initialize(),l._invalidate(o),!1!==l.notify(o,"beforeUpdate")){o.tooltip._data=o.data;var s=o.buildOrUpdateControllers();a.each(o.data.datasets,function(t,e){o.getDatasetMeta(e).controller.buildOrUpdateElements()},o),o.updateLayout(),o.options.animation&&o.options.animation.duration&&a.each(s,function(t){t.reset()}),o.updateDatasets(),o.tooltip.initialize(),o.lastActive=[],l.notify(o,"afterUpdate"),o._bufferedRender?o._bufferedRequest={duration:e.duration,easing:e.easing,lazy:e.lazy}:o.render(e)}},updateLayout:function(){!1!==l.notify(this,"beforeLayout")&&(r.update(this,this.width,this.height),l.notify(this,"afterScaleUpdate"),l.notify(this,"afterLayout"))},updateDatasets:function(){if(!1!==l.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t<e;++t)this.updateDataset(t);l.notify(this,"afterDatasetsUpdate")}},updateDataset:function(t){var e=this.getDatasetMeta(t),i={meta:e,index:t};!1!==l.notify(this,"beforeDatasetUpdate",[i])&&(e.controller.update(),l.notify(this,"afterDatasetUpdate",[i]))},render:function(e){var i=this;e&&"object"==typeof e||(e={duration:e,lazy:arguments[1]});var n=e.duration,o=e.lazy;if(!1!==l.notify(i,"beforeRender")){var r=i.options.animation,s=function(t){l.notify(i,"afterRender"),a.callback(r&&r.onComplete,[t],i)};if(r&&(void 0!==n&&0!==n||void 0===n&&0!==r.duration)){var u=new t.Animation({numSteps:(n||r.duration)/16.66,easing:e.easing||r.easing,render:function(t,e){var i=a.easing.effects[e.easing],n=e.currentStep,o=n/e.numSteps;t.draw(i(o),o,n)},onAnimationProgress:r.onProgress,onAnimationComplete:s});t.animationService.addAnimation(i,u,n,o)}else i.draw(),s(new t.Animation({numSteps:0,chart:i}));return i}},draw:function(t){var e=this;e.clear(),a.isNullOrUndef(t)&&(t=1),e.transition(t),!1!==l.notify(e,"beforeDraw",[t])&&(a.each(e.boxes,function(t){t.draw(e.chartArea)},e),e.scale&&e.scale.draw(),e.drawDatasets(t),e._drawTooltip(t),l.notify(e,"afterDraw",[t]))},transition:function(t){for(var e=0,i=(this.data.datasets||[]).length;e<i;++e)this.isDatasetVisible(e)&&this.getDatasetMeta(e).controller.transition(t);this.tooltip.transition(t)},drawDatasets:function(t){var e=this;if(!1!==l.notify(e,"beforeDatasetsDraw",[t])){for(var i=(e.data.datasets||[]).length-1;i>=0;--i)e.isDatasetVisible(i)&&e.drawDataset(i,t);l.notify(e,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var i=this.getDatasetMeta(t),n={meta:i,index:t,easingValue:e};!1!==l.notify(this,"beforeDatasetDraw",[n])&&(i.controller.draw(e),l.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,i={tooltip:e,easingValue:t};!1!==l.notify(this,"beforeTooltipDraw",[i])&&(e.draw(),l.notify(this,"afterTooltipDraw",[i]))},getElementAtEvent:function(t){return o.modes.single(this,t)},getElementsAtEvent:function(t){return o.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return o.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,i){var n=o.modes[e];return"function"==typeof n?n(this,t,i):[]},getDatasetAtEvent:function(t){return o.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var i=e._meta[this.id];return i||(i=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null}),i},getVisibleDatasetCount:function(){for(var t=0,e=0,i=this.data.datasets.length;e<i;++e)this.isDatasetVisible(e)&&t++;return t},isDatasetVisible:function(t){var e=this.getDatasetMeta(t);return"boolean"==typeof e.hidden?!e.hidden:!this.data.datasets[t].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroyDatasetMeta:function(t){var e=this.id,i=this.data.datasets[t],n=i._meta&&i._meta[e];n&&(n.controller.destroy(),delete i._meta[e])},destroy:function(){var e,i,n=this,o=n.canvas;for(n.stop(),e=0,i=n.data.datasets.length;e<i;++e)n.destroyDatasetMeta(e);o&&(n.unbindEvents(),a.canvas.clear(n),s.releaseContext(n.ctx),n.canvas=null,n.ctx=null),l.notify(n,"destroy"),delete t.instances[n.id]},toBase64Image:function(){return this.canvas.toDataURL.apply(this.canvas,arguments)},initToolTip:function(){var e=this;e.tooltip=new t.Tooltip({_chart:e,_chartInstance:e,_data:e.data,_options:e.options.tooltips},e)},bindEvents:function(){var t=this,e=t._listeners={},i=function(){t.eventHandler.apply(t,arguments)};a.each(t.options.events,function(n){s.addEventListener(t,n,i),e[n]=i}),t.options.responsive&&(i=function(){t.resize()},s.addEventListener(t,"resize",i),e.resize=i)},unbindEvents:function(){var t=this,e=t._listeners;e&&(delete t._listeners,a.each(e,function(e,i){s.removeEventListener(t,i,e)}))},updateHoverStyle:function(t,e,i){var n,a,o,r=i?"setHoverStyle":"removeHoverStyle";for(a=0,o=t.length;a<o;++a)(n=t[a])&&this.getDatasetMeta(n._datasetIndex).controller[r](n)},eventHandler:function(t){var e=this,i=e.tooltip;if(!1!==l.notify(e,"beforeEvent",[t])){e._bufferedRender=!0,e._bufferedRequest=null;var n=e.handleEvent(t);i&&(n=i._start?i.handleEvent(t):n|i.handleEvent(t)),l.notify(e,"afterEvent",[t]);var a=e._bufferedRequest;return a?e.render(a):n&&!e.animating&&(e.stop(),e.render(e.options.hover.animationDuration,!0)),e._bufferedRender=!1,e._bufferedRequest=null,e}},handleEvent:function(t){var e,i=this,n=i.options||{},o=n.hover;return i.lastActive=i.lastActive||[],"mouseout"===t.type?i.active=[]:i.active=i.getElementsAtEventForMode(t,o.mode,o),a.callback(n.onHover||n.hover.onHover,[t.native,i.active],i),"mouseup"!==t.type&&"click"!==t.type||n.onClick&&n.onClick.call(i,t.native,i.active),i.lastActive.length&&i.updateHoverStyle(i.lastActive,o.mode,!1),i.active.length&&o.mode&&i.updateHoverStyle(i.active,o.mode,!0),e=!a.arrayEquals(i.active,i.lastActive),i.lastActive=i.active,e}}),t.Controller=t}},{25:25,28:28,30:30,31:31,45:45,48:48}],24:[function(t,e,i){"use strict";var n=t(45);e.exports=function(t){var e=["push","pop","shift","splice","unshift"];function i(t,i){var n=t._chartjs;if(n){var a=n.listeners,o=a.indexOf(i);-1!==o&&a.splice(o,1),a.length>0||(e.forEach(function(e){delete t[e]}),delete t._chartjs)}}t.DatasetController=function(t,e){this.initialize(t,e)},n.extend(t.DatasetController.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){this.chart=t,this.index=e,this.linkScales(),this.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),i=t.getDataset();null!==e.xAxisID&&e.xAxisID in t.chart.scales||(e.xAxisID=i.xAxisID||t.chart.options.scales.xAxes[0].id),null!==e.yAxisID&&e.yAxisID in t.chart.scales||(e.yAxisID=i.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},reset:function(){this.update(!0)},destroy:function(){this._data&&i(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,i=this.getMeta(),n=this.getDataset().data||[],a=i.data;for(t=0,e=n.length;t<e;++t)a[t]=a[t]||this.createMetaData(t);i.dataset=i.dataset||this.createMetaDataset()},addElementAndReset:function(t){var e=this.createMetaData(t);this.getMeta().data.splice(t,0,e),this.updateElement(e,t,!0)},buildOrUpdateElements:function(){var t,a,o=this,r=o.getDataset(),s=r.data||(r.data=[]);o._data!==s&&(o._data&&i(o._data,o),a=o,(t=s)._chartjs?t._chartjs.listeners.push(a):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[a]}}),e.forEach(function(e){var i="onData"+e.charAt(0).toUpperCase()+e.slice(1),a=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value:function(){var e=Array.prototype.slice.call(arguments),o=a.apply(this,e);return n.each(t._chartjs.listeners,function(t){"function"==typeof t[i]&&t[i].apply(t,e)}),o}})})),o._data=s),o.resyncElements()},update:n.noop,transition:function(t){for(var e=this.getMeta(),i=e.data||[],n=i.length,a=0;a<n;++a)i[a].transition(t);e.dataset&&e.dataset.transition(t)},draw:function(){var t=this.getMeta(),e=t.data||[],i=e.length,n=0;for(t.dataset&&t.dataset.draw();n<i;++n)e[n].draw()},removeHoverStyle:function(t,e){var i=this.chart.data.datasets[t._datasetIndex],a=t._index,o=t.custom||{},r=n.valueAtIndexOrDefault,s=t._model;s.backgroundColor=o.backgroundColor?o.backgroundColor:r(i.backgroundColor,a,e.backgroundColor),s.borderColor=o.borderColor?o.borderColor:r(i.borderColor,a,e.borderColor),s.borderWidth=o.borderWidth?o.borderWidth:r(i.borderWidth,a,e.borderWidth)},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=n.valueAtIndexOrDefault,r=n.getHoverColor,s=t._model;s.backgroundColor=a.hoverBackgroundColor?a.hoverBackgroundColor:o(e.hoverBackgroundColor,i,r(s.backgroundColor)),s.borderColor=a.hoverBorderColor?a.hoverBorderColor:o(e.hoverBorderColor,i,r(s.borderColor)),s.borderWidth=a.hoverBorderWidth?a.hoverBorderWidth:o(e.hoverBorderWidth,i,s.borderWidth)},resyncElements:function(){var t=this.getMeta(),e=this.getDataset().data,i=t.data.length,n=e.length;n<i?t.data.splice(n,i-n):n>i&&this.insertElements(i,n-i)},insertElements:function(t,e){for(var i=0;i<e;++i)this.addElementAndReset(t+i)},onDataPush:function(){this.insertElements(this.getDataset().data.length-1,arguments.length)},onDataPop:function(){this.getMeta().data.pop()},onDataShift:function(){this.getMeta().data.shift()},onDataSplice:function(t,e){this.getMeta().data.splice(t,e),this.insertElements(t,arguments.length-2)},onDataUnshift:function(){this.insertElements(0,arguments.length)}}),t.DatasetController.extend=n.inherits}},{45:45}],25:[function(t,e,i){"use strict";var n=t(45);e.exports={_set:function(t,e){return n.merge(this[t]||(this[t]={}),e)}}},{45:45}],26:[function(t,e,i){"use strict";var n=t(3),a=t(45);var o=function(t){a.extend(this,t),this.initialize.apply(this,arguments)};a.extend(o.prototype,{initialize:function(){this.hidden=!1},pivot:function(){var t=this;return t._view||(t._view=a.clone(t._model)),t._start={},t},transition:function(t){var e=this,i=e._model,a=e._start,o=e._view;return i&&1!==t?(o||(o=e._view={}),a||(a=e._start={}),function(t,e,i,a){var o,r,s,l,u,d,c,h,f,g=Object.keys(i);for(o=0,r=g.length;o<r;++o)if(d=i[s=g[o]],e.hasOwnProperty(s)||(e[s]=d),(l=e[s])!==d&&"_"!==s[0]){if(t.hasOwnProperty(s)||(t[s]=l),(c=typeof d)==typeof(u=t[s]))if("string"===c){if((h=n(u)).valid&&(f=n(d)).valid){e[s]=f.mix(h,a).rgbString();continue}}else if("number"===c&&isFinite(u)&&isFinite(d)){e[s]=u+(d-u)*a;continue}e[s]=d}}(a,o,i,t),e):(e._view=i,e._start=null,e)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return a.isNumber(this._model.x)&&a.isNumber(this._model.y)}}),o.extend=a.inherits,e.exports=o},{3:3,45:45}],27:[function(t,e,i){"use strict";var n=t(3),a=t(25),o=t(45);e.exports=function(t){function e(t,e,i){var n;return"string"==typeof t?(n=parseInt(t,10),-1!==t.indexOf("%")&&(n=n/100*e.parentNode[i])):n=t,n}function i(t){return null!=t&&"none"!==t}function r(t,n,a){var o=document.defaultView,r=t.parentNode,s=o.getComputedStyle(t)[n],l=o.getComputedStyle(r)[n],u=i(s),d=i(l),c=Number.POSITIVE_INFINITY;return u||d?Math.min(u?e(s,t,a):c,d?e(l,r,a):c):"none"}o.configMerge=function(){return o.merge(o.clone(arguments[0]),[].slice.call(arguments,1),{merger:function(e,i,n,a){var r=i[e]||{},s=n[e];"scales"===e?i[e]=o.scaleMerge(r,s):"scale"===e?i[e]=o.merge(r,[t.scaleService.getScaleDefaults(s.type),s]):o._merger(e,i,n,a)}})},o.scaleMerge=function(){return o.merge(o.clone(arguments[0]),[].slice.call(arguments,1),{merger:function(e,i,n,a){if("xAxes"===e||"yAxes"===e){var r,s,l,u=n[e].length;for(i[e]||(i[e]=[]),r=0;r<u;++r)l=n[e][r],s=o.valueOrDefault(l.type,"xAxes"===e?"category":"linear"),r>=i[e].length&&i[e].push({}),!i[e][r].type||l.type&&l.type!==i[e][r].type?o.merge(i[e][r],[t.scaleService.getScaleDefaults(s),l]):o.merge(i[e][r],l)}else o._merger(e,i,n,a)}})},o.where=function(t,e){if(o.isArray(t)&&Array.prototype.filter)return t.filter(e);var i=[];return o.each(t,function(t){e(t)&&i.push(t)}),i},o.findIndex=Array.prototype.findIndex?function(t,e,i){return t.findIndex(e,i)}:function(t,e,i){i=void 0===i?t:i;for(var n=0,a=t.length;n<a;++n)if(e.call(i,t[n],n,t))return n;return-1},o.findNextWhere=function(t,e,i){o.isNullOrUndef(i)&&(i=-1);for(var n=i+1;n<t.length;n++){var a=t[n];if(e(a))return a}},o.findPreviousWhere=function(t,e,i){o.isNullOrUndef(i)&&(i=t.length);for(var n=i-1;n>=0;n--){var a=t[n];if(e(a))return a}},o.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},o.almostEquals=function(t,e,i){return Math.abs(t-e)<i},o.almostWhole=function(t,e){var i=Math.round(t);return i-e<t&&i+e>t},o.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},o.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},o.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},o.log10=Math.log10?function(t){return Math.log10(t)}:function(t){var e=Math.log(t)*Math.LOG10E,i=Math.round(e);return t===Math.pow(10,i)?i:e},o.toRadians=function(t){return t*(Math.PI/180)},o.toDegrees=function(t){return t*(180/Math.PI)},o.getAngleFromPoint=function(t,e){var i=e.x-t.x,n=e.y-t.y,a=Math.sqrt(i*i+n*n),o=Math.atan2(n,i);return o<-.5*Math.PI&&(o+=2*Math.PI),{angle:o,distance:a}},o.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},o.aliasPixel=function(t){return t%2==0?0:.5},o.splineCurve=function(t,e,i,n){var a=t.skip?e:t,o=e,r=i.skip?e:i,s=Math.sqrt(Math.pow(o.x-a.x,2)+Math.pow(o.y-a.y,2)),l=Math.sqrt(Math.pow(r.x-o.x,2)+Math.pow(r.y-o.y,2)),u=s/(s+l),d=l/(s+l),c=n*(u=isNaN(u)?0:u),h=n*(d=isNaN(d)?0:d);return{previous:{x:o.x-c*(r.x-a.x),y:o.y-c*(r.y-a.y)},next:{x:o.x+h*(r.x-a.x),y:o.y+h*(r.y-a.y)}}},o.EPSILON=Number.EPSILON||1e-14,o.splineCurveMonotone=function(t){var e,i,n,a,r,s,l,u,d,c=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),h=c.length;for(e=0;e<h;++e)if(!(n=c[e]).model.skip){if(i=e>0?c[e-1]:null,(a=e<h-1?c[e+1]:null)&&!a.model.skip){var f=a.model.x-n.model.x;n.deltaK=0!==f?(a.model.y-n.model.y)/f:0}!i||i.model.skip?n.mK=n.deltaK:!a||a.model.skip?n.mK=i.deltaK:this.sign(i.deltaK)!==this.sign(n.deltaK)?n.mK=0:n.mK=(i.deltaK+n.deltaK)/2}for(e=0;e<h-1;++e)n=c[e],a=c[e+1],n.model.skip||a.model.skip||(o.almostEquals(n.deltaK,0,this.EPSILON)?n.mK=a.mK=0:(r=n.mK/n.deltaK,s=a.mK/n.deltaK,(u=Math.pow(r,2)+Math.pow(s,2))<=9||(l=3/Math.sqrt(u),n.mK=r*l*n.deltaK,a.mK=s*l*n.deltaK)));for(e=0;e<h;++e)(n=c[e]).model.skip||(i=e>0?c[e-1]:null,a=e<h-1?c[e+1]:null,i&&!i.model.skip&&(d=(n.model.x-i.model.x)/3,n.model.controlPointPreviousX=n.model.x-d,n.model.controlPointPreviousY=n.model.y-d*n.mK),a&&!a.model.skip&&(d=(a.model.x-n.model.x)/3,n.model.controlPointNextX=n.model.x+d,n.model.controlPointNextY=n.model.y+d*n.mK))},o.nextItem=function(t,e,i){return i?e>=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},o.previousItem=function(t,e,i){return i?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},o.niceNum=function(t,e){var i=Math.floor(o.log10(t)),n=t/Math.pow(10,i);return(e?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10)*Math.pow(10,i)},o.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},o.getRelativePosition=function(t,e){var i,n,a=t.originalEvent||t,r=t.currentTarget||t.srcElement,s=r.getBoundingClientRect(),l=a.touches;l&&l.length>0?(i=l[0].clientX,n=l[0].clientY):(i=a.clientX,n=a.clientY);var u=parseFloat(o.getStyle(r,"padding-left")),d=parseFloat(o.getStyle(r,"padding-top")),c=parseFloat(o.getStyle(r,"padding-right")),h=parseFloat(o.getStyle(r,"padding-bottom")),f=s.right-s.left-u-c,g=s.bottom-s.top-d-h;return{x:i=Math.round((i-s.left-u)/f*r.width/e.currentDevicePixelRatio),y:n=Math.round((n-s.top-d)/g*r.height/e.currentDevicePixelRatio)}},o.getConstraintWidth=function(t){return r(t,"max-width","clientWidth")},o.getConstraintHeight=function(t){return r(t,"max-height","clientHeight")},o.getMaximumWidth=function(t){var e=t.parentNode;if(!e)return t.clientWidth;var i=parseInt(o.getStyle(e,"padding-left"),10),n=parseInt(o.getStyle(e,"padding-right"),10),a=e.clientWidth-i-n,r=o.getConstraintWidth(t);return isNaN(r)?a:Math.min(a,r)},o.getMaximumHeight=function(t){var e=t.parentNode;if(!e)return t.clientHeight;var i=parseInt(o.getStyle(e,"padding-top"),10),n=parseInt(o.getStyle(e,"padding-bottom"),10),a=e.clientHeight-i-n,r=o.getConstraintHeight(t);return isNaN(r)?a:Math.min(a,r)},o.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},o.retinaScale=function(t,e){var i=t.currentDevicePixelRatio=e||window.devicePixelRatio||1;if(1!==i){var n=t.canvas,a=t.height,o=t.width;n.height=a*i,n.width=o*i,t.ctx.scale(i,i),n.style.height||n.style.width||(n.style.height=a+"px",n.style.width=o+"px")}},o.fontString=function(t,e,i){return e+" "+t+"px "+i},o.longestText=function(t,e,i,n){var a=(n=n||{}).data=n.data||{},r=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(a=n.data={},r=n.garbageCollect=[],n.font=e),t.font=e;var s=0;o.each(i,function(e){null!=e&&!0!==o.isArray(e)?s=o.measureText(t,a,r,s,e):o.isArray(e)&&o.each(e,function(e){null==e||o.isArray(e)||(s=o.measureText(t,a,r,s,e))})});var l=r.length/2;if(l>i.length){for(var u=0;u<l;u++)delete a[r[u]];r.splice(0,l)}return s},o.measureText=function(t,e,i,n,a){var o=e[a];return o||(o=e[a]=t.measureText(a).width,i.push(a)),o>n&&(n=o),n},o.numberOfLabelLines=function(t){var e=1;return o.each(t,function(t){o.isArray(t)&&t.length>e&&(e=t.length)}),e},o.color=n?function(t){return t instanceof CanvasGradient&&(t=a.global.defaultColor),n(t)}:function(t){return console.error("Color.js not found!"),t},o.getHoverColor=function(t){return t instanceof CanvasPattern?t:o.color(t).saturate(.5).darken(.1).rgbString()}}},{25:25,3:3,45:45}],28:[function(t,e,i){"use strict";var n=t(45);function a(t,e){return t.native?{x:t.x,y:t.y}:n.getRelativePosition(t,e)}function o(t,e){var i,n,a,o,r;for(n=0,o=t.data.datasets.length;n<o;++n)if(t.isDatasetVisible(n))for(a=0,r=(i=t.getDatasetMeta(n)).data.length;a<r;++a){var s=i.data[a];s._view.skip||e(s)}}function r(t,e){var i=[];return o(t,function(t){t.inRange(e.x,e.y)&&i.push(t)}),i}function s(t,e,i,n){var a=Number.POSITIVE_INFINITY,r=[];return o(t,function(t){if(!i||t.inRange(e.x,e.y)){var o=t.getCenterPoint(),s=n(e,o);s<a?(r=[t],a=s):s===a&&r.push(t)}}),r}function l(t){var e=-1!==t.indexOf("x"),i=-1!==t.indexOf("y");return function(t,n){var a=e?Math.abs(t.x-n.x):0,o=i?Math.abs(t.y-n.y):0;return Math.sqrt(Math.pow(a,2)+Math.pow(o,2))}}function u(t,e,i){var n=a(e,t);i.axis=i.axis||"x";var o=l(i.axis),u=i.intersect?r(t,n):s(t,n,!1,o),d=[];return u.length?(t.data.datasets.forEach(function(e,i){if(t.isDatasetVisible(i)){var n=t.getDatasetMeta(i).data[u[0]._index];n&&!n._view.skip&&d.push(n)}}),d):[]}e.exports={modes:{single:function(t,e){var i=a(e,t),n=[];return o(t,function(t){if(t.inRange(i.x,i.y))return n.push(t),n}),n.slice(0,1)},label:u,index:u,dataset:function(t,e,i){var n=a(e,t);i.axis=i.axis||"xy";var o=l(i.axis),u=i.intersect?r(t,n):s(t,n,!1,o);return u.length>0&&(u=t.getDatasetMeta(u[0]._datasetIndex).data),u},"x-axis":function(t,e){return u(t,e,{intersect:!1})},point:function(t,e){return r(t,a(e,t))},nearest:function(t,e,i){var n=a(e,t);i.axis=i.axis||"xy";var o=l(i.axis),r=s(t,n,i.intersect,o);return r.length>1&&r.sort(function(t,e){var i=t.getArea()-e.getArea();return 0===i&&(i=t._datasetIndex-e._datasetIndex),i}),r.slice(0,1)},x:function(t,e,i){var n=a(e,t),r=[],s=!1;return o(t,function(t){t.inXRange(n.x)&&r.push(t),t.inRange(n.x,n.y)&&(s=!0)}),i.intersect&&!s&&(r=[]),r},y:function(t,e,i){var n=a(e,t),r=[],s=!1;return o(t,function(t){t.inYRange(n.y)&&r.push(t),t.inRange(n.x,n.y)&&(s=!0)}),i.intersect&&!s&&(r=[]),r}}}},{45:45}],29:[function(t,e,i){"use strict";t(25)._set("global",{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",showLines:!0,elements:{},layout:{padding:{top:0,right:0,bottom:0,left:0}}}),e.exports=function(){var t=function(t,e){return this.construct(t,e),this};return t.Chart=t,t}},{25:25}],30:[function(t,e,i){"use strict";var n=t(45);function a(t,e){return n.where(t,function(t){return t.position===e})}function o(t,e){t.forEach(function(t,e){return t._tmpIndex_=e,t}),t.sort(function(t,i){var n=e?i:t,a=e?t:i;return n.weight===a.weight?n._tmpIndex_-a._tmpIndex_:n.weight-a.weight}),t.forEach(function(t){delete t._tmpIndex_})}e.exports={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,t.boxes.push(e)},removeBox:function(t,e){var i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure:function(t,e,i){for(var n,a=["fullWidth","position","weight"],o=a.length,r=0;r<o;++r)n=a[r],i.hasOwnProperty(n)&&(e[n]=i[n])},update:function(t,e,i){if(t){var r=t.options.layout||{},s=n.options.toPadding(r.padding),l=s.left,u=s.right,d=s.top,c=s.bottom,h=a(t.boxes,"left"),f=a(t.boxes,"right"),g=a(t.boxes,"top"),p=a(t.boxes,"bottom"),m=a(t.boxes,"chartArea");o(h,!0),o(f,!1),o(g,!0),o(p,!1);var v=e-l-u,b=i-d-c,x=b/2,y=(e-v/2)/(h.length+f.length),k=(i-x)/(g.length+p.length),M=v,w=b,S=[];n.each(h.concat(f,g,p),function(t){var e,i=t.isHorizontal();i?(e=t.update(t.fullWidth?v:M,k),w-=e.height):(e=t.update(y,w),M-=e.width),S.push({horizontal:i,minSize:e,box:t})});var C=0,_=0,D=0,I=0;n.each(g.concat(p),function(t){if(t.getPadding){var e=t.getPadding();C=Math.max(C,e.left),_=Math.max(_,e.right)}}),n.each(h.concat(f),function(t){if(t.getPadding){var e=t.getPadding();D=Math.max(D,e.top),I=Math.max(I,e.bottom)}});var P=l,A=u,T=d,F=c;n.each(h.concat(f),N),n.each(h,function(t){P+=t.width}),n.each(f,function(t){A+=t.width}),n.each(g.concat(p),N),n.each(g,function(t){T+=t.height}),n.each(p,function(t){F+=t.height}),n.each(h.concat(f),function(t){var e=n.findNextWhere(S,function(e){return e.box===t}),i={left:0,right:0,top:T,bottom:F};e&&t.update(e.minSize.width,w,i)}),P=l,A=u,T=d,F=c,n.each(h,function(t){P+=t.width}),n.each(f,function(t){A+=t.width}),n.each(g,function(t){T+=t.height}),n.each(p,function(t){F+=t.height});var O=Math.max(C-P,0);P+=O,A+=Math.max(_-A,0);var R=Math.max(D-T,0);T+=R,F+=Math.max(I-F,0);var L=i-T-F,z=e-P-A;z===M&&L===w||(n.each(h,function(t){t.height=L}),n.each(f,function(t){t.height=L}),n.each(g,function(t){t.fullWidth||(t.width=z)}),n.each(p,function(t){t.fullWidth||(t.width=z)}),w=L,M=z);var B=l+O,W=d+R;n.each(h.concat(g),V),B+=M,W+=w,n.each(f,V),n.each(p,V),t.chartArea={left:P,top:T,right:P+M,bottom:T+w},n.each(m,function(e){e.left=t.chartArea.left,e.top=t.chartArea.top,e.right=t.chartArea.right,e.bottom=t.chartArea.bottom,e.update(M,w)})}function N(t){var e=n.findNextWhere(S,function(e){return e.box===t});if(e)if(t.isHorizontal()){var i={left:Math.max(P,C),right:Math.max(A,_),top:0,bottom:0};t.update(t.fullWidth?v:M,b/2,i)}else t.update(e.minSize.width,w)}function V(t){t.isHorizontal()?(t.left=t.fullWidth?l:P,t.right=t.fullWidth?e-u:P+M,t.top=W,t.bottom=W+t.height,W=t.bottom):(t.left=B,t.right=B+t.width,t.top=T,t.bottom=T+w,B=t.right)}}}},{45:45}],31:[function(t,e,i){"use strict";var n=t(25),a=t(45);n._set("global",{plugins:{}}),e.exports={_plugins:[],_cacheId:0,register:function(t){var e=this._plugins;[].concat(t).forEach(function(t){-1===e.indexOf(t)&&e.push(t)}),this._cacheId++},unregister:function(t){var e=this._plugins;[].concat(t).forEach(function(t){var i=e.indexOf(t);-1!==i&&e.splice(i,1)}),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e,i){var n,a,o,r,s,l=this.descriptors(t),u=l.length;for(n=0;n<u;++n)if("function"==typeof(s=(o=(a=l[n]).plugin)[e])&&((r=[t].concat(i||[])).push(a.options),!1===s.apply(o,r)))return!1;return!0},descriptors:function(t){var e=t.$plugins||(t.$plugins={});if(e.id===this._cacheId)return e.descriptors;var i=[],o=[],r=t&&t.config||{},s=r.options&&r.options.plugins||{};return this._plugins.concat(r.plugins||[]).forEach(function(t){if(-1===i.indexOf(t)){var e=t.id,r=s[e];!1!==r&&(!0===r&&(r=a.clone(n.global.plugins[e])),i.push(t),o.push({plugin:t,options:r||{}}))}}),e.descriptors=o,e.id=this._cacheId,o},_invalidate:function(t){delete t.$plugins}}},{25:25,45:45}],32:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45),r=t(34);function s(t){var e,i,n=[];for(e=0,i=t.length;e<i;++e)n.push(t[e].label);return n}function l(t,e,i){var n=t.getPixelForTick(e);return i&&(n-=0===e?(t.getPixelForTick(1)-n)/2:(n-t.getPixelForTick(e-1))/2),n}n._set("scale",{display:!0,position:"left",offset:!1,gridLines:{display:!0,color:"rgba(0, 0, 0, 0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",zeroLineBorderDash:[],zeroLineBorderDashOffset:0,offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{display:!1,labelString:"",lineHeight:1.2,padding:{top:4,bottom:4}},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:0,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:r.formatters.values,minor:{},major:{}}}),e.exports=function(t){function e(t,e,i){return o.isArray(e)?o.longestText(t,i,e):t.measureText(e).width}function i(t){var e=o.valueOrDefault,i=n.global,a=e(t.fontSize,i.defaultFontSize),r=e(t.fontStyle,i.defaultFontStyle),s=e(t.fontFamily,i.defaultFontFamily);return{size:a,style:r,family:s,font:o.fontString(a,r,s)}}function r(t){return o.options.toLineHeight(o.valueOrDefault(t.lineHeight,1.2),o.valueOrDefault(t.fontSize,n.global.defaultFontSize))}t.Scale=a.extend({getPadding:function(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}},getTicks:function(){return this._ticks},mergeTicksOptions:function(){var t=this.options.ticks;for(var e in!1===t.minor&&(t.minor={display:!1}),!1===t.major&&(t.major={display:!1}),t)"major"!==e&&"minor"!==e&&(void 0===t.minor[e]&&(t.minor[e]=t[e]),void 0===t.major[e]&&(t.major[e]=t[e]))},beforeUpdate:function(){o.callback(this.options.beforeUpdate,[this])},update:function(t,e,i){var n,a,r,s,l,u,d=this;for(d.beforeUpdate(),d.maxWidth=t,d.maxHeight=e,d.margins=o.extend({left:0,right:0,top:0,bottom:0},i),d.longestTextCache=d.longestTextCache||{},d.beforeSetDimensions(),d.setDimensions(),d.afterSetDimensions(),d.beforeDataLimits(),d.determineDataLimits(),d.afterDataLimits(),d.beforeBuildTicks(),l=d.buildTicks()||[],d.afterBuildTicks(),d.beforeTickToLabelConversion(),r=d.convertTicksToLabels(l)||d.ticks,d.afterTickToLabelConversion(),d.ticks=r,n=0,a=r.length;n<a;++n)s=r[n],(u=l[n])?u.label=s:l.push(u={label:s,major:!1});return d._ticks=l,d.beforeCalculateTickRotation(),d.calculateTickRotation(),d.afterCalculateTickRotation(),d.beforeFit(),d.fit(),d.afterFit(),d.afterUpdate(),d.minSize},afterUpdate:function(){o.callback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){o.callback(this.options.beforeSetDimensions,[this])},setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0},afterSetDimensions:function(){o.callback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){o.callback(this.options.beforeDataLimits,[this])},determineDataLimits:o.noop,afterDataLimits:function(){o.callback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){o.callback(this.options.beforeBuildTicks,[this])},buildTicks:o.noop,afterBuildTicks:function(){o.callback(this.options.afterBuildTicks,[this])},beforeTickToLabelConversion:function(){o.callback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var t=this.options.ticks;this.ticks=this.ticks.map(t.userCallback||t.callback,this)},afterTickToLabelConversion:function(){o.callback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){o.callback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var t=this,e=t.ctx,n=t.options.ticks,a=s(t._ticks),r=i(n);e.font=r.font;var l=n.minRotation||0;if(a.length&&t.options.display&&t.isHorizontal())for(var u,d=o.longestText(e,r.font,a,t.longestTextCache),c=d,h=t.getPixelForTick(1)-t.getPixelForTick(0)-6;c>h&&l<n.maxRotation;){var f=o.toRadians(l);if(u=Math.cos(f),Math.sin(f)*d>t.maxHeight){l--;break}l++,c=u*d}t.labelRotation=l},afterCalculateTickRotation:function(){o.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){o.callback(this.options.beforeFit,[this])},fit:function(){var t=this,n=t.minSize={width:0,height:0},a=s(t._ticks),l=t.options,u=l.ticks,d=l.scaleLabel,c=l.gridLines,h=l.display,f=t.isHorizontal(),g=i(u),p=l.gridLines.tickMarkLength;if(n.width=f?t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:h&&c.drawTicks?p:0,n.height=f?h&&c.drawTicks?p:0:t.maxHeight,d.display&&h){var m=r(d)+o.options.toPadding(d.padding).height;f?n.height+=m:n.width+=m}if(u.display&&h){var v=o.longestText(t.ctx,g.font,a,t.longestTextCache),b=o.numberOfLabelLines(a),x=.5*g.size,y=t.options.ticks.padding;if(f){t.longestLabelWidth=v;var k=o.toRadians(t.labelRotation),M=Math.cos(k),w=Math.sin(k)*v+g.size*b+x*(b-1)+x;n.height=Math.min(t.maxHeight,n.height+w+y),t.ctx.font=g.font;var S=e(t.ctx,a[0],g.font),C=e(t.ctx,a[a.length-1],g.font);0!==t.labelRotation?(t.paddingLeft="bottom"===l.position?M*S+3:M*x+3,t.paddingRight="bottom"===l.position?M*x+3:M*C+3):(t.paddingLeft=S/2+3,t.paddingRight=C/2+3)}else u.mirror?v=0:v+=y+x,n.width=Math.min(t.maxWidth,n.width+v),t.paddingTop=g.size/2,t.paddingBottom=g.size/2}t.handleMargins(),t.width=n.width,t.height=n.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){o.callback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(o.isNullOrUndef(t))return NaN;if("number"==typeof t&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},getLabelForIndex:o.noop,getPixelForValue:o.noop,getValueForPixel:o.noop,getPixelForTick:function(t){var e=this,i=e.options.offset;if(e.isHorizontal()){var n=(e.width-(e.paddingLeft+e.paddingRight))/Math.max(e._ticks.length-(i?0:1),1),a=n*t+e.paddingLeft;i&&(a+=n/2);var o=e.left+Math.round(a);return o+=e.isFullWidth()?e.margins.left:0}var r=e.height-(e.paddingTop+e.paddingBottom);return e.top+t*(r/(e._ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var i=(e.width-(e.paddingLeft+e.paddingRight))*t+e.paddingLeft,n=e.left+Math.round(i);return n+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,i,n,a,r=this,s=r.isHorizontal(),l=r.options.ticks.minor,u=t.length,d=o.toRadians(r.labelRotation),c=Math.cos(d),h=r.longestLabelWidth*c,f=[];for(l.maxTicksLimit&&(a=l.maxTicksLimit),s&&(e=!1,(h+l.autoSkipPadding)*u>r.width-(r.paddingLeft+r.paddingRight)&&(e=1+Math.floor((h+l.autoSkipPadding)*u/(r.width-(r.paddingLeft+r.paddingRight)))),a&&u>a&&(e=Math.max(e,Math.floor(u/a)))),i=0;i<u;i++)n=t[i],(e>1&&i%e>0||i%e==0&&i+e>=u)&&i!==u-1&&delete n.label,f.push(n);return f},draw:function(t){var e=this,a=e.options;if(a.display){var s=e.ctx,u=n.global,d=a.ticks.minor,c=a.ticks.major||d,h=a.gridLines,f=a.scaleLabel,g=0!==e.labelRotation,p=e.isHorizontal(),m=d.autoSkip?e._autoSkip(e.getTicks()):e.getTicks(),v=o.valueOrDefault(d.fontColor,u.defaultFontColor),b=i(d),x=o.valueOrDefault(c.fontColor,u.defaultFontColor),y=i(c),k=h.drawTicks?h.tickMarkLength:0,M=o.valueOrDefault(f.fontColor,u.defaultFontColor),w=i(f),S=o.options.toPadding(f.padding),C=o.toRadians(e.labelRotation),_=[],D=e.options.gridLines.lineWidth,I="right"===a.position?e.right:e.right-D-k,P="right"===a.position?e.right+k:e.right,A="bottom"===a.position?e.top+D:e.bottom-k-D,T="bottom"===a.position?e.top+D+k:e.bottom+D;if(o.each(m,function(i,n){if(!o.isNullOrUndef(i.label)){var r,s,c,f,v,b,x,y,M,w,S,F,O,R,L=i.label;n===e.zeroLineIndex&&a.offset===h.offsetGridLines?(r=h.zeroLineWidth,s=h.zeroLineColor,c=h.zeroLineBorderDash,f=h.zeroLineBorderDashOffset):(r=o.valueAtIndexOrDefault(h.lineWidth,n),s=o.valueAtIndexOrDefault(h.color,n),c=o.valueOrDefault(h.borderDash,u.borderDash),f=o.valueOrDefault(h.borderDashOffset,u.borderDashOffset));var z="middle",B="middle",W=d.padding;if(p){var N=k+W;"bottom"===a.position?(B=g?"middle":"top",z=g?"right":"center",R=e.top+N):(B=g?"middle":"bottom",z=g?"left":"center",R=e.bottom-N);var V=l(e,n,h.offsetGridLines&&m.length>1);V<e.left&&(s="rgba(0,0,0,0)"),V+=o.aliasPixel(r),O=e.getPixelForTick(n)+d.labelOffset,v=x=M=S=V,b=A,y=T,w=t.top,F=t.bottom+D}else{var E,H="left"===a.position;d.mirror?(z=H?"left":"right",E=W):(z=H?"right":"left",E=k+W),O=H?e.right-E:e.left+E;var j=l(e,n,h.offsetGridLines&&m.length>1);j<e.top&&(s="rgba(0,0,0,0)"),j+=o.aliasPixel(r),R=e.getPixelForTick(n)+d.labelOffset,v=I,x=P,M=t.left,S=t.right+D,b=y=w=F=j}_.push({tx1:v,ty1:b,tx2:x,ty2:y,x1:M,y1:w,x2:S,y2:F,labelX:O,labelY:R,glWidth:r,glColor:s,glBorderDash:c,glBorderDashOffset:f,rotation:-1*C,label:L,major:i.major,textBaseline:B,textAlign:z})}}),o.each(_,function(t){if(h.display&&(s.save(),s.lineWidth=t.glWidth,s.strokeStyle=t.glColor,s.setLineDash&&(s.setLineDash(t.glBorderDash),s.lineDashOffset=t.glBorderDashOffset),s.beginPath(),h.drawTicks&&(s.moveTo(t.tx1,t.ty1),s.lineTo(t.tx2,t.ty2)),h.drawOnChartArea&&(s.moveTo(t.x1,t.y1),s.lineTo(t.x2,t.y2)),s.stroke(),s.restore()),d.display){s.save(),s.translate(t.labelX,t.labelY),s.rotate(t.rotation),s.font=t.major?y.font:b.font,s.fillStyle=t.major?x:v,s.textBaseline=t.textBaseline,s.textAlign=t.textAlign;var i=t.label;if(o.isArray(i))for(var n=i.length,a=1.5*b.size,r=e.isHorizontal()?0:-a*(n-1)/2,l=0;l<n;++l)s.fillText(""+i[l],0,r),r+=a;else s.fillText(i,0,0);s.restore()}}),f.display){var F,O,R=0,L=r(f)/2;if(p)F=e.left+(e.right-e.left)/2,O="bottom"===a.position?e.bottom-L-S.bottom:e.top+L+S.top;else{var z="left"===a.position;F=z?e.left+L+S.top:e.right-L-S.top,O=e.top+(e.bottom-e.top)/2,R=z?-.5*Math.PI:.5*Math.PI}s.save(),s.translate(F,O),s.rotate(R),s.textAlign="center",s.textBaseline="middle",s.fillStyle=M,s.font=w.font,s.fillText(f.labelString,0,0),s.restore()}if(h.drawBorder){s.lineWidth=o.valueAtIndexOrDefault(h.lineWidth,0),s.strokeStyle=o.valueAtIndexOrDefault(h.color,0);var B=e.left,W=e.right+D,N=e.top,V=e.bottom+D,E=o.aliasPixel(s.lineWidth);p?(N=V="top"===a.position?e.bottom:e.top,N+=E,V+=E):(B=W="left"===a.position?e.right:e.left,B+=E,W+=E),s.beginPath(),s.moveTo(B,N),s.lineTo(W,V),s.stroke()}}}})}},{25:25,26:26,34:34,45:45}],33:[function(t,e,i){"use strict";var n=t(25),a=t(45),o=t(30);e.exports=function(t){t.scaleService={constructors:{},defaults:{},registerScaleType:function(t,e,i){this.constructors[t]=e,this.defaults[t]=a.clone(i)},getScaleConstructor:function(t){return this.constructors.hasOwnProperty(t)?this.constructors[t]:void 0},getScaleDefaults:function(t){return this.defaults.hasOwnProperty(t)?a.merge({},[n.scale,this.defaults[t]]):{}},updateScaleDefaults:function(t,e){this.defaults.hasOwnProperty(t)&&(this.defaults[t]=a.extend(this.defaults[t],e))},addScalesToLayout:function(t){a.each(t.scales,function(e){e.fullWidth=e.options.fullWidth,e.position=e.options.position,e.weight=e.options.weight,o.addBox(t,e)})}}}},{25:25,30:30,45:45}],34:[function(t,e,i){"use strict";var n=t(45);e.exports={formatters:{values:function(t){return n.isArray(t)?t:""+t},linear:function(t,e,i){var a=i.length>3?i[2]-i[1]:i[1]-i[0];Math.abs(a)>1&&t!==Math.floor(t)&&(a=t-Math.floor(t));var o=n.log10(Math.abs(a)),r="";if(0!==t){var s=-1*Math.floor(o);s=Math.max(Math.min(s,20),0),r=t.toFixed(s)}else r="0";return r},logarithmic:function(t,e,i){var a=t/Math.pow(10,Math.floor(n.log10(t)));return 0===t?"0":1===a||2===a||5===a||0===e||e===i.length-1?t.toExponential():""}}}},{45:45}],35:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45);n._set("global",{tooltips:{enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:o.noop,title:function(t,e){var i="",n=e.labels,a=n?n.length:0;if(t.length>0){var o=t[0];o.xLabel?i=o.xLabel:a>0&&o.index<a&&(i=n[o.index])}return i},afterTitle:o.noop,beforeBody:o.noop,beforeLabel:o.noop,label:function(t,e){var i=e.datasets[t.datasetIndex].label||"";return i&&(i+=": "),i+=t.yLabel},labelColor:function(t,e){var i=e.getDatasetMeta(t.datasetIndex).data[t.index]._view;return{borderColor:i.borderColor,backgroundColor:i.backgroundColor}},labelTextColor:function(){return this._options.bodyFontColor},afterLabel:o.noop,afterBody:o.noop,beforeFooter:o.noop,footer:o.noop,afterFooter:o.noop}}}),e.exports=function(t){function e(t,e){var i=o.color(t);return i.alpha(e*i.alpha()).rgbaString()}function i(t,e){return e&&(o.isArray(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function r(t){var e=n.global,i=o.valueOrDefault;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,bodyFontColor:t.bodyFontColor,_bodyFontFamily:i(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:i(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:i(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:i(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:i(t.titleFontStyle,e.defaultFontStyle),titleFontSize:i(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:i(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:i(t.footerFontStyle,e.defaultFontStyle),footerFontSize:i(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}t.Tooltip=a.extend({initialize:function(){this._model=r(this._options),this._lastActive=[]},getTitle:function(){var t=this._options.callbacks,e=t.beforeTitle.apply(this,arguments),n=t.title.apply(this,arguments),a=t.afterTitle.apply(this,arguments),o=[];return o=i(o=i(o=i(o,e),n),a)},getBeforeBody:function(){var t=this._options.callbacks.beforeBody.apply(this,arguments);return o.isArray(t)?t:void 0!==t?[t]:[]},getBody:function(t,e){var n=this,a=n._options.callbacks,r=[];return o.each(t,function(t){var o={before:[],lines:[],after:[]};i(o.before,a.beforeLabel.call(n,t,e)),i(o.lines,a.label.call(n,t,e)),i(o.after,a.afterLabel.call(n,t,e)),r.push(o)}),r},getAfterBody:function(){var t=this._options.callbacks.afterBody.apply(this,arguments);return o.isArray(t)?t:void 0!==t?[t]:[]},getFooter:function(){var t=this._options.callbacks,e=t.beforeFooter.apply(this,arguments),n=t.footer.apply(this,arguments),a=t.afterFooter.apply(this,arguments),o=[];return o=i(o=i(o=i(o,e),n),a)},update:function(e){var i,n,a,s,l,u,d,c,h,f,g,p,m,v,b,x,y,k,M,w,S=this,C=S._options,_=S._model,D=S._model=r(C),I=S._active,P=S._data,A={xAlign:_.xAlign,yAlign:_.yAlign},T={x:_.x,y:_.y},F={width:_.width,height:_.height},O={x:_.caretX,y:_.caretY};if(I.length){D.opacity=1;var R=[],L=[];O=t.Tooltip.positioners[C.position].call(S,I,S._eventPosition);var z=[];for(i=0,n=I.length;i<n;++i)z.push((x=I[i],y=void 0,k=void 0,void 0,void 0,y=x._xScale,k=x._yScale||x._scale,M=x._index,w=x._datasetIndex,{xLabel:y?y.getLabelForIndex(M,w):"",yLabel:k?k.getLabelForIndex(M,w):"",index:M,datasetIndex:w,x:x._model.x,y:x._model.y}));C.filter&&(z=z.filter(function(t){return C.filter(t,P)})),C.itemSort&&(z=z.sort(function(t,e){return C.itemSort(t,e,P)})),o.each(z,function(t){R.push(C.callbacks.labelColor.call(S,t,S._chart)),L.push(C.callbacks.labelTextColor.call(S,t,S._chart))}),D.title=S.getTitle(z,P),D.beforeBody=S.getBeforeBody(z,P),D.body=S.getBody(z,P),D.afterBody=S.getAfterBody(z,P),D.footer=S.getFooter(z,P),D.x=Math.round(O.x),D.y=Math.round(O.y),D.caretPadding=C.caretPadding,D.labelColors=R,D.labelTextColors=L,D.dataPoints=z,A=function(t,e){var i,n,a,o,r,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",c="center";s.y<e.height?c="top":s.y>l.height-e.height&&(c="bottom");var h=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===c?(i=function(t){return t<=h},n=function(t){return t>h}):(i=function(t){return t<=e.width/2},n=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},o=function(t){return t-e.width-s.caretSize-s.caretPadding<0},r=function(t){return t<=f?"top":"bottom"},i(s.x)?(d="left",a(s.x)&&(d="center",c=r(s.y))):n(s.x)&&(d="right",o(s.x)&&(d="center",c=r(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:c}}(this,F=function(t,e){var i=t._chart.ctx,n=2*e.yPadding,a=0,r=e.body,s=r.reduce(function(t,e){return t+e.before.length+e.lines.length+e.after.length},0);s+=e.beforeBody.length+e.afterBody.length;var l=e.title.length,u=e.footer.length,d=e.titleFontSize,c=e.bodyFontSize,h=e.footerFontSize;n+=l*d,n+=l?(l-1)*e.titleSpacing:0,n+=l?e.titleMarginBottom:0,n+=s*c,n+=s?(s-1)*e.bodySpacing:0,n+=u?e.footerMarginTop:0,n+=u*h,n+=u?(u-1)*e.footerSpacing:0;var f=0,g=function(t){a=Math.max(a,i.measureText(t).width+f)};return i.font=o.fontString(d,e._titleFontStyle,e._titleFontFamily),o.each(e.title,g),i.font=o.fontString(c,e._bodyFontStyle,e._bodyFontFamily),o.each(e.beforeBody.concat(e.afterBody),g),f=e.displayColors?c+2:0,o.each(r,function(t){o.each(t.before,g),o.each(t.lines,g),o.each(t.after,g)}),f=0,i.font=o.fontString(h,e._footerFontStyle,e._footerFontFamily),o.each(e.footer,g),{width:a+=2*e.xPadding,height:n}}(this,D)),a=D,s=F,l=A,u=S._chart,d=a.x,c=a.y,h=a.caretSize,f=a.caretPadding,g=a.cornerRadius,p=l.xAlign,m=l.yAlign,v=h+f,b=g+f,"right"===p?d-=s.width:"center"===p&&((d-=s.width/2)+s.width>u.width&&(d=u.width-s.width),d<0&&(d=0)),"top"===m?c+=v:c-="bottom"===m?s.height+v:s.height/2,"center"===m?"left"===p?d+=v:"right"===p&&(d-=v):"left"===p?d-=b:"right"===p&&(d+=b),T={x:d,y:c}}else D.opacity=0;return D.xAlign=A.xAlign,D.yAlign=A.yAlign,D.x=T.x,D.y=T.y,D.width=F.width,D.height=F.height,D.caretX=O.x,D.caretY=O.y,S._model=D,e&&C.custom&&C.custom.call(S,D),S},drawCaret:function(t,e){var i=this._chart.ctx,n=this._view,a=this.getCaretPosition(t,e,n);i.lineTo(a.x1,a.y1),i.lineTo(a.x2,a.y2),i.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,i){var n,a,o,r,s,l,u=i.caretSize,d=i.cornerRadius,c=i.xAlign,h=i.yAlign,f=t.x,g=t.y,p=e.width,m=e.height;if("center"===h)s=g+m/2,"left"===c?(a=(n=f)-u,o=n,r=s+u,l=s-u):(a=(n=f+p)+u,o=n,r=s-u,l=s+u);else if("left"===c?(n=(a=f+d+u)-u,o=a+u):"right"===c?(n=(a=f+p-d-u)-u,o=a+u):(n=(a=i.caretX)-u,o=a+u),"top"===h)s=(r=g)-u,l=r;else{s=(r=g+m)+u,l=r;var v=o;o=n,n=v}return{x1:n,x2:a,x3:o,y1:r,y2:s,y3:l}},drawTitle:function(t,i,n,a){var r=i.title;if(r.length){n.textAlign=i._titleAlign,n.textBaseline="top";var s,l,u=i.titleFontSize,d=i.titleSpacing;for(n.fillStyle=e(i.titleFontColor,a),n.font=o.fontString(u,i._titleFontStyle,i._titleFontFamily),s=0,l=r.length;s<l;++s)n.fillText(r[s],t.x,t.y),t.y+=u+d,s+1===r.length&&(t.y+=i.titleMarginBottom-d)}},drawBody:function(t,i,n,a){var r=i.bodyFontSize,s=i.bodySpacing,l=i.body;n.textAlign=i._bodyAlign,n.textBaseline="top",n.font=o.fontString(r,i._bodyFontStyle,i._bodyFontFamily);var u=0,d=function(e){n.fillText(e,t.x+u,t.y),t.y+=r+s};n.fillStyle=e(i.bodyFontColor,a),o.each(i.beforeBody,d);var c=i.displayColors;u=c?r+2:0,o.each(l,function(s,l){var u=e(i.labelTextColors[l],a);n.fillStyle=u,o.each(s.before,d),o.each(s.lines,function(o){c&&(n.fillStyle=e(i.legendColorBackground,a),n.fillRect(t.x,t.y,r,r),n.lineWidth=1,n.strokeStyle=e(i.labelColors[l].borderColor,a),n.strokeRect(t.x,t.y,r,r),n.fillStyle=e(i.labelColors[l].backgroundColor,a),n.fillRect(t.x+1,t.y+1,r-2,r-2),n.fillStyle=u),d(o)}),o.each(s.after,d)}),u=0,o.each(i.afterBody,d),t.y-=s},drawFooter:function(t,i,n,a){var r=i.footer;r.length&&(t.y+=i.footerMarginTop,n.textAlign=i._footerAlign,n.textBaseline="top",n.fillStyle=e(i.footerFontColor,a),n.font=o.fontString(i.footerFontSize,i._footerFontStyle,i._footerFontFamily),o.each(r,function(e){n.fillText(e,t.x,t.y),t.y+=i.footerFontSize+i.footerSpacing}))},drawBackground:function(t,i,n,a,o){n.fillStyle=e(i.backgroundColor,o),n.strokeStyle=e(i.borderColor,o),n.lineWidth=i.borderWidth;var r=i.xAlign,s=i.yAlign,l=t.x,u=t.y,d=a.width,c=a.height,h=i.cornerRadius;n.beginPath(),n.moveTo(l+h,u),"top"===s&&this.drawCaret(t,a),n.lineTo(l+d-h,u),n.quadraticCurveTo(l+d,u,l+d,u+h),"center"===s&&"right"===r&&this.drawCaret(t,a),n.lineTo(l+d,u+c-h),n.quadraticCurveTo(l+d,u+c,l+d-h,u+c),"bottom"===s&&this.drawCaret(t,a),n.lineTo(l+h,u+c),n.quadraticCurveTo(l,u+c,l,u+c-h),"center"===s&&"left"===r&&this.drawCaret(t,a),n.lineTo(l,u+h),n.quadraticCurveTo(l,u,l+h,u),n.closePath(),n.fill(),i.borderWidth>0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var i={width:e.width,height:e.height},n={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,o=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&o&&(this.drawBackground(n,e,t,i,a),n.x+=e.xPadding,n.y+=e.yPadding,this.drawTitle(n,e,t,a),this.drawBody(n,e,t,a),this.drawFooter(n,e,t,a))}},handleEvent:function(t){var e,i=this,n=i._options;return i._lastActive=i._lastActive||[],"mouseout"===t.type?i._active=[]:i._active=i._chart.getElementsAtEventForMode(t,n.mode,n),(e=!o.arrayEquals(i._active,i._lastActive))&&(i._lastActive=i._active,(n.enabled||n.custom)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0),i.pivot())),e}}),t.Tooltip.positioners={average:function(t){if(!t.length)return!1;var e,i,n=0,a=0,o=0;for(e=0,i=t.length;e<i;++e){var r=t[e];if(r&&r.hasValue()){var s=r.tooltipPosition();n+=s.x,a+=s.y,++o}}return{x:Math.round(n/o),y:Math.round(a/o)}},nearest:function(t,e){var i,n,a,r=e.x,s=e.y,l=Number.POSITIVE_INFINITY;for(i=0,n=t.length;i<n;++i){var u=t[i];if(u&&u.hasValue()){var d=u.getCenterPoint(),c=o.distanceBetweenPoints(e,d);c<l&&(l=c,a=u)}}if(a){var h=a.tooltipPosition();r=h.x,s=h.y}return{x:r,y:s}}}}},{25:25,26:26,45:45}],36:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45);n._set("global",{elements:{arc:{backgroundColor:n.global.defaultColor,borderColor:"#fff",borderWidth:2}}}),e.exports=a.extend({inLabelRange:function(t){var e=this._view;return!!e&&Math.pow(t-e.x,2)<Math.pow(e.radius+e.hoverRadius,2)},inRange:function(t,e){var i=this._view;if(i){for(var n=o.getAngleFromPoint(i,{x:t,y:e}),a=n.angle,r=n.distance,s=i.startAngle,l=i.endAngle;l<s;)l+=2*Math.PI;for(;a>l;)a-=2*Math.PI;for(;a<s;)a+=2*Math.PI;var u=a>=s&&a<=l,d=r>=i.innerRadius&&r<=i.outerRadius;return u&&d}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,i=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,i=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},draw:function(){var t=this._chart.ctx,e=this._view,i=e.startAngle,n=e.endAngle;t.beginPath(),t.arc(e.x,e.y,e.outerRadius,i,n),t.arc(e.x,e.y,e.innerRadius,n,i,!0),t.closePath(),t.strokeStyle=e.borderColor,t.lineWidth=e.borderWidth,t.fillStyle=e.backgroundColor,t.fill(),t.lineJoin="bevel",e.borderWidth&&t.stroke()}})},{25:25,26:26,45:45}],37:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45),r=n.global;n._set("global",{elements:{line:{tension:.4,backgroundColor:r.defaultColor,borderWidth:3,borderColor:r.defaultColor,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}}),e.exports=a.extend({draw:function(){var t,e,i,n,a=this._view,s=this._chart.ctx,l=a.spanGaps,u=this._children.slice(),d=r.elements.line,c=-1;for(this._loop&&u.length&&u.push(u[0]),s.save(),s.lineCap=a.borderCapStyle||d.borderCapStyle,s.setLineDash&&s.setLineDash(a.borderDash||d.borderDash),s.lineDashOffset=a.borderDashOffset||d.borderDashOffset,s.lineJoin=a.borderJoinStyle||d.borderJoinStyle,s.lineWidth=a.borderWidth||d.borderWidth,s.strokeStyle=a.borderColor||r.defaultColor,s.beginPath(),c=-1,t=0;t<u.length;++t)e=u[t],i=o.previousItem(u,t),n=e._view,0===t?n.skip||(s.moveTo(n.x,n.y),c=t):(i=-1===c?i:u[c],n.skip||(c!==t-1&&!l||-1===c?s.moveTo(n.x,n.y):o.canvas.lineTo(s,i._view,e._view),c=t));s.stroke(),s.restore()}})},{25:25,26:26,45:45}],38:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45),r=n.global.defaultColor;function s(t){var e=this._view;return!!e&&Math.abs(t-e.x)<e.radius+e.hitRadius}n._set("global",{elements:{point:{radius:3,pointStyle:"circle",backgroundColor:r,borderColor:r,borderWidth:1,hitRadius:1,hoverRadius:4,hoverBorderWidth:1}}}),e.exports=a.extend({inRange:function(t,e){var i=this._view;return!!i&&Math.pow(t-i.x,2)+Math.pow(e-i.y,2)<Math.pow(i.hitRadius+i.radius,2)},inLabelRange:s,inXRange:s,inYRange:function(t){var e=this._view;return!!e&&Math.abs(t-e.y)<e.radius+e.hitRadius},getCenterPoint:function(){var t=this._view;return{x:t.x,y:t.y}},getArea:function(){return Math.PI*Math.pow(this._view.radius,2)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y,padding:t.radius+t.borderWidth}},draw:function(t){var e=this._view,i=this._model,a=this._chart.ctx,s=e.pointStyle,l=e.radius,u=e.x,d=e.y,c=o.color,h=0;e.skip||(a.strokeStyle=e.borderColor||r,a.lineWidth=o.valueOrDefault(e.borderWidth,n.global.elements.point.borderWidth),a.fillStyle=e.backgroundColor||r,void 0!==t&&(i.x<t.left||1.01*t.right<i.x||i.y<t.top||1.01*t.bottom<i.y)&&(i.x<t.left?h=(u-i.x)/(t.left-i.x):1.01*t.right<i.x?h=(i.x-u)/(i.x-t.right):i.y<t.top?h=(d-i.y)/(t.top-i.y):1.01*t.bottom<i.y&&(h=(i.y-d)/(i.y-t.bottom)),h=Math.round(100*h)/100,a.strokeStyle=c(a.strokeStyle).alpha(h).rgbString(),a.fillStyle=c(a.fillStyle).alpha(h).rgbString()),o.canvas.drawPoint(a,s,l,u,d))}})},{25:25,26:26,45:45}],39:[function(t,e,i){"use strict";var n=t(25),a=t(26);function o(t){return void 0!==t._view.width}function r(t){var e,i,n,a,r=t._view;if(o(t)){var s=r.width/2;e=r.x-s,i=r.x+s,n=Math.min(r.y,r.base),a=Math.max(r.y,r.base)}else{var l=r.height/2;e=Math.min(r.x,r.base),i=Math.max(r.x,r.base),n=r.y-l,a=r.y+l}return{left:e,top:n,right:i,bottom:a}}n._set("global",{elements:{rectangle:{backgroundColor:n.global.defaultColor,borderColor:n.global.defaultColor,borderSkipped:"bottom",borderWidth:0}}}),e.exports=a.extend({draw:function(){var t,e,i,n,a,o,r,s=this._chart.ctx,l=this._view,u=l.borderWidth;if(l.horizontal?(t=l.base,e=l.x,i=l.y-l.height/2,n=l.y+l.height/2,a=e>t?1:-1,o=1,r=l.borderSkipped||"left"):(t=l.x-l.width/2,e=l.x+l.width/2,i=l.y,a=1,o=(n=l.base)>i?1:-1,r=l.borderSkipped||"bottom"),u){var d=Math.min(Math.abs(t-e),Math.abs(i-n)),c=(u=u>d?d:u)/2,h=t+("left"!==r?c*a:0),f=e+("right"!==r?-c*a:0),g=i+("top"!==r?c*o:0),p=n+("bottom"!==r?-c*o:0);h!==f&&(i=g,n=p),g!==p&&(t=h,e=f)}s.beginPath(),s.fillStyle=l.backgroundColor,s.strokeStyle=l.borderColor,s.lineWidth=u;var m=[[t,n],[t,i],[e,i],[e,n]],v=["bottom","left","top","right"].indexOf(r,0);function b(t){return m[(v+t)%4]}-1===v&&(v=0);var x=b(0);s.moveTo(x[0],x[1]);for(var y=1;y<4;y++)x=b(y),s.lineTo(x[0],x[1]);s.fill(),u&&s.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var i=!1;if(this._view){var n=r(this);i=t>=n.left&&t<=n.right&&e>=n.top&&e<=n.bottom}return i},inLabelRange:function(t,e){if(!this._view)return!1;var i=r(this);return o(this)?t>=i.left&&t<=i.right:e>=i.top&&e<=i.bottom},inXRange:function(t){var e=r(this);return t>=e.left&&t<=e.right},inYRange:function(t){var e=r(this);return t>=e.top&&t<=e.bottom},getCenterPoint:function(){var t,e,i=this._view;return o(this)?(t=i.x,e=(i.y+i.base)/2):(t=(i.x+i.base)/2,e=i.y),{x:t,y:e}},getArea:function(){var t=this._view;return t.width*Math.abs(t.y-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})},{25:25,26:26}],40:[function(t,e,i){"use strict";e.exports={},e.exports.Arc=t(36),e.exports.Line=t(37),e.exports.Point=t(38),e.exports.Rectangle=t(39)},{36:36,37:37,38:38,39:39}],41:[function(t,e,i){"use strict";var n=t(42);i=e.exports={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,i,n,a,o){if(o){var r=Math.min(o,n/2),s=Math.min(o,a/2);t.moveTo(e+r,i),t.lineTo(e+n-r,i),t.quadraticCurveTo(e+n,i,e+n,i+s),t.lineTo(e+n,i+a-s),t.quadraticCurveTo(e+n,i+a,e+n-r,i+a),t.lineTo(e+r,i+a),t.quadraticCurveTo(e,i+a,e,i+a-s),t.lineTo(e,i+s),t.quadraticCurveTo(e,i,e+r,i)}else t.rect(e,i,n,a)},drawPoint:function(t,e,i,n,a){var o,r,s,l,u,d;if(!e||"object"!=typeof e||"[object HTMLImageElement]"!==(o=e.toString())&&"[object HTMLCanvasElement]"!==o){if(!(isNaN(i)||i<=0)){switch(e){default:t.beginPath(),t.arc(n,a,i,0,2*Math.PI),t.closePath(),t.fill();break;case"triangle":t.beginPath(),u=(r=3*i/Math.sqrt(3))*Math.sqrt(3)/2,t.moveTo(n-r/2,a+u/3),t.lineTo(n+r/2,a+u/3),t.lineTo(n,a-2*u/3),t.closePath(),t.fill();break;case"rect":d=1/Math.SQRT2*i,t.beginPath(),t.fillRect(n-d,a-d,2*d,2*d),t.strokeRect(n-d,a-d,2*d,2*d);break;case"rectRounded":var c=i/Math.SQRT2,h=n-c,f=a-c,g=Math.SQRT2*i;t.beginPath(),this.roundedRect(t,h,f,g,g,i/2),t.closePath(),t.fill();break;case"rectRot":d=1/Math.SQRT2*i,t.beginPath(),t.moveTo(n-d,a),t.lineTo(n,a+d),t.lineTo(n+d,a),t.lineTo(n,a-d),t.closePath(),t.fill();break;case"cross":t.beginPath(),t.moveTo(n,a+i),t.lineTo(n,a-i),t.moveTo(n-i,a),t.lineTo(n+i,a),t.closePath();break;case"crossRot":t.beginPath(),s=Math.cos(Math.PI/4)*i,l=Math.sin(Math.PI/4)*i,t.moveTo(n-s,a-l),t.lineTo(n+s,a+l),t.moveTo(n-s,a+l),t.lineTo(n+s,a-l),t.closePath();break;case"star":t.beginPath(),t.moveTo(n,a+i),t.lineTo(n,a-i),t.moveTo(n-i,a),t.lineTo(n+i,a),s=Math.cos(Math.PI/4)*i,l=Math.sin(Math.PI/4)*i,t.moveTo(n-s,a-l),t.lineTo(n+s,a+l),t.moveTo(n-s,a+l),t.lineTo(n+s,a-l),t.closePath();break;case"line":t.beginPath(),t.moveTo(n-i,a),t.lineTo(n+i,a),t.closePath();break;case"dash":t.beginPath(),t.moveTo(n,a),t.lineTo(n+i,a),t.closePath()}t.stroke()}}else t.drawImage(e,n-e.width/2,a-e.height/2,e.width,e.height)},clipArea:function(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()},unclipArea:function(t){t.restore()},lineTo:function(t,e,i,n){if(i.steppedLine)return"after"===i.steppedLine&&!n||"after"!==i.steppedLine&&n?t.lineTo(e.x,i.y):t.lineTo(i.x,e.y),void t.lineTo(i.x,i.y);i.tension?t.bezierCurveTo(n?e.controlPointPreviousX:e.controlPointNextX,n?e.controlPointPreviousY:e.controlPointNextY,n?i.controlPointNextX:i.controlPointPreviousX,n?i.controlPointNextY:i.controlPointPreviousY,i.x,i.y):t.lineTo(i.x,i.y)}};n.clear=i.clear,n.drawRoundedRectangle=function(t){t.beginPath(),i.roundedRect.apply(i,arguments),t.closePath()}},{42:42}],42:[function(t,e,i){"use strict";var n,a={noop:function(){},uid:(n=0,function(){return n++}),isNullOrUndef:function(t){return null==t},isArray:Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isObject:function(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)},valueOrDefault:function(t,e){return void 0===t?e:t},valueAtIndexOrDefault:function(t,e,i){return a.valueOrDefault(a.isArray(t)?t[e]:t,i)},callback:function(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)},each:function(t,e,i,n){var o,r,s;if(a.isArray(t))if(r=t.length,n)for(o=r-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;o<r;o++)e.call(i,t[o],o);else if(a.isObject(t))for(r=(s=Object.keys(t)).length,o=0;o<r;o++)e.call(i,t[s[o]],s[o])},arrayEquals:function(t,e){var i,n,o,r;if(!t||!e||t.length!==e.length)return!1;for(i=0,n=t.length;i<n;++i)if(o=t[i],r=e[i],o instanceof Array&&r instanceof Array){if(!a.arrayEquals(o,r))return!1}else if(o!==r)return!1;return!0},clone:function(t){if(a.isArray(t))return t.map(a.clone);if(a.isObject(t)){for(var e={},i=Object.keys(t),n=i.length,o=0;o<n;++o)e[i[o]]=a.clone(t[i[o]]);return e}return t},_merger:function(t,e,i,n){var o=e[t],r=i[t];a.isObject(o)&&a.isObject(r)?a.merge(o,r,n):e[t]=a.clone(r)},_mergerIf:function(t,e,i){var n=e[t],o=i[t];a.isObject(n)&&a.isObject(o)?a.mergeIf(n,o):e.hasOwnProperty(t)||(e[t]=a.clone(o))},merge:function(t,e,i){var n,o,r,s,l,u=a.isArray(e)?e:[e],d=u.length;if(!a.isObject(t))return t;for(n=(i=i||{}).merger||a._merger,o=0;o<d;++o)if(e=u[o],a.isObject(e))for(l=0,s=(r=Object.keys(e)).length;l<s;++l)n(r[l],t,e,i);return t},mergeIf:function(t,e){return a.merge(t,e,{merger:a._mergerIf})},extend:function(t){for(var e=function(e,i){t[i]=e},i=1,n=arguments.length;i<n;++i)a.each(arguments[i],e);return t},inherits:function(t){var e=this,i=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return e.apply(this,arguments)},n=function(){this.constructor=i};return n.prototype=e.prototype,i.prototype=new n,i.extend=a.inherits,t&&a.extend(i.prototype,t),i.__super__=e.prototype,i}};e.exports=a,a.callCallback=a.callback,a.indexOf=function(t,e,i){return Array.prototype.indexOf.call(t,e,i)},a.getValueOrDefault=a.valueOrDefault,a.getValueAtIndexOrDefault=a.valueAtIndexOrDefault},{}],43:[function(t,e,i){"use strict";var n=t(42),a={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return(t-=1)*t*t+1},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-((t-=1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return t*t*t*t*t},easeOutQuint:function(t){return(t-=1)*t*t*t*t+1},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return 1-Math.cos(t*(Math.PI/2))},easeOutSine:function(t){return Math.sin(t*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t)-1)},easeInExpo:function(t){return 0===t?0:Math.pow(2,10*(t-1))},easeOutExpo:function(t){return 1===t?1:1-Math.pow(2,-10*t)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(2-Math.pow(2,-10*--t))},easeInCirc:function(t){return t>=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i))},easeOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:1===t?1:(i||(i=.3),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/i)+1)},easeInOutElastic:function(t){var e=1.70158,i=0,n=1;return 0===t?0:2==(t/=.5)?1:(i||(i=.45),n<1?(n=1,e=i/4):e=i/(2*Math.PI)*Math.asin(1/n),t<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},easeInBack:function(t){return t*t*(2.70158*t-1.70158)},easeOutBack:function(t){return(t-=1)*t*(2.70158*t+1.70158)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-a.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*a.easeInBounce(2*t):.5*a.easeOutBounce(2*t-1)+.5}};e.exports={effects:a},n.easingEffects=a},{42:42}],44:[function(t,e,i){"use strict";var n=t(42);e.exports={toLineHeight:function(t,e){var i=(""+t).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t},toPadding:function(t){var e,i,a,o;return n.isObject(t)?(e=+t.top||0,i=+t.right||0,a=+t.bottom||0,o=+t.left||0):e=i=a=o=+t||0,{top:e,right:i,bottom:a,left:o,height:e+a,width:o+i}},resolve:function(t,e,i){var a,o,r;for(a=0,o=t.length;a<o;++a)if(void 0!==(r=t[a])&&(void 0!==e&&"function"==typeof r&&(r=r(e)),void 0!==i&&n.isArray(r)&&(r=r[i]),void 0!==r))return r}}},{42:42}],45:[function(t,e,i){"use strict";e.exports=t(42),e.exports.easing=t(43),e.exports.canvas=t(41),e.exports.options=t(44)},{41:41,42:42,43:43,44:44}],46:[function(t,e,i){e.exports={acquireContext:function(t){return t&&t.canvas&&(t=t.canvas),t&&t.getContext("2d")||null}}},{}],47:[function(t,e,i){"use strict";var n=t(45),a="$chartjs",o="chartjs-",r=o+"render-monitor",s=o+"render-animation",l=["animationstart","webkitAnimationStart"],u={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function d(t,e){var i=n.getStyle(t,e),a=i&&i.match(/^(\d+)(\.\d+)?px$/);return a?Number(a[1]):void 0}var c=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function h(t,e,i){t.addEventListener(e,i,c)}function f(t,e,i){t.removeEventListener(e,i,c)}function g(t,e,i,n,a){return{type:t,chart:e,native:a||null,x:void 0!==i?i:null,y:void 0!==n?n:null}}function p(t,e,i){var u,d,c,f,p,m,v,b,x=t[a]||(t[a]={}),y=x.resizer=function(t){var e=document.createElement("div"),i=o+"size-monitor",n="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;";e.style.cssText=n,e.className=i,e.innerHTML='<div class="'+i+'-expand" style="'+n+'"><div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div></div><div class="'+i+'-shrink" style="'+n+'"><div style="position:absolute;width:200%;height:200%;left:0; top:0"></div></div>';var a=e.childNodes[0],r=e.childNodes[1];e._reset=function(){a.scrollLeft=1e6,a.scrollTop=1e6,r.scrollLeft=1e6,r.scrollTop=1e6};var s=function(){e._reset(),t()};return h(a,"scroll",s.bind(a,"expand")),h(r,"scroll",s.bind(r,"shrink")),e}((u=function(){if(x.resizer)return e(g("resize",i))},c=!1,f=[],function(){f=Array.prototype.slice.call(arguments),d=d||this,c||(c=!0,n.requestAnimFrame.call(window,function(){c=!1,u.apply(d,f)}))}));m=function(){if(x.resizer){var e=t.parentNode;e&&e!==y.parentNode&&e.insertBefore(y,e.firstChild),y._reset()}},v=(p=t)[a]||(p[a]={}),b=v.renderProxy=function(t){t.animationName===s&&m()},n.each(l,function(t){h(p,t,b)}),v.reflow=!!p.offsetParent,p.classList.add(r)}function m(t){var e,i,o,s=t[a]||{},u=s.resizer;delete s.resizer,i=(e=t)[a]||{},(o=i.renderProxy)&&(n.each(l,function(t){f(e,t,o)}),delete i.renderProxy),e.classList.remove(r),u&&u.parentNode&&u.parentNode.removeChild(u)}e.exports={_enabled:"undefined"!=typeof window&&"undefined"!=typeof document,initialize:function(){var t,e,i,n="from{opacity:0.99}to{opacity:1}";e="@-webkit-keyframes "+s+"{"+n+"}@keyframes "+s+"{"+n+"}."+r+"{-webkit-animation:"+s+" 0.001s;animation:"+s+" 0.001s;}",i=(t=this)._style||document.createElement("style"),t._style||(t._style=i,e="/* Chart.js */\n"+e,i.setAttribute("type","text/css"),document.getElementsByTagName("head")[0].appendChild(i)),i.appendChild(document.createTextNode(e))},acquireContext:function(t,e){"string"==typeof t?t=document.getElementById(t):t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas);var i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){var i=t.style,n=t.getAttribute("height"),o=t.getAttribute("width");if(t[a]={initial:{height:n,width:o,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",null===o||""===o){var r=d(t,"width");void 0!==r&&(t.width=r)}if(null===n||""===n)if(""===t.style.height)t.height=t.width/(e.options.aspectRatio||2);else{var s=d(t,"height");void 0!==r&&(t.height=s)}}(t,e),i):null},releaseContext:function(t){var e=t.canvas;if(e[a]){var i=e[a].initial;["height","width"].forEach(function(t){var a=i[t];n.isNullOrUndef(a)?e.removeAttribute(t):e.setAttribute(t,a)}),n.each(i.style||{},function(t,i){e.style[i]=t}),e.width=e.width,delete e[a]}},addEventListener:function(t,e,i){var o=t.canvas;if("resize"!==e){var r=i[a]||(i[a]={});h(o,e,(r.proxies||(r.proxies={}))[t.id+"_"+e]=function(e){var a,o,r,s;i((o=t,r=u[(a=e).type]||a.type,s=n.getRelativePosition(a,o),g(r,o,s.x,s.y,a)))})}else p(o,i,t)},removeEventListener:function(t,e,i){var n=t.canvas;if("resize"!==e){var o=((i[a]||{}).proxies||{})[t.id+"_"+e];o&&f(n,e,o)}else m(n)}},n.addEvent=h,n.removeEvent=f},{45:45}],48:[function(t,e,i){"use strict";var n=t(45),a=t(46),o=t(47),r=o._enabled?o:a;e.exports=n.extend({initialize:function(){},acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},r)},{45:45,46:46,47:47}],49:[function(t,e,i){"use strict";e.exports={},e.exports.filler=t(50),e.exports.legend=t(51),e.exports.title=t(52)},{50:50,51:51,52:52}],50:[function(t,e,i){"use strict";var n=t(25),a=t(40),o=t(45);n._set("global",{plugins:{filler:{propagate:!0}}});var r={dataset:function(t){var e=t.fill,i=t.chart,n=i.getDatasetMeta(e),a=n&&i.isDatasetVisible(e)&&n.dataset._children||[],o=a.length||0;return o?function(t,e){return e<o&&a[e]._view||null}:null},boundary:function(t){var e=t.boundary,i=e?e.x:null,n=e?e.y:null;return function(t){return{x:null===i?t.x:i,y:null===n?t.y:n}}}};function s(t,e,i){var n,a=t._model||{},o=a.fill;if(void 0===o&&(o=!!a.backgroundColor),!1===o||null===o)return!1;if(!0===o)return"origin";if(n=parseFloat(o,10),isFinite(n)&&Math.floor(n)===n)return"-"!==o[0]&&"+"!==o[0]||(n=e+n),!(n===e||n<0||n>=i)&&n;switch(o){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return o;default:return!1}}function l(t){var e,i=t.el._model||{},n=t.el._scale||{},a=t.fill,o=null;if(isFinite(a))return null;if("start"===a?o=void 0===i.scaleBottom?n.bottom:i.scaleBottom:"end"===a?o=void 0===i.scaleTop?n.top:i.scaleTop:void 0!==i.scaleZero?o=i.scaleZero:n.getBasePosition?o=n.getBasePosition():n.getBasePixel&&(o=n.getBasePixel()),null!=o){if(void 0!==o.x&&void 0!==o.y)return o;if("number"==typeof o&&isFinite(o))return{x:(e=n.isHorizontal())?o:null,y:e?null:o}}return null}function u(t,e,i){var n,a=t[e].fill,o=[e];if(!i)return a;for(;!1!==a&&-1===o.indexOf(a);){if(!isFinite(a))return a;if(!(n=t[a]))return!1;if(n.visible)return a;o.push(a),a=n.fill}return!1}function d(t){return t&&!t.skip}function c(t,e,i,n,a){var r;if(n&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r<n;++r)o.canvas.lineTo(t,e[r-1],e[r]);for(t.lineTo(i[a-1].x,i[a-1].y),r=a-1;r>0;--r)o.canvas.lineTo(t,i[r],i[r-1],!0)}}e.exports={id:"filler",afterDatasetsUpdate:function(t,e){var i,n,o,d,c,h,f,g=(t.data.datasets||[]).length,p=e.propagate,m=[];for(n=0;n<g;++n)d=null,(o=(i=t.getDatasetMeta(n)).dataset)&&o._model&&o instanceof a.Line&&(d={visible:t.isDatasetVisible(n),fill:s(o,n,g),chart:t,el:o}),i.$filler=d,m.push(d);for(n=0;n<g;++n)(d=m[n])&&(d.fill=u(m,n,p),d.boundary=l(d),d.mapper=(void 0,f=void 0,h=(c=d).fill,f="dataset",!1===h?null:(isFinite(h)||(f="boundary"),r[f](c))))},beforeDatasetDraw:function(t,e){var i=e.meta.$filler;if(i){var a=t.ctx,r=i.el,s=r._view,l=r._children||[],u=i.mapper,h=s.backgroundColor||n.global.defaultColor;u&&h&&l.length&&(o.canvas.clipArea(a,t.chartArea),function(t,e,i,n,a,o){var r,s,l,u,h,f,g,p=e.length,m=n.spanGaps,v=[],b=[],x=0,y=0;for(t.beginPath(),r=0,s=p+!!o;r<s;++r)h=i(u=e[l=r%p]._view,l,n),f=d(u),g=d(h),f&&g?(x=v.push(u),y=b.push(h)):x&&y&&(m?(f&&v.push(u),g&&b.push(h)):(c(t,v,b,x,y),x=y=0,v=[],b=[]));c(t,v,b,x,y),t.closePath(),t.fillStyle=a,t.fill()}(a,l,u,s,h,r._loop),o.canvas.unclipArea(a))}}}},{25:25,40:40,45:45}],51:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45),r=t(30),s=o.noop;function l(t,e){return t.usePointStyle?e*Math.SQRT2:t.boxWidth}n._set("global",{legend:{display:!0,position:"top",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var i=e.datasetIndex,n=this.chart,a=n.getDatasetMeta(i);a.hidden=null===a.hidden?!n.data.datasets[i].hidden:null,n.update()},onHover:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data;return o.isArray(e.datasets)?e.datasets.map(function(e,i){return{text:e.label,fillStyle:o.isArray(e.backgroundColor)?e.backgroundColor[0]:e.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:e.borderCapStyle,lineDash:e.borderDash,lineDashOffset:e.borderDashOffset,lineJoin:e.borderJoinStyle,lineWidth:e.borderWidth,strokeStyle:e.borderColor,pointStyle:e.pointStyle,datasetIndex:i}},this):[]}}},legendCallback:function(t){var e=[];e.push('<ul class="'+t.id+'-legend">');for(var i=0;i<t.data.datasets.length;i++)e.push('<li><span style="background-color:'+t.data.datasets[i].backgroundColor+'"></span>'),t.data.datasets[i].label&&e.push(t.data.datasets[i].label),e.push("</li>");return e.push("</ul>"),e.join("")}});var u=a.extend({initialize:function(t){o.extend(this,t),this.legendHitBoxes=[],this.doughnutMode=!1},beforeUpdate:s,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:s,beforeSetDimensions:s,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:s,beforeBuildLabels:s,buildLabels:function(){var t=this,e=t.options.labels||{},i=o.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter(function(i){return e.filter(i,t.chart.data)})),t.options.reverse&&i.reverse(),t.legendItems=i},afterBuildLabels:s,beforeFit:s,fit:function(){var t=this,e=t.options,i=e.labels,a=e.display,r=t.ctx,s=n.global,u=o.valueOrDefault,d=u(i.fontSize,s.defaultFontSize),c=u(i.fontStyle,s.defaultFontStyle),h=u(i.fontFamily,s.defaultFontFamily),f=o.fontString(d,c,h),g=t.legendHitBoxes=[],p=t.minSize,m=t.isHorizontal();if(m?(p.width=t.maxWidth,p.height=a?10:0):(p.width=a?10:0,p.height=t.maxHeight),a)if(r.font=f,m){var v=t.lineWidths=[0],b=t.legendItems.length?d+i.padding:0;r.textAlign="left",r.textBaseline="top",o.each(t.legendItems,function(e,n){var a=l(i,d)+d/2+r.measureText(e.text).width;v[v.length-1]+a+i.padding>=t.width&&(b+=d+i.padding,v[v.length]=t.left),g[n]={left:0,top:0,width:a,height:d},v[v.length-1]+=a+i.padding}),p.height+=b}else{var x=i.padding,y=t.columnWidths=[],k=i.padding,M=0,w=0,S=d+x;o.each(t.legendItems,function(t,e){var n=l(i,d)+d/2+r.measureText(t.text).width;w+S>p.height&&(k+=M+i.padding,y.push(M),M=0,w=0),M=Math.max(M,n),w+=S,g[e]={left:0,top:0,width:n,height:d}}),k+=M,y.push(M),p.width+=k}t.width=p.width,t.height=p.height},afterFit:s,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,i=e.labels,a=n.global,r=a.elements.line,s=t.width,u=t.lineWidths;if(e.display){var d,c=t.ctx,h=o.valueOrDefault,f=h(i.fontColor,a.defaultFontColor),g=h(i.fontSize,a.defaultFontSize),p=h(i.fontStyle,a.defaultFontStyle),m=h(i.fontFamily,a.defaultFontFamily),v=o.fontString(g,p,m);c.textAlign="left",c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=v;var b=l(i,g),x=t.legendHitBoxes,y=t.isHorizontal();d=y?{x:t.left+(s-u[0])/2,y:t.top+i.padding,line:0}:{x:t.left+i.padding,y:t.top+i.padding,line:0};var k=g+i.padding;o.each(t.legendItems,function(n,l){var f,p,m,v,M,w=c.measureText(n.text).width,S=b+g/2+w,C=d.x,_=d.y;y?C+S>=s&&(_=d.y+=k,d.line++,C=d.x=t.left+(s-u[d.line])/2):_+k>t.bottom&&(C=d.x=C+t.columnWidths[d.line]+i.padding,_=d.y=t.top+i.padding,d.line++),function(t,i,n){if(!(isNaN(b)||b<=0)){c.save(),c.fillStyle=h(n.fillStyle,a.defaultColor),c.lineCap=h(n.lineCap,r.borderCapStyle),c.lineDashOffset=h(n.lineDashOffset,r.borderDashOffset),c.lineJoin=h(n.lineJoin,r.borderJoinStyle),c.lineWidth=h(n.lineWidth,r.borderWidth),c.strokeStyle=h(n.strokeStyle,a.defaultColor);var s=0===h(n.lineWidth,r.borderWidth);if(c.setLineDash&&c.setLineDash(h(n.lineDash,r.borderDash)),e.labels&&e.labels.usePointStyle){var l=g*Math.SQRT2/2,u=l/Math.SQRT2,d=t+u,f=i+u;o.canvas.drawPoint(c,n.pointStyle,l,d,f)}else s||c.strokeRect(t,i,b,g),c.fillRect(t,i,b,g);c.restore()}}(C,_,n),x[l].left=C,x[l].top=_,f=n,p=w,v=b+(m=g/2)+C,M=_+m,c.fillText(f.text,v,M),f.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(v,M),c.lineTo(v+p,M),c.stroke()),y?d.x+=S+i.padding:d.y+=k})}},handleEvent:function(t){var e=this,i=e.options,n="mouseup"===t.type?"click":t.type,a=!1;if("mousemove"===n){if(!i.onHover)return}else{if("click"!==n)return;if(!i.onClick)return}var o=t.x,r=t.y;if(o>=e.left&&o<=e.right&&r>=e.top&&r<=e.bottom)for(var s=e.legendHitBoxes,l=0;l<s.length;++l){var u=s[l];if(o>=u.left&&o<=u.left+u.width&&r>=u.top&&r<=u.top+u.height){if("click"===n){i.onClick.call(e,t.native,e.legendItems[l]),a=!0;break}if("mousemove"===n){i.onHover.call(e,t.native,e.legendItems[l]),a=!0;break}}}return a}});function d(t,e){var i=new u({ctx:t.ctx,options:e,chart:t});r.configure(t,i,e),r.addBox(t,i),t.legend=i}e.exports={id:"legend",_element:u,beforeInit:function(t){var e=t.options.legend;e&&d(t,e)},beforeUpdate:function(t){var e=t.options.legend,i=t.legend;e?(o.mergeIf(e,n.global.legend),i?(r.configure(t,i,e),i.options=e):d(t,e)):i&&(r.removeBox(t,i),delete t.legend)},afterEvent:function(t,e){var i=t.legend;i&&i.handleEvent(e)}}},{25:25,26:26,30:30,45:45}],52:[function(t,e,i){"use strict";var n=t(25),a=t(26),o=t(45),r=t(30),s=o.noop;n._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,lineHeight:1.2,padding:10,position:"top",text:"",weight:2e3}});var l=a.extend({initialize:function(t){o.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:s,update:function(t,e,i){var n=this;return n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n.margins=i,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n.beforeBuildLabels(),n.buildLabels(),n.afterBuildLabels(),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate(),n.minSize},afterUpdate:s,beforeSetDimensions:s,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:s,beforeBuildLabels:s,buildLabels:s,afterBuildLabels:s,beforeFit:s,fit:function(){var t=this,e=o.valueOrDefault,i=t.options,a=i.display,r=e(i.fontSize,n.global.defaultFontSize),s=t.minSize,l=o.isArray(i.text)?i.text.length:1,u=o.options.toLineHeight(i.lineHeight,r),d=a?l*u+2*i.padding:0;t.isHorizontal()?(s.width=t.maxWidth,s.height=d):(s.width=d,s.height=t.maxHeight),t.width=s.width,t.height=s.height},afterFit:s,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,i=o.valueOrDefault,a=t.options,r=n.global;if(a.display){var s,l,u,d=i(a.fontSize,r.defaultFontSize),c=i(a.fontStyle,r.defaultFontStyle),h=i(a.fontFamily,r.defaultFontFamily),f=o.fontString(d,c,h),g=o.options.toLineHeight(a.lineHeight,d),p=g/2+a.padding,m=0,v=t.top,b=t.left,x=t.bottom,y=t.right;e.fillStyle=i(a.fontColor,r.defaultFontColor),e.font=f,t.isHorizontal()?(l=b+(y-b)/2,u=v+p,s=y-b):(l="left"===a.position?b+p:y-p,u=v+(x-v)/2,s=x-v,m=Math.PI*("left"===a.position?-.5:.5)),e.save(),e.translate(l,u),e.rotate(m),e.textAlign="center",e.textBaseline="middle";var k=a.text;if(o.isArray(k))for(var M=0,w=0;w<k.length;++w)e.fillText(k[w],0,M,s),M+=g;else e.fillText(k,0,0,s);e.restore()}}});function u(t,e){var i=new l({ctx:t.ctx,options:e,chart:t});r.configure(t,i,e),r.addBox(t,i),t.titleBlock=i}e.exports={id:"title",_element:l,beforeInit:function(t){var e=t.options.title;e&&u(t,e)},beforeUpdate:function(t){var e=t.options.title,i=t.titleBlock;e?(o.mergeIf(e,n.global.title),i?(r.configure(t,i,e),i.options=e):u(t,e)):i&&(r.removeBox(t,i),delete t.titleBlock)}}},{25:25,26:26,30:30,45:45}],53:[function(t,e,i){"use strict";e.exports=function(t){var e=t.Scale.extend({getLabels:function(){var t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels},determineDataLimits:function(){var t,e=this,i=e.getLabels();e.minIndex=0,e.maxIndex=i.length-1,void 0!==e.options.ticks.min&&(t=i.indexOf(e.options.ticks.min),e.minIndex=-1!==t?t:e.minIndex),void 0!==e.options.ticks.max&&(t=i.indexOf(e.options.ticks.max),e.maxIndex=-1!==t?t:e.maxIndex),e.min=i[e.minIndex],e.max=i[e.maxIndex]},buildTicks:function(){var t=this,e=t.getLabels();t.ticks=0===t.minIndex&&t.maxIndex===e.length-1?e:e.slice(t.minIndex,t.maxIndex+1)},getLabelForIndex:function(t,e){var i=this,n=i.chart.data,a=i.isHorizontal();return n.yLabels&&!a?i.getRightValue(n.datasets[e].data[t]):i.ticks[t-i.minIndex]},getPixelForValue:function(t,e){var i,n=this,a=n.options.offset,o=Math.max(n.maxIndex+1-n.minIndex-(a?0:1),1);if(null!=t&&(i=n.isHorizontal()?t.x:t.y),void 0!==i||void 0!==t&&isNaN(e)){t=i||t;var r=n.getLabels().indexOf(t);e=-1!==r?r:e}if(n.isHorizontal()){var s=n.width/o,l=s*(e-n.minIndex);return a&&(l+=s/2),n.left+Math.round(l)}var u=n.height/o,d=u*(e-n.minIndex);return a&&(d+=u/2),n.top+Math.round(d)},getPixelForTick:function(t){return this.getPixelForValue(this.ticks[t],t+this.minIndex,null)},getValueForPixel:function(t){var e=this,i=e.options.offset,n=Math.max(e._ticks.length-(i?0:1),1),a=e.isHorizontal(),o=(a?e.width:e.height)/n;return t-=a?e.left:e.top,i&&(t-=o/2),(t<=0?0:Math.round(t/o))+e.minIndex},getBasePixel:function(){return this.bottom}});t.scaleService.registerScaleType("category",e,{position:"bottom"})}},{}],54:[function(t,e,i){"use strict";var n=t(25),a=t(45),o=t(34);e.exports=function(t){var e={position:"left",ticks:{callback:o.formatters.linear}},i=t.LinearScaleBase.extend({determineDataLimits:function(){var t=this,e=t.options,i=t.chart,n=i.data.datasets,o=t.isHorizontal();function r(e){return o?e.xAxisID===t.id:e.yAxisID===t.id}t.min=null,t.max=null;var s=e.stacked;if(void 0===s&&a.each(n,function(t,e){if(!s){var n=i.getDatasetMeta(e);i.isDatasetVisible(e)&&r(n)&&void 0!==n.stack&&(s=!0)}}),e.stacked||s){var l={};a.each(n,function(n,o){var s=i.getDatasetMeta(o),u=[s.type,void 0===e.stacked&&void 0===s.stack?o:"",s.stack].join(".");void 0===l[u]&&(l[u]={positiveValues:[],negativeValues:[]});var d=l[u].positiveValues,c=l[u].negativeValues;i.isDatasetVisible(o)&&r(s)&&a.each(n.data,function(i,n){var a=+t.getRightValue(i);isNaN(a)||s.data[n].hidden||(d[n]=d[n]||0,c[n]=c[n]||0,e.relativePoints?d[n]=100:a<0?c[n]+=a:d[n]+=a)})}),a.each(l,function(e){var i=e.positiveValues.concat(e.negativeValues),n=a.min(i),o=a.max(i);t.min=null===t.min?n:Math.min(t.min,n),t.max=null===t.max?o:Math.max(t.max,o)})}else a.each(n,function(e,n){var o=i.getDatasetMeta(n);i.isDatasetVisible(n)&&r(o)&&a.each(e.data,function(e,i){var n=+t.getRightValue(e);isNaN(n)||o.data[i].hidden||(null===t.min?t.min=n:n<t.min&&(t.min=n),null===t.max?t.max=n:n>t.max&&(t.max=n))})});t.min=isFinite(t.min)&&!isNaN(t.min)?t.min:0,t.max=isFinite(t.max)&&!isNaN(t.max)?t.max:1,this.handleTickRangeOptions()},getTickLimit:function(){var t,e=this.options.ticks;if(this.isHorizontal())t=Math.min(e.maxTicksLimit?e.maxTicksLimit:11,Math.ceil(this.width/50));else{var i=a.valueOrDefault(e.fontSize,n.global.defaultFontSize);t=Math.min(e.maxTicksLimit?e.maxTicksLimit:11,Math.ceil(this.height/(2*i)))}return t},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e=this,i=e.start,n=+e.getRightValue(t),a=e.end-i;return e.isHorizontal()?e.left+e.width/a*(n-i):e.bottom-e.height/a*(n-i)},getValueForPixel:function(t){var e=this,i=e.isHorizontal(),n=i?e.width:e.height,a=(i?t-e.left:e.bottom-t)/n;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}});t.scaleService.registerScaleType("linear",i,e)}},{25:25,34:34,45:45}],55:[function(t,e,i){"use strict";var n=t(45);e.exports=function(t){var e=n.noop;t.LinearScaleBase=t.Scale.extend({getRightValue:function(e){return"string"==typeof e?+e:t.Scale.prototype.getRightValue.call(this,e)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var i=n.sign(t.min),a=n.sign(t.max);i<0&&a<0?t.max=0:i>0&&a>0&&(t.min=0)}var o=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),o!==r&&t.min>=t.max&&(o?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:e,handleDirectionalChanges:e,buildTicks:function(){var t=this,e=t.options.ticks,i=t.getTickLimit(),a={maxTicks:i=Math.max(2,i),min:e.min,max:e.max,stepSize:n.valueOrDefault(e.fixedStepSize,e.stepSize)},o=t.ticks=function(t,e){var i,a=[];if(t.stepSize&&t.stepSize>0)i=t.stepSize;else{var o=n.niceNum(e.max-e.min,!1);i=n.niceNum(o/(t.maxTicks-1),!0)}var r=Math.floor(e.min/i)*i,s=Math.ceil(e.max/i)*i;t.min&&t.max&&t.stepSize&&n.almostWhole((t.max-t.min)/t.stepSize,i/1e3)&&(r=t.min,s=t.max);var l=(s-r)/i;l=n.almostEquals(l,Math.round(l),i/1e3)?Math.round(l):Math.ceil(l);var u=1;i<1&&(u=Math.pow(10,i.toString().length-2),r=Math.round(r*u)/u,s=Math.round(s*u)/u),a.push(void 0!==t.min?t.min:r);for(var d=1;d<l;++d)a.push(Math.round((r+d*i)*u)/u);return a.push(void 0!==t.max?t.max:s),a}(a,t);t.handleDirectionalChanges(),t.max=n.max(o),t.min=n.min(o),e.reverse?(o.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(e)}})}},{45:45}],56:[function(t,e,i){"use strict";var n=t(45),a=t(34);e.exports=function(t){var e={position:"left",ticks:{callback:a.formatters.logarithmic}},i=t.Scale.extend({determineDataLimits:function(){var t=this,e=t.options,i=t.chart,a=i.data.datasets,o=t.isHorizontal();function r(e){return o?e.xAxisID===t.id:e.yAxisID===t.id}t.min=null,t.max=null,t.minNotZero=null;var s=e.stacked;if(void 0===s&&n.each(a,function(t,e){if(!s){var n=i.getDatasetMeta(e);i.isDatasetVisible(e)&&r(n)&&void 0!==n.stack&&(s=!0)}}),e.stacked||s){var l={};n.each(a,function(a,o){var s=i.getDatasetMeta(o),u=[s.type,void 0===e.stacked&&void 0===s.stack?o:"",s.stack].join(".");i.isDatasetVisible(o)&&r(s)&&(void 0===l[u]&&(l[u]=[]),n.each(a.data,function(e,i){var n=l[u],a=+t.getRightValue(e);isNaN(a)||s.data[i].hidden||a<0||(n[i]=n[i]||0,n[i]+=a)}))}),n.each(l,function(e){if(e.length>0){var i=n.min(e),a=n.max(e);t.min=null===t.min?i:Math.min(t.min,i),t.max=null===t.max?a:Math.max(t.max,a)}})}else n.each(a,function(e,a){var o=i.getDatasetMeta(a);i.isDatasetVisible(a)&&r(o)&&n.each(e.data,function(e,i){var n=+t.getRightValue(e);isNaN(n)||o.data[i].hidden||n<0||(null===t.min?t.min=n:n<t.min&&(t.min=n),null===t.max?t.max=n:n>t.max&&(t.max=n),0!==n&&(null===t.minNotZero||n<t.minNotZero)&&(t.minNotZero=n))})});this.handleTickRangeOptions()},handleTickRangeOptions:function(){var t=this,e=t.options.ticks,i=n.valueOrDefault;t.min=i(e.min,t.min),t.max=i(e.max,t.max),t.min===t.max&&(0!==t.min&&null!==t.min?(t.min=Math.pow(10,Math.floor(n.log10(t.min))-1),t.max=Math.pow(10,Math.floor(n.log10(t.max))+1)):(t.min=1,t.max=10)),null===t.min&&(t.min=Math.pow(10,Math.floor(n.log10(t.max))-1)),null===t.max&&(t.max=0!==t.min?Math.pow(10,Math.floor(n.log10(t.min))+1):10),null===t.minNotZero&&(t.min>0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(n.log10(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,i=!t.isHorizontal(),a={min:e.min,max:e.max},o=t.ticks=function(t,e){var i,a,o=[],r=n.valueOrDefault,s=r(t.min,Math.pow(10,Math.floor(n.log10(e.min)))),l=Math.floor(n.log10(e.max)),u=Math.ceil(e.max/Math.pow(10,l));0===s?(i=Math.floor(n.log10(e.minNotZero)),a=Math.floor(e.minNotZero/Math.pow(10,i)),o.push(s),s=a*Math.pow(10,i)):(i=Math.floor(n.log10(s)),a=Math.floor(s/Math.pow(10,i)));for(var d=i<0?Math.pow(10,Math.abs(i)):1;o.push(s),10==++a&&(a=1,d=++i>=0?1:d),s=Math.round(a*Math.pow(10,i)*d)/d,i<l||i===l&&a<u;);var c=r(t.max,s);return o.push(c),o}(a,t);t.max=n.max(o),t.min=n.min(o),e.reverse?(i=!i,t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),i&&o.reverse()},convertTicksToLabels:function(){this.tickValues=this.ticks.slice(),t.Scale.prototype.convertTicksToLabels.call(this)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForTick:function(t){return this.getPixelForValue(this.tickValues[t])},_getFirstTickValue:function(t){var e=Math.floor(n.log10(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},getPixelForValue:function(e){var i,a,o,r,s,l=this,u=l.options.ticks.reverse,d=n.log10,c=l._getFirstTickValue(l.minNotZero),h=0;return e=+l.getRightValue(e),u?(o=l.end,r=l.start,s=-1):(o=l.start,r=l.end,s=1),l.isHorizontal()?(i=l.width,a=u?l.right:l.left):(i=l.height,s*=-1,a=u?l.top:l.bottom),e!==o&&(0===o&&(i-=h=n.getValueOrDefault(l.options.ticks.fontSize,t.defaults.global.defaultFontSize),o=c),0!==e&&(h+=i/(d(r)-d(o))*(d(e)-d(o))),a+=s*h),a},getValueForPixel:function(e){var i,a,o,r,s=this,l=s.options.ticks.reverse,u=n.log10,d=s._getFirstTickValue(s.minNotZero);if(l?(a=s.end,o=s.start):(a=s.start,o=s.end),s.isHorizontal()?(i=s.width,r=l?s.right-e:e-s.left):(i=s.height,r=l?e-s.top:s.bottom-e),r!==a){if(0===a){var c=n.getValueOrDefault(s.options.ticks.fontSize,t.defaults.global.defaultFontSize);r-=c,i-=c,a=d}r*=u(o)-u(a),r/=i,r=Math.pow(10,u(a)+r)}return r}});t.scaleService.registerScaleType("logarithmic",i,e)}},{34:34,45:45}],57:[function(t,e,i){"use strict";var n=t(25),a=t(45),o=t(34);e.exports=function(t){var e=n.global,i={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0, 0, 0, 0.1)",lineWidth:1},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:o.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function r(t){var e=t.options;return e.angleLines.display||e.pointLabels.display?t.chart.data.labels.length:0}function s(t){var i=t.options.pointLabels,n=a.valueOrDefault(i.fontSize,e.defaultFontSize),o=a.valueOrDefault(i.fontStyle,e.defaultFontStyle),r=a.valueOrDefault(i.fontFamily,e.defaultFontFamily);return{size:n,style:o,family:r,font:a.fontString(n,o,r)}}function l(t,e,i,n,a){return t===n||t===a?{start:e-i/2,end:e+i/2}:t<n||t>a?{start:e-i-5,end:e}:{start:e,end:e+i+5}}function u(t,e,i,n){if(a.isArray(e))for(var o=i.y,r=1.5*n,s=0;s<e.length;++s)t.fillText(e[s],i.x,o),o+=r;else t.fillText(e,i.x,i.y)}function d(t){return a.isNumber(t)?t:0}var c=t.LinearScaleBase.extend({setDimensions:function(){var t=this,i=t.options,n=i.ticks;t.width=t.maxWidth,t.height=t.maxHeight,t.xCenter=Math.round(t.width/2),t.yCenter=Math.round(t.height/2);var o=a.min([t.height,t.width]),r=a.valueOrDefault(n.fontSize,e.defaultFontSize);t.drawingArea=i.display?o/2-(r/2+n.backdropPaddingY):o/2},determineDataLimits:function(){var t=this,e=t.chart,i=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY;a.each(e.data.datasets,function(o,r){if(e.isDatasetVisible(r)){var s=e.getDatasetMeta(r);a.each(o.data,function(e,a){var o=+t.getRightValue(e);isNaN(o)||s.data[a].hidden||(i=Math.min(o,i),n=Math.max(o,n))})}}),t.min=i===Number.POSITIVE_INFINITY?0:i,t.max=n===Number.NEGATIVE_INFINITY?0:n,t.handleTickRangeOptions()},getTickLimit:function(){var t=this.options.ticks,i=a.valueOrDefault(t.fontSize,e.defaultFontSize);return Math.min(t.maxTicksLimit?t.maxTicksLimit:11,Math.ceil(this.drawingArea/(1.5*i)))},convertTicksToLabels:function(){var e=this;t.LinearScaleBase.prototype.convertTicksToLabels.call(e),e.pointLabels=e.chart.data.labels.map(e.options.pointLabels.callback,e)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t,e;this.options.pointLabels.display?function(t){var e,i,n,o=s(t),u=Math.min(t.height/2,t.width/2),d={r:t.width,l:0,t:t.height,b:0},c={};t.ctx.font=o.font,t._pointLabelSizes=[];var h,f,g,p=r(t);for(e=0;e<p;e++){n=t.getPointPosition(e,u),h=t.ctx,f=o.size,g=t.pointLabels[e]||"",i=a.isArray(g)?{w:a.longestText(h,h.font,g),h:g.length*f+1.5*(g.length-1)*f}:{w:h.measureText(g).width,h:f},t._pointLabelSizes[e]=i;var m=t.getIndexAngle(e),v=a.toDegrees(m)%360,b=l(v,n.x,i.w,0,180),x=l(v,n.y,i.h,90,270);b.start<d.l&&(d.l=b.start,c.l=m),b.end>d.r&&(d.r=b.end,c.r=m),x.start<d.t&&(d.t=x.start,c.t=m),x.end>d.b&&(d.b=x.end,c.b=m)}t.setReductions(u,d,c)}(this):(t=this,e=Math.min(t.height/2,t.width/2),t.drawingArea=Math.round(e),t.setCenterPoint(0,0,0,0))},setReductions:function(t,e,i){var n=e.l/Math.sin(i.l),a=Math.max(e.r-this.width,0)/Math.sin(i.r),o=-e.t/Math.cos(i.t),r=-Math.max(e.b-this.height,0)/Math.cos(i.b);n=d(n),a=d(a),o=d(o),r=d(r),this.drawingArea=Math.min(Math.round(t-(n+a)/2),Math.round(t-(o+r)/2)),this.setCenterPoint(n,a,o,r)},setCenterPoint:function(t,e,i,n){var a=this,o=a.width-e-a.drawingArea,r=t+a.drawingArea,s=i+a.drawingArea,l=a.height-n-a.drawingArea;a.xCenter=Math.round((r+o)/2+a.left),a.yCenter=Math.round((s+l)/2+a.top)},getIndexAngle:function(t){return t*(2*Math.PI/r(this))+(this.chart.options&&this.chart.options.startAngle?this.chart.options.startAngle:0)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(null===t)return 0;var i=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*i:(t-e.min)*i},getPointPosition:function(t,e){var i=this.getIndexAngle(t)-Math.PI/2;return{x:Math.round(Math.cos(i)*e)+this.xCenter,y:Math.round(Math.sin(i)*e)+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(){var t=this.min,e=this.max;return this.getPointPositionForValue(0,this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0)},draw:function(){var t=this,i=t.options,n=i.gridLines,o=i.ticks,l=a.valueOrDefault;if(i.display){var d=t.ctx,c=this.getIndexAngle(0),h=l(o.fontSize,e.defaultFontSize),f=l(o.fontStyle,e.defaultFontStyle),g=l(o.fontFamily,e.defaultFontFamily),p=a.fontString(h,f,g);a.each(t.ticks,function(i,s){if(s>0||o.reverse){var u=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]);if(n.display&&0!==s&&function(t,e,i,n){var o=t.ctx;if(o.strokeStyle=a.valueAtIndexOrDefault(e.color,n-1),o.lineWidth=a.valueAtIndexOrDefault(e.lineWidth,n-1),t.options.gridLines.circular)o.beginPath(),o.arc(t.xCenter,t.yCenter,i,0,2*Math.PI),o.closePath(),o.stroke();else{var s=r(t);if(0===s)return;o.beginPath();var l=t.getPointPosition(0,i);o.moveTo(l.x,l.y);for(var u=1;u<s;u++)l=t.getPointPosition(u,i),o.lineTo(l.x,l.y);o.closePath(),o.stroke()}}(t,n,u,s),o.display){var f=l(o.fontColor,e.defaultFontColor);if(d.font=p,d.save(),d.translate(t.xCenter,t.yCenter),d.rotate(c),o.showLabelBackdrop){var g=d.measureText(i).width;d.fillStyle=o.backdropColor,d.fillRect(-g/2-o.backdropPaddingX,-u-h/2-o.backdropPaddingY,g+2*o.backdropPaddingX,h+2*o.backdropPaddingY)}d.textAlign="center",d.textBaseline="middle",d.fillStyle=f,d.fillText(i,0,-u),d.restore()}}}),(i.angleLines.display||i.pointLabels.display)&&function(t){var i=t.ctx,n=t.options,o=n.angleLines,l=n.pointLabels;i.lineWidth=o.lineWidth,i.strokeStyle=o.color;var d,c,h,f,g=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),p=s(t);i.textBaseline="top";for(var m=r(t)-1;m>=0;m--){if(o.display){var v=t.getPointPosition(m,g);i.beginPath(),i.moveTo(t.xCenter,t.yCenter),i.lineTo(v.x,v.y),i.stroke(),i.closePath()}if(l.display){var b=t.getPointPosition(m,g+5),x=a.valueAtIndexOrDefault(l.fontColor,m,e.defaultFontColor);i.font=p.font,i.fillStyle=x;var y=t.getIndexAngle(m),k=a.toDegrees(y);i.textAlign=0===(f=k)||180===f?"center":f<180?"left":"right",d=k,c=t._pointLabelSizes[m],h=b,90===d||270===d?h.y-=c.h/2:(d>270||d<90)&&(h.y-=c.h),u(i,t.pointLabels[m]||"",b,p.size)}}}(t)}}});t.scaleService.registerScaleType("radialLinear",c,i)}},{25:25,34:34,45:45}],58:[function(t,e,i){"use strict";var n=t(1);n="function"==typeof n?n:window.moment;var a=t(25),o=t(45),r=Number.MIN_SAFE_INTEGER||-9007199254740991,s=Number.MAX_SAFE_INTEGER||9007199254740991,l={millisecond:{common:!0,size:1,steps:[1,2,5,10,20,50,100,250,500]},second:{common:!0,size:1e3,steps:[1,2,5,10,30]},minute:{common:!0,size:6e4,steps:[1,2,5,10,30]},hour:{common:!0,size:36e5,steps:[1,2,3,6,12]},day:{common:!0,size:864e5,steps:[1,2,5]},week:{common:!1,size:6048e5,steps:[1,2,3,4]},month:{common:!0,size:2628e6,steps:[1,2,3]},quarter:{common:!1,size:7884e6,steps:[1,2,3,4]},year:{common:!0,size:3154e7}},u=Object.keys(l);function d(t,e){return t-e}function c(t){var e,i,n,a={},o=[];for(e=0,i=t.length;e<i;++e)a[n=t[e]]||(a[n]=!0,o.push(n));return o}function h(t,e,i,n){var a=function(t,e,i){for(var n,a,o,r=0,s=t.length-1;r>=0&&r<=s;){if(a=t[(n=r+s>>1)-1]||null,o=t[n],!a)return{lo:null,hi:o};if(o[e]<i)r=n+1;else{if(!(a[e]>i))return{lo:a,hi:o};s=n-1}}return{lo:o,hi:null}}(t,e,i),o=a.lo?a.hi?a.lo:t[t.length-2]:t[0],r=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=r[e]-o[e],l=s?(i-o[e])/s:0,u=(r[n]-o[n])*l;return o[n]+u}function f(t,e){var i=e.parser,a=e.parser||e.format;return"function"==typeof i?i(t):"string"==typeof t&&"string"==typeof a?n(t,a):(t instanceof n||(t=n(t)),t.isValid()?t:"function"==typeof a?a(t):t)}function g(t,e){if(o.isNullOrUndef(t))return null;var i=e.options.time,n=f(e.getRightValue(t),i);return n.isValid()?(i.round&&n.startOf(i.round),n.valueOf()):null}function p(t){for(var e=u.indexOf(t)+1,i=u.length;e<i;++e)if(l[u[e]].common)return u[e]}function m(t,e,i,a){var r,d=a.time,c=d.unit||function(t,e,i,n){var a,o,r,d=u.length;for(a=u.indexOf(t);a<d-1;++a)if(r=(o=l[u[a]]).steps?o.steps[o.steps.length-1]:s,o.common&&Math.ceil((i-e)/(r*o.size))<=n)return u[a];return u[d-1]}(d.minUnit,t,e,i),h=p(c),f=o.valueOrDefault(d.stepSize,d.unitStepSize),g="week"===c&&d.isoWeekday,m=a.ticks.major.enabled,v=l[c],b=n(t),x=n(e),y=[];for(f||(f=function(t,e,i,n){var a,o,r,s=e-t,u=l[i],d=u.size,c=u.steps;if(!c)return Math.ceil(s/(n*d));for(a=0,o=c.length;a<o&&(r=c[a],!(Math.ceil(s/(d*r))<=n));++a);return r}(t,e,c,i)),g&&(b=b.isoWeekday(g),x=x.isoWeekday(g)),b=b.startOf(g?"day":c),(x=x.startOf(g?"day":c))<e&&x.add(1,c),r=n(b),m&&h&&!g&&!d.round&&(r.startOf(h),r.add(~~((b-r)/(v.size*f))*f,c));r<x;r.add(f,c))y.push(+r);return y.push(+r),y}e.exports=function(t){var e=t.Scale.extend({initialize:function(){if(!n)throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com");this.mergeTicksOptions(),t.Scale.prototype.initialize.call(this)},update:function(){var e=this.options;return e.time&&e.time.format&&console.warn("options.time.format is deprecated and replaced by options.time.parser."),t.Scale.prototype.update.apply(this,arguments)},getRightValue:function(e){return e&&void 0!==e.t&&(e=e.t),t.Scale.prototype.getRightValue.call(this,e)},determineDataLimits:function(){var t,e,i,a,l,u,h=this,f=h.chart,p=h.options.time,m=p.unit||"day",v=s,b=r,x=[],y=[],k=[];for(t=0,i=f.data.labels.length;t<i;++t)k.push(g(f.data.labels[t],h));for(t=0,i=(f.data.datasets||[]).length;t<i;++t)if(f.isDatasetVisible(t))if(l=f.data.datasets[t].data,o.isObject(l[0]))for(y[t]=[],e=0,a=l.length;e<a;++e)u=g(l[e],h),x.push(u),y[t][e]=u;else x.push.apply(x,k),y[t]=k.slice(0);else y[t]=[];k.length&&(k=c(k).sort(d),v=Math.min(v,k[0]),b=Math.max(b,k[k.length-1])),x.length&&(x=c(x).sort(d),v=Math.min(v,x[0]),b=Math.max(b,x[x.length-1])),v=g(p.min,h)||v,b=g(p.max,h)||b,v=v===s?+n().startOf(m):v,b=b===r?+n().endOf(m)+1:b,h.min=Math.min(v,b),h.max=Math.max(v+1,b),h._horizontal=h.isHorizontal(),h._table=[],h._timestamps={data:x,datasets:y,labels:k}},buildTicks:function(){var t,e,i,a,o,r,s,d,c,v,b,x,y=this,k=y.min,M=y.max,w=y.options,S=w.time,C=[],_=[];switch(w.ticks.source){case"data":C=y._timestamps.data;break;case"labels":C=y._timestamps.labels;break;case"auto":default:C=m(k,M,y.getLabelCapacity(k),w)}for("ticks"===w.bounds&&C.length&&(k=C[0],M=C[C.length-1]),k=g(S.min,y)||k,M=g(S.max,y)||M,t=0,e=C.length;t<e;++t)(i=C[t])>=k&&i<=M&&_.push(i);return y.min=k,y.max=M,y._unit=S.unit||function(t,e,i,a){var o,r,s=n.duration(n(a).diff(n(i)));for(o=u.length-1;o>=u.indexOf(e);o--)if(r=u[o],l[r].common&&s.as(r)>=t.length)return r;return u[e?u.indexOf(e):0]}(_,S.minUnit,y.min,y.max),y._majorUnit=p(y._unit),y._table=function(t,e,i,n){if("linear"===n||!t.length)return[{time:e,pos:0},{time:i,pos:1}];var a,o,r,s,l,u=[],d=[e];for(a=0,o=t.length;a<o;++a)(s=t[a])>e&&s<i&&d.push(s);for(d.push(i),a=0,o=d.length;a<o;++a)l=d[a+1],r=d[a-1],s=d[a],void 0!==r&&void 0!==l&&Math.round((l+r)/2)===s||u.push({time:s,pos:a/(o-1)});return u}(y._timestamps.data,k,M,w.distribution),y._offsets=(a=y._table,o=_,r=k,s=M,b=0,x=0,(d=w).offset&&o.length&&(d.time.min||(c=o.length>1?o[1]:s,v=o[0],b=(h(a,"time",c,"pos")-h(a,"time",v,"pos"))/2),d.time.max||(c=o[o.length-1],v=o.length>1?o[o.length-2]:r,x=(h(a,"time",c,"pos")-h(a,"time",v,"pos"))/2)),{left:b,right:x}),y._labelFormat=function(t,e){var i,n,a,o=t.length;for(i=0;i<o;i++){if(0!==(n=f(t[i],e)).millisecond())return"MMM D, YYYY h:mm:ss.SSS a";0===n.second()&&0===n.minute()&&0===n.hour()||(a=!0)}return a?"MMM D, YYYY h:mm:ss a":"MMM D, YYYY"}(y._timestamps.data,S),function(t,e){var i,a,o,r,s=[];for(i=0,a=t.length;i<a;++i)o=t[i],r=!!e&&o===+n(o).startOf(e),s.push({value:o,major:r});return s}(_,y._majorUnit)},getLabelForIndex:function(t,e){var i=this.chart.data,n=this.options.time,a=i.labels&&t<i.labels.length?i.labels[t]:"",r=i.datasets[e].data[t];return o.isObject(r)&&(a=this.getRightValue(r)),n.tooltipFormat?f(a,n).format(n.tooltipFormat):"string"==typeof a?a:f(a,n).format(this._labelFormat)},tickFormatFunction:function(t,e,i,n){var a=this.options,r=t.valueOf(),s=a.time.displayFormats,l=s[this._unit],u=this._majorUnit,d=s[u],c=t.clone().startOf(u).valueOf(),h=a.ticks.major,f=h.enabled&&u&&d&&r===c,g=t.format(n||(f?d:l)),p=f?h:a.ticks.minor,m=o.valueOrDefault(p.callback,p.userCallback);return m?m(g,e,i):g},convertTicksToLabels:function(t){var e,i,a=[];for(e=0,i=t.length;e<i;++e)a.push(this.tickFormatFunction(n(t[e].value),e,t));return a},getPixelForOffset:function(t){var e=this,i=e._horizontal?e.width:e.height,n=e._horizontal?e.left:e.top,a=h(e._table,"time",t,"pos");return n+i*(e._offsets.left+a)/(e._offsets.left+1+e._offsets.right)},getPixelForValue:function(t,e,i){var n=null;if(void 0!==e&&void 0!==i&&(n=this._timestamps.datasets[i][e]),null===n&&(n=g(t,this)),null!==n)return this.getPixelForOffset(n)},getPixelForTick:function(t){var e=this.getTicks();return t>=0&&t<e.length?this.getPixelForOffset(e[t].value):null},getValueForPixel:function(t){var e=this,i=e._horizontal?e.width:e.height,a=e._horizontal?e.left:e.top,o=(i?(t-a)/i:0)*(e._offsets.left+1+e._offsets.left)-e._offsets.right,r=h(e._table,"pos",o,"time");return n(r)},getLabelWidth:function(t){var e=this.options.ticks,i=this.ctx.measureText(t).width,n=o.toRadians(e.maxRotation),r=Math.cos(n),s=Math.sin(n);return i*r+o.valueOrDefault(e.fontSize,a.global.defaultFontSize)*s},getLabelCapacity:function(t){var e=this,i=e.options.time.displayFormats.millisecond,a=e.tickFormatFunction(n(t),0,[],i),o=e.getLabelWidth(a),r=e.isHorizontal()?e.width:e.height,s=Math.floor(r/o);return s>0?s:1}});t.scaleService.registerScaleType("time",e,{position:"bottom",distribution:"linear",bounds:"data",time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}})}},{1:1,25:25,45:45}]},{},[7])(7)});
\ No newline at end of file
Binary file image/www/js/Chart.min.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/js/ch-plug-anno.min.js	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,10 @@
+/*!
+ * chartjs-plugin-annotation.js
+ * http://chartjs.org/
+ * Version: 0.5.7
+ *
+ * Copyright 2016 Evert Timberg
+ * Released under the MIT license
+ * https://github.com/chartjs/Chart.Annotation.js/blob/master/LICENSE.md
+ */
+!function e(t,n,i){function o(r,l){if(!n[r]){if(!t[r]){var s="function"==typeof require&&require;if(!l&&s)return s(r,!0);if(a)return a(r,!0);var c=new Error("Cannot find module '"+r+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[r]={exports:{}};t[r][0].call(u.exports,function(e){var n=t[r][1][e];return o(n?n:e)},u,u.exports,e,t,n,i)}return n[r].exports}for(var a="function"==typeof require&&require,r=0;r<i.length;r++)o(i[r]);return o}({1:[function(e,t,n){},{}],2:[function(e,t,n){t.exports=function(t){function n(e){a.decorate(e,"afterDataLimits",function(e,t){e&&e(t),a.adjustScaleRange(t)})}function i(e){return function(t,n){var i=t.annotation.options.drawTime;a.elements(t).filter(function(t){return e===(t.options.drawTime||i)}).forEach(function(e){e.transition(n).draw()})}}var o=t.helpers,a=e("./helpers.js")(t),r=e("./events.js")(t),l=t.Annotation.types;return{beforeInit:function(e){var t=e.options,i=e.annotation={elements:{},options:a.initConfig(t.annotation||{}),onDestroy:[],firstRun:!0,supported:!1};e.ensureScalesHaveIDs(),t.scales&&(i.supported=!0,o.each(t.scales.xAxes,n),o.each(t.scales.yAxes,n))},beforeUpdate:function(e){var t=e.annotation;if(t.supported){t.firstRun?t.firstRun=!1:t.options=a.initConfig(e.options.annotation||{});var n=[];t.options.annotations.forEach(function(i){var o=i.id||a.objectId();if(!t.elements[o]&&l[i.type]){var r=l[i.type],s=new r({id:o,options:i,chartInstance:e});s.initialize(),t.elements[o]=s,i.id=o,n.push(o)}else t.elements[o]&&n.push(o)}),Object.keys(t.elements).forEach(function(e){n.indexOf(e)===-1&&(t.elements[e].destroy(),delete t.elements[e])})}},afterScaleUpdate:function(e){a.elements(e).forEach(function(e){e.configure()})},beforeDatasetsDraw:i("beforeDatasetsDraw"),afterDatasetsDraw:i("afterDatasetsDraw"),afterDraw:i("afterDraw"),afterInit:function(e){var t=e.annotation.options.events;if(o.isArray(t)&&t.length>0){var n=e.chart.canvas,i=r.dispatcher.bind(e);r.collapseHoverEvents(t).forEach(function(t){o.addEvent(n,t,i),e.annotation.onDestroy.push(function(){o.removeEvent(n,t,i)})})}},destroy:function(e){for(var t=e.annotation.onDestroy;t.length>0;)t.pop()()}}}},{"./events.js":4,"./helpers.js":5}],3:[function(e,t,n){t.exports=function(e){var t=e.helpers,n=e.Element.extend({initialize:function(){this.hidden=!1,this.hovering=!1,this._model=t.clone(this._model)||{},this.setDataLimits()},destroy:function(){},setDataLimits:function(){},configure:function(){},inRange:function(){},getCenterPoint:function(){},getWidth:function(){},getHeight:function(){},getArea:function(){},draw:function(){}});return n}},{}],4:[function(e,t,n){t.exports=function(t){function n(e){var t=!1,n=e.filter(function(e){switch(e){case"mouseenter":case"mouseover":case"mouseout":case"mouseleave":return t=!0,!1;default:return!0}});return t&&n.indexOf("mousemove")===-1&&n.push("mousemove"),n}function i(e){var t=this.annotation,i=a.elements(this),r=o.getRelativePosition(e,this.chart),l=a.getNearestItems(i,r),s=n(t.options.events),c=t.options.dblClickSpeed,u=[],f=a.getEventHandlerName(e.type),d=(l||{}).options;if("mousemove"===e.type&&(l&&!l.hovering?["mouseenter","mouseover"].forEach(function(t){var n=a.getEventHandlerName(t),i=a.createMouseEvent(t,e);l.hovering=!0,"function"==typeof d[n]&&u.push([d[n],i,l])}):l||i.forEach(function(t){if(t.hovering){t.hovering=!1;var n=t.options;["mouseout","mouseleave"].forEach(function(i){var o=a.getEventHandlerName(i),r=a.createMouseEvent(i,e);"function"==typeof n[o]&&u.push([n[o],r,t])})}})),l&&s.indexOf("dblclick")>-1&&"function"==typeof d.onDblclick){if("click"===e.type&&"function"==typeof d.onClick)return clearTimeout(l.clickTimeout),l.clickTimeout=setTimeout(function(){delete l.clickTimeout,d.onClick.call(l,e)},c),e.stopImmediatePropagation(),void e.preventDefault();"dblclick"===e.type&&l.clickTimeout&&(clearTimeout(l.clickTimeout),delete l.clickTimeout)}l&&"function"==typeof d[f]&&0===u.length&&u.push([d[f],e,l]),u.length>0&&(e.stopImmediatePropagation(),e.preventDefault(),u.forEach(function(e){e[0].call(e[2],e[1])}))}var o=t.helpers,a=e("./helpers.js")(t);return{dispatcher:i,collapseHoverEvents:n}}},{"./helpers.js":5}],5:[function(e,t,n){function i(){}function o(e){var t=e.annotation.elements;return Object.keys(t).map(function(e){return t[e]})}function a(){return Math.random().toString(36).substr(2,6)}function r(e){return null!==e&&"undefined"!=typeof e&&("number"==typeof e?isFinite(e):!!e)}function l(e,t,n){var i="$";e[i+t]||(e[t]?(e[i+t]=e[t].bind(e),e[t]=function(){var o=[e[i+t]].concat(Array.prototype.slice.call(arguments));return n.apply(e,o)}):e[t]=function(){var t=[void 0].concat(Array.prototype.slice.call(arguments));return n.apply(e,t)})}function s(e,t){e.forEach(function(e){(t?e[t]:e)()})}function c(e){return"on"+e[0].toUpperCase()+e.substring(1)}function u(e,t){try{return new MouseEvent(e,t)}catch(n){try{var i=document.createEvent("MouseEvent");return i.initMouseEvent(e,t.canBubble,t.cancelable,t.view,t.detail,t.screenX,t.screenY,t.clientX,t.clientY,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,t.button,t.relatedTarget),i}catch(o){var a=document.createEvent("Event");return a.initEvent(e,t.canBubble,t.cancelable),a}}}t.exports=function(e){function t(t){return t=h.configMerge(e.Annotation.defaults,t),h.isArray(t.annotations)&&t.annotations.forEach(function(t){t.label=h.configMerge(e.Annotation.labelDefaults,t.label)}),t}function n(e,t,n,i){var o=t.filter(function(t){return!!t._model.ranges[e]}).map(function(t){return t._model.ranges[e]}),a=o.map(function(e){return Number(e.min)}).reduce(function(e,t){return isFinite(t)&&!isNaN(t)&&t<e?t:e},n),r=o.map(function(e){return Number(e.max)}).reduce(function(e,t){return isFinite(t)&&!isNaN(t)&&t>e?t:e},i);return{min:a,max:r}}function f(e){var t=n(e.id,o(e.chart),e.min,e.max);"undefined"==typeof e.options.ticks.min&&"undefined"==typeof e.options.ticks.suggestedMin&&(e.min=t.min),"undefined"==typeof e.options.ticks.max&&"undefined"==typeof e.options.ticks.suggestedMax&&(e.max=t.max),e.handleTickRangeOptions&&e.handleTickRangeOptions()}function d(e,t){var n=Number.POSITIVE_INFINITY;return e.filter(function(e){return e.inRange(t.x,t.y)}).reduce(function(e,i){var o=i.getCenterPoint(),a=h.distanceBetweenPoints(t,o);return a<n?(e=[i],n=a):a===n&&e.push(i),e},[]).sort(function(e,t){var n=e.getArea(),i=t.getArea();return n>i||n<i?n-i:e._index-t._index}).slice(0,1)[0]}var h=e.helpers;return{initConfig:t,elements:o,callEach:s,noop:i,objectId:a,isValid:r,decorate:l,adjustScaleRange:f,getNearestItems:d,getEventHandlerName:c,createMouseEvent:u}}},{}],6:[function(e,t,n){var i=e("chart.js");i="function"==typeof i?i:window.Chart,i.Annotation=i.Annotation||{},i.Annotation.drawTimeOptions={afterDraw:"afterDraw",afterDatasetsDraw:"afterDatasetsDraw",beforeDatasetsDraw:"beforeDatasetsDraw"},i.Annotation.defaults={drawTime:"afterDatasetsDraw",dblClickSpeed:350,events:[],annotations:[]},i.Annotation.labelDefaults={backgroundColor:"rgba(0,0,0,0.8)",fontFamily:i.defaults.global.defaultFontFamily,fontSize:i.defaults.global.defaultFontSize,fontStyle:"bold",fontColor:"#fff",xPadding:6,yPadding:6,cornerRadius:6,position:"center",xAdjust:0,yAdjust:0,enabled:!1,content:null},i.Annotation.Element=e("./element.js")(i),i.Annotation.types={line:e("./types/line.js")(i),box:e("./types/box.js")(i)};var o=e("./annotation.js")(i);t.exports=o,i.pluginService.register(o)},{"./annotation.js":2,"./element.js":3,"./types/box.js":7,"./types/line.js":8,"chart.js":1}],7:[function(e,t,n){t.exports=function(t){var n=e("../helpers.js")(t),i=t.Annotation.Element.extend({setDataLimits:function(){var e=this._model,t=this.options,i=this.chartInstance,o=i.scales[t.xScaleID],a=i.scales[t.yScaleID],r=i.chartArea;if(e.ranges={},r){var l=0,s=0;o&&(l=n.isValid(t.xMin)?t.xMin:o.getPixelForValue(r.left),s=n.isValid(t.xMax)?t.xMax:o.getPixelForValue(r.right),e.ranges[t.xScaleID]={min:Math.min(l,s),max:Math.max(l,s)}),a&&(l=n.isValid(t.yMin)?t.yMin:a.getPixelForValue(r.bottom),s=n.isValid(t.yMax)?t.yMax:a.getPixelForValue(r.top),e.ranges[t.yScaleID]={min:Math.min(l,s),max:Math.max(l,s)})}},configure:function(){var e=this._model,t=this.options,i=this.chartInstance,o=i.scales[t.xScaleID],a=i.scales[t.yScaleID],r=i.chartArea;e.clip={x1:r.left,x2:r.right,y1:r.top,y2:r.bottom};var l,s,c=r.left,u=r.top,f=r.right,d=r.bottom;o&&(l=n.isValid(t.xMin)?o.getPixelForValue(t.xMin):r.left,s=n.isValid(t.xMax)?o.getPixelForValue(t.xMax):r.right,c=Math.min(l,s),f=Math.max(l,s)),a&&(l=n.isValid(t.yMin)?a.getPixelForValue(t.yMin):r.bottom,s=n.isValid(t.yMax)?a.getPixelForValue(t.yMax):r.top,u=Math.min(l,s),d=Math.max(l,s)),e.left=c,e.top=u,e.right=f,e.bottom=d,e.borderColor=t.borderColor,e.borderWidth=t.borderWidth,e.backgroundColor=t.backgroundColor},inRange:function(e,t){var n=this._model;return n&&e>=n.left&&e<=n.right&&t>=n.top&&t<=n.bottom},getCenterPoint:function(){var e=this._model;return{x:(e.right+e.left)/2,y:(e.bottom+e.top)/2}},getWidth:function(){var e=this._model;return Math.abs(e.right-e.left)},getHeight:function(){var e=this._model;return Math.abs(e.bottom-e.top)},getArea:function(){return this.getWidth()*this.getHeight()},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.fillStyle=e.backgroundColor;var n=e.right-e.left,i=e.bottom-e.top;t.fillRect(e.left,e.top,n,i),t.strokeRect(e.left,e.top,n,i),t.restore()}});return i}},{"../helpers.js":5}],8:[function(e,t,n){t.exports=function(t){function n(e){var t=(e.x2-e.x1)/(e.y2-e.y1),n=e.x1||0;this.m=t,this.b=n,this.getX=function(i){return t*(i-e.y1)+n},this.getY=function(i){return(i-n)/t+e.y1},this.intersects=function(e,t,n){n=n||.001;var i=this.getY(e),o=this.getX(t);return(!isFinite(i)||Math.abs(t-i)<n)&&(!isFinite(o)||Math.abs(e-o)<n)}}function i(e,t,n,i,o){var a=e.line,s={},c=0,u=0;switch(!0){case e.mode==l&&"top"==e.labelPosition:u=o+e.labelYAdjust,c=t/2+e.labelXAdjust,s.y=e.y1+u,s.x=(isFinite(a.m)?a.getX(s.y):e.x1)-c;break;case e.mode==l&&"bottom"==e.labelPosition:u=n+o+e.labelYAdjust,c=t/2+e.labelXAdjust,s.y=e.y2-u,s.x=(isFinite(a.m)?a.getX(s.y):e.x1)-c;break;case e.mode==r&&"left"==e.labelPosition:c=i+e.labelXAdjust,u=-(n/2)+e.labelYAdjust,s.x=e.x1+c,s.y=a.getY(s.x)+u;break;case e.mode==r&&"right"==e.labelPosition:c=t+i+e.labelXAdjust,u=-(n/2)+e.labelYAdjust,s.x=e.x2-c,s.y=a.getY(s.x)+u;break;default:s.x=(e.x1+e.x2-t)/2+e.labelXAdjust,s.y=(e.y1+e.y2-n)/2+e.labelYAdjust}return s}var o=t.helpers,a=e("../helpers.js")(t),r="horizontal",l="vertical",s=t.Annotation.Element.extend({setDataLimits:function(){var e=this._model,t=this.options;e.ranges={},e.ranges[t.scaleID]={min:t.value,max:t.endValue||t.value}},configure:function(){var e,t,l=this._model,s=this.options,c=this.chartInstance,u=c.chart.ctx,f=c.scales[s.scaleID];if(f&&(e=a.isValid(s.value)?f.getPixelForValue(s.value):NaN,t=a.isValid(s.endValue)?f.getPixelForValue(s.endValue):e),!isNaN(e)){var d=c.chartArea;l.clip={x1:d.left,x2:d.right,y1:d.top,y2:d.bottom},this.options.mode==r?(l.x1=d.left,l.x2=d.right,l.y1=e,l.y2=t):(l.y1=d.top,l.y2=d.bottom,l.x1=e,l.x2=t),l.line=new n(l),l.mode=s.mode,l.labelBackgroundColor=s.label.backgroundColor,l.labelFontFamily=s.label.fontFamily,l.labelFontSize=s.label.fontSize,l.labelFontStyle=s.label.fontStyle,l.labelFontColor=s.label.fontColor,l.labelXPadding=s.label.xPadding,l.labelYPadding=s.label.yPadding,l.labelCornerRadius=s.label.cornerRadius,l.labelPosition=s.label.position,l.labelXAdjust=s.label.xAdjust,l.labelYAdjust=s.label.yAdjust,l.labelEnabled=s.label.enabled,l.labelContent=s.label.content,u.font=o.fontString(l.labelFontSize,l.labelFontStyle,l.labelFontFamily);var h=u.measureText(l.labelContent).width,b=u.measureText("M").width,p=i(l,h,b,l.labelXPadding,l.labelYPadding);l.labelX=p.x-l.labelXPadding,l.labelY=p.y-l.labelYPadding,l.labelWidth=h+2*l.labelXPadding,l.labelHeight=b+2*l.labelYPadding,l.borderColor=s.borderColor,l.borderWidth=s.borderWidth,l.borderDash=s.borderDash||[],l.borderDashOffset=s.borderDashOffset||0}},inRange:function(e,t){var n=this._model;return n.line&&n.line.intersects(e,t,this.getHeight())||n.labelEnabled&&n.labelContent&&e>=n.labelX&&e<=n.labelX+n.labelWidth&&t>=n.labelY&&t<=n.labelY+n.labelHeight},getCenterPoint:function(){return{x:(this._model.x2+this._model.x1)/2,y:(this._model.y2+this._model.y1)/2}},getWidth:function(){return Math.abs(this._model.right-this._model.left)},getHeight:function(){return this._model.borderWidth||1},getArea:function(){return Math.sqrt(Math.pow(this.getWidth(),2)+Math.pow(this.getHeight(),2))},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;e.clip&&(t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.setLineDash&&t.setLineDash(e.borderDash),t.lineDashOffset=e.borderDashOffset,t.beginPath(),t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.stroke(),e.labelEnabled&&e.labelContent&&(t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.fillStyle=e.labelBackgroundColor,o.drawRoundedRectangle(t,e.labelX,e.labelY,e.labelWidth,e.labelHeight,e.labelCornerRadius),t.fill(),t.font=o.fontString(e.labelFontSize,e.labelFontStyle,e.labelFontFamily),t.fillStyle=e.labelFontColor,t.textAlign="center",t.textBaseline="middle",t.fillText(e.labelContent,e.labelX+e.labelWidth/2,e.labelY+e.labelHeight/2)),t.restore())}});return s}},{"../helpers.js":5}]},{},[6]);
\ No newline at end of file
Binary file image/www/js/ch-plug-anno.min.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/js/jquery-1.12.4.min.js	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,5 @@
+/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=la(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=ma(b);function pa(){}pa.prototype=d.filters=d.pseudos,d.setFilters=new pa,g=fa.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=R.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=S.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(Q," ")}),h=h.slice(c.length));for(g in d.filter)!(e=W[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fa.error(a):z(a,i).slice(0)};function qa(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){n.each(b,function(b,c){n.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==n.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return n.each(arguments,function(a,b){var c;while((c=n.inArray(b,f,c))>-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0;
+}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=n._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}}),function(){var a;l.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,e;return c=d.getElementsByTagName("body")[0],c&&c.style?(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(d.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(e),a):void 0}}();var T=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,U=new RegExp("^(?:([+-])=|)("+T+")([a-z%]*)$","i"),V=["Top","Right","Bottom","Left"],W=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)};function X(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return n.css(a,b,"")},i=h(),j=c&&c[3]||(n.cssNumber[b]?"":"px"),k=(n.cssNumber[b]||"px"!==j&&+i)&&U.exec(n.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,n.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var Y=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)Y(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav></:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:l.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/<tbody/i;function ia(a){Z.test(a.type)&&(a.defaultChecked=a.checked)}function ja(a,b,c,d,e){for(var f,g,h,i,j,k,m,o=a.length,p=ca(b),q=[],r=0;o>r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"<table>"!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[n.expando])return a;var b,c,e,f=a.type,g=a,h=this.fixHooks[f];h||(this.fixHooks[f]=h=ma.test(f)?this.mouseHooks:la.test(f)?this.keyHooks:{}),e=h.props?this.props.concat(h.props):this.props,a=new n.Event(g),b=e.length;while(b--)c=e[b],a[c]=g[c];return a.target||(a.target=g.srcElement||d),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,h.filter?h.filter(a,g):a},props:"altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,e,f,g=b.button,h=b.fromElement;return null==a.pageX&&null!=b.clientX&&(e=a.target.ownerDocument||d,f=e.documentElement,c=e.body,a.pageX=b.clientX+(f&&f.scrollLeft||c&&c.scrollLeft||0)-(f&&f.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(f&&f.scrollTop||c&&c.scrollTop||0)-(f&&f.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&h&&(a.relatedTarget=h===a.target?b.toElement:h),a.which||void 0===g||(a.which=1&g?1:2&g?3:4&g?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==ra()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===ra()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return n.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c){var d=n.extend(new n.Event,c,{type:a,isSimulated:!0});n.event.trigger(d,null,b),d.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=d.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)}:function(a,b,c){var d="on"+b;a.detachEvent&&("undefined"==typeof a[d]&&(a[d]=null),a.detachEvent(d,c))},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?pa:qa):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={constructor:n.Event,isDefaultPrevented:qa,isPropagationStopped:qa,isImmediatePropagationStopped:qa,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=pa,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=pa,a&&!this.isSimulated&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=pa,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||n.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.submit||(n.event.special.submit={setup:function(){return n.nodeName(this,"form")?!1:void n.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=n.nodeName(b,"input")||n.nodeName(b,"button")?n.prop(b,"form"):void 0;c&&!n._data(c,"submit")&&(n.event.add(c,"submit._submit",function(a){a._submitBubble=!0}),n._data(c,"submit",!0))})},postDispatch:function(a){a._submitBubble&&(delete a._submitBubble,this.parentNode&&!a.isTrigger&&n.event.simulate("submit",this.parentNode,a))},teardown:function(){return n.nodeName(this,"form")?!1:void n.event.remove(this,"._submit")}}),l.change||(n.event.special.change={setup:function(){return ka.test(this.nodeName)?("checkbox"!==this.type&&"radio"!==this.type||(n.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._justChanged=!0)}),n.event.add(this,"click._change",function(a){this._justChanged&&!a.isTrigger&&(this._justChanged=!1),n.event.simulate("change",this,a)})),!1):void n.event.add(this,"beforeactivate._change",function(a){var b=a.target;ka.test(b.nodeName)&&!n._data(b,"change")&&(n.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||n.event.simulate("change",this.parentNode,a)}),n._data(b,"change",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return n.event.remove(this,"._change"),!ka.test(this.nodeName)}}),l.focusin||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a))};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=n._data(d,b);e||d.addEventListener(a,c,!0),n._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=n._data(d,b)-1;e?n._data(d,b,e):(d.removeEventListener(a,c,!0),n._removeData(d,b))}}}),n.fn.extend({on:function(a,b,c,d){return sa(this,a,b,c,d)},one:function(a,b,c,d){return sa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=qa),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ta=/ jQuery\d+="(?:null|\d+)"/g,ua=new RegExp("<(?:"+ba+")[\\s/>]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/<script|<style|<link/i,xa=/checked\s*(?:[^=]|=\s*.checked.)/i,ya=/^true\/(.*)/,za=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Ja[0].contentWindow||Ja[0].contentDocument).document,b.write(),b.close(),c=La(a,b),Ja.detach()),Ka[a]=c),c}var Na=/^margin/,Oa=new RegExp("^("+T+")(?!px)[a-z%]+$","i"),Pa=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e},Qa=d.documentElement;!function(){var b,c,e,f,g,h,i=d.createElement("div"),j=d.createElement("div");if(j.style){j.style.cssText="float:left;opacity:.5",l.opacity="0.5"===j.style.opacity,l.cssFloat=!!j.style.cssFloat,j.style.backgroundClip="content-box",j.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===j.style.backgroundClip,i=d.createElement("div"),i.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",j.innerHTML="",i.appendChild(j),l.boxSizing=""===j.style.boxSizing||""===j.style.MozBoxSizing||""===j.style.WebkitBoxSizing,n.extend(l,{reliableHiddenOffsets:function(){return null==b&&k(),f},boxSizingReliable:function(){return null==b&&k(),e},pixelMarginRight:function(){return null==b&&k(),c},pixelPosition:function(){return null==b&&k(),b},reliableMarginRight:function(){return null==b&&k(),g},reliableMarginLeft:function(){return null==b&&k(),h}});function k(){var k,l,m=d.documentElement;m.appendChild(i),j.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",b=e=h=!1,c=g=!0,a.getComputedStyle&&(l=a.getComputedStyle(j),b="1%"!==(l||{}).top,h="2px"===(l||{}).marginLeft,e="4px"===(l||{width:"4px"}).width,j.style.marginRight="50%",c="4px"===(l||{marginRight:"4px"}).marginRight,k=j.appendChild(d.createElement("div")),k.style.cssText=j.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",k.style.marginRight=k.style.width="0",j.style.width="1px",g=!parseFloat((a.getComputedStyle(k)||{}).marginRight),j.removeChild(k)),j.style.display="none",f=0===j.getClientRects().length,f&&(j.style.display="",j.innerHTML="<table><tr><td></td><td>t</td></tr></table>",j.childNodes[0].style.borderCollapse="separate",k=j.getElementsByTagName("td"),k[0].style.cssText="margin:0;border:0;padding:0;display:none",f=0===k[0].offsetHeight,f&&(k[0].style.display="",k[1].style.display="none",f=0===k[0].offsetHeight)),m.removeChild(i)}}}();var Ra,Sa,Ta=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ra=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c.getPropertyValue(b)||c[b]:void 0,""!==g&&void 0!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),c&&!l.pixelMarginRight()&&Oa.test(g)&&Na.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f),void 0===g?g:g+""}):Qa.currentStyle&&(Ra=function(a){return a.currentStyle},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Oa.test(g)&&!Ta.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Ua(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Va=/alpha\([^)]*\)/i,Wa=/opacity\s*=\s*([^)]*)/i,Xa=/^(none|table(?!-c[ea]).+)/,Ya=new RegExp("^("+T+")(.*)$","i"),Za={position:"absolute",visibility:"hidden",display:"block"},$a={letterSpacing:"0",fontWeight:"400"},_a=["Webkit","O","Moz","ms"],ab=d.createElement("div").style;function bb(a){if(a in ab)return a;var b=a.charAt(0).toUpperCase()+a.slice(1),c=_a.length;while(c--)if(a=_a[c]+b,a in ab)return a}function cb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=n._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&W(d)&&(f[g]=n._data(d,"olddisplay",Ma(d.nodeName)))):(e=W(d),(c&&"none"!==c||!e)&&n._data(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function db(a,b,c){var d=Ya.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function eb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+V[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+V[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+V[f]+"Width",!0,e))):(g+=n.css(a,"padding"+V[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+V[f]+"Width",!0,e)));return g}function fb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ra(a),g=l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Sa(a,b,f),(0>e||null==e)&&(e=a.style[b]),Oa.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+eb(a,b,c||(g?"border":"content"),d,f)+"px"}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Sa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":l.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;if(b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=U.exec(c))&&e[1]&&(c=X(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(n.cssNumber[h]?"":"px")),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Sa(a,b,d)),"normal"===f&&b in $a&&(f=$a[b]),""===c||c?(e=parseFloat(f),c===!0||isFinite(e)?e||0:f):f}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?Xa.test(n.css(a,"display"))&&0===a.offsetWidth?Pa(a,Za,function(){return fb(a,b,d)}):fb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ra(a);return db(a,c,d?eb(a,b,d,l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),l.opacity||(n.cssHooks.opacity={get:function(a,b){return Wa.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=n.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===n.trim(f.replace(Va,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Va.test(f)?f.replace(Va,e):f+" "+e)}}),n.cssHooks.marginRight=Ua(l.reliableMarginRight,function(a,b){return b?Pa(a,{display:"inline-block"},Sa,[a,"marginRight"]):void 0}),n.cssHooks.marginLeft=Ua(l.reliableMarginLeft,function(a,b){return b?(parseFloat(Sa(a,"marginLeft"))||(n.contains(a.ownerDocument,a)?a.getBoundingClientRect().left-Pa(a,{
+marginLeft:0},function(){return a.getBoundingClientRect().left}):0))+"px":void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+V[d]+b]=f[d]||f[d-2]||f[0];return e}},Na.test(a)||(n.cssHooks[a+b].set=db)}),n.fn.extend({css:function(a,b){return Y(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Ra(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return cb(this,!0)},hide:function(){return cb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){W(this)?n(this).show():n(this).hide()})}});function gb(a,b,c,d,e){return new gb.prototype.init(a,b,c,d,e)}n.Tween=gb,gb.prototype={constructor:gb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||n.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=gb.propHooks[this.prop];return a&&a.get?a.get(this):gb.propHooks._default.get(this)},run:function(a){var b,c=gb.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):gb.propHooks._default.set(this),this}},gb.prototype.init.prototype=gb.prototype,gb.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[n.cssProps[a.prop]]&&!n.cssHooks[a.prop]?a.elem[a.prop]=a.now:n.style(a.elem,a.prop,a.now+a.unit)}}},gb.propHooks.scrollTop=gb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},n.fx=gb.prototype.init,n.fx.step={};var hb,ib,jb=/^(?:toggle|show|hide)$/,kb=/queueHooks$/;function lb(){return a.setTimeout(function(){hb=void 0}),hb=n.now()}function mb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=V[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function nb(a,b,c){for(var d,e=(qb.tweeners[b]||[]).concat(qb.tweeners["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ob(a,b,c){var d,e,f,g,h,i,j,k,m=this,o={},p=a.style,q=a.nodeType&&W(a),r=n._data(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,m.always(function(){m.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=n.css(a,"display"),k="none"===j?n._data(a,"olddisplay")||Ma(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(l.inlineBlockNeedsLayout&&"inline"!==Ma(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",l.shrinkWrapBlocks()||m.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],jb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(o))"inline"===("none"===j?Ma(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=n._data(a,"fxshow",{}),f&&(r.hidden=!q),q?n(a).show():m.done(function(){n(a).hide()}),m.done(function(){var b;n._removeData(a,"fxshow");for(b in o)n.style(a,b,o[b])});for(d in o)g=nb(q?r[d]:0,d,m),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function pb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function qb(a,b,c){var d,e,f=0,g=qb.prefilters.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=hb||lb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{},easing:n.easing._default},c),originalProperties:b,originalOptions:c,startTime:hb||lb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(pb(k,j.opts.specialEasing);g>f;f++)if(d=qb.prefilters[f].call(j,a,k,j.opts))return n.isFunction(d.stop)&&(n._queueHooks(j.elem,j.opts.queue).stop=n.proxy(d.stop,d)),d;return n.map(k,nb,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(qb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return X(c.elem,a,U.exec(b),c),c}]},tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.match(G);for(var c,d=0,e=a.length;e>d;d++)c=a[d],qb.tweeners[c]=qb.tweeners[c]||[],qb.tweeners[c].unshift(b)},prefilters:[ob],prefilter:function(a,b){b?qb.prefilters.unshift(a):qb.prefilters.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(W).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=qb(this,n.extend({},a),f);(e||n._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=n._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&kb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=n._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(mb(b,!0),a,d,e)}}),n.each({slideDown:mb("show"),slideUp:mb("hide"),slideToggle:mb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=n.timers,c=0;for(hb=n.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||n.fx.stop(),hb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){ib||(ib=a.setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){a.clearInterval(ib),ib=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(b,c){return b=n.fx?n.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a,b=d.createElement("input"),c=d.createElement("div"),e=d.createElement("select"),f=e.appendChild(d.createElement("option"));c=d.createElement("div"),c.setAttribute("className","t"),c.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=c.getElementsByTagName("a")[0],b.setAttribute("type","checkbox"),c.appendChild(b),a=c.getElementsByTagName("a")[0],a.style.cssText="top:1px",l.getSetAttribute="t"!==c.className,l.style=/top/.test(a.getAttribute("style")),l.hrefNormalized="/a"===a.getAttribute("href"),l.checkOn=!!b.value,l.optSelected=f.selected,l.enctype=!!d.createElement("form").enctype,e.disabled=!0,l.optDisabled=!f.disabled,b=d.createElement("input"),b.setAttribute("value",""),l.input=""===b.getAttribute("value"),b.value="t",b.setAttribute("type","radio"),l.radioValue="t"===b.value}();var rb=/\r/g,sb=/[\x20\t\r\n\f]+/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a)).replace(sb," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&(l.optDisabled?!c.disabled:null===c.getAttribute("disabled"))&&(!c.parentNode.disabled||!n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)if(d=e[g],n.inArray(n.valHooks.option.get(d),f)>-1)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>-1:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var tb,ub,vb=n.expr.attrHandle,wb=/^(?:checked|selected)$/i,xb=l.getSetAttribute,yb=l.input;n.fn.extend({attr:function(a,b){return Y(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),e=n.attrHooks[b]||(n.expr.match.bool.test(b)?ub:tb)),void 0!==c?null===c?void n.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=n.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(G);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)?yb&&xb||!wb.test(c)?a[d]=!1:a[n.camelCase("default-"+c)]=a[d]=!1:n.attr(a,c,""),a.removeAttribute(xb?c:d)}}),ub={set:function(a,b,c){return b===!1?n.removeAttr(a,c):yb&&xb||!wb.test(c)?a.setAttribute(!xb&&n.propFix[c]||c,c):a[n.camelCase("default-"+c)]=a[c]=!0,c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=vb[b]||n.find.attr;yb&&xb||!wb.test(b)?vb[b]=function(a,b,d){var e,f;return d||(f=vb[b],vb[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,vb[b]=f),e}:vb[b]=function(a,b,c){return c?void 0:a[n.camelCase("default-"+b)]?b.toLowerCase():null}}),yb&&xb||(n.attrHooks.value={set:function(a,b,c){return n.nodeName(a,"input")?void(a.defaultValue=b):tb&&tb.set(a,b,c)}}),xb||(tb={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},vb.id=vb.name=vb.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},n.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:tb.set},n.attrHooks.contenteditable={set:function(a,b,c){tb.set(a,""===b?!1:b,c)}},n.each(["width","height"],function(a,b){n.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),l.style||(n.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var zb=/^(?:input|select|textarea|button|object)$/i,Ab=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return Y(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return a=n.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),n.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&n.isXMLDoc(a)||(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,"tabindex");return b?parseInt(b,10):zb.test(a.nodeName)||Ab.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),l.hrefNormalized||n.each(["href","src"],function(a,b){n.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this}),l.enctype||(n.propFix.enctype="encoding");var Bb=/[\t\r\n\f]/g;function Cb(a){return n.attr(a,"class")||""}n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,Cb(this)))});if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,Cb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):n.isFunction(a)?this.each(function(c){n(this).toggleClass(a.call(this,c,Cb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=n(this),f=a.match(G)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=Cb(this),b&&n._data(this,"__className__",b),n.attr(this,"class",b||a===!1?"":n._data(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+Cb(c)+" ").replace(Bb," ").indexOf(b)>-1)return!0;return!1}}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Db=a.location,Eb=n.now(),Fb=/\?/,Gb=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;n.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=n.trim(b+"");return e&&!n.trim(e.replace(Gb,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():n.error("Invalid JSON: "+b)},n.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new a.DOMParser,c=d.parseFromString(b,"text/xml")):(c=new a.ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||n.error("Invalid XML: "+b),c};var Hb=/#.*$/,Ib=/([?&])_=[^&]*/,Jb=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Kb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Lb=/^(?:GET|HEAD)$/,Mb=/^\/\//,Nb=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ob={},Pb={},Qb="*/".concat("*"),Rb=Db.href,Sb=Nb.exec(Rb.toLowerCase())||[];function Tb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(G)||[];if(n.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Ub(a,b,c,d){var e={},f=a===Pb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Vb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&n.extend(!0,a,c),a}function Wb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Xb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Rb,type:"GET",isLocal:Kb.test(Sb[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Qb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Vb(Vb(a,n.ajaxSettings),b):Vb(n.ajaxSettings,a)},ajaxPrefilter:Tb(Ob),ajaxTransport:Tb(Pb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var d,e,f,g,h,i,j,k,l=n.ajaxSetup({},c),m=l.context||l,o=l.context&&(m.nodeType||m.jquery)?n(m):n.event,p=n.Deferred(),q=n.Callbacks("once memory"),r=l.statusCode||{},s={},t={},u=0,v="canceled",w={readyState:0,getResponseHeader:function(a){var b;if(2===u){if(!k){k={};while(b=Jb.exec(g))k[b[1].toLowerCase()]=b[2]}b=k[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===u?g:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return u||(a=t[c]=t[c]||a,s[a]=b),this},overrideMimeType:function(a){return u||(l.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>u)for(b in a)r[b]=[r[b],a[b]];else w.always(a[w.status]);return this},abort:function(a){var b=a||v;return j&&j.abort(b),y(0,b),this}};if(p.promise(w).complete=q.add,w.success=w.done,w.error=w.fail,l.url=((b||l.url||Rb)+"").replace(Hb,"").replace(Mb,Sb[1]+"//"),l.type=c.method||c.type||l.method||l.type,l.dataTypes=n.trim(l.dataType||"*").toLowerCase().match(G)||[""],null==l.crossDomain&&(d=Nb.exec(l.url.toLowerCase()),l.crossDomain=!(!d||d[1]===Sb[1]&&d[2]===Sb[2]&&(d[3]||("http:"===d[1]?"80":"443"))===(Sb[3]||("http:"===Sb[1]?"80":"443")))),l.data&&l.processData&&"string"!=typeof l.data&&(l.data=n.param(l.data,l.traditional)),Ub(Ob,l,c,w),2===u)return w;i=n.event&&l.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),l.type=l.type.toUpperCase(),l.hasContent=!Lb.test(l.type),f=l.url,l.hasContent||(l.data&&(f=l.url+=(Fb.test(f)?"&":"?")+l.data,delete l.data),l.cache===!1&&(l.url=Ib.test(f)?f.replace(Ib,"$1_="+Eb++):f+(Fb.test(f)?"&":"?")+"_="+Eb++)),l.ifModified&&(n.lastModified[f]&&w.setRequestHeader("If-Modified-Since",n.lastModified[f]),n.etag[f]&&w.setRequestHeader("If-None-Match",n.etag[f])),(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&w.setRequestHeader("Content-Type",l.contentType),w.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+("*"!==l.dataTypes[0]?", "+Qb+"; q=0.01":""):l.accepts["*"]);for(e in l.headers)w.setRequestHeader(e,l.headers[e]);if(l.beforeSend&&(l.beforeSend.call(m,w,l)===!1||2===u))return w.abort();v="abort";for(e in{success:1,error:1,complete:1})w[e](l[e]);if(j=Ub(Pb,l,c,w)){if(w.readyState=1,i&&o.trigger("ajaxSend",[w,l]),2===u)return w;l.async&&l.timeout>0&&(h=a.setTimeout(function(){w.abort("timeout")},l.timeout));try{u=1,j.send(s,y)}catch(x){if(!(2>u))throw x;y(-1,x)}}else y(-1,"No Transport");function y(b,c,d,e){var k,s,t,v,x,y=c;2!==u&&(u=2,h&&a.clearTimeout(h),j=void 0,g=e||"",w.readyState=b>0?4:0,k=b>=200&&300>b||304===b,d&&(v=Wb(l,w,d)),v=Xb(l,v,w,k),k?(l.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(n.lastModified[f]=x),x=w.getResponseHeader("etag"),x&&(n.etag[f]=x)),204===b||"HEAD"===l.type?y="nocontent":304===b?y="notmodified":(y=v.state,s=v.data,t=v.error,k=!t)):(t=y,!b&&y||(y="error",0>b&&(b=0))),w.status=b,w.statusText=(c||y)+"",k?p.resolveWith(m,[s,y,w]):p.rejectWith(m,[w,y,t]),w.statusCode(r),r=void 0,i&&o.trigger(k?"ajaxSuccess":"ajaxError",[w,l,k?s:t]),q.fireWith(m,[w,y]),i&&(o.trigger("ajaxComplete",[w,l]),--n.active||n.event.trigger("ajaxStop")))}return w},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax(n.extend({url:a,type:b,dataType:e,data:c,success:d},n.isPlainObject(a)&&a))}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){if(n.isFunction(a))return this.each(function(b){n(this).wrapAll(a.call(this,b))});if(this[0]){var b=n(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return n.isFunction(a)?this.each(function(b){n(this).wrapInner(a.call(this,b))}):this.each(function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}});function Yb(a){return a.style&&a.style.display||n.css(a,"display")}function Zb(a){if(!n.contains(a.ownerDocument||d,a))return!0;while(a&&1===a.nodeType){if("none"===Yb(a)||"hidden"===a.type)return!0;a=a.parentNode}return!1}n.expr.filters.hidden=function(a){return l.reliableHiddenOffsets()?a.offsetWidth<=0&&a.offsetHeight<=0&&!a.getClientRects().length:Zb(a)},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var $b=/%20/g,_b=/\[\]$/,ac=/\r?\n/g,bc=/^(?:submit|button|image|reset|file)$/i,cc=/^(?:input|select|textarea|keygen)/i;function dc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||_b.test(a)?d(a,e):dc(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)dc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)dc(c,a[c],b,e);return d.join("&").replace($b,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&cc.test(this.nodeName)&&!bc.test(a)&&(this.checked||!Z.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(ac,"\r\n")}}):{name:b.name,value:c.replace(ac,"\r\n")}}).get()}}),n.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return this.isLocal?ic():d.documentMode>8?hc():/^(get|post|head|put|delete|options)$/i.test(this.type)&&hc()||ic()}:hc;var ec=0,fc={},gc=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in fc)fc[a](void 0,!0)}),l.cors=!!gc&&"withCredentials"in gc,gc=l.ajax=!!gc,gc&&n.ajaxTransport(function(b){if(!b.crossDomain||l.cors){var c;return{send:function(d,e){var f,g=b.xhr(),h=++ec;if(g.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(f in b.xhrFields)g[f]=b.xhrFields[f];b.mimeType&&g.overrideMimeType&&g.overrideMimeType(b.mimeType),b.crossDomain||d["X-Requested-With"]||(d["X-Requested-With"]="XMLHttpRequest");for(f in d)void 0!==d[f]&&g.setRequestHeader(f,d[f]+"");g.send(b.hasContent&&b.data||null),c=function(a,d){var f,i,j;if(c&&(d||4===g.readyState))if(delete fc[h],c=void 0,g.onreadystatechange=n.noop,d)4!==g.readyState&&g.abort();else{j={},f=g.status,"string"==typeof g.responseText&&(j.text=g.responseText);try{i=g.statusText}catch(k){i=""}f||!b.isLocal||b.crossDomain?1223===f&&(f=204):f=j.text?200:404}j&&e(f,i,j,g.getAllResponseHeaders())},b.async?4===g.readyState?a.setTimeout(c):g.onreadystatechange=fc[h]=c:c()},abort:function(){c&&c(void 0,!0)}}}});function hc(){try{return new a.XMLHttpRequest}catch(b){}}function ic(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=d.head||n("head")[0]||d.documentElement;return{send:function(e,f){b=d.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||f(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var jc=[],kc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=jc.pop()||n.expando+"_"+Eb++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(kc.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&kc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(kc,"$1"+e):b.jsonp!==!1&&(b.url+=(Fb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?n(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,jc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||d;var e=x.exec(a),f=!c&&[];return e?[b.createElement(e[1])]:(e=ja([a],b,f),f&&f.length&&n(f).remove(),n.merge([],e.childNodes))};var lc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&lc)return lc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=n.trim(a.slice(h,a.length)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};function mc(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,n.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?("undefined"!=typeof e.getBoundingClientRect&&(d=e.getBoundingClientRect()),c=mc(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Qa})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return Y(this,function(a,d,e){var f=mc(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Ua(l.pixelPosition,function(a,c){return c?(c=Sa(a,b),Oa.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({
+padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return Y(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var nc=a.jQuery,oc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=oc),b&&a.jQuery===n&&(a.jQuery=nc),n},b||(a.jQuery=a.$=n),n});
Binary file image/www/js/jquery-1.12.4.min.js.gz has changed
Binary file image/www/js/modl/babel.js.gz has changed
Binary file image/www/js/modl/brloader.js.gz has changed
Binary file image/www/js/utils/common.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/js/webui.js	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,142 @@
+document.getElementById("test").innerHTML = "WebSocket is not connected";
+
+var websocket = new WebSocket('ws://'+location.hostname+'/ws');
+var slider = document.getElementById("myRange");
+
+
+var BBScreen={
+
+    MLT_pv: function(s) {
+	$("#mlt_pv").text(s);
+    },
+    MLT_sp: function(s) {
+	$("#mlt_sp").text(s);
+    },
+    MLT_power: function(s) {
+	$("#mlt_power").text(s);
+    },
+    MLT_led: function(s) {
+	if (s == 0)
+	    $("#mlt_led").css("background-color", "#270");
+	else
+	    $("#mlt_led").css("background-color", "#5E0");
+    },
+    Pump_led: function(s) {
+	if (s == 0)
+	    $("#pump_led").css("background-color", "#820");
+	else
+	    $("#pump_led").css("background-color", "#F40");
+    },
+    HLT_pv: function(s) {
+	$("#hlt_pv").text(s);
+    },
+    HLT_sp: function(s) {
+	$("#hlt_sp").text(s);
+    },
+    HLT_power: function(s) {
+	$("#hlt_power").text(s);
+    },
+    HLT_led: function(s) {
+	if (s == 0)
+	    $("#hlt_led").css("background-color", "#270");
+	else
+	    $("#hlt_led").css("background-color", "#5E0");
+    },
+    Topmessage: function(s) {
+	$("#top_msg").text(s);
+    },
+    Timer: function(s) {
+	$("#timer").text(s);
+    },
+};
+
+var BrewBoard = {
+
+    p_msg:function(e){
+	var b=this;
+	var a=JSON.parse(e);
+	var c={
+	    mlt_pv:function(v){
+		BBScreen.MLT_pv(v)
+	    },
+	    mlt_sp:function(v){
+		BBScreen.MLT_sp(v)
+	    },
+	    mlt_power:function(v){
+		BBScreen.MLT_power(v)
+	    },
+	    mlt_led:function(v){
+		BBScreen.MLT_led(v)
+	    },
+	    pump_led:function(v){
+		BBScreen.Pump_led(v)
+	    },
+	    hlt_pv:function(v){
+		BBScreen.HLT_pv(v)
+	    },
+	    hlt_sp:function(v){
+		BBScreen.HLT_sp(v)
+	    },
+	    hlt_power:function(v){
+		BBScreen.HLT_power(v)
+	    },
+	    hlt_led:function(v){
+		BBScreen.HLT_led(v)
+	    },
+	    top_msg:function(v){
+		BBScreen.Topmessage(v)
+	    },
+	    timer:function(v){
+		BBScreen.Timer(v)
+	    },
+	};
+	console.log(e);
+	$.each(a,function(k,v){
+	    if (typeof(c[k])!="undefined"){
+		c[k](v)
+	    }
+	});
+    }
+};
+
+
+function sendMsg() {
+	//websocket.send("sup playa!");
+	websocket.send('L50');
+	console.log('Sent message to websocket');
+}
+
+
+websocket.onopen = function(evt) {
+	console.log('WebSocket connection opened');
+//	websocket.send("It's open! Hooray!!!");
+	document.getElementById("test").innerHTML = "WebSocket is connected!";
+}
+
+/*
+ * Process received websocket messages.
+ */
+websocket.onmessage = function(evt) {
+    var msg = evt.data;
+    var value;
+    switch (msg.charAt(0)) {
+	case '{':
+		BrewBoard.p_msg(evt.data);
+		break;
+	default:
+		document.getElementById("output").innerHTML = evt.data;
+		break;
+    }
+}
+
+websocket.onclose = function(evt) {
+	console.log('Websocket connection closed');
+	document.getElementById("test").innerHTML = "WebSocket closed";
+}
+
+websocket.onerror = function(evt) {
+	console.log('Websocket error: ' + evt);
+	document.getElementById("test").innerHTML = "WebSocket error????!!!1!!";
+}
+
+
Binary file image/www/js/zlib/adler32.js.gz has changed
Binary file image/www/js/zlib/constants.js.gz has changed
Binary file image/www/js/zlib/crc32.js.gz has changed
Binary file image/www/js/zlib/deflate.js.gz has changed
Binary file image/www/js/zlib/gzheader.js.gz has changed
Binary file image/www/js/zlib/inffast.js.gz has changed
Binary file image/www/js/zlib/inflate.js.gz has changed
Binary file image/www/js/zlib/inftrees.js.gz has changed
Binary file image/www/js/zlib/messages.js.gz has changed
Binary file image/www/js/zlib/trees.js.gz has changed
Binary file image/www/js/zlib/zstream.js.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/log	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1 @@
+../log/
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/logfiles.json	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1 @@
+{"Dir":[{"Folder":"/log","Files":[{"File":"br1806302319.json","Size":1073,"Date":1530397502},{"File":"br1807011111.json","Size":11510,"Date":1530445220},{"File":"br1806301536.json","Size":761,"Date":1530369610},{"File":"br1806301624.json","Size":8165,"Date":1530375054},{"File":"br1806301750.json","Size":18921,"Date":1530384530},{"File":"br1806302023.json","Size":9399,"Date":1530390360}]}]}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/logs.html	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Brew Logfiles</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 
+  <script src="js/jquery-1.12.4.min.js"></script>
+  <link rel="stylesheet" href="app/styles/web.css">
+</head>
+<body>
+
+ <div id="tmap"></div>
+ <table class="directory">
+  <thead>
+   <tr class="directory">
+    <th>Bestand</th>
+    <th>Grootte</th>
+    <th>Datum</th>
+    <th>Actie</th>
+    <th>Actie</th>
+   </tr>
+  </thead>
+  <tbody id="tbody">
+  </tbody>
+ </table>
+
+
+ <br>
+ <form action="index.html"><input type="submit" class="button" value="Hoofdmenu"></form>
+
+ <script>
+
+$.getJSON("/logfiles.json", function(data) {
+  var folder = data.Dir.map(function(item) {
+    return item.Folder;
+  });
+
+  $('#tmap').append([folder]);
+
+  $.each(data.Dir[0].Files, function(idx, obj) {
+    var ct = new Date();
+    ct.setTime(obj.Date * 1000);
+    var str = "";
+
+    if (ct.getDate() < 10)
+      str += "0";
+    str += ct.getDate() + "-";
+    if (ct.getMonth() < 9)
+      str += "0";
+    str += (ct.getMonth() + 1) + "-" + ct.getFullYear() + " ";
+    if (ct.getHours() < 10)
+      str += "0";
+    str += ct.getHours() + ":";
+    if (ct.getMinutes() < 10)
+      str += "0";
+    str += ct.getMinutes();
+
+    var eachrow = "<tr class=\"directory\">"
+    + "<td>" + obj.File + "</td>"
+    + "<td>" + obj.Size + " bytes</td>"
+    + "<td>" + str + "</td>"
+    + "<td><a href=\"/log/" + obj.File + "\">Download</a></td>"
+    + "<td><a href=\"/chart.html?show=/log/" + obj.File + "\">Grafiek</a></td>"
+    + "</tr>";
+    $('#tbody').append(eachrow);
+  });
+});
+
+ </script>
+
+</body>
+</html>
+
Binary file image/www/vnc.html.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/vnc.html.orig	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<html class="noVNC_loading">
+<head>
+
+    <!--
+    noVNC example: simple example using default UI
+    Copyright (C) 2012 Joel Martin
+    Copyright (C) 2016 Samuel Mannehed for Cendio AB
+    Copyright (C) 2016 Pierre Ossman for Cendio AB
+    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+
+    Connect parameters are provided in query string:
+        http://example.com/?host=HOST&port=PORT&encrypt=1
+    or the fragment:
+        http://example.com/#host=HOST&port=PORT&encrypt=1
+    -->
+    <title>noVNC</title>
+
+    <meta charset="utf-8" />
+
+    <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
+                Remove this if you use the .htaccess -->
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+
+    <!-- Icons (see Makefile for what the sizes are for) -->
+    <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
+    <link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png">
+    <link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png">
+    <link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png">
+    <link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
+    <link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png">
+    <link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png">
+    <link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
+    <link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png">
+    <link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
+    <link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png">
+    <link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
+    <link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png">
+    <!-- Firefox currently mishandles SVG, see #1419039
+    <link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg">
+    -->
+    <!-- Repeated last so that legacy handling will pick this -->
+    <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
+
+    <!-- Apple iOS Safari settings -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+    <!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
+    <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
+    <link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
+    <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
+    <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
+
+    <!-- Stylesheets -->
+    <link rel="stylesheet" href="app/styles/base.css" />
+
+    <!--
+    <script type='text/javascript'
+        src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
+    -->
+
+    <!-- this is included as a normal file in order to catch script-loading errors as well -->
+    <script type="text/javascript" src="app/error-handler.js"></script>
+
+    <!-- begin scripts -->
+    <!-- promise polyfills promises for IE11 -->
+    <script src="js/promise.js"></script>
+    <!-- ES2015/ES6 modules polyfill -->
+    <script type="module">
+        window._noVNC_has_module_support = true;
+    </script>
+    <script>
+        window.addEventListener("load", function() {
+            if (window._noVNC_has_module_support) return;
+            var loader = document.createElement("script");
+            loader.src = "js/modl/brloader.js";
+            document.head.appendChild(loader);
+        });
+    </script>
+    <!-- actual script modules -->
+    <script type="module" crossorigin="anonymous" src="app/ui.js"></script>
+    <!-- end scripts -->
+</head>
+
+<body>
+
+    <div id="noVNC_fallback_error" class="noVNC_center">
+        <div>
+            <div>noVNC encountered an error:</div>
+            <br>
+            <div id="noVNC_fallback_errormsg"></div>
+        </div>
+    </div>
+
+    <!-- noVNC Control Bar -->
+    <div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
+
+        <div id="noVNC_control_bar">
+            <div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
+
+            <div class="noVNC_scroll">
+
+            <h1 class="noVNC_logo" translate="no"><span>no</span><br />VNC</h1>
+
+            <!-- Drag/Pan the viewport -->
+            <input type="image" alt="viewport drag" src="app/images/drag.svg"
+                id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
+                title="Move/Drag Viewport" />
+
+            <!--noVNC Touch Device only buttons-->
+            <div id="noVNC_mobile_buttons">
+                <input type="image" alt="No mousebutton" src="app/images/mouse_none.svg"
+                    id="noVNC_mouse_button0" class="noVNC_button"
+                    title="Active Mouse Button"/>
+                <input type="image" alt="Left mousebutton" src="app/images/mouse_left.svg"
+                    id="noVNC_mouse_button1" class="noVNC_button"
+                    title="Active Mouse Button"/>
+                <input type="image" alt="Middle mousebutton" src="app/images/mouse_middle.svg"
+                    id="noVNC_mouse_button2" class="noVNC_button"
+                    title="Active Mouse Button"/>
+                <input type="image" alt="Right mousebutton" src="app/images/mouse_right.svg"
+                    id="noVNC_mouse_button4" class="noVNC_button"
+                    title="Active Mouse Button"/>
+                <input type="image" alt="Keyboard" src="app/images/keyboard.svg"
+                    id="noVNC_keyboard_button" class="noVNC_button"
+                    value="Keyboard" title="Show Keyboard" />
+            </div>
+
+            <!-- Extra manual keys -->
+            <div id="noVNC_extra_keys">
+                <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
+                    id="noVNC_toggle_extra_keys_button" class="noVNC_button"
+                    title="Show Extra Keys"/>
+                <div class="noVNC_vcenter">
+                <div id="noVNC_modifiers" class="noVNC_panel">
+                    <input type="image" alt="Ctrl" src="app/images/ctrl.svg"
+                        id="noVNC_toggle_ctrl_button" class="noVNC_button"
+                        title="Toggle Ctrl"/>
+                    <input type="image" alt="Alt" src="app/images/alt.svg"
+                        id="noVNC_toggle_alt_button" class="noVNC_button"
+                        title="Toggle Alt"/>
+                    <input type="image" alt="Tab" src="app/images/tab.svg"
+                        id="noVNC_send_tab_button" class="noVNC_button"
+                        title="Send Tab"/>
+                    <input type="image" alt="Esc" src="app/images/esc.svg"
+                        id="noVNC_send_esc_button" class="noVNC_button"
+                        title="Send Escape"/>
+                    <input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
+                        id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
+                        title="Send Ctrl-Alt-Del" />
+                </div>
+                </div>
+            </div>
+
+            <!-- Shutdown/Reboot -->
+            <input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
+                id="noVNC_power_button" class="noVNC_button"
+                title="Shutdown/Reboot..." />
+            <div class="noVNC_vcenter">
+            <div id="noVNC_power" class="noVNC_panel">
+                <div class="noVNC_heading">
+                    <img src="app/images/power.svg"> Power
+                </div>
+                <input type="button" id="noVNC_shutdown_button" value="Shutdown" />
+                <input type="button" id="noVNC_reboot_button" value="Reboot" />
+                <input type="button" id="noVNC_reset_button" value="Reset" />
+            </div>
+            </div>
+
+            <!-- Clipboard -->
+            <input type="image" alt="Clipboard" src="app/images/clipboard.svg"
+                id="noVNC_clipboard_button" class="noVNC_button"
+                title="Clipboard" />
+            <div class="noVNC_vcenter">
+            <div id="noVNC_clipboard" class="noVNC_panel">
+                <div class="noVNC_heading">
+                    <img src="app/images/clipboard.svg"> Clipboard
+                </div>
+                <textarea id="noVNC_clipboard_text" rows=5></textarea>
+                <br />
+                <input id="noVNC_clipboard_clear_button" type="button"
+                    value="Clear" class="noVNC_submit" />
+            </div>
+            </div>
+
+            <!-- Toggle fullscreen -->
+            <input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
+                id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
+                title="Fullscreen" />
+
+            <!-- Settings -->
+            <input type="image" alt="Settings" src="app/images/settings.svg"
+                id="noVNC_settings_button" class="noVNC_button"
+                title="Settings" />
+            <div class="noVNC_vcenter">
+            <div id="noVNC_settings" class="noVNC_panel">
+                <ul>
+                    <li class="noVNC_heading">
+                        <img src="app/images/settings.svg"> Settings
+                    </li>
+                    <li>
+                        <label><input id="noVNC_setting_shared" type="checkbox" /> Shared Mode</label>
+                    </li>
+                    <li>
+                        <label><input id="noVNC_setting_view_only" type="checkbox" /> View Only</label>
+                    </li>
+                    <li><hr></li>
+                    <li>
+                        <label><input id="noVNC_setting_view_clip" type="checkbox" /> Clip to Window</label>
+                    </li>
+                    <li>
+                        <label for="noVNC_setting_resize">Scaling Mode:</label>
+                        <select id="noVNC_setting_resize" name="vncResize">
+                            <option value="off">None</option>
+                            <option value="scale">Local Scaling</option>
+                            <option value="remote">Remote Resizing</option>
+                        </select>
+                    </li>
+                    <li><hr></li>
+                    <li>
+                        <div class="noVNC_expander">Advanced</div>
+                        <div><ul>
+                            <li>
+                                <label for="noVNC_setting_repeaterID">Repeater ID:</label>
+                                <input id="noVNC_setting_repeaterID" type="input" value="" />
+                            </li>
+                            <li>
+                                <div class="noVNC_expander">WebSocket</div>
+                                <div><ul>
+                                    <li>
+                                        <label><input id="noVNC_setting_encrypt" type="checkbox" /> Encrypt</label>
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_host">Host:</label>
+                                        <input id="noVNC_setting_host" />
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_port">Port:</label>
+                                        <input id="noVNC_setting_port" type="number" />
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_path">Path:</label>
+                                        <input id="noVNC_setting_path" type="input" value="websockify" />
+                                    </li>
+                                </ul></div>
+                            </li>
+                            <li><hr></li>
+                            <li>
+                                <label><input id="noVNC_setting_reconnect" type="checkbox" /> Automatic Reconnect</label>
+                            </li>
+                            <li>
+                                <label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
+                                <input id="noVNC_setting_reconnect_delay" type="number" />
+                            </li>
+                            <li><hr></li>
+                            <!-- Logging selection dropdown -->
+                            <li>
+                                <label>Logging:
+                                    <select id="noVNC_setting_logging" name="vncLogging">
+                                    </select>
+                                </label>
+                            </li>
+                        </ul></div>
+                    </li>
+                </ul>
+            </div>
+            </div>
+
+            <!-- Connection Controls -->
+            <input type="image" alt="Disconnect" src="app/images/disconnect.svg"
+                id="noVNC_disconnect_button" class="noVNC_button"
+                title="Disconnect" />
+
+            </div>
+        </div>
+
+        <div id="noVNC_control_bar_hint"></div>
+
+    </div> <!-- End of noVNC_control_bar -->
+
+    <!-- Status Dialog -->
+    <div id="noVNC_status"></div>
+
+    <!-- Connect button -->
+    <div class="noVNC_center">
+        <div id="noVNC_connect_dlg">
+            <div class="noVNC_logo" translate="no"><span>no</span>VNC</div>
+            <div id="noVNC_connect_button"><div>
+                <img src="app/images/connect.svg"> Connect
+            </div></div>
+        </div>
+    </div>
+
+    <!-- Password Dialog -->
+    <div class="noVNC_center noVNC_connect_layer">
+    <div id="noVNC_password_dlg" class="noVNC_panel"><form>
+        <ul>
+            <li>
+                <label>Password:</label>
+                <input id="noVNC_password_input" type="password" />
+            </li>
+            <li>
+                <input id="noVNC_password_button" type="submit" value="Send Password" class="noVNC_submit" />
+            </li>
+        </ul>
+    </form></div>
+    </div>
+
+    <!-- Transition Screens -->
+    <div id="noVNC_transition">
+        <div id="noVNC_transition_text"></div>
+        <div>
+        <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit" />
+        </div>
+        <div class="noVNC_spinner"></div>
+    </div>
+
+    <!-- This is where the RFB elements will attach -->
+    <div id="noVNC_container">
+        <!-- Note that Google Chrome on Android doesn't respect any of these,
+             html attributes which attempt to disable text suggestions on the
+             on-screen keyboard. Let's hope Chrome implements the ime-mode
+             style for example -->
+        <textarea id="noVNC_keyboardinput" autocapitalize="off"
+            autocorrect="off" autocomplete="off" spellcheck="false"
+            mozactionhint="Enter" tabindex="-1"></textarea>
+    </div>
+
+    <audio id="noVNC_bell">
+        <source src="app/sounds/bell.oga" type="audio/ogg">
+        <source src="app/sounds/bell.mp3" type="audio/mpeg">
+    </audio>
+ </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/www/webui.html	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Web User Interface</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 
+  <script src="js/jquery-1.12.4.min.js"></script>
+  <link rel="stylesheet" href="css/style.css">
+</head>
+<body>
+
+ <div id="webui_table">
+  <table style='width: 100%; padding: 0px;'>
+   <tr style="color: yellow; background-color: blue; height: 26px;">
+    <th colspan="3"><span id="top_msg">top_msg</span></th>
+   </tr>
+   <tr>
+    <td style="width: 20%;">
+     <button style="float: left; margin-left: 5px;" type="button" class="appbutton">Hand</button>
+     <button style="float: left; margin-left: 5px; margin-top: 12px;" type="button" class="appbutton">+sp</button>
+    </td>
+    <td style="width: 60%;">
+     <table id="mlt_table">
+      <tr>
+       <td style="width: 10%; text-align: center;">
+	<div id="pump_led" class="LEDred"></div>
+       </td>
+       <td colspan="2" style='font-size: 10px; text-align: center;'>MLT</td>
+       <td style="width: 10%; text-align: center;">
+	<div  id="mlt_led" class="LEDgreen"></div>
+       </td>
+      </tr>
+      <tr><td colspan="4" style='border: 1px solid; font-size: 40px; text-align: right;'><span id="mlt_pv">---</span></td></tr>
+      <tr><td colspan="2" style='width: 50%; font-size: 10px;'><span id="mlt_sp">44.00</span></td>
+	  <td colspan="2" style='width: 50%; font-size: 10px;'><span id="mlt_power">100%</span></td></tr>
+     </table>
+    </td>
+    <td style="width: 20%;">
+     <button style="float: left; margin-left: 5px;" type="button" class="appbutton">Auto</button>
+     <button style="float: left; margin-left: 5px; margin-top: 12px;" type="button" class="appbutton">-sp</button>
+    </td>
+   </tr>
+   <tr>
+    <td><button style="float: left; margin-left: 5px;" type="button" class="appbutton">+1m</button></td>
+    <td><div id="timer">01:23:45</div></td>
+    <td><button style="float: left; margin-left: 5px;" type="button" class="appbutton">-1m</button></td>
+   </tr>
+   <tr>
+    <td><button style="float: left; margin-left: 5px;" type="button" class="appbutton">+sp</button>
+	<button style="float: left; margin-left: 5px; margin-top: 12px;" type="button" class="appbutton">Info</button></td>
+    <td>
+     <table id="hlt_table">
+      <tr>
+       <td style="width: 10%; text-align: center;">
+       </td>
+       <td colspan="2" style='font-size: 10px; text-align: center;'>HLT</td>
+       <td style="width: 10%; text-align: center;">
+        <div id="hlt_led" class="LEDgreen"></div>
+       </td>
+      </tr>
+      <tr><td colspan="4" style='border: 1px solid; font-size: 40px; text-align: right;'><span id="hlt_pv">---</span></td></tr>
+      <tr><td colspan="2" style='font-size: 10px;'><span id="hlt_sp">44.00</span></td>
+          <td colspan="2" style='font-size: 10px;'><span id="hlt_power">100%</span></td></tr>
+     </table>
+    </td>
+    <td><button style="float: left; margin-left: 5px;" type="button" class="appbutton">-sp</button>
+	<button style="float: left; margin-left: 5px; margin-top: 12px;" type="button" class="appbutton">Tools</button></td>
+   </tr>
+  </table>
+ </div>
+
+ <br>
+ <form action="index.html"><input type="submit" class="button" value="Hoofdmenu"></form>
+
+<p id="test"></p>
+
+<script type="text/javascript" src="js/webui.js"></script>
+
+</body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/Kconfig.projbuild	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,93 @@
+menu "BrewBoard Configuration"
+
+
+choice TEMP_SENSORS
+    prompt "Select Temperature Sensors"
+    default TEMP_SENSORS_ONEWIRE
+    help
+        Select to use Real or Fake Temperature Sensors
+
+config TEMP_SENSORS_ONEWIRE
+    bool "Real Sensors"
+    help
+        Select this to use real Sensors connected on the One-Wire busses
+
+config TEMP_SENSORS_SIMULATOR
+    bool "Fake Sensors"
+    help
+        Select this to use Fake Sensors for development
+
+endchoice
+
+
+config ONE_WIRE_MLT
+    int "OneWire MLT GPIO number"
+	range 0 34
+	default 27
+	depends on TEMP_SENSORS_ONEWIRE
+	help
+		GPIO number (IOxx) to access the DS18B20 for the MLT and Boil kettle.
+
+		Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used.
+
+		GPIOs 35-39 are input-only so cannot be used to drive the One Wire Bus.
+
+config ONE_WIRE_HLT
+    int "OneWire HLT GPIO number"
+        range 0 34
+        default 26
+	depends on TEMP_SENSORS_ONEWIRE
+        help
+                GPIO number (IOxx) to access the DS18B20 for the HLT kettle.
+
+                Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used.
+
+                GPIOs 35-39 are input-only so cannot be used to drive the One Wire Bus.
+
+config SSR_MLT_GPIO
+    int "MLT SSR GPIO number"
+        range 0 34
+        default 33
+        help
+                GPIO number (IOxx) to the SSR for the MLT kettle.
+
+                Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to drive a SSR.
+
+                GPIOs 35-39 are input-only so cannot be used as outputs.
+
+config SSR_HLT_GPIO
+    int "HLT SSR GPIO number"
+        range 0 34
+        default 32
+        help
+                GPIO number (IOxx) to the SSR for the HLT kettle.
+
+                Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to drive a SSR.
+
+                GPIOs 35-39 are input-only so cannot be used as outputs.
+
+config SSR_PUMP_GPIO
+    int "PUMP SSR GPIO number"
+        range 0 34
+        default 12
+        help
+                GPIO number (IOxx) to the SSR for the Pump.
+
+                Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to drive a SSR.
+
+                GPIOs 35-39 are input-only so cannot be used as outputs.
+
+
+config BUZZER_GPIO
+    int "Piezo Buzzer  GPIO number"
+        range 0 34
+        default 25
+        help
+                GPIO number (IOxx) to the Piezo Buzzer.
+
+                Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to drive a SSR.
+
+                GPIOs 35-39 are input-only so cannot be used as outputs.
+
+
+endmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/automation.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,1272 @@
+/**
+ * @file automation.c
+ * @brief Automation functions.
+ */
+
+#include "config.h"
+
+int	    			BoilPower = 100;
+int				LastMashStep = 0;
+char        			temp_buf[64];
+char				logline[128];
+char				strftime_buf[64];
+bool        			loop;
+bool				CoolBeep = false;
+bool				Resume = false;
+bool				pumpRest = false;
+bool				updateRuntime = false;
+bool        			NewMinute = false;
+bool				TempReached = false;
+uint8_t     			MashState = MASH_NONE;
+float       			temp_MLT;
+float				MinMash = 38.0;
+float				MaxMash = 80.0;
+uint32_t    			power_MLT = 0;
+uint32_t			power_HLT = 0;
+uint32_t			counts = 0;
+float                           stageTemp = 0.0;
+uint16_t                        stageTime = 0;
+uint16_t                        TimeWhirlPool = 0;
+uint32_t                        TimeLeft = 0;
+uint32_t                        TimeSpent = 0;
+uint32_t                        SecsCount = 0;
+uint32_t                        pumpTime = 0;
+uint32_t                        TimeBrewing = 0;
+uint16_t                        Steady = 0;
+bool                            _NewMinute = false;
+bool                            _UseHLT = false;
+bool				_Prompt = false;
+
+extern bool			System_TimeOk;
+extern sButton			Buttons[MAXBUTTONS];
+extern int			Main_Screen;
+extern DS18B20_State            *ds18b20_state;
+extern DRIVER_State             *driver_state;
+extern SemaphoreHandle_t        xSemaphoreDS18B20;
+extern SemaphoreHandle_t        xSemaphoreDriver;
+extern double                   Output;
+extern time_t			now;
+extern struct tm		timeinfo;
+
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+extern float                    Fake_MLT;
+extern float                    Fake_HLT;
+#endif
+
+static const char		*TAG = "automation";
+
+
+/*
+ * Automation init function that only runs once when a
+ * new screen is entered.
+ */
+bool Automation_Init(void)
+{
+    switch (Main_Screen) {
+	case MAIN_AUTO_INIT:
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+		Fake_MLT = recipe.MashStep[0].Temperature - 10;
+		Fake_HLT = recipe.SpargeTemp - 15;
+                if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) {
+                    ds18b20_state->mlt_temperature = ((int)(Fake_MLT * 16)) / 16.0;
+                    ds18b20_state->hlt_temperature = ((int)(Fake_HLT * 16)) / 16.0;
+                    xSemaphoreGive(xSemaphoreDS18B20);
+                }
+#endif
+                for (int i = 0; i < 7; i++) {
+                    if (recipe.MashStep[i].Resttime)
+                        LastMashStep = i;
+                }
+                ESP_LOGI(TAG, "Last mash step %d", LastMashStep);
+
+                // Check for a crashed session.
+                if (runtime.AutoModeStarted) {
+                    TopMessage("Brouwen hervatten?");
+                    Buttons_Add( 40, 100, 80, 40, "Ja", 0);
+                    Buttons_Add(200, 100, 80, 40, "Nee",  1);
+                    Buttons_Show();
+                    SoundPlay(SOUND_Prompt);
+                    loop = true;
+                    while (loop) {
+                        switch (Buttons_Scan()) {
+                            case 0:     loop = false;
+                                        Resume = true;
+                                        Main_Screen = runtime.StageResume;
+                                        TimeLeft = runtime.StageTimeLeft;
+					TimeBrewing = runtime.TimeBrewing;
+					_UseHLT = runtime.UseHLT;
+                                        MashState = MASH_NONE;
+                                        pumpTime = 0;
+                                        pumpRest = false;
+					if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+					    driver_state->enable = true;
+					    if (_UseHLT) {
+					    	driver_state->hlt_sp = recipe.SpargeTemp;
+						driver_state->hlt_mode = HLT_MODE_BANG;
+					    }
+					    xSemaphoreGive(xSemaphoreDriver);
+					}
+					ESP_LOGI(TAG, "Resume brew screen %d, time left %d", Main_Screen, TimeLeft);
+					log_begin((time_t)0);
+					update_json();
+					log_annotation(ANNOTATION_SYSTEM, "Resume");
+					return true;
+                                        break;
+
+                            case 1:     loop = false;
+                                        Resume = false;
+                                        break;
+
+                            default:
+                                        break;
+                        }
+                        vTaskDelay(50 / portTICK_PERIOD_MS);
+                    }
+                    Buttons_Clear();
+                    TFT_fillScreen(_bg);
+                }
+
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    driver_state->enable = true;
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+                runtime.AutoModeStarted = true;
+                runtime.UseHLT = _UseHLT = false;
+		runtime.TimeBrewing = 0;
+                TimeBrewing = 0;
+		runtime.StageResume = MAIN_AUTO_INIT;
+		runtime.StageTimeLeft = 0;
+		runtime.HopAddition = 0;
+		runtime.Logfile[0] = '\0';
+		runtime.PumpCooling = false;
+		write_runtime();
+                power_MLT = power_HLT = counts = 0;
+		log_clean();
+		vTaskDelay(250 / portTICK_PERIOD_MS);	// Allow some time
+                break;
+
+	case MAIN_AUTO_DELAYSTART:
+		break;
+
+	case MAIN_AUTO_HEATUP:
+		if (runtime.UseHLT) {
+		    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			driver_state->hlt_mode = HLT_MODE_BANG;
+			xSemaphoreGive(xSemaphoreDriver);
+		    }
+		    TopMessage("Spoelwater opwarmen");
+		    MLT_info(71, 26, false);
+		    HLT_info(71,150, false, false);
+		}
+		break;
+
+	case MAIN_AUTO_MASH_IN:
+        case MAIN_AUTO_MASH_1:
+        case MAIN_AUTO_MASH_2:
+        case MAIN_AUTO_MASH_3:
+        case MAIN_AUTO_MASH_4:
+        case MAIN_AUTO_MASH_5:
+        case MAIN_AUTO_MASH_6:
+        case MAIN_AUTO_MASH_OUT:
+                if (Main_Screen == MAIN_AUTO_MASH_IN) {
+                    MinMash = 38.0;
+                    MaxMash = recipe.MashStep[1].Temperature + 10.0;
+                    TimeBrewing = 0;
+		    runtime.TimeBrewing = 0;
+                    if (System_TimeOk) {
+                        time(&now);
+                        localtime_r(&now, &timeinfo);
+                        log_begin(now);
+			runtime.BrewStart = now;
+                    } else {
+                        log_begin((time_t)0);
+			runtime.BrewStart = (time_t)0;
+                    }
+		    updateRuntime = true;
+                } else if (Main_Screen == MAIN_AUTO_MASH_OUT) {
+                    MinMash = 75.0;
+                    MaxMash = 80.0;
+                } else {
+                    MinMash = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN - 1].Temperature;
+                    if (recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN + 1].Resttime) {
+                        MaxMash = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN + 1].Temperature;
+                    } else {
+                        MaxMash = 75.0;
+                    }
+                }
+                MashState = MASH_NONE;
+                pumpTime = 0;
+                pumpRest = false;
+		runtime.StageResume = Main_Screen;
+		updateRuntime = true;
+                TopMessage("Maischen");
+                MLT_info(71, 26, false);
+                if (_UseHLT) {
+                    HLT_info(71,170, false, true);
+                }
+                break;
+
+	case MAIN_AUTO_TOBOIL:
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    driver_state->mlt_sp = stageTemp = config.BoilTemperature;
+                    driver_state->mlt_mode = MLT_MODE_EXT;
+                    driver_state->hlt_sp = 0.0;
+                    driver_state->hlt_mode = HLT_MODE_NONE;
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+
+                runtime.StageResume = Main_Screen;
+                updateRuntime = true;
+		TempReached = false;
+
+                TopMessage("Naar koken");
+                MLT_info(71, 26, false);
+                Buttons_Add(  5,  30, 60, 40, "+sp",  0);
+                Buttons_Add(255,  30, 60, 40, "-sp",  1);
+                Buttons_Show();
+                ESP_LOGI(TAG, "Mash done, going to boil.");
+                break;
+
+	case MAIN_AUTO_BOILING:
+                if (Resume) {
+                    TimeLeft = runtime.StageTimeLeft * 60;
+                } else {
+                    // +1 minute for flameout and 2 seconds for a smooth transition.
+                    runtime.StageTimeLeft = TimeLeft = (recipe.BoilTime * 60) + 60;
+                    runtime.StageResume = Main_Screen;
+                    runtime.HopAddition = 0;
+                }
+                SecsCount = 0;
+
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    driver_state->mlt_sp = stageTemp = config.BoilTemperature;
+                    driver_state->mlt_mode = MLT_MODE_EXT;
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+                SoundPlay(SOUND_TempReached);
+                BoilPower = equipment.BoilPower;
+                updateRuntime = true;
+                TopMessage("Koken");
+                MLT_info(71, 26, false);
+                Buttons_Add(  3,  30, 60, 40, "+sp",  0);
+                Buttons_Add(257,  30, 60, 40, "-sp",  1);
+                Buttons_Add(  3, 190, 60, 40, "+1m",  2);
+                Buttons_Add(257, 190, 60, 40, "-1m",  3);
+                Buttons_Show();
+                ESP_LOGI(TAG, "Boil temperature reached, boil %d minutes", (TimeLeft / 60) -1);
+		log_annotation(ANNOTATION_STAGE, "Koken");
+                break;
+
+	case MAIN_AUTO_COOLING_H:
+	case MAIN_AUTO_COOLING_M:
+	case MAIN_AUTO_COOLING_C:
+		TempReached = false;
+                runtime.StageResume = Main_Screen;
+                runtime.StageTimeLeft = 0;
+                updateRuntime = true;
+                if ((Main_Screen == MAIN_AUTO_COOLING_H) && (! recipe.Whirlpool7)) {
+                    // Skip cooling before whirlpool 74 degrees
+                    Main_Screen = MAIN_AUTO_COOLING_M;
+                    return true; //goto startover;
+                }
+                if ((Main_Screen == MAIN_AUTO_COOLING_M) && (! recipe.Whirlpool6)) {
+                    // Skip cooling before whirlpool 63 degrees.
+                    Main_Screen = MAIN_AUTO_COOLING_C;
+                    return true; //goto startover;
+                }
+                TopMessage("Start koelen?");
+                Buttons_Add( 40, 100, 80, 40, "Start", 0);
+                Buttons_Add(200, 100, 80, 40, "Stop",  1);
+		Buttons[1].dark = true;
+                Buttons_Show();
+                SoundPlay(SOUND_Prompt);
+		_Prompt = true;
+                break;
+
+	case MAIN_AUTO_WHIRLPOOL7:
+	case MAIN_AUTO_WHIRLPOOL6:
+	case MAIN_AUTO_WHIRLPOOL2:
+		TempReached = true;
+                runtime.StageResume = Main_Screen;
+                updateRuntime = true;
+                if ((Main_Screen == MAIN_AUTO_WHIRLPOOL9) && (! recipe.Whirlpool9)) {
+                    // Skip whirlpool 93 degrees.
+                    Main_Screen = MAIN_AUTO_COOLING_H;
+                    return true; //goto startover;
+                }
+                if ((Main_Screen == MAIN_AUTO_WHIRLPOOL7) && (! recipe.Whirlpool7)) {
+                    // Skip whirlpool 74 degrees.
+                    Main_Screen = MAIN_AUTO_COOLING_M;
+                    return true; //goto startover;
+                }
+                if ((Main_Screen == MAIN_AUTO_WHIRLPOOL6) && (! recipe.Whirlpool6)) {
+                    // Skip whirlpool 63 degrees.
+                    Main_Screen = MAIN_AUTO_COOLING_C;
+                    return true; //goto startover;
+                }
+                if ((Main_Screen == MAIN_AUTO_WHIRLPOOL2) && (! recipe.Whirlpool2)) {
+                    // Skip final whirlpool.
+                    Main_Screen = MAIN_AUTO_DONE;
+                    return true; //goto startover;
+                }
+
+                TopMessage("Start Whirlpool?");
+                Buttons_Add( 40, 100, 80, 40, "Start", 0);
+                Buttons_Add(200, 100, 80, 40, "Stop",  1);
+		Buttons[1].dark = true;
+                Buttons_Show();
+                SoundPlay(SOUND_Prompt);
+		_Prompt = true;
+                break;
+
+	case MAIN_AUTO_DONE:
+	case MAIN_AUTO_ABORT:
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    driver_state->enable = false;
+                    driver_state->mlt_mode = MLT_MODE_NONE;
+                    driver_state->mlt_sp = 0.0;
+                    driver_state->hlt_mode = HLT_MODE_NONE;
+                    driver_state->hlt_sp = 0.0;
+                    driver_state->pump_run = 0;
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+                _fg = TFT_YELLOW;
+                TFT_setFont(DEJAVU24_FONT, NULL);
+                if (Main_Screen == MAIN_AUTO_DONE) {
+                    TFT_print("Brouwen is gereed.", CENTER, CENTER);
+                    ESP_LOGI(TAG, "Brew is done");
+                    SoundPlay(SOUND_End);
+                } else {
+                    TFT_print("Brouwen is afgebroken.", CENTER, CENTER);
+                    ESP_LOGI(TAG, "Brew is aborted");
+                    SoundPlay(SOUND_Warn);
+                }
+                log_close();
+		runtime.Logfile[0] = '\0';
+		runtime.BrewStart = (time_t)0;
+                runtime.AutoModeStarted = false;
+                runtime.StageResume = MAIN_MODE_FREE;
+		runtime.PumpCooling = false;
+		runtime.HopAddition = 0;
+		runtime.TimeBrewing = 0;
+		runtime.StageTimeLeft = 0;
+                updateRuntime = true;
+                Buttons_Add(130, 200, 60, 40, "Ok", 0);
+		Buttons[0].dark = true;
+                Buttons_Show();
+                break;
+
+	default:
+		break;
+    }
+
+    return false;
+}
+
+
+
+/*
+ * Automation loop screens. Mostly non-blocking.
+ */
+bool Automation_Loop(void)
+{
+    static bool beeped = false;
+    char	tmp[32];
+    uint16_t	y;
+
+    switch (Main_Screen) {
+
+	case MAIN_AUTO_INIT:
+		/*
+		 * Present selected equipment and recipe.
+		 */
+		read_recipe(config.RecipeRec);
+		y = 28;
+		TopMessage("Automaat");
+		TFT_setFont(DEFAULT_FONT, NULL);
+		ShowText(2,y,"Installatie", equipment.Name);
+		y += 16;
+		ShowText(2,y,"Recept", recipe.Name);
+		y += 16;
+		ShowFloat(2, y, "Maisch in", " C", recipe.MashStep[0].Temperature, 2);
+		ShowFloat(162, y, "Spoelwater", " C", recipe.SpargeTemp, 2);
+		y += 16;
+		_fg = TFT_WHITE;
+		TFT_print("Maisch stap", 2, y);
+		TFT_print("Temp.", 200, y);
+		TFT_print("Rust", 260, y);
+		_fg = TFT_YELLOW;
+		y += 16;
+		for (int i = 1; i < 8; i++) {
+		    if (recipe.MashStep[i].Resttime) {
+			TFT_print(recipe.MashStep[i].Name, 2, y);
+			sprintf(tmp, "%.2f", recipe.MashStep[i].Temperature);
+			TFT_print(tmp, 200, y);
+			sprintf(tmp, "%2d min", recipe.MashStep[i].Resttime);
+			TFT_print(tmp, 260, y);
+			y += 16;
+		    }
+		}
+		ShowInteger(2, y, "Kooktijd", " miniuten", recipe.BoilTime);
+		y += 16;
+		if (recipe.Additions) {
+		    _fg = TFT_YELLOW;
+		    sprintf(tmp, "%d ", recipe.Additions);
+		    TFT_print(tmp, 2, y);
+		    _fg = TFT_WHITE;
+		    TFT_print("toevoegingen om", LASTX, y);
+		    _fg = TFT_YELLOW;
+		    for (int i = 1; i <= recipe.Additions; i++) {
+			sprintf(tmp, " %d", recipe.Addition[i-1].Time);
+			TFT_print(tmp, LASTX, y);
+		    }
+		    _fg = TFT_WHITE;
+		    TFT_print(" minuten", LASTX, y);
+		} else {
+		    _fg = TFT_WHITE;
+		    TFT_print("Geen hop toevoegingen.", 2, y);
+		}
+		y += 16;
+		ShowFloat(2, y, "Koelen tot", " C", recipe.CoolTemp, 2);
+		if (recipe.Whirlpool9) {
+		    ShowInteger(2, y, "Whirlpool 88..100 graden", " minuten", recipe.Whirlpool9);
+		    y += 16;
+		}
+		if (recipe.Whirlpool7) {
+		    ShowInteger(2, y, "Whirlpool 71..77 graden", " minuten", recipe.Whirlpool7);
+		    y += 16;
+		}
+		if (recipe.Whirlpool6) {
+		    ShowInteger(2, y, "Whirlpool 60..66 graden", " minuten", recipe.Whirlpool6);
+		    y += 16;
+		}
+		if (recipe.Whirlpool2) {
+		    ShowInteger(2, y, "Whirlpool koud", " minuten", recipe.Whirlpool2);
+		    y += 16;
+		}
+		Buttons_Add(  0, 210, 70, 30, "Stop"   , 0);
+		Buttons_Add(250, 210, 70, 30, "Start"  , 1);
+		Buttons[0].dark = true;
+		Buttons_Show();
+		loop = true;
+		while (loop) {
+		    switch (Buttons_Scan()) {
+			case 0:         loop = false;
+					Main_Screen = MAIN_AUTO_ABORT;
+					break;
+
+			case 1:         loop = false;
+					break;
+
+			default:        break;
+		    }
+		    vTaskDelay(20 / portTICK_PERIOD_MS);
+		}
+		if (Main_Screen == MAIN_AUTO_ABORT)
+		    break;
+
+                _UseHLT = false;
+		_bg = TFT_BLACK;
+		TFT_fillScreen(_bg);
+                TopMessage("Maisch water aanwezig?");
+		Buttons_Clear();
+                Buttons_Add( 40, 100, 80, 40, "Ja", 0);
+                Buttons_Add(200, 100, 80, 40, "Nee",  1);
+                Buttons_Show();
+                SoundPlay(SOUND_Prompt);
+                loop = true;
+                while (loop) {
+                    switch (Buttons_Scan()) {
+                        case 0:         loop = false;
+                                        break;
+
+                        case 1:         loop = false;
+                                        Main_Screen = MAIN_AUTO_ABORT;
+                                        break;
+
+                        default:        break;
+                    }
+                    vTaskDelay(20 / portTICK_PERIOD_MS);
+                }
+                if (Main_Screen == MAIN_AUTO_ABORT)
+                    break;
+
+		if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) {
+                    TopMessage("Spoelwater aanwezig?");
+                    SoundPlay(SOUND_Prompt);
+                    loop = true;
+                    while (loop) {
+                    	switch (Buttons_Scan()) {
+                            case 0:         loop = false;
+                                        _UseHLT = true;
+                                        break;
+
+                            case 1:         loop = false;
+                                        break;
+
+                            default:        break;
+                    	}
+                   	vTaskDelay(20 / portTICK_PERIOD_MS);
+                    }
+                    runtime.UseHLT = _UseHLT;
+		} else {
+		    runtime.UseHLT = _UseHLT = false;
+		}
+                updateRuntime = true;
+                if (_UseHLT) {
+                    /*
+                     * Calculate HLT setpoint for pre-heat. Substract the
+                     * available Mash rest times, asume 0.5 degrees/minute
+                     * heat capacity during mash.
+                     */
+                    int AvailableTime = 0;
+                    for (int i = 1; i < 6; i++) // Only normal Mash steps
+                        AvailableTime += recipe.MashStep[i].Resttime;
+                    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                        driver_state->hlt_sp = recipe.SpargeTemp - ((AvailableTime / 2) + 2);
+                        ESP_LOGI(TAG, "HLT preheat set to %4.1f", driver_state->hlt_sp);
+                        xSemaphoreGive(xSemaphoreDriver);
+                    }
+                }
+                Buttons_Clear();
+                Main_Screen = MAIN_AUTO_DELAYSTART;
+                break;
+
+	case MAIN_AUTO_DELAYSTART:
+                Main_Screen = MAIN_AUTO_HEATUP;
+                break;
+
+	case MAIN_AUTO_HEATUP:
+                if (! runtime.UseHLT) {         // Skip if HLT is off
+                    Main_Screen = MAIN_AUTO_MASH_IN;
+                    break;
+                }
+
+                MLT_info(71, 26, true);
+                HLT_info(71,150, true, false);
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    if (driver_state->hlt_pv >= driver_state->hlt_sp) {
+                        Main_Screen = MAIN_AUTO_MASH_IN;
+                        driver_state->hlt_sp = recipe.SpargeTemp;       // Set final setpoint
+                    }
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+                break;
+
+	case MAIN_AUTO_MASH_IN:
+	case MAIN_AUTO_MASH_1:
+	case MAIN_AUTO_MASH_2:
+	case MAIN_AUTO_MASH_3:
+	case MAIN_AUTO_MASH_4:
+	case MAIN_AUTO_MASH_5:
+	case MAIN_AUTO_MASH_6:
+	case MAIN_AUTO_MASH_OUT:
+                temp_MLT = 0.0;
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    temp_MLT = driver_state->mlt_pv;
+
+		    if (MashState == MASH_ADD || MashState == MASH_REMOVE) {
+			driver_state->pump_run = 0;
+		    } else if (MashState != MASH_NONE) {
+                        if (Main_Screen == MAIN_AUTO_MASH_IN) {
+                            driver_state->pump_run = (equipment.PumpPreMash && ! pumpRest) ? 1 : 0;
+                        } else if (Main_Screen == MAIN_AUTO_MASH_OUT) {
+                            driver_state->pump_run = (equipment.PumpMashOut && ! pumpRest) ? 1 : 0;
+                        } else {
+                            driver_state->pump_run = (equipment.PumpOnMash && ! pumpRest) ? 1 : 0;
+                        }
+                    }
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+                if (MashState == MASH_NONE) {
+                    stageTemp = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Temperature;
+                    stageTime = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Resttime;
+                    TempReached = false;
+                    if (stageTime == 0) {
+                        ESP_LOGI(TAG, "Mash step %d skipped", Main_Screen - MAIN_AUTO_MASH_IN);
+                        Main_Screen++;
+                        break;
+                    }
+                    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                        driver_state->mlt_sp = stageTemp;
+                        driver_state->mlt_mode = MLT_MODE_PID;
+                        xSemaphoreGive(xSemaphoreDriver);
+                    }
+                    MashState = MASH_WAITTEMP;
+                    ESP_LOGI(TAG, "Mash step %d time: %d temp: %4.1f  min: %4.1f max: %4.1f",
+                                    Main_Screen - MAIN_AUTO_MASH_IN, stageTime, stageTemp, MinMash, MaxMash);
+
+		    if (Main_Screen > MAIN_AUTO_MASH_IN) {
+			// Do not annotate before the log is open.
+			if (Main_Screen == MAIN_AUTO_MASH_OUT) {
+			    log_annotation(ANNOTATION_STAGE, "Uitmaischen");
+			} else {
+                    	    sprintf(logline, "Maisch: %d", Main_Screen - MAIN_AUTO_MASH_IN);
+		    	    log_annotation(ANNOTATION_STAGE, logline);
+			}
+		    }
+
+                    if (strlen(recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Name)) {
+                        TopMessage(recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Name);
+                    } else {
+                        sprintf(temp_buf, "Maisch stap #%d", Main_Screen - MAIN_AUTO_MASH_IN);
+                        TopMessage(temp_buf);
+                    }
+                    Buttons_Add(  5,  30, 60, 40, "+sp",   0);
+                    Buttons_Add(255,  30, 60, 40, "-sp",   1);
+                    Buttons_Show();
+
+                } else if (MashState == MASH_WAITTEMP) {
+                    pumpRest = false;
+                    if (temp_MLT < stageTemp) {
+                        Steady = 0;
+                    }
+                    if ((temp_MLT >= stageTemp) && (Steady > 10)) {
+                        SoundPlay(SOUND_TempReached);
+                        TempReached = true;
+                        MashState = MASH_REST;
+                        if (Main_Screen == MAIN_AUTO_MASH_IN) {
+                            TimerSet(0);
+                        } else {
+                            if (Resume && (runtime.StageTimeLeft < stageTime))
+                                TimerSet(runtime.StageTimeLeft * 60);
+                            else
+                                TimerSet(stageTime * 60);
+                        }
+			Resume = false;
+			runtime.StageTimeLeft = TimeLeft / 60;
+			updateRuntime = true;
+                        ESP_LOGI(TAG, "Mash step %d temperature reached, rest time %d", Main_Screen - MAIN_AUTO_MASH_IN, TimeLeft / 60);
+                        Buttons_Clear();
+                        Buttons_Add(  0, 120, 60, 40, "+1m",   0);
+                        Buttons_Add(260, 120, 60, 40, "-1m",   1);
+                        Buttons_Show();
+                    }
+                    switch (Buttons_Scan()) {
+                        case 0:         if (stageTemp < MaxMash) {
+                                            stageTemp += 0.25;
+                                            if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                                driver_state->mlt_sp = stageTemp;
+                                                xSemaphoreGive(xSemaphoreDriver);
+                                            }
+                                        }
+                                        break;
+
+                        case 1:         if (stageTemp > MinMash) {
+                                            stageTemp -= 0.25;
+                                            if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                                driver_state->mlt_sp = stageTemp;
+                                                xSemaphoreGive(xSemaphoreDriver);
+                                            }
+                                        }
+                                        break;
+
+                        default:
+                                        break;
+                    }
+		    if (NewMinute)
+			updateRuntime = true;
+
+                } else if (MashState == MASH_REST) {
+                    /*
+                     * Mash step rest time and pump control
+                     */
+                    if (((Main_Screen == MAIN_AUTO_MASH_OUT) && equipment.PumpMashOut) ||
+                        ((Main_Screen != MAIN_AUTO_MASH_OUT) && equipment.PumpOnMash)) {
+                        float DeltaTemp = equipment.PumpRest * stageTemp / 120; // Maximum temperature drop before heating again.
+                        if (pumpTime >= (equipment.PumpCycle + equipment.PumpRest) || ((stageTemp - temp_MLT) > DeltaTemp)) {
+                            pumpTime = 0;
+                        }
+                        if (pumpTime >= equipment.PumpCycle) {
+                            if (! pumpRest) {
+                                pumpRest = true;
+                                ESP_LOGI(TAG, "Pump rest");
+                            }
+                        } else {
+                            if (pumpRest) {
+                                pumpRest = false;
+                                ESP_LOGI(TAG, "Pump start");
+                            }
+                        }
+                    }
+                    if (TimeLeft) {
+                        switch (Buttons_Scan()) {
+                            case 0:     TimeLeft += 60;
+					runtime.StageTimeLeft = TimeLeft / 60;
+					updateRuntime = true;
+                                        break;
+
+                            case 1:     if (TimeLeft < 60)
+                                            TimeLeft = 0;
+                                        else
+                                            TimeLeft -= 60;
+					runtime.StageTimeLeft = TimeLeft / 60;
+					updateRuntime = true;
+                                        break;
+
+                            default:    break;
+                        }
+                    }
+
+                    if (TimeLeft == 0) {
+			runtime.StageTimeLeft = TimeLeft / 60;
+			updateRuntime = true;
+                        if ((Main_Screen == MAIN_AUTO_MASH_IN) && config.AskAdd) {
+                            /*
+                             * Add Mash prompt.
+                             */
+			    log_annotation(ANNOTATION_EVENT, "Mout storten");
+                            Buttons_Clear();
+                            Buttons_Add(  5,120, 60, 40, "Halt", 0);
+			    Buttons[0].dark = true;
+                            Buttons_Add(255,120, 60, 40, "Ok",   1);
+                            Buttons_Show();
+                            _fg = TFT_WHITE;
+                            _bg = TFT_BLACK;
+                            TFT_setFont(DEJAVU24_FONT, NULL);
+                            TFT_print("Mout storten?", CENTER, 135);
+                            SoundPlay(SOUND_Prompt);
+			    MashState = MASH_ADD;
+			    ESP_LOGI(TAG, "Mash add prompt");
+			    break;
+                        }
+                        if (((Main_Screen - MAIN_AUTO_MASH_IN) == LastMashStep) && config.AskIodine) {
+                            /*
+                             * Iodone test prompt.
+                             */
+			    log_annotation(ANNOTATION_EVENT, "Jodium test");
+                            TFT_fillRect(0, 120, 320, 50, TFT_BLACK);
+                            Buttons_Clear();
+                            Buttons_Add(  5,120, 60, 40, "Halt", 0);
+			    Buttons[0].dark = true;
+                            Buttons_Add(255,120, 60, 40, "Ok",   1);
+                            Buttons_Show();
+                            _fg = TFT_WHITE;
+                            _bg = TFT_BLACK;
+                            TFT_setFont(DEJAVU24_FONT, NULL);
+                            TFT_print("Jodium test?", CENTER, 127);
+                            SoundPlay(SOUND_Prompt);
+                            beeped = false;
+                            TimerSet(config.IodineTime * 60);
+			    MashState = MASH_IODINE;
+			    ESP_LOGI(TAG, "Mash iodine test prompt");
+			    break;
+                        }
+                        if ((Main_Screen == MAIN_AUTO_MASH_OUT) && config.AskRemove) {
+                            /*
+                             * Mash remove prompt.
+                             */
+			    log_annotation(ANNOTATION_EVENT, "Mout verwijderen");
+                            TFT_fillRect(0, 120, 320, 50, TFT_BLACK);
+                            Buttons_Clear();
+                            Buttons_Add(  5,120, 60, 40, "Halt", 0);
+			    Buttons[0].dark = true;
+                            Buttons_Add(255,120, 60, 40, "Ok",   1);
+                            Buttons_Show();
+                            _fg = TFT_WHITE;
+                            _bg = TFT_BLACK;
+                            TFT_setFont(DEJAVU18_FONT, NULL);
+                            TFT_print("Mout verwijderen?", CENTER, 135);
+                            SoundPlay(SOUND_Prompt);
+			    MashState = MASH_REMOVE;
+			    ESP_LOGI(TAG, "Mash remove prompt");
+			    break;
+                        }
+                        if (Main_Screen != MAIN_AUTO_ABORT)
+                            Main_Screen++;
+                        TempReached = false;
+                    } else {
+                        TimerShow(TimeLeft, 65, 122);
+			if (NewMinute) {
+			    runtime.StageTimeLeft = TimeLeft / 60;
+			    updateRuntime = true;
+			}
+                    }
+		} else if (MashState == MASH_ADD) {
+		    switch (Buttons_Scan()) {
+			case 0:     Main_Screen = MAIN_AUTO_ABORT;
+				    break;
+			case 1:     Main_Screen++;
+				    break;
+			default:    break;
+		    }
+		} else if (MashState == MASH_IODINE) {
+		    if (TimeSpent % 45 == 0) {
+			if (! beeped) {
+			    SoundPlay(SOUND_Warn);
+			    beeped = true;
+			}
+		    } else {
+			beeped = false;
+		    }
+		    switch (Buttons_Scan()) {
+			case 0:     Main_Screen = MAIN_AUTO_ABORT;
+				    break;
+			case 1:     Main_Screen++;
+				    break;
+			default:    break;
+		    }
+		    if (TimeLeft == 0) {
+			Main_Screen++;
+		    }
+		} else if (MashState == MASH_REMOVE) {
+		    switch (Buttons_Scan()) {
+			case 0:     Main_Screen = MAIN_AUTO_ABORT;
+				    break;
+			case 1:     Main_Screen++;
+				    break;
+			default:    break;
+		    }
+                } /* MashState */
+                MLT_info(71, 26, true);
+                if (_UseHLT) {
+                    HLT_info(71, 170, true, true);
+                }
+                break;
+
+	case MAIN_AUTO_TOBOIL:
+                Output = 255;
+                /*
+                 * Go to the boil temperature and wait until it is steady.
+                 */
+                if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    if (driver_state->mlt_pv < stageTemp) {
+                        Steady = 0;
+                    } else {
+                        if (Steady > 10) {
+                            Main_Screen = MAIN_AUTO_BOILING;
+			    TempReached = true;
+                        }
+                    }
+                    driver_state->pump_run = (equipment.PumpOnBoil && (driver_state->mlt_pv < equipment.PumpMaxTemp)) ? 1 : 0;
+                    xSemaphoreGive(xSemaphoreDriver);
+                }
+
+                MLT_info(71, 26, true);
+                switch (Buttons_Scan()) {
+                    case 0:     if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    driver_state->mlt_sp += 0.25;
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    case 1:     if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    driver_state->mlt_sp -= 0.25;
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    default:    break;
+                }
+		if (Resume)
+		    Resume = false;
+                break;
+
+	case MAIN_AUTO_BOILING:
+		if (Resume)
+		    Resume = false;
+                if (NewMinute) {
+                    if ((runtime.HopAddition < recipe.Additions) && (TimeLeft <= ((recipe.Addition[runtime.HopAddition].Time * 60) + 60))) {
+                        ESP_LOGI(TAG, "Hop addition %d at %d minutes", runtime.HopAddition + 1, recipe.Addition[runtime.HopAddition].Time);
+                        TopMessage(recipe.Addition[runtime.HopAddition].Name);
+                        sprintf(logline, "Hopgift %d %s", runtime.HopAddition + 1, recipe.Addition[runtime.HopAddition].Name);
+			log_annotation(ANNOTATION_EVENT, logline);
+                        SoundPlay(SOUND_AddHop);
+                        runtime.HopAddition++;
+                    } else {
+                        TopMessage("Koken");
+                    }
+                    runtime.StageTimeLeft = TimeLeft / 60;
+                    updateRuntime = true;
+                }
+                if (TimeLeft < 60) {
+		    if (Output) {
+			log_annotation(ANNOTATION_STAGE, "Vlamuit");
+		    }
+                    // Flameout
+                    Output = 0;
+                } else if (driver_state->mlt_pv >= stageTemp) {
+                    Output = (int)((BoilPower * 255.0) / 100.0);
+                    if (Buttons[4].x == -1) {
+                        Buttons_Add(  3,110, 60, 40, "+%", 4);
+                        Buttons_Add(257,110, 60, 40, "-%", 5);
+                        Buttons_Show();
+                    }
+                } else {
+                    Output = 255;
+                    if (Buttons[4].x != -1) {
+                        Buttons[4].x = Buttons[5].x = -1;
+                        Buttons_Show();
+                        TFT_fillRect(  3,110, 60, 40, TFT_BLACK);
+                        TFT_fillRect(257,110, 60, 40, TFT_BLACK);
+                    }
+                }
+
+                MLT_info(71, 26, true);
+                TimerShow(TimeLeft, 65, 190);
+
+                switch (Buttons_Scan()) {
+                    case 0:     if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    driver_state->mlt_sp += 0.25;
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    case 1:     if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    driver_state->mlt_sp -= 0.25;
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    case 2:     TimeLeft += 60;
+                                break;
+
+                    case 3:     if (TimeLeft > 60)
+                                    TimeLeft -= 60;
+                                else
+                                    TimeLeft = 0;
+                                break;
+
+                    case 4:     if (BoilPower < 100)
+                                    BoilPower++;
+                                break;
+
+                    case 5:     if (BoilPower > 0)
+                                    BoilPower--;
+                                break;
+
+                    default:    break;
+                }
+
+                if (TimeLeft == 0) {
+                    Main_Screen = MAIN_AUTO_WHIRLPOOL9;
+                    ESP_LOGI(TAG, "Boil is ready");
+                }
+                break;
+
+	case MAIN_AUTO_COOLING_H:
+	case MAIN_AUTO_COOLING_M:
+	case MAIN_AUTO_COOLING_C:
+		if (_Prompt) {
+		    /*
+		     * Prompt mode
+		     */
+		    switch (Buttons_Scan()) {
+			case 0: _Prompt = false;
+				Buttons_Clear();
+				break;
+			case 1: Main_Screen = MAIN_AUTO_DONE;
+				Buttons_Clear();
+				return true; //goto startover;
+				break;
+			default:
+				break;
+		    }
+
+		    if (! _Prompt) {
+			/*
+			 * Starting cooling, setup the screen.
+			 */
+			Buttons_Clear();
+			TFT_fillScreen(_bg);
+			if (Main_Screen == MAIN_AUTO_COOLING_H) {
+			    stageTemp = 77.0;
+			} else if (Main_Screen == MAIN_AUTO_COOLING_M) {
+			    stageTemp = 66.0;
+			} else {
+			    stageTemp = recipe.CoolTemp;
+			}
+			CoolBeep = false;
+			ESP_LOGI(TAG, "Start cooling from %6.2f to %4.1f", ds18b20_state->mlt_temperature, stageTemp);
+			if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			    driver_state->mlt_mode = MLT_MODE_OFF;
+			    driver_state->mlt_sp = stageTemp;
+			    xSemaphoreGive(xSemaphoreDriver);
+			}
+			log_annotation(ANNOTATION_STAGE, "Koelen");
+			TopMessage("Koelen");
+			MLT_info(71, 26, false);
+			Buttons_Add(  5, 200, 60, 40, "Stop", 0);
+			Buttons[0].dark = true;
+			Buttons_Add(  5,  26, 60, 40, "+1",   1);
+			Buttons_Add(255,  26, 60, 40, "-1",   2);
+			/*
+			 * The next key is not a mistake, but we need a key entry which
+			 * will later become the pump key. The keyscan routine will find
+			 * the original key if pressed.
+			 */
+			Buttons_Add(255,  26, 60, 40, "-1",   3);
+			Buttons_Show();
+		    }
+		} else {
+		    /*
+		     * Not in prompt mode.
+		     */
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+		    if (Fake_MLT > 12.0) {
+			if (driver_state->pump_run)
+			    Fake_MLT -= 0.00025 * (Fake_MLT - 12.0);
+			else
+			    Fake_MLT -= 0.00015 * (Fake_MLT - 12.0);
+		    }
+		    if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) {
+			ds18b20_state->mlt_temperature = ((int)(Fake_MLT * 16)) / 16.0;
+			xSemaphoreGive(xSemaphoreDS18B20);
+		    }
+#endif
+                    MLT_info(71, 26, true);
+                    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    	/*
+                    	 * If the pump safe temperature is reached, add the control button.
+		    	 * Redefine key number 3 if it is at the position of key 2.
+                    	 */
+                    	if ((driver_state->mlt_pv < equipment.PumpMaxTemp) && (Buttons[3].x == Buttons[2].x) &&(Buttons[3].y == Buttons[2].y)) {
+                            Buttons_Add(255, 200, 60, 40, "Pomp", 3);
+                            Buttons_Show();
+                    	}
+                    	xSemaphoreGive(xSemaphoreDriver);
+                    }
+                    switch (Buttons_Scan()) {
+                    	case 1:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    if (Main_Screen == MAIN_AUTO_COOLING_H) {
+                                        if (driver_state->mlt_sp < 77.0)
+                                            driver_state->mlt_sp += 1.0;
+                                    } else if (Main_Screen == MAIN_AUTO_COOLING_M) {
+                                        if (driver_state->mlt_sp < 66.0)
+                                            driver_state->mlt_sp += 1.0;
+                                    } else if (Main_Screen == MAIN_AUTO_COOLING_C) {
+                                        if (driver_state->mlt_sp < 30.0)
+                                            driver_state->mlt_sp += 1.0;
+                                    }
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    	case 0: Buttons_Add( 60, 150, 90, 40, "Stoppen", 4);
+				Buttons[4].dark = true;
+                                Buttons_Add(170, 150, 90, 40, "Sorry",   5);
+                                Buttons_Show();
+                                break;
+
+                    	case 2: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    if (Main_Screen == MAIN_AUTO_COOLING_H) {
+                                        if (driver_state->mlt_sp > 71.0)
+                                            driver_state->mlt_sp -= 1.0;
+                                    } else if (Main_Screen == MAIN_AUTO_COOLING_M) {
+                                        if (driver_state->mlt_sp > 60.0)
+                                            driver_state->mlt_sp -= 1.0;
+                                    } else if (Main_Screen == MAIN_AUTO_COOLING_C) {
+                                        if (driver_state->mlt_sp > 10.0)
+                                            driver_state->mlt_sp -= 1.0;
+                                    }
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+                    	case 3: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    if (driver_state->mlt_pv < equipment.PumpMaxTemp) {
+                                        if (driver_state->pump_run)
+                                            driver_state->pump_run = 0;
+                                        else
+                                            driver_state->pump_run = 1;
+                                    } else {
+                                        driver_state->pump_run = 0;
+                                    }
+				    runtime.PumpCooling = driver_state->pump_run;
+				    updateRuntime = true;
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                                break;
+
+		    	case 4:	Main_Screen++;
+				break;
+
+		    	case 5:	Buttons[4].x = Buttons[5].x = -1;
+				TFT_fillRect(60, 150, 200, 40, TFT_BLACK);
+				break;
+
+		    	default:
+				break;
+                    }
+
+                    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                    	if (! CoolBeep && (driver_state->mlt_pv < (driver_state->mlt_sp + 2.0))) {
+                            SoundPlay(SOUND_Warn);
+                            CoolBeep = true;
+                    	}
+                    	if (driver_state->mlt_pv <= driver_state->mlt_sp) {
+                            SoundPlay(SOUND_TempReached);
+                            Main_Screen++;
+                    	}
+                    	xSemaphoreGive(xSemaphoreDriver);
+                    }
+		}
+                break;
+
+	case MAIN_AUTO_WHIRLPOOL9:
+	case MAIN_AUTO_WHIRLPOOL7:
+	case MAIN_AUTO_WHIRLPOOL6:
+	case MAIN_AUTO_WHIRLPOOL2:
+		if (_Prompt) {
+
+		    switch (Buttons_Scan()) {
+			case 0: _Prompt = false;
+				break;
+			case 1: if (Main_Screen == MAIN_AUTO_WHIRLPOOL2) {
+				    Main_Screen = MAIN_AUTO_DONE;
+				} else {
+				    Main_Screen++;
+				}
+				Buttons_Clear();
+				return true; //goto startover;
+				break;
+			default:
+				break;
+		    }
+
+		    if (! _Prompt) {
+			/*
+			 * Prepare the screen for the actual whirpool.
+			 */
+			Buttons_Clear();
+			TFT_fillScreen(_bg);
+			if (Main_Screen == MAIN_AUTO_WHIRLPOOL9) {
+			    TimeWhirlPool = recipe.Whirlpool9;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				driver_state->mlt_sp = 93.0;
+				driver_state->mlt_mode = MLT_MODE_PID;
+				xSemaphoreGive(xSemaphoreDriver);
+			    }
+			    TopMessage("Whirlpool 88..100");
+			} else if (Main_Screen == MAIN_AUTO_WHIRLPOOL7) {
+			    TimeWhirlPool = recipe.Whirlpool7;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				driver_state->mlt_sp = 74.0;
+				driver_state->mlt_mode = MLT_MODE_PID;
+				xSemaphoreGive(xSemaphoreDriver);
+			    }
+			    TopMessage("Whirlpool 71..77");
+			} else if (Main_Screen == MAIN_AUTO_WHIRLPOOL6) {
+			    TimeWhirlPool = recipe.Whirlpool6;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				driver_state->mlt_sp = 63.0;
+				driver_state->mlt_mode = MLT_MODE_PID;
+				xSemaphoreGive(xSemaphoreDriver);
+			    }
+			    TopMessage("Whirlpool 60..66");
+			} else {
+			    TimeWhirlPool = recipe.Whirlpool2;
+			    TopMessage("Koude whirlpool");
+			}
+			if (Resume) {
+			    TimeWhirlPool = runtime.StageTimeLeft;
+			}
+
+			/*
+			 * If the pump is allowed at the current temperature, turn it on.
+			 */
+			if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			    driver_state->pump_run = (driver_state->mlt_pv < equipment.PumpMaxTemp) ? 1 : 0;
+			    xSemaphoreGive(xSemaphoreDriver);
+			}
+			log_annotation(ANNOTATION_STAGE, "Whirlpool");
+
+			TimerSet(TimeWhirlPool * 60);
+			runtime.StageTimeLeft = TimeWhirlPool;
+			updateRuntime = true;
+			MLT_info(71, 26, false);
+			ESP_LOGI(TAG, "Whirlpool %d minutes, sp %4.1f", TimeWhirlPool, driver_state->mlt_sp);
+			Buttons_Add(255, 120, 60, 40, "+1m", 0);
+			Buttons_Add(  5, 120, 60, 40, "-1m", 1);
+			Buttons_Add(130, 200, 60, 40, "Pomp",  2);
+			Buttons_Show();
+		    }
+		} else {
+		    /*
+		     * Not running in prompt mode, do the whirlpool.
+		     */
+                    if (TimeLeft == 120) {
+                    	/*
+                    	 * Drop the temperature when whirlpool is almost ready.
+                    	 * If we are lucky the heater element will cool down so
+                    	 * the next cooling stage will not wast too much energy.
+                    	 */
+                    	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                            if (Main_Screen == MAIN_AUTO_WHIRLPOOL9)
+                            	driver_state->mlt_sp = 88.0;
+                            if (Main_Screen == MAIN_AUTO_WHIRLPOOL7)
+                            	driver_state->mlt_sp = 71.0;
+                            if (Main_Screen == MAIN_AUTO_WHIRLPOOL6)
+                            	driver_state->mlt_sp = 60.0;
+                            xSemaphoreGive(xSemaphoreDriver);
+                    	}
+                    }
+
+                    MLT_info(71, 26, true);
+                    TimerShow(TimeLeft, 65, 122);
+                    switch (Buttons_Scan()) {
+                    	case 0:     TimeLeft += 60;
+                                break;
+                    	case 1:     if (TimeLeft > 60)
+                                    TimeLeft -= 60;
+                                else
+                                    TimeLeft = 0;
+                                break;
+                    	case 2:     if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+                                    if (driver_state->mlt_pv < equipment.PumpMaxTemp) {
+                                        if (driver_state->pump_run)
+                                            driver_state->pump_run = 0;
+                                        else
+                                            driver_state->pump_run = 1;
+                                    } else {
+                                        driver_state->pump_run = 0;
+                                    }
+                                    xSemaphoreGive(xSemaphoreDriver);
+                                }
+                    }
+
+                    if (NewMinute) {
+                    	runtime.StageTimeLeft = TimeLeft / 60;
+                    	updateRuntime = true;
+                    }
+
+                    if ((TimeLeft == 0)) {
+                    	if (Main_Screen == MAIN_AUTO_WHIRLPOOL9) {
+                            Main_Screen = MAIN_AUTO_COOLING_H;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			    	driver_state->pump_run = runtime.PumpCooling;
+			    	xSemaphoreGive(xSemaphoreDriver);
+			    }
+                    	} else if (Main_Screen == MAIN_AUTO_WHIRLPOOL7) {
+                            Main_Screen = MAIN_AUTO_COOLING_M;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			    	driver_state->pump_run = runtime.PumpCooling;
+			    	xSemaphoreGive(xSemaphoreDriver);
+			    }
+                    	} else if (Main_Screen == MAIN_AUTO_WHIRLPOOL6) {
+                            Main_Screen = MAIN_AUTO_COOLING_C;
+			    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			    	driver_state->pump_run = runtime.PumpCooling;
+			    	xSemaphoreGive(xSemaphoreDriver);
+			    }
+                        } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL2) {
+                            Main_Screen = MAIN_AUTO_DONE;
+                    	}
+                    }
+		}
+                break;
+
+	case MAIN_AUTO_DONE:
+	case MAIN_AUTO_ABORT:
+		if (Buttons_Scan() == 0)
+                    Main_Screen = MAIN_MODE_FREE;
+                break;
+
+	default:
+		break;
+    }
+
+    return false;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/automation.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,22 @@
+/**
+ * @file automation.h
+ * @brief Brew automation.
+ */
+#ifndef	_AUTOMATION_H
+#define	_AUTOMATION_H
+
+
+/**
+ * @brief Automation init fases
+ * @return true if should jump to startover.
+ */
+bool Automation_Init(void);
+
+/**
+ * @brief Automation loop screens. Non-blocking (mostly).
+ * #return true if should jump to startover.
+ */
+bool Automation_Loop(void);
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/brewboard.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,180 @@
+/* BrewVoard
+
+*/
+
+#include "config.h"
+
+
+static const char *TAG = "brewboard";
+
+static TaskHandle_t		xTaskTFT     = NULL;
+static TaskHandle_t		xTaskDS18B20 = NULL;
+static TaskHandle_t		xTaskDriver  = NULL;
+static TaskHandle_t		xTaskSound   = NULL;
+static TaskHandle_t		xTaskSDcard  = NULL;
+static TaskHandle_t		xTaskWifi    = NULL;
+
+extern SemaphoreHandle_t	xSemaphoreDS18B20;
+extern SemaphoreHandle_t	xSemaphoreDriver;
+extern SemaphoreHandle_t	xSemaphoreWiFi;
+extern WIFI_State		*wifi_state;
+
+
+int 				Main_Screen = MAIN_MODE_UNKNOWN;
+int				Old_Screen = MAIN_MODE_UNKNOWN;
+bool				System_TimeOk = false;
+
+
+
+void app_main()
+{
+    int		tempy;
+    char	temp[64];
+
+    ESP_LOGI(TAG, "Starting");
+    init_tft_display();
+
+    TFT_setFont(DEJAVU24_FONT, NULL);
+    _fg = TFT_YELLOW;
+    tempy = TFT_getfontheight() + 4;
+    sprintf(temp, "BrewBoard %s", VERSION);
+    TFT_print(temp, CENTER, 4);
+
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    _fg = TFT_CYAN;
+    TFT_print("Mount /spiffs ", 0, LASTY+tempy);
+    ESP_LOGI(TAG, "Initializing SPIFFS");
+        
+    esp_vfs_spiffs_conf_t conf = {
+	.base_path = "/spiffs",
+	.partition_label = NULL,
+	.max_files = 5,
+	.format_if_mount_failed = true
+    };
+	    
+    // Use settings defined above to initialize and mount SPIFFS filesystem.
+    // Note: esp_vfs_spiffs_register is an all-in-one convenience function.
+    esp_err_t ret = esp_vfs_spiffs_register(&conf);
+
+    if (ret != ESP_OK) {
+	if (ret == ESP_FAIL) {
+	    ESP_LOGE(TAG, "Failed to mount or format filesystem");
+	} else if (ret == ESP_ERR_NOT_FOUND) {
+	    ESP_LOGE(TAG, "Failed to find SPIFFS partition");
+	} else {
+	    ESP_LOGE(TAG, "Failed to initialize SPIFFS (%d)", ret);
+	}
+	_fg = TFT_RED;
+	TFT_print("error\r\n", LASTX, LASTY);
+	return; // Stop application.
+    }
+
+    size_t total = 0, used = 0;
+    ret = esp_spiffs_info(NULL, &total, &used);
+    if (ret != ESP_OK) {
+	ESP_LOGE(TAG, "Failed to get SPIFFS partition information");
+	_fg = TFT_RED;
+	TFT_print("error\r\n", LASTX, LASTY);
+	return; // Stop application.
+    } else {
+	ESP_LOGI(TAG, "Partition size: %d, used: %d - %d%%", total, used, (used * 100) / total);
+    }
+    TFT_print("Ok\r\n", LASTX, LASTY);
+
+    // Just to debug, list the /spiffs filesystem.
+#if 1
+    DIR *dir = opendir("/spiffs");
+    struct dirent* de = readdir(dir);
+    while (de) {
+	if (de->d_type == DT_REG) {
+	    printf("F ");
+	}
+	if (de->d_type == DT_DIR) {
+	    printf("D ");
+	}
+	printf("%s\n", de->d_name);
+	de = readdir(dir);
+    }
+    closedir(dir);
+#endif
+
+    /*
+     * Read or create configuration
+     */
+    TFT_print("Ophalen configuratie ", LASTX, LASTY);
+    read_config();
+    read_equipment(config.EquipmentRec);
+    read_runtime();
+//    unlink("/spiffs/etc/recipe.conf");
+    read_recipe(config.RecipeRec);
+    TFT_print("Ok\r\n", LASTX, LASTY);
+
+    // Set the Touchscreen calibration/
+    TS_set_calibration(config.ts_xleft, config.ts_xright, config.ts_ytop, config.ts_ybottom);
+
+    /*
+     * TZ names don't work, so set the TZ the hard way.
+     * This is the setting for Europe/Amsterdam.
+     */
+    setenv("TZ", "CET-01CEST-02,M3.4.0,M10.4.0", 1);
+    tzset();
+
+    xSemaphoreDS18B20 = xSemaphoreCreateMutex();
+    xSemaphoreDriver  = xSemaphoreCreateMutex();
+
+    TFT_print("Starten taken ", LASTX, LASTY);
+    xTaskCreate(&task_tft,     "task_tft",      6144, NULL, 4, &xTaskTFT);
+    vTaskDelay(400 / portTICK_PERIOD_MS);
+    xTaskCreate(&task_ds18b20, "task_ds18b20",  2560, NULL, 8, &xTaskDS18B20);
+    xTaskCreate(&task_driver,  "task_driver",   2560, NULL, 8, &xTaskDriver);
+    xTaskCreate(&task_sound,   "task_sound",    3072, NULL,15, &xTaskSound);
+    xTaskCreate(&task_sdcard,  "task_sdcard",   8192, NULL,10, &xTaskSDcard);
+    /* disable the default wifi logging */
+    esp_log_level_set("wifi", ESP_LOG_NONE);
+    xTaskCreate(&task_wifi,    "task_wifi",     4096, NULL, 3, &xTaskWifi);
+    // Task for MQTT
+    TFT_print(" Ok\r\nConnecting ", LASTX, LASTY);
+
+    int wait = 20;
+    while (wait) {
+	vTaskDelay(500 / portTICK_PERIOD_MS);
+	TFT_print(".", LASTX, LASTY);
+    	if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+	    if (wifi_state->STA_connected == true)
+		wait = 0;
+	    else
+		wait--;
+	    xSemaphoreGive(xSemaphoreWiFi);
+    	}
+    }
+
+    Main_Screen = MAIN_MODE_FREE;
+    TFT_print(" Ok\r\n", LASTX, LASTY);
+    SoundPlay(SOUND_StartUp);
+
+    /* Do not write to the TFT during VNC startup to avoid race conditions */
+    VncStartup();
+    start_http_websocket();
+    vTaskDelay(1000 / portTICK_PERIOD_MS);
+
+    /*
+     * A small useless delay
+     */
+    vTaskDelay(1000 / portTICK_PERIOD_MS);
+
+//    static char cBuffer[ 1024 ];
+
+    /*
+     * Main application loop.
+     */
+    while (1) {
+	vTaskDelay(20000 / portTICK_PERIOD_MS);
+
+//	vTaskList( cBuffer );
+//	printf("Name            State   Prio    Stack   Num\n");
+//	printf("--------------- ------- ------- ------- -------\n");
+//	printf("%s\n", cBuffer);
+    }
+    // Not reached.
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/buttons.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,999 @@
+/**
+ * @file buttons.c
+ * @brief The touch buttons.
+ */
+
+#include "config.h"
+
+
+sButton				Buttons[MAXBUTTONS];	// 40 buttons on a screen.
+int				level = 1;		// Keyboard level.
+
+extern uint8_t			VNC_pointer_button;
+extern uint16_t			VNC_pointer_x;
+extern uint16_t			VNC_pointer_y;
+
+//static const char		*TAG = "buttons";
+
+#define	EDIT_TYPE_TEXT		0
+#define	EDIT_TYPE_INT		1
+#define	EDIT_TYPE_FLOAT		2
+
+
+/***************************************************************************/
+
+
+void WaitTouchReleased(void)
+{
+    int		tx, ty;
+
+    // Do raw reads.
+    while (TFT_read_touch(&tx, &ty, 1) == 1) {
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+}
+
+
+
+void Buttons_Clear(void)
+{
+    int		i;
+
+    for (i = 0; i < MAXBUTTONS; i++) {
+	if ((Buttons[i].x != -1) && strlen(Buttons[i].text)) {
+	    TFT_fillRect(Buttons[i].x, Buttons[i].y, Buttons[i].w, Buttons[i].h, TFT_BLACK);
+	}
+	Buttons[i].x = -1;
+    }
+}
+
+
+
+void Buttons_Add(int x, int y, int w, int h, char *txt, int order)
+{
+    Buttons[order].x = x;
+    Buttons[order].y = y;
+    Buttons[order].w = w;
+    Buttons[order].h = h;
+    strncpy(Buttons[order].text, txt, 11);
+    Buttons[order].dark = Buttons[order].small = false;
+}
+
+
+
+void Buttons_Show(void)
+{
+    int		i;
+    const color_t KEY_NORM        = { 191, 191, 191};
+    const color_t KEY_DARK        = {  95,  95,  95};
+    const color_t KEY_LOCK        = { 127, 127, 255};
+
+    _fg = TFT_BLACK;
+
+    for (i = 0; i < MAXBUTTONS; i++) {
+	if (Buttons[i].x != -1) {
+
+	    if (Buttons[i].lock) {
+		_fg = TFT_BLACK;
+		TFT_fillRoundRect(Buttons[i].x, Buttons[i].y, Buttons[i].w, Buttons[i].h, 5, KEY_LOCK);
+	    } else if (Buttons[i].dark) {
+		_fg = TFT_WHITE;
+		TFT_fillRoundRect(Buttons[i].x, Buttons[i].y, Buttons[i].w, Buttons[i].h, 5, KEY_DARK);
+	    } else {
+		_fg = TFT_BLACK;
+	    	TFT_fillRoundRect(Buttons[i].x, Buttons[i].y, Buttons[i].w, Buttons[i].h, 5, KEY_NORM);
+	    }
+	    TFT_drawRoundRect(Buttons[i].x, Buttons[i].y, Buttons[i].w, Buttons[i].h, 5, TFT_LIGHTGREY);
+	    if (Buttons[i].small)
+		TFT_setFont(DEFAULT_FONT, NULL);	// DEF_SMALL_FONT
+	    else
+		TFT_setFont(DEJAVU18_FONT, NULL);
+	    font_transparent = 1;
+	    TFT_print(Buttons[i].text, 
+		      Buttons[i].x + ((Buttons[i].w - TFT_getStringWidth(Buttons[i].text)) / 2), 
+		      Buttons[i].y + 1 + ((Buttons[i].h - TFT_getfontheight()) / 2));
+	}
+    }
+
+    font_transparent = 0;
+}
+
+
+
+int Buttons_Search(int x, int y)
+{
+    int		i;
+
+    for (i = 0; i < MAXBUTTONS; i++) {
+	if (Buttons[i].x != -1) {
+	    if ((x >= Buttons[i].x) && (x <= (Buttons[i].x + Buttons[i].w)) &&
+		(y >= Buttons[i].y) && (y <= (Buttons[i].y + Buttons[i].h))) {
+		return i;
+	    }
+	}
+    }
+
+    return -1;
+}
+
+
+
+int Buttons_Scan(void)
+{
+    int		tx, ty, rc;
+    static int	ox = -1, oy = -1, vx = -1, vy = -1;
+    static bool t_pressed = false, v_pressed = false;
+
+    if (TFT_read_touch(&tx, &ty, 0)) {
+	t_pressed = true;
+	ox = tx;
+	oy = ty;
+	return -1;
+    } else if (t_pressed) {
+	rc = Buttons_Search(ox, oy);
+	t_pressed = false;
+	ox = oy = -1;
+	return rc;
+    }
+
+    if (VNC_pointer_button & 0x07) {
+	v_pressed = true;
+	vx = VNC_pointer_x;
+	vy = VNC_pointer_y;
+	return -1;
+    } else if (v_pressed) {
+	rc = Buttons_Search(vx, vy);
+	v_pressed = false;
+	vx = vy = -1;
+	return rc;
+    }
+
+    return -1;
+}
+
+
+
+static const uint8_t alphalow_tab[] = { 'q','w','e','r','t','y','u','i','o','p',
+					'a','s','d','f','g','h','j','k','l',
+					'z','x','c','v','b','n','m' };
+
+/* Level 1 keys */
+void B_AlphaLow(void)
+{
+    Buttons_Clear();
+
+    Buttons_Add(  2, 80,28,36,"q", 0);
+    Buttons_Add( 34, 80,28,36,"w", 1);
+    Buttons_Add( 66, 80,28,36,"e", 2);
+    Buttons_Add( 98, 80,28,36,"r", 3);
+    Buttons_Add(130, 80,28,36,"t", 4);
+    Buttons_Add(162, 80,28,36,"y", 5);
+    Buttons_Add(194, 80,28,36,"u", 6);
+    Buttons_Add(226, 80,28,36,"i", 7);
+    Buttons_Add(258, 80,28,36,"o", 8);
+    Buttons_Add(290, 80,28,36,"p", 9);
+
+    Buttons_Add( 18,120,28,36,"a",10);
+    Buttons_Add( 50,120,28,36,"s",11);
+    Buttons_Add( 82,120,28,36,"d",12);
+    Buttons_Add(114,120,28,36,"f",13);
+    Buttons_Add(146,120,28,36,"g",14);
+    Buttons_Add(178,120,28,36,"h",15);
+    Buttons_Add(210,120,28,36,"j",16);
+    Buttons_Add(242,120,28,36,"k",17);
+    Buttons_Add(274,120,28,36,"l",18);
+
+    Buttons_Add( 50,160,28,36,"z",19);
+    Buttons_Add( 82,160,28,36,"x",20);
+    Buttons_Add(114,160,28,36,"c",21);
+    Buttons_Add(146,160,28,36,"v",22);
+    Buttons_Add(178,160,28,36,"b",23);
+    Buttons_Add(210,160,28,36,"n",24);
+    Buttons_Add(242,160,28,36,"m",25);
+
+    Buttons_Add(  2,160,42,36,"caps",26);
+    Buttons[26].dark = true;
+    Buttons[26].small = true;
+    Buttons[26].lock = false;
+    Buttons_Add(276,160,42,36,"del",27);
+    Buttons[27].dark = true;
+    Buttons[27].small = true;
+
+    Buttons_Add(  2,200,60,36,"123",28);
+    Buttons[28].dark = true;
+    Buttons_Add( 72,200,28,36,"/",29);
+    Buttons_Add(108,200,104,36," ",30);
+    Buttons_Add(220,200,28,36,".",31);
+    Buttons_Add(258,200,60,36,"Ok",32);
+    Buttons[32].dark = true;
+
+    Buttons_Show();
+}
+
+
+
+static const uint8_t alphacaps_tab[] = { 'Q','W','E','R','T','Y','U','I','O','P',
+					 'A','S','D','F','G','H','J','K','L',
+					 'Z','X','C','V','B','N','M' };
+
+/* Level 2 and 3 keys */
+void B_AlphaCaps(int level)
+{
+    Buttons_Clear();
+
+    Buttons_Add(  2, 80,28,36,"Q", 0);
+    Buttons_Add( 34, 80,28,36,"W", 1);
+    Buttons_Add( 66, 80,28,36,"E", 2);
+    Buttons_Add( 98, 80,28,36,"R", 3);
+    Buttons_Add(130, 80,28,36,"T", 4);
+    Buttons_Add(162, 80,28,36,"Y", 5);
+    Buttons_Add(194, 80,28,36,"U", 6);
+    Buttons_Add(226, 80,28,36,"I", 7);
+    Buttons_Add(258, 80,28,36,"O", 8);
+    Buttons_Add(290, 80,28,36,"P", 9);
+
+    Buttons_Add( 18,120,28,36,"A",10);
+    Buttons_Add( 50,120,28,36,"S",11);
+    Buttons_Add( 82,120,28,36,"D",12);
+    Buttons_Add(114,120,28,36,"F",13);
+    Buttons_Add(146,120,28,36,"G",14);
+    Buttons_Add(178,120,28,36,"H",15);
+    Buttons_Add(210,120,28,36,"J",16);
+    Buttons_Add(242,120,28,36,"K",17);
+    Buttons_Add(274,120,28,36,"L",18);
+
+    Buttons_Add( 50,160,28,36,"Z",19);
+    Buttons_Add( 82,160,28,36,"X",20);
+    Buttons_Add(114,160,28,36,"C",21);
+    Buttons_Add(146,160,28,36,"V",22);
+    Buttons_Add(178,160,28,36,"B",23);
+    Buttons_Add(210,160,28,36,"N",24);
+    Buttons_Add(242,160,28,36,"M",25);
+
+    Buttons_Add(  2,160,42,36,"caps",26);
+    Buttons[26].dark = true;
+    Buttons[26].small = true;
+    if (level == 3)
+	Buttons[26].lock = true;
+    else
+	Buttons[26].lock = false;
+    Buttons_Add(276,160,42,36,"del",27);
+    Buttons[27].dark = true;
+    Buttons[27].small = true;
+
+    Buttons_Add(  2,200,60,36,"123",28);
+    Buttons[28].dark = true;
+    Buttons_Add( 72,200,28,36,"/",29);
+    Buttons_Add(108,204,100,36," ",30);
+    Buttons_Add(220,200,28,36,".",31);
+    Buttons_Add(258,200,60,36,"Ok",32);
+    Buttons[32].dark = true;
+
+    Buttons_Show();
+}
+
+
+
+static const uint8_t nums1_tab[] = { '1','2','3','4','5','6','7','8','9','0', 
+				     '!','@','#','$','/','^','&','*','(',')', 
+				     '-','\'','\"',':',';',',','?' };
+
+/* Level 4 keys */
+void B_Nums1(void)
+{
+    Buttons_Clear();
+
+    Buttons_Add(  2, 80,28,36,"1", 0);
+    Buttons_Add( 34, 80,28,36,"2", 1);
+    Buttons_Add( 66, 80,28,36,"3", 2);
+    Buttons_Add( 98, 80,28,36,"4", 3);
+    Buttons_Add(130, 80,28,36,"5", 4);
+    Buttons_Add(162, 80,28,36,"6", 5);
+    Buttons_Add(194, 80,28,36,"7", 6);
+    Buttons_Add(226, 80,28,36,"8", 7);
+    Buttons_Add(258, 80,28,36,"9", 8);
+    Buttons_Add(290, 80,28,36,"0", 9);
+
+    Buttons_Add(  2,120,28,36,"!",10);
+    Buttons_Add( 34,120,28,36,"@",11);
+    Buttons_Add( 66,120,28,36,"#",12);
+    Buttons_Add( 98,120,28,36,"$",13);
+    Buttons_Add(130,120,28,36,"/",14);
+    Buttons_Add(162,120,28,36,"^",15);
+    Buttons_Add(194,120,28,36,"&",16);
+    Buttons_Add(226,120,28,36,"*",17);
+    Buttons_Add(258,120,28,36,"(",18);
+    Buttons_Add(290,120,28,36,")",19);
+
+    Buttons_Add( 50,160,28,36,"-",20);
+    Buttons_Add( 82,160,28,36,"'",21);
+    Buttons_Add(114,160,28,36,"\"",22);
+    Buttons_Add(146,160,28,36,":",23);
+    Buttons_Add(178,160,28,36,";",24);
+    Buttons_Add(210,160,28,36,",",25);
+    Buttons_Add(242,160,28,36,"?",26);
+
+    Buttons_Add(  2,160,42,36,"1/2",27);
+    Buttons[27].dark = true;
+    Buttons_Add(276,160,42,36,"del",28);
+    Buttons[28].dark = true;
+    Buttons[28].small = true;
+
+    Buttons_Add(  2,200,60,36,"ABC",29);
+    Buttons[29].dark = true;
+    Buttons_Add( 72,200,28,36,"/",30);
+    Buttons_Add(108,204,100,36," ",31);
+    Buttons_Add(220,200,28,36,".",32);
+    Buttons_Add(258,200,60,36,"Ok",33);
+    Buttons[33].dark = true;
+
+    Buttons_Show();
+}
+
+
+static const uint8_t nums2_tab[] = { '+','*','/','=','<','>','{','}','[',']',
+	                             ' ',' ',' ',' ','%','~','`',' ',' ',' ',
+				     '_','\\','|',' ',' ',' ',' ' };
+
+/* Level 5 */
+void B_Nums2(void)
+{
+    Buttons_Clear();
+
+    Buttons_Add(  2, 80,28,36,"+", 0);
+    Buttons_Add( 34, 80,28,36,"*", 1);
+    Buttons_Add( 66, 80,28,36,"/", 2);
+    Buttons_Add( 98, 80,28,36,"=", 3);
+    Buttons_Add(130, 80,28,36,"<", 4);
+    Buttons_Add(162, 80,28,36,">", 5);
+    Buttons_Add(194, 80,28,36,"{", 6);
+    Buttons_Add(226, 80,28,36,"}", 7);
+    Buttons_Add(258, 80,28,36,"[", 8);
+    Buttons_Add(290, 80,28,36,"]", 9);
+
+    Buttons_Add(  2,120,28,36," ",10);
+    Buttons_Add( 34,120,28,36," ",11);
+    Buttons_Add( 66,120,28,36," ",12);
+    Buttons_Add( 98,120,28,36," ",13);
+    Buttons_Add(130,120,28,36,"%",14);
+    Buttons_Add(162,120,28,36,"~",15);
+    Buttons_Add(194,120,28,36,"`",16);
+    Buttons_Add(226,120,28,36," ",17);
+    Buttons_Add(258,120,28,36," ",18);
+    Buttons_Add(290,120,28,36," ",19);
+
+    Buttons_Add( 50,160,28,36,"_",20);
+    Buttons_Add( 82,160,28,36,"\\",21);
+    Buttons_Add(114,160,28,36,"|",22);
+    Buttons_Add(146,160,28,36," ",23);
+    Buttons_Add(178,160,28,36," ",24);
+    Buttons_Add(210,160,28,36," ",25);
+    Buttons_Add(242,160,28,36," ",26);
+
+    Buttons_Add(  2,160,42,36,"2/2",27);
+    Buttons[27].dark = true;
+    Buttons_Add(276,160,42,36,"del",28);
+    Buttons[28].dark = true;
+    Buttons[28].small = true;
+
+    Buttons_Add(  2,200,60,36,"ABC",29);
+    Buttons[29].dark = true;
+    Buttons_Add( 72,200,28,36,"/",30);
+    Buttons_Add(108,204,100,36," ",31);
+    Buttons_Add(220,200,28,36,".",32);
+    Buttons_Add(258,200,60,36,"Ok",33);
+    Buttons[33].dark = true;
+
+    Buttons_Show();
+}
+
+
+static const uint8_t digits_tab[] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', '.' };
+
+void B_Digits(void)
+{
+    Buttons_Clear();
+
+    Buttons_Add( 61,200,147,36,"0", 9);
+
+    Buttons_Add( 61,160,47,36,"1", 0);
+    Buttons_Add(111,160,47,36,"2", 1);
+    Buttons_Add(161,160,47,36,"3", 2);
+    Buttons_Add(211,160,47,36,"+",10);
+    Buttons[10].dark = true;
+
+    Buttons_Add( 61,120,47,36,"4", 3);
+    Buttons_Add(111,120,47,36,"5", 4);
+    Buttons_Add(161,120,47,36,"6", 5);
+    Buttons_Add(211,120,47,36,"-",11);
+    Buttons[11].dark = true;
+
+    Buttons_Add( 61, 80,47,36,"7", 6);
+    Buttons_Add(111, 80,47,36,"8", 7);
+    Buttons_Add(161, 80,47,36,"9", 8);
+    Buttons_Add(211, 80,47,36,".",12);
+    Buttons[12].dark = true;
+
+    Buttons_Add(211,200,47,36,"del",13);
+    Buttons[13].dark = true;
+    Buttons[13].small = true;
+
+    Buttons_Add(271,200,47,36,"Ok",14);
+    Buttons[14].dark = true;
+
+    Buttons_Show();
+}
+
+
+
+int KeyBoardAll(void)
+{
+    int		key;
+
+    key = Buttons_Scan();
+    if (key == -1)
+	return -1;
+
+    switch (level) {
+	    case 1:	if (key >= 0 && key <= 25) {
+			    key = alphalow_tab[key];
+	    		} else if (key == 26) {
+			    level = 2;
+			    B_AlphaCaps(level);
+			} else if (key == 27) {
+			    key = 127;
+			} else if (key == 28) {
+			    level = 4;
+			    B_Nums1();
+			} else if (key == 29) {
+			    key = '/';
+			} else if (key == 30) {
+			    key = ' ';
+			} else if (key == 31) {
+			    key = '.';
+			} else if (key == 32) {
+			    key = 0;
+			}
+			break;
+	    case 2:
+	    case 3:	if (key >= 0 && key <= 25) {
+			    key = alphacaps_tab[key];
+			    if (level == 2) {
+				level = 1;
+				B_AlphaLow();
+			    }
+			} else if (key == 26 && level == 3) {
+			    level = 1;
+			    B_AlphaLow();
+			} else if (key == 26 && level == 2) {
+			    level = 3;
+			    B_AlphaCaps(level);
+			} else if (key == 27) {
+			    key = 127;
+			} else if (key == 28) {
+			    level = 4;
+			    B_Nums1();
+			} else if (key == 29) {
+			    key = '/';
+			} else if (key == 30) {
+			    key = ' ';
+			} else if (key == 31) {
+			    key = '.';
+			} else if (key == 32) {
+			    key = 0;
+			}
+			break;
+	    case 4:	if (key >= 0 && key <= 26) {
+			    key = nums1_tab[key];
+			} else if (key == 27) {
+			    level = 5;
+			    B_Nums2();
+			} else if (key == 28) {
+			    key = 127;
+			} else if (key == 29) {
+			    level = 1;
+			    B_AlphaLow();
+			} else if (key == 30) {
+			    key = '/';
+			} else if (key == 31) {
+			    key = ' ';
+			} else if (key == 32) {
+			    key = '.';
+			} else if (key == 33) {
+			    key = 0;
+			}
+			break;
+	    case 5:	if (key >= 0 && key <= 26) {
+			    key = nums2_tab[key];
+			} else if (key == 27) {
+			    level = 4;
+			    B_Nums1();
+			} else if (key == 28) {
+			    key = 127;
+			} else if (key == 29) {
+			    level = 1;
+			    B_AlphaLow();
+			} else if (key == 30) {
+			    key = '/';
+			} else if (key == 31) {
+			    key = ' ';
+			} else if (key == 32) {
+			    key = '.';
+			} else if (key == 33) {
+			    key = 0;
+			}
+			break;
+    }
+
+    return key;
+}
+
+
+int KeyBoardDigits(void)
+{
+    int         key;
+
+    key = Buttons_Scan();
+    if (key == -1)
+	return -1;
+
+    if (key >= 0 && key <= 12) {
+	key = digits_tab[key];
+    } else if (key == 13) {
+	key = 127;
+    } else if  (key == 14) {
+	key = 0;
+    }
+
+    return key;
+}
+
+
+
+/**************************************************************************/
+/*
+ *  Data show and edit functions.
+ */
+
+void ShowText(uint16_t x, uint16_t y, char *label, char *txt)
+{
+    _fg = TFT_LIGHTGREY;
+    TFT_print(label, x, y);
+    _fg = TFT_YELLOW;
+    TFT_print(" ", LASTX, LASTY);
+    TFT_print(txt, LASTX, LASTY);
+}
+
+
+
+void ShowInteger(uint16_t x, uint16_t y, char *label, char *suffix, int val)
+{
+    char        tmp[32];
+
+    _fg = TFT_LIGHTGREY;
+    TFT_print(label, x, y);
+    _fg = TFT_YELLOW;
+   sprintf(tmp, " %d", val);
+   TFT_print(tmp, LASTX, LASTY);
+   if (suffix) {
+        _fg = TFT_LIGHTGREY;
+        TFT_print(suffix, LASTX, LASTY);
+   }
+}
+
+
+
+void ShowBool(uint16_t x, uint16_t y, char *label, bool val)
+{
+    _fg = TFT_LIGHTGREY;
+    TFT_print(label, x, y);
+    _fg = TFT_YELLOW;
+    if (val)
+        TFT_print(" J", LASTX, LASTY);
+    else
+        TFT_print(" N", LASTX, LASTY);
+}
+
+
+
+void ShowSSR2(uint16_t x, uint16_t y, int val)
+{
+    _fg = TFT_LIGHTGREY;
+    TFT_print("SSR2 ", x, y);
+   _fg = TFT_YELLOW;
+   TFT_clearStringRect(TFT_X, TFT_Y, "HLT en MLT");
+
+   switch (val) {
+	case SSR2_OFF:		TFT_print("Uit", LASTX, LASTY);
+				break;
+	case SSR2_HLT_SHARE:	TFT_print("HLT of MLT", LASTX, LASTY);
+				break;
+
+	case SSR2_HLT_IND:	TFT_print("HLT en MLT", LASTX, LASTY);
+				break;
+
+	case SSR2_ON_IDLE:	TFT_print("Idle", LASTX, LASTY);
+				break;
+
+	default:		TFT_print("N/A", LASTX, LASTY);
+   }
+}
+
+
+
+void ShowFloat(uint16_t x, uint16_t y, char *label, char *suffix, float val, int decimals)
+{
+    char        tmp[32];
+
+    _fg = TFT_LIGHTGREY;
+    TFT_print(label, x, y);
+    _fg = TFT_YELLOW;
+    sprintf(tmp, " %.*f", decimals, val);
+    TFT_print(tmp, LASTX, LASTY);
+    if (suffix) {
+        _fg = TFT_LIGHTGREY;
+        TFT_print(suffix, LASTX, LASTY);
+    }
+}
+
+
+
+void ShowDouble(uint16_t x, uint16_t y, char *label, char *suffix, double val, int decimals)
+{
+    char        tmp[32];
+
+    _fg = TFT_LIGHTGREY;
+    TFT_print(label, x, y);
+    _fg = TFT_YELLOW;
+    sprintf(tmp, " %.*f", decimals, val);
+    TFT_print(tmp, LASTX, LASTY);
+    if (suffix) {
+	_fg = TFT_LIGHTGREY;
+	TFT_print(suffix, LASTX, LASTY);
+    }
+}
+
+
+
+void Editer(char *label, char *txt, char *errmsg, int len, int type)
+{
+    int		key;
+
+    _bg = TFT_BLACK;
+    TFT_fillScreen(_bg);
+    TFT_resetclipwin();
+    TopMessage("Wijzigen");
+    _fg = TFT_LIGHTGREY;
+    TFT_setFont(DEFAULT_FONT, NULL);
+    TFT_print(label, 2, 28);
+    if (strlen(errmsg)) {
+	_fg = TFT_RED;
+	TFT_print(errmsg, 2, 60);
+    }
+    TFT_fillRect(2, 40, 10 * len, 14, TFT_BLUE);
+    _fg = TFT_YELLOW;
+    _bg = TFT_BLUE;
+    TFT_print(txt, 2, 41);
+    TFT_fillRect(TFT_X, 50, 10, 4, TFT_GREEN);
+
+    switch(type) {
+	case EDIT_TYPE_INT:	
+			B_Digits();
+			break;
+
+	default:	level = 1;
+			B_AlphaLow();
+			break;
+    }
+
+    while (1) {
+	switch (type) {
+	   case  EDIT_TYPE_INT:
+		   	key = KeyBoardDigits();
+			break;
+
+	    default:	key = KeyBoardAll();
+			break;
+	}
+
+	if (key != -1) {
+
+	    if (key >= 32 && key <= 126 && strlen(txt) < len) {
+	    	// Append key
+		txt[strlen(txt) + 1] = '\0';
+	    	txt[strlen(txt)] = key;
+	    } else if (key == 127 && strlen(txt)) {
+	    	// Delete key
+	    	txt[strlen(txt) - 1] = '\0';
+	    }
+
+	    _fg = TFT_YELLOW;
+	    _bg = TFT_BLACK;
+	    TFT_setFont(DEFAULT_FONT, NULL);
+	    TFT_fillRect(2, 40, 10 * len, 14, TFT_BLUE);
+	    TFT_print(txt, 2, 41);
+	    TFT_fillRect(TFT_X, 50, 10, 4, TFT_GREEN);
+	}
+
+	if (key == 0)
+	    break;
+
+	vTaskDelay(20 / portTICK_PERIOD_MS);
+    }
+}
+
+
+
+void EditText(char *label, char *txt, int len)
+{
+    char		errmsg[40];
+
+    errmsg[0] = '\0';
+    while (1) {
+    	Editer(label, txt, errmsg, len, EDIT_TYPE_TEXT);
+	if (strlen(txt))
+	    break;
+	sprintf(errmsg, "Tekst veld mag niet leeg zijn");
+    }
+}
+
+
+
+void EditTextMin(char *label, char *txt, int len, int min)
+{
+    char                errmsg[40];
+
+    errmsg[0] = '\0';
+    while (1) {
+	Editer(label, txt, errmsg, len, EDIT_TYPE_TEXT);
+	if (strlen(txt) >= min)
+	    break;
+	sprintf(errmsg, "Tekst veld moet tussen %d en %d lang zijn", min, len);
+    }
+}
+
+
+
+void EditInt(char *label, int *val, int min, int max)
+{
+    char	*valstr, errmsg[40];
+    int		newval;
+
+    errmsg[0] = '\0';
+    valstr = malloc(20);
+    sprintf(valstr, "%d", *val);
+
+    while (1) {
+	Editer(label, valstr, errmsg, 8, EDIT_TYPE_INT);
+	newval = atoi(valstr);
+	if (newval < min || newval > max) {
+	    sprintf(errmsg, "De waarde moet tussen %d en %d zijn.", min, max);
+	} else {
+	    break;
+	}
+    }
+
+    *val = newval;
+    free(valstr);
+}
+
+
+
+void EditUint8(char *label, uint8_t *val, uint8_t min, uint8_t max)
+{
+    char        *valstr, errmsg[40];
+    uint8_t	newval;
+
+    errmsg[0] = '\0';
+    valstr = malloc(20);
+    sprintf(valstr, "%d", *val);
+
+    while (1) {
+	Editer(label, valstr, errmsg, 5, EDIT_TYPE_INT);
+	newval = atoi(valstr);
+	if (newval < min || newval > max) {
+	    sprintf(errmsg, "De waarde moet tussen %d en %d zijn.", min, max);
+	} else {
+	    break;
+	}
+    }
+
+    *val = newval;
+    free(valstr);
+}
+
+
+
+void EditUint16(char *label, uint16_t *val, uint16_t min, uint16_t max)
+{   
+    char        *valstr, errmsg[40];
+    uint16_t     newval;
+
+    errmsg[0] = '\0';
+    valstr = malloc(20);
+    sprintf(valstr, "%d", *val);
+
+    while (1) {
+	Editer(label, valstr, errmsg, 5, EDIT_TYPE_INT);
+	newval = atoi(valstr);
+	if (newval < min || newval > max) {
+	    sprintf(errmsg, "De waarde moet tussen %d en %d zijn.", min, max);
+	} else {
+	    break;
+	}
+    }
+
+    *val = newval;
+    free(valstr);
+}
+
+
+
+void EditFloat(char *label, float *val, float min, float max, int decimals)
+{
+    char        *valstr, errmsg[40];
+    float	newval;
+
+    errmsg[0] = '\0';
+    valstr = malloc(30);
+    sprintf(valstr, " %.*f", decimals, *val);
+
+    while (1) {
+	Editer(label, valstr, errmsg, 25, EDIT_TYPE_INT);
+	newval = atof(valstr);
+	if (newval < min || newval > max) {
+	    sprintf(errmsg, "De waarde moet tussen %.2f en %.2f zijn.", min, max);
+	} else {
+	    break;
+	}
+    }
+
+    *val = newval;
+    free(valstr);
+}
+
+
+
+void EditDouble(char *label, double *val, double min, double max, int decimals)
+{
+    char        *valstr, errmsg[40];
+    double	newval;
+
+    errmsg[0] = '\0';
+    valstr = malloc(30);
+    sprintf(valstr, " %.*f", decimals, *val);
+
+    while (1) {
+	Editer(label, valstr, errmsg, 25, EDIT_TYPE_INT);
+	newval = atof(valstr);
+	if (newval < min || newval > max) {
+	    sprintf(errmsg, "De waarde moet tussen %.2f en %.2f zijn.", min, max);
+	} else {
+	    break;
+	}
+    }
+
+    *val = newval;
+    free(valstr);
+}
+
+
+
+void EditBool(char *label, bool *val)
+{
+    bool	loop = true, value = *val;
+    int		curpos;
+
+    _bg = TFT_BLACK;
+    TFT_fillScreen(_bg);
+    TFT_resetclipwin();
+    TopMessage("Wijzigen");
+    _fg = TFT_LIGHTGREY;
+    TFT_setFont(DEFAULT_FONT, NULL);
+    TFT_print(label, 2, 28);
+    curpos = TFT_X;
+    _fg = TFT_YELLOW;
+    (value) ? TFT_print(" J ", curpos, 28) : TFT_print(" N ", curpos, 28);
+
+    Buttons_Clear();
+    Buttons_Add( 40, 100, 80, 40, "J/N", 0);
+    Buttons_Add(200, 100, 80, 40, "Ok",  1);
+    Buttons_Show();
+
+    while (loop) {
+	switch (Buttons_Scan()) {
+	    case 0:	TFT_setFont(DEFAULT_FONT, NULL);
+			_fg = TFT_YELLOW;
+		    	if (value) {
+			    value = false;
+			    TFT_print(" N ", curpos, 28);
+			} else {
+			    value = true;
+			    TFT_print(" J ", curpos, 28);
+			}
+			break;
+
+	    case 1:	loop = false;
+			break;
+
+	    default:	break;
+	}
+	vTaskDelay(20 / portTICK_PERIOD_MS);
+    }
+    *val = value;
+}
+
+
+
+void EditSSR2(int *val)
+{
+    bool        loop = true;
+    int		value = *val;
+    int		key;
+
+    _bg = TFT_BLACK;
+    TFT_fillScreen(_bg);
+    TFT_resetclipwin();
+    TopMessage("Wijzigen");
+    TFT_setFont(DEFAULT_FONT, NULL);
+    ShowSSR2(2, 28, value);
+
+    Buttons_Clear();
+    Buttons_Add( 20,  60,120, 40, "Uit", 0);
+    Buttons_Add(180,  60,120, 40, "HLT of MLT",  1);
+    Buttons_Add( 20, 130,120, 40, "HLT en MLT",  2);
+    Buttons_Add(180, 130,120, 40, "Idle",  3);
+    Buttons_Add(120, 200, 80, 40, "Ok",  4);
+    Buttons_Show();
+
+    while (loop) {
+	key = Buttons_Scan();
+
+	if (key >= 0 && key <= 3) {
+	    value = key;
+	    TFT_setFont(DEFAULT_FONT, NULL);
+	    ShowSSR2(2, 28, value);
+	} else if (key == 4) {
+	    loop = false;
+	}
+        vTaskDelay(20 / portTICK_PERIOD_MS);
+    }
+    *val = value;
+}
+
+
+
+int Confirm(char *top, char *ack, char *nak)
+{
+    int		rc = false;
+    bool	loop = true;
+
+    TFT_fillScreen(TFT_BLACK);
+    TopMessage(top);
+    Buttons_Clear();
+    Buttons_Add( 40, 100, 80, 40, ack, 0);
+    Buttons_Add(200, 100, 80, 40, nak, 1);
+    Buttons_Show();
+    SoundPlay(SOUND_Prompt);
+
+    while (loop) {
+	switch (Buttons_Scan()) {
+	    case 0:	loop = false;
+			rc = true;
+			break;
+
+	    case 1:	loop = false;
+			rc = false;
+			break;
+
+	    default:
+			break;
+	}
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+
+    Buttons_Clear();
+    return rc;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/buttons.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,230 @@
+/**
+ * @file buttons.h
+ * @brief The touch buttons.
+ */
+
+#ifndef _BUTTONS_H
+#define _BUTTONS_H
+
+#define MAXBUTTONS              40
+
+
+/**
+ * @brief Definition of the touch screen buttons.
+ */
+typedef struct {
+    int         x;              ///< Starting X position, -1 is terminator.
+    int         y;              ///< Starting Y position
+    int         w;              ///< Width
+    int         h;              ///< Height
+    char        text[11];       ///< Button text
+    bool	dark;		///< Alternate dark color.
+    bool	small;		///< Use small font.
+    bool	lock;		///< Indicate locked color.
+} sButton;
+
+
+/**
+ * @brief Wait for the touch screen is released.
+ */
+void WaitTouchReleased(void);
+
+/**
+ * @brief Clear the configured buttons list
+ */
+void Buttons_Clear(void);
+
+/**
+ * @brief Add a button to the buttons list.
+ * @param x The X start position
+ * @param y The Y start position
+ * @param w The width of the button in pixels
+ * @param h The height of the button in pixels.
+ * @param text The text to display on the button.
+ *             If the text is empty, no key will be shown.
+ *             Use this for other areas that must act as a key.
+ * @param order The order number in the list.
+ */
+void Buttons_Add(int x, int y, int w, int h, char *txt, int order);
+
+/**
+ * @brief Draw all defined buttons in the list on the screen.
+ */
+void Buttons_Show(void);
+
+/**
+ * @brief Search if the x and y coordinates belong to a button.
+ * @param x The touched X position.
+ * @param y The touched Y position.
+ * @return The order number if a valid button, -1 is not valid.
+ */
+int Buttons_Search(int x, int y);
+
+/**
+ * @brief Scan for the touch of a button. A touch is only valid after the
+ *        screen is left untouched. So a short or long press makes no difference.
+ * @return The order number for a valid button, -1 if not valid.
+ */
+int Buttons_Scan(void);
+
+/**
+ * @brief Scan keyboard, all characters, 4 screens.
+ * @return The ASCII code of the key, or -1 if no key is pressed.
+ */
+int KeyBoardAll(void);
+
+/**
+ * @brief Show data text field.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param label The field name text
+ * @param txt The field text data
+ */
+void ShowText(uint16_t x, uint16_t y, char *label, char *txt);
+
+/**
+ * @brief Show integer data field.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param label The field name text
+ * @param suffix Some text after the data value or NULL
+ * @param val The integer data value to show.
+ */
+void ShowInteger(uint16_t x, uint16_t y, char *label, char *suffix, int val);
+
+/**
+ * @brief Show boolean data field.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param label The field name text
+ * @param val The bolean value, displays 'J' or 'N'
+ */
+void ShowBool(uint16_t x, uint16_t y, char *label, bool val);
+
+/**
+ * @brief Show SSR2 value.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param val The SSR2 value.
+ */
+void ShowSSR2(uint16_t x, uint16_t y, int val);
+
+/**
+ * @brief Show float data field.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param label The field name text
+ * @param suffix Some text after the data value or NULL
+ * @param val The float data value to show.
+ * @param decimals The number of decimals to show.
+ */
+void ShowFloat(uint16_t x, uint16_t y, char *label, char *suffix, float val, int decimals);
+
+/**
+ @brief Show double data field.
+ * @param x The X position on the screen
+ * @param y The Y position on the screen
+ * @param label The field name text
+ * @param suffix Some text after the data value or NULL
+ * @param val The float data value to show.
+ * @param decimals The number of decimals to show.
+ */
+ void ShowDouble(uint16_t x, uint16_t y, char *label, char *suffix, double val, int decimals);
+
+/**
+ * @brief Edit data field. A complete new screen is used.
+ * @param label The field name text
+ * @param txt The field text data
+ * @param errmsg The error message to show after an input error.
+ * @param len The total length of the data field.
+ * @param type The keyboard type to use.
+ */
+void Editer(char *label, char *txt, char *errmsg, int len, int type);
+
+/**
+ * @brief Edit data field. A complete new screen is used.
+ * @param label The field name text
+ * @param txt The field text data
+ * @param len The total length of the data field.
+ */
+void EditText(char *label, char *txt, int len);
+
+/**
+ * @brief Edit data field. A complete new screen is used.
+ * @param label The field name text
+ * @param txt The field text data
+ * @param len The total length of the data field.
+ * @param min The minimum field length.
+ */
+ void EditTextMin(char *label, char *txt, int len, int min);
+
+/**
+ * @brief Edit integer field. A complete new screen is used.
+ * @param label The field name text
+ * @param val The field integer data
+ * @param min The minimum value of the data.
+ * @param max The maximum value of the data.
+ */
+void EditInt(char *label, int *val, int min, int max);
+
+/**
+ * @brief Edit uint8_t field. A complete new screen is used.
+ * @param label The field name text
+ * @param val The field uint8_t data
+ * @param min The minimum value of the data.
+ * @param max The maximum value of the data.
+ */
+void EditUint8(char *label, uint8_t *val, uint8_t min, uint8_t max);
+
+/**
+ * @brief Edit uint16_t field. A complete new screen is used.
+ * @param label The field name text
+ * @param val The field uint16_t data
+ * @param min The minimum value of the data.
+ * @param max The maximum value of the data.
+ */
+void EditUint16(char *label, uint16_t *val, uint16_t min, uint16_t max);
+
+/**
+ * @brief Edit float field. A complete new screen is used.
+ * @param label The field name text
+ * @param val The field float data
+ * @param min The minimum value of the data.
+ * @param max The maximum value of the data.
+ * @param decimals The number of decimals to show.
+ */
+void EditFloat(char *label, float *val, float min, float max, int decimals);
+
+/**
+ * @brief Edit double field. A complete new screen is used.
+ * @param label The field name text
+ * @param val The field float data
+ * @param min The minimum value of the data.
+ * @param max The maximum value of the data.
+ * @param decimals The number of decimals to show.
+ */
+void EditDouble(char *label, double *val, double min, double max, int decimals);
+
+/**
+ * @brief Edit a boolean value
+ * @param label The field name text
+ * @param val The field integer data
+ */
+void EditBool(char *label, bool *val);
+
+/**
+ * @bried Edit SSR2 value.
+ * @param val The SSR2 value.
+ */
+void EditSSR2(int *val);
+
+/**
+ * @brief Confirm or not choice. Uses the whole screen.
+ * @param top The top screen message.
+ * #param yes The text for the ack button.
+ * @param no The text for the nack button.
+ * @return true when ack, or false.
+ */
+int Confirm(char *top, char *ack, char *nak);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/ca_cert.pem	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEEzCCAvugAwIBAgIBGzANBgkqhkiG9w0BAQUFADCBhzELMAkGA1UEBhMCTkwx
+EjAQBgNVBAgTCU4tSG9sbGFuZDERMA8GA1UEBxMISUptdWlkZW4xDTALBgNVBAoT
+BE1CU0UxEDAOBgNVBAsMB01CU0VfRVUxFTATBgNVBAMTDE1CU0UgUm9vdCBDQTEZ
+MBcGCSqGSIb3DQEJARYKY2EubWJzZS5ldTAeFw0xODA5MzAxNzQ3MDBaFw0yODA5
+MzAxNzQ3MDBaMIGNMQswCQYDVQQGEwJOTDESMBAGA1UECBMJTi1Ib2xsYW5kMREw
+DwYDVQQHEwhJSm11aWRlbjENMAsGA1UEChMETUJTRTEQMA4GA1UECwwHTUJTRV9F
+VTEYMBYGA1UEAxMPc2VhcG9ydC5tYnNlLnltMRwwGgYJKoZIhvcNAQkBFg1hZG1p
+bkBtYnNlLmV1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArA7LmULa
+Mav+wDHowQcu8PBTWeBSzuNQLVJmxrfWFM2FysQDD+3fSA54vzri1rOACOK7JG3e
+DlkzxwFjzwlAktIzCFkkfA6u+CM+TeTqZKo1shH6qFsVDkAu4ev0tSf1ml18boZR
+32b8OJrn5DR/TnXZiDBA2VPk8gdMLgG8DaNe8vnjzg8oc7i/9lElNT1P5nwZHLFC
+aRRvLBjw5e2YLQxIMeowkae2btyQSqG7EXoiZVYjLs5BrC2NrXfGuPc3s6Im9SLU
+xfcBwBj1LLPsN3OdMIq+jcJcblTPnqdq3w7HNDbRq8UJ8vkHY6GxS7I1FqvLg6QB
+eKxIbWTBCAhi/wIDAQABo4GBMH8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUNG9Y
+5nVkYsP9jA7DfXMLjwiLT4IwCwYDVR0PBAQDAgXgMDAGA1UdHwQpMCcwJaAjoCGG
+H2h0dHA6Ly9jZXJ0Lm1ic2UuZXUvbWJzZS1ldS5jcmwwEQYJYIZIAYb4QgEBBAQD
+AgZAMA0GCSqGSIb3DQEBBQUAA4IBAQCpU6ZVfE6U9w3mcDZe6aIMsH8PfK6fJUxP
+a2RX1pthu0vmveTyqAcQaQK76QxWbeeGeG0dMxP7viU1c1Mf/u9lhGTGBXN5X5tZ
+8V95H0NbpGyy+DV8N91TEfzHMcOED1K3MRKr/HpaDFtQCYgi+m0BFdQ8CREVkAeX
+3UtB1XmjIuaIiR9+lGrgdaopl03hc2RRLewuW8f6dELpBm+mPRnbCQC/1c1hyDa0
+APoUXafueFnA4bUf6vssIH3V0hezemTD3iNYXE+/xQF2zKqRoDcXROSlA9FvalfX
+OzxVIfaSUxfqt7bX5pfVl9fYSQes5QizMoCb6uP4OhvENyICUHeM
+-----END CERTIFICATE-----
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/calibration.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,142 @@
+/**
+ * @file calibration.c
+ * @brief TFT screen calibration.
+ */
+
+#include "config.h"
+
+
+static const char		*TAG = "calibration";
+
+
+extern uint16_t                 tp_xleft;
+extern uint16_t                 tp_xright;
+extern uint16_t                 tp_ytop;
+extern uint16_t                 tp_ybottom;
+
+
+// Distance of the testpoints from the corders.
+#define	CAL_DISTANCE		15
+
+
+int TS_set_calibration(int xleft, int xright, int ytop, int ybottom)
+{
+    if ((xleft > xright) || (ybottom > ytop)) {
+	ESP_LOGE(TAG, "Touch Calibration error X=%d..%d Y=%d..%d", xleft, xright, ybottom, ytop);
+	return 1;
+    }
+
+    tp_xleft = xleft;
+    tp_xright = xright;
+    tp_ytop = ytop;
+    tp_ybottom = ybottom;
+    ESP_LOGI(TAG, "Touch Calibration X=%d..%d Y=%d..%d", xleft, xright, ybottom, ytop);
+
+    return 0;
+}
+
+
+
+void Calibration_Init(void)
+{
+    _fg = TFT_ORANGE;
+    TFT_setFont(DEJAVU24_FONT, NULL);
+    TFT_print("Scherm calibratie", CENTER, 60);
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    TFT_print("Druk op de oplichtende stippen", CENTER, 90);
+}
+
+
+
+/*
+ * Run in a blocking loop
+ */
+void Calibration_Loop(void)
+{
+    int		tx, ty, pass, cx[8], cy[8], xl = 0, xh = 0, yl = 0, yh = 0;
+    int		offset;
+
+    pass = 0;
+    ESP_LOGI(TAG, "Touchscreen calibration");
+
+    while (1) {
+	switch (pass) {
+	    case 0:
+	    case 4:	TFT_fillCircle(320 - CAL_DISTANCE, 240 - CAL_DISTANCE, 5, TFT_BLACK);
+			TFT_fillCircle(CAL_DISTANCE,  CAL_DISTANCE, 5, TFT_YELLOW);
+			xl = 2800;
+			xh = 3800;
+			yl = 2800;
+			yh = 3800;
+			break;
+	    case 1:
+	    case 5:	TFT_fillCircle(CAL_DISTANCE,  CAL_DISTANCE, 5, TFT_BLACK);
+			TFT_fillCircle(320 - CAL_DISTANCE, CAL_DISTANCE, 5, TFT_YELLOW);
+			xl = 2800;
+			xh = 3800;
+			yl = 100;
+			yh = 1000;
+			break;
+	    case 2:
+	    case 6:	TFT_fillCircle(320 - CAL_DISTANCE, CAL_DISTANCE, 5, TFT_BLACK);
+			TFT_fillCircle(CAL_DISTANCE, 240 - CAL_DISTANCE, 5, TFT_YELLOW);
+			xl = 100;
+			xh = 1000;
+			yl = 2800;
+			yh = 3800;
+			break;
+	    case 3:
+	    case 7:	TFT_fillCircle(CAL_DISTANCE, 240 - CAL_DISTANCE, 5, TFT_BLACK);
+			TFT_fillCircle(320 - CAL_DISTANCE, 240 - CAL_DISTANCE, 5, TFT_YELLOW);
+			xl = 100;
+			xh = 1000;
+			yl = 100;
+			yh = 1000;
+			break;
+	}
+	if (TFT_read_touch(&tx, &ty, 1)) {  // Raw read touch.
+	    printf(" Calibrarion loop %d %d %d\n", pass, tx, ty);
+	    if ((tx >= xl) && (tx <= xh) && (ty >= yl) && (ty <= yh)) {
+	    	cx[pass] = tx;
+	    	cy[pass] = ty;
+	    	pass++;
+	    	if (pass == 8) {
+		    break;
+	    	}
+	    } else {
+		SoundPlay(SOUND_Warn);
+	    }
+	    WaitTouchReleased();
+	}
+	vTaskDelay(100 / portTICK_PERIOD_MS);
+    }
+
+    TFT_fillScreen(TFT_BLACK);
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    TFT_print("Scherm calibratie is klaar.", CENTER, 60);
+    /*
+     * Calculate the new calibration values.
+     * The testdots were 10 pixels of the corners, 
+     * so take that in account.
+     */
+    offset = (CAL_DISTANCE * 3800) / 240;
+    config.ts_xleft   = ((cx[2] + cx[3] + cx[6] + cx[7]) / 4) - offset;
+    config.ts_xright  = ((cx[0] + cx[1] + cx[4] + cx[5]) / 4) + offset;
+    offset = (CAL_DISTANCE * 3800) / 320;
+    config.ts_ytop    = ((cy[0] + cy[2] + cy[4] + cy[6]) / 4) + offset;
+    config.ts_ybottom = ((cy[1] + cy[3] + cy[5] + cy[7]) / 4) - offset;
+    TS_set_calibration(config.ts_xleft, config.ts_xright, config.ts_ytop, config.ts_ybottom);
+    write_config();
+
+    Buttons_Add(130, 200, 60, 40, "Ok", 0);
+    Buttons_Show();
+    while (1) {
+	vTaskDelay(20 / portTICK_PERIOD_MS);
+	if (Buttons_Scan() == 0) {
+	    return;
+	    break;
+    	}
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/calibration.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,31 @@
+/**
+ * @file calibration.h
+ * @brief TFT screeen calibration
+ */
+#ifndef	_CALIBRATION_H
+#define	_CALIBRATION_H
+
+
+/**
+ * @brief Set Touchscreen calibration values.
+ *        Values are for the default portrait mode, in this application
+ *        the screen is used in landscape mode.
+ * @param xleft X left side    (landscape bottom)
+ * @param xright X right side  (landscape top)
+ * @param ytop Y top           (landscape left)
+ * @param ybottom Y botto      (landscape right)
+ */
+int TS_set_calibration(int xleft, int xright, int ytop, int ybottom);
+
+/**
+ * @brief Initialize for TFT calibration
+ */
+void Calibration_Init(void);
+
+/**
+ * @brief Do the TFT calibration
+ */
+void Calibration_Loop(void);
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/component.mk	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,5 @@
+#
+# "main" pseudo-component makefile.
+#
+# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/config.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,513 @@
+/**
+ * @file config.c
+ * @brief BrewBoard configuration files.
+ */
+
+#include "config.h"
+
+static const char *TAG = "config";
+
+
+void write_config() {
+    uint8_t *dst = (uint8_t *)&config;
+    FILE *f = fopen("/spiffs/etc/config.conf", "w+");
+	      
+    if (f == NULL) {
+	ESP_LOGE(TAG, "write /spiffs/etc/config.conf failed");
+	return;
+    }
+
+    size_t bytes = fwrite(dst, 1, sizeof(config), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/config.conf written %d bytes", bytes);
+}
+
+
+
+void read_config() {
+    uint8_t	*dst;
+    uint8_t	mac_addr[8] = {0};
+    FILE	*f = fopen("/spiffs/etc/config.conf", "r");
+
+    if (f == NULL) {
+	// No configuration yet, create it.
+	esp_efuse_mac_get_default(mac_addr);
+	config.Version = 1;
+	config.Unit = 'C';
+	config.BoilTemperature = 99.0;
+	config.AskAdd = true;
+	config.AskRemove = true;
+	config.AskIodine = true;
+	config.IodineTime = 30;
+	config.EquipmentRec = 1;
+	sprintf(config.hostname, "brewboard-%02x%02x%02x", mac_addr[3], mac_addr[4], mac_addr[5]);
+	sprintf(config.ap_ssid, "brewboardAP-%02x%02x%02x", mac_addr[3], mac_addr[4], mac_addr[5]);
+	sprintf(config.ap_pwd, "%s", "triplehop");
+	config.ap_channel = 5;
+	config.ap_ssid_hidden = 0;
+	config.ap_bandwidth = WIFI_BW_HT20;
+	config.ts_xleft = 0;
+	config.ts_xright = 3600;
+	config.ts_ytop = 3600;
+	config.ts_ybottom = 0;
+	config.RecipeRec = 1;
+	sprintf(config.uuid, "c0ffeeee-dead-beef-cafe-%02x%02x%02x%02x%02x%02x", 
+			mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); 
+	write_config();
+    } else {
+	dst = (uint8_t*)&config;
+	size_t bytes = fread(dst, 1, sizeof(config), f);
+	fclose(f);
+	ESP_LOGI(TAG, "/spiffs/etc/config.conf read %d bytes", bytes);
+	if (config.AskIodine && ! config.IodineTime) {
+	    config.IodineTime = 30;
+	    write_config();
+	}
+	if (strlen(config.uuid) !=36) {
+	    esp_efuse_mac_get_default(mac_addr);
+	    sprintf(config.uuid, "c0ffeeee-dead-beef-cafe-%02x%02x%02x%02x%02x%02x", 
+			    mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
+	    write_config();
+	}
+    }
+}
+
+
+
+void append_equipment() {
+    uint8_t *dst = (uint8_t *)&equipment;
+    FILE *f = fopen("/spiffs/etc/equipments.conf", "a");
+	      
+    if (f == NULL) {
+	ESP_LOGE(TAG, "append /spiffs/etc/equipments.conf failed");
+	return;
+    }
+	        
+    size_t bytes = fwrite(dst, 1, sizeof(equipment), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/equipments.conf appended %d bytes", bytes);
+}
+
+
+
+void read_equipment(int RecNo) {
+    uint8_t *dst;
+    FILE  *f = fopen("/spiffs/etc/equipments.conf", "r");
+	      
+    if (f == NULL) {
+	// No configuration yet, create it.
+	equipment.Version = 1;
+	equipment.Record = 1;
+	sprintf(equipment.Name, "default");
+	equipment.BoilPower = 80;
+	equipment.MashPower = 100;
+	equipment.PumpCycle = 8;
+	equipment.PumpRest = 2;
+	equipment.PumpPreMash = true;
+	equipment.PumpOnMash = true;
+	equipment.PumpMashOut = true;
+	equipment.PumpOnBoil = false;
+	equipment.PumpMaxTemp = 80;
+	equipment.PIDPipe = true;
+	equipment.SSR2 = 0;
+	equipment.TempHLT = 85.0;
+	equipment.PID_kP = 150.0;
+	equipment.PID_kI = 1.5;
+	equipment.PID_kD = 15000.0;
+	equipment.PID_POn = PID_P_ON_E;
+	equipment.SampleTime = 5000;
+	append_equipment();
+    } else {
+	dst = (uint8_t*)&equipment;
+	while (1) {
+	    size_t bytes = fread(dst, 1, sizeof(equipment), f);
+	    if (bytes && equipment.Record == RecNo) {
+		fclose(f);
+		ESP_LOGI(TAG, "/spiffs/etc/equipments.conf read %d bytes, record %d: %s", bytes, RecNo, equipment.Name);
+		return;
+	    }
+	    if (bytes == 0)
+		break;
+	}
+	fclose(f);
+	ESP_LOGI(TAG, "/spiffs/etc/equipments.conf read error, record %d not found", RecNo);
+    }
+}
+
+
+
+void write_equipment(int RecNo)
+{
+    uint8_t     *dst = (uint8_t *)&equipment;
+    FILE        *f;
+
+    f = fopen("/spiffs/etc/equipments.conf", "r+");
+    if (f == NULL) {
+	ESP_LOGE(TAG, "write /spiffs/etc/equipments.conf failed");
+	return;
+    }
+    fseek(f, (RecNo - 1) * sizeof(equipment), SEEK_SET);
+    size_t bytes = fwrite(dst, 1, sizeof(equipment), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/equipments.conf update record %d, %d bytes", RecNo, bytes);
+}
+
+
+
+void delete_equipment(int RecNo)
+{
+    int		RecNow = 1;
+    FILE	*n, *o;
+    uint8_t	*dst;
+    size_t	bytes;
+
+    n = fopen("/spiffs/etc/equipments.new", "a");
+    if (n == NULL) {
+	ESP_LOGE(TAG, "cannot create /spiffs/etc/equipments.new");
+	return;
+    }
+    o = fopen("/spiffs/etc/equipments.conf", "r");
+    if (o == NULL) {
+	ESP_LOGE(TAG, "cannot open spiffs/etc/equipments.conf for reading");
+	fclose(n);
+	return;
+    }
+
+    dst = (uint8_t*)&equipment;
+    while (true) {
+	bytes = fread(dst, 1, sizeof(equipment), o);
+	if (bytes == 0)
+	    break;
+
+	if (equipment.Record == RecNo) {
+	    // Record to delete, don't copy
+	    printf("Ditch %d\n", RecNo);
+	} else {
+	    if ((config.EquipmentRec == equipment.Record) && (config.EquipmentRec != RecNow)) {
+		// We need to change the default record.
+		config.EquipmentRec = RecNow;
+		write_config();
+	    }
+	    printf("Copy %d to %d\n", equipment.Record, RecNow);
+	    equipment.Record = RecNow;
+	    fwrite(dst, 1, sizeof(equipment), n);
+	    RecNow++;
+	}
+    }
+    fclose(o);
+    fclose(n);
+
+    rename("/spiffs/etc/equipments.conf", "/spiffs/etc/equipments.old");
+    rename("/spiffs/etc/equipments.new", "/spiffs/etc/equipments.conf");
+    unlink("/spiffs/etc/equipments.old");
+}
+
+
+
+int add_station(uint8_t *SSID, uint8_t *Password)
+{
+    FILE	*f;
+    uint8_t     *dst = (uint8_t *)&wifiStation;
+
+    if (read_station(SSID) >= 0) {
+	ESP_LOGE(TAG, "add_station %s already excists", SSID);
+	return -1;
+    }
+
+    f = fopen("/spiffs/etc/stations.conf", "a+");
+    if (f == NULL) {
+	ESP_LOGE(TAG, "write /spiffs/etc/stations.conf failed");
+	return -1;
+    }
+    memset(dst, 0, sizeof(wifiStation));
+    sprintf(wifiStation.SSID, "%s", (char *)SSID);
+    sprintf(wifiStation.Password, "%s", (char *)Password);
+    wifiStation.hide = false;
+    fwrite(dst, 1, sizeof(wifiStation), f);
+    fclose(f);
+
+    ESP_LOGI(TAG, "add_station %s record: %d", (char *)SSID, read_station(SSID));
+    /* Return the record number */
+    return read_station(SSID);
+}
+
+
+
+int read_station(uint8_t *SSID)
+{
+    uint8_t     *dst = (uint8_t *)&wifiStation;
+    static int	rc;
+    FILE	*f;
+    size_t	bytes;
+
+    if ((SSID == NULL) || (strlen((char *)SSID) == 0)) {
+	ESP_LOGI(TAG, "read_station(NULL)");
+	return -1;
+    }
+
+    memset(dst, 0, sizeof(wifiStation));
+    f = fopen("/spiffs/etc/stations.conf", "r+");
+    if (f == NULL) {
+	f = fopen("/spiffs/etc/stations.conf", "w+");
+	fclose(f);
+	ESP_LOGI(TAG, "/spiffs/etc/stations.conf created, return -1");
+	return -1;
+    }
+
+    rc = 0;
+    fseek(f, 0, SEEK_SET);
+
+    while (1) {
+        bytes = fread(dst, 1, sizeof(wifiStation), f);
+	if (bytes < sizeof(wifiStation)) {
+	    fclose(f);
+	    memset(dst, 0, sizeof(wifiStation));
+	    return -1;
+	}
+	if (strcmp(wifiStation.SSID, (char *)SSID) == 0) {
+	    // Fount it
+	    fclose(f);
+//	    ESP_LOGI(TAG, "read_station %s record %d", (char *)SSID, rc);
+	    return rc;
+	}
+	rc++;
+    }
+    return -1;
+}
+
+
+
+void remove_station(uint8_t *SSID)
+{
+    FILE        *n, *o;
+    uint8_t     *dst;
+    size_t      bytes;
+
+    n = fopen("/spiffs/etc/stations.new", "a");
+    if (n == NULL) {
+        ESP_LOGE(TAG, "cannot create /spiffs/etc/stations.new");
+        return;
+    }
+    o = fopen("/spiffs/etc/stations.conf", "r");
+    if (o == NULL) {
+        ESP_LOGE(TAG, "cannot open spiffs/etc/stations.conf for reading");
+        fclose(n);
+        return;
+    }
+
+    dst = (uint8_t*)&wifiStation;
+    while (true) {
+        bytes = fread(dst, 1, sizeof(wifiStation), o);
+        if (bytes == 0)
+            break;
+
+        if ((strcmp((char *)SSID, wifiStation.SSID) == 0) || (strlen(wifiStation.SSID) == 0)) {
+            // Record to delete, don't copy
+        } else {
+            fwrite(dst, 1, sizeof(wifiStation), n);
+        }
+    }
+    fclose(o);
+    fclose(n);
+
+    rename("/spiffs/etc/stations.conf", "/spiffs/etc/stations.old");
+    rename("/spiffs/etc/stations.new", "/spiffs/etc/stations.conf");
+    unlink("/spiffs/etc/stations.old");
+}
+
+
+
+int blacklist_station(uint8_t *SSID)
+{
+	return -1;
+}
+
+
+
+void write_runtime()
+{
+    uint8_t	*dst = (uint8_t *)&runtime;
+    FILE	*f = fopen("/spiffs/etc/runtime.conf", "w+");
+
+    if (f == NULL) {
+	ESP_LOGE(TAG, "write /spiffs/etc/runtime.conf failed");
+	return;
+    }
+
+    size_t bytes = fwrite(dst, 1, sizeof(runtime), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/runtime.conf written %d bytes", bytes);
+}
+
+
+
+void read_runtime() 
+{
+    uint8_t	*dst;
+    FILE	*f = fopen("/spiffs/etc/runtime.conf", "r");
+
+    if (f == NULL) {
+	// No runtime yet, create it.
+	runtime.Version = 1;
+	runtime.AutoModeStarted = false;
+	runtime.StageResume = 0;
+	runtime.StageTimeLeft = 0;
+	runtime.HopAddition = 0;
+	runtime.ManualMLT = 45.0;
+	runtime.ManualHLT = 45.0;
+	runtime.BrewStart = (time_t)0;
+	runtime.Logfile[0] = '\0';
+	runtime.PumpCooling = false;
+	runtime.TimeBrewing = 0;
+	write_runtime();
+    } else {
+	dst = (uint8_t*)&runtime;
+	size_t bytes = fread(dst, 1, sizeof(runtime), f);
+	fclose(f);
+	ESP_LOGI(TAG, "/spiffs/etc/runtime.conf read %d bytes", bytes);
+#if 0
+	printf("Auto started     %s\n", runtime.AutoModeStarted ? "yes":"no");
+	printf("Stage resume     %d\n", runtime.StageResume);
+	printf("Stage time left  %d\n", runtime.StageTimeLeft);
+	printf("Hop addition     %d\n", runtime.HopAddition);
+	printf("Brew start       %d\n", (int)runtime.BrewStart);
+	printf("Log file         %s\n", runtime.Logfile);
+	printf("Pump cooling     %s\n", runtime.PumpCooling ? "yes":"no");
+	printf("Time brewing     %d\n", runtime.TimeBrewing);
+#endif
+    }
+}
+
+
+
+void append_recipe() {
+    uint8_t *dst = (uint8_t *)&recipe;
+    FILE *f = fopen("/spiffs/etc/recipe.conf", "a");
+
+    if (f == NULL) {
+	ESP_LOGE(TAG, "append /spiffs/etc/recipe.conf failed");
+	return;
+    }
+
+    size_t bytes = fwrite(dst, 1, sizeof(recipe), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/recipe.conf appended %d bytes", bytes);
+}
+
+
+
+void write_recipe(int RecNo)
+{
+    uint8_t     *dst = (uint8_t *)&recipe;
+    FILE        *f = fopen("/spiffs/etc/recipe.conf", "r+");
+
+    if (f == NULL) {
+	ESP_LOGE(TAG, "write /spiffs/etc/recipe.conf failed");
+	return;
+    }
+
+    fseek(f, (RecNo - 1) * sizeof(recipe), SEEK_SET);
+    size_t bytes = fwrite(dst, 1, sizeof(recipe), f);
+    fclose(f);
+    ESP_LOGI(TAG, "/spiffs/etc/recipe.conf written record %d, %d bytes", RecNo, bytes);
+}
+
+
+
+void read_recipe(int RecNo)
+{
+    uint8_t     *dst;
+    FILE        *f = fopen("/spiffs/etc/recipe.conf", "r");
+
+    if (f == NULL) {
+	// No runtime yet, create it.
+	dst = (uint8_t*)&recipe;
+	memset(dst, 0, sizeof(recipe));
+	recipe.Version = 1;
+	recipe.Record = 1;
+	sprintf(recipe.Name, "Recipe 1");
+	sprintf(recipe.Code, "001");
+	sprintf(recipe.MashStep[0].Name, "Mash-in");
+	recipe.MashStep[0].Temperature = 67.5;
+	recipe.MashStep[0].Resttime = 1;
+	sprintf(recipe.MashStep[1].Name, "Mash");
+	recipe.MashStep[1].Temperature = 67.0;
+	recipe.MashStep[1].Resttime = 75;
+	sprintf(recipe.MashStep[7].Name, "Mash-out");
+	recipe.MashStep[7].Temperature = 78.0; 
+	recipe.MashStep[7].Resttime = 5;
+	recipe.BoilTime = 60;
+	recipe.Additions = 2;
+	sprintf(recipe.Addition[0].Name, "Hop");
+	recipe.Addition[0].Time = 60;
+	recipe.Addition[0].Type = ADDITION_HOP;
+	sprintf(recipe.Addition[1].Name, "Hop");
+	recipe.Addition[1].Time = 10;
+	recipe.Addition[1].Type = ADDITION_HOP;
+	recipe.CoolTemp = 20.0;
+	recipe.Whirlpool9 = 0;
+	recipe.Whirlpool7 = 0;
+	recipe.Whirlpool6 = 0;
+	recipe.Whirlpool2 = 0;
+	recipe.SpargeTemp = 85.0;
+        append_recipe();
+    } else {
+	dst = (uint8_t*)&recipe;
+	fseek(f, (RecNo - 1) * sizeof(recipe), SEEK_SET);
+	size_t bytes = fread(dst, 1, sizeof(recipe), f);
+	fclose(f);
+	ESP_LOGI(TAG, "/spiffs/etc/recipe.conf read record %d, %d bytes", RecNo, bytes);
+    }
+}
+
+
+
+void delete_recipe(int RecNo)
+{
+    int         RecNow = 1;
+    FILE        *n, *o;
+    uint8_t     *dst;
+    size_t      bytes;
+
+    n = fopen("/spiffs/etc/recipe.new", "a");
+    if (n == NULL) {
+        ESP_LOGE(TAG, "cannot create /spiffs/etc/recipe.new");
+        return;
+    }
+    o = fopen("/spiffs/etc/recipe.conf", "r");
+    if (o == NULL) {
+        ESP_LOGE(TAG, "cannot open spiffs/etc/recipe.conf for reading");
+        fclose(n);
+        return;
+    }
+
+    dst = (uint8_t*)&recipe;
+    while (true) {
+        bytes = fread(dst, 1, sizeof(recipe), o);
+        if (bytes == 0)
+            break;
+
+        if (recipe.Record == RecNo) {
+            // Record to delete, don't copy
+            printf("Ditch %d\n", RecNo);
+        } else {
+            if ((config.RecipeRec == recipe.Record) && (config.RecipeRec != RecNow)) {
+                // We need to change the default record.
+                config.RecipeRec = RecNow;
+                write_config();
+            }
+            printf("Copy %d to %d\n", recipe.Record, RecNow);
+            recipe.Record = RecNow;
+            fwrite(dst, 1, sizeof(recipe), n);
+            RecNow++;
+        }
+    }
+    fclose(o);
+    fclose(n);
+
+    rename("/spiffs/etc/recipe.conf", "/spiffs/etc/recipe.old");
+    rename("/spiffs/etc/recipe.new", "/spiffs/etc/recipe.conf");
+    unlink("/spiffs/etc/recipe.old");
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/config.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,385 @@
+/**
+ * @file config.h
+ * @brief The BrewBoard configuration data. These are stored in the 
+ *        spiffs filesystem in a flash partition.
+ */
+
+#ifndef	_CONFIG_H
+#define	_CONFIG_H
+
+// Global includes for the project
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <time.h>
+#include <errno.h>
+#include <sys/unistd.h>
+#include <sys/fcntl.h>
+#include <sys/time.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "freertos/event_groups.h"
+#include "freertos/queue.h"
+#include "driver/i2c.h"
+#include "esp_log.h"
+#include "esp_spiffs.h"
+#include "esp_event_loop.h"
+#include "esp_wifi.h"
+#include "esp_wifi_types.h"
+#include "esp_system.h"
+
+#include "esp_ota_ops.h"
+#include "esp_http_client.h"
+#include "esp_https_ota.h"
+
+#include "esp_vfs.h"
+#include "esp_vfs_fat.h"
+#include "nvs.h"
+#include "nvs_flash.h"
+#include "rom/crc.h"
+#include "mdns.h"
+#include "lwip/sockets.h"
+#include "lwip/api.h"
+#include "lwip/err.h"
+#include "lwip/netdb.h"
+#include "lwip/dns.h"
+#include "lwip/opt.h"
+#include "lwip/memp.h"
+#include "lwip/ip.h"
+#include "lwip/raw.h"
+#include "lwip/udp.h"
+#include "sdkconfig.h"
+#include "lwip/apps/sntp.h"
+#include "expat.h"
+
+#include "tftspi.h"
+#include "tft.h"
+#include "PID_v1.h"
+#include "vnc-server.h"
+#include "websocket_server.h"
+
+#include "buttons.h"
+#include "calibration.h"
+#include "automation.h"
+#include "manual.h"
+#include "setup.h"
+#include "recipes.h"
+#include "files.h"
+#include "updates.h"
+
+#include "task_tft.h"
+#include "task_ds18b20.h"
+#include "task_sdcard.h"
+#include "task_driver.h"
+#include "task_wifi.h"
+#include "task_sound.h"
+#include "task_http.h"
+
+
+#define	VERSION		"0.2.2"
+
+
+/**
+ * @brief Main mode different screens
+ */
+typedef enum
+{
+    MAIN_MODE_UNKNOWN = -1,		///< During system startup
+    MAIN_MODE_FREE = 0,			///< Main menu, do nothing
+    MAIN_MODE_CALIBRATION,		///< TFT calibration
+    MAIN_INFO,				///< Info/about screen
+    MAIN_TOOLS,				///< Main tools screen
+    MAIN_TOOLS_SETUP,			///< Main setup screen
+    MAIN_TOOLS_SETUP_CONFIG,		///< Main setup configuration
+    MAIN_TOOLS_SETUP_CO_EDIT,		///< Main setup confguration editor
+    MAIN_TOOLS_SETUP_EQUIPMENT,		///< Main setup equipment
+    MAIN_TOOLS_SETUP_EQ_EDIT,		///< Main setup edit equipment
+    MAIN_TOOLS_SETUP_WIFI,		///< Main setup WiFi
+    MAIN_TOOLS_SETUP_WIFI_CUR,		///< Main setup WiFi current connection.
+    MAIN_TOOLS_SETUP_WIFI_CON,		///< Main setup WiFi connect.
+    MAIN_TOOLS_SETUP_WIFI_NEW,		///< Main setup WiFi new connection.
+    MAIN_TOOLS_SETUP_CALIBRATION,	///< Main setup TFT calibration
+    MAIN_TOOLS_RECIPE,			///< Main recipes screen.
+    MAIN_TOOLS_RECIPE_EDIT,		///< Main recipes editor.
+    MAIN_TOOLS_FILES,			///< Main files.
+    MAIN_TOOLS_UPDATES,			///< Main Updates.
+    MAIN_AUTO_INIT,			///< Automatic start
+    MAIN_AUTO_DELAYSTART,		///< Delayed start
+    MAIN_AUTO_HEATUP,			///< Heatup the system
+    MAIN_AUTO_MASH_IN = 100,		///< Mash-in
+    MAIN_AUTO_MASH_1,			///< Mash step 1
+    MAIN_AUTO_MASH_2,			///< Mash step 2
+    MAIN_AUTO_MASH_3,			///< Mash step 3
+    MAIN_AUTO_MASH_4,			///< Mash step 4
+    MAIN_AUTO_MASH_5,			///< Mash step 5
+    MAIN_AUTO_MASH_6,			///< Mash step 6
+    MAIN_AUTO_MASH_OUT,			///< Mash-out
+    MAIN_AUTO_TOBOIL,			///< Going to boil
+    MAIN_AUTO_BOILING,			///< Boiling
+    MAIN_AUTO_WHIRLPOOL9,		///< Whirlpool
+    MAIN_AUTO_COOLING_H,		///< Cooling hot type
+    MAIN_AUTO_WHIRLPOOL7,		///< Whirlpool
+    MAIN_AUTO_COOLING_M,		///< Cooling medium type
+    MAIN_AUTO_WHIRLPOOL6,		///< Whirlpool
+    MAIN_AUTO_COOLING_C,		///< Cooling cold type
+    MAIN_AUTO_WHIRLPOOL2,		///< Whirlpool
+    MAIN_AUTO_DONE,			///< Finished
+    MAIN_AUTO_ABORT,			///< Aborted
+    MAIN_MANUAL_INIT = 200,		///< Manual control init
+    MAIN_MANUAL_MAIN,			///< Manual control main menu
+} MAIN_MODE;
+
+typedef enum {
+    MASH_NONE = 0,			///< Initial Mash state
+    MASH_WAITTEMP,			///< Wait to reach temperature.
+    MASH_REST,				///< Mash rest
+    MASH_ADD,				///< Add mash wait
+    MASH_IODINE,			///< Wait iodine test
+    MASH_REMOVE,			///< Wait mash remove
+} MASH_TYPE;
+
+typedef enum {
+    SSR2_OFF = 0,			///< SSR2 not used.
+    SSR2_HLT_SHARE,			///< SSR2 for HLT shared with MLT
+    SSR2_HLT_IND,			///< SSR2 for HLT independend.
+    SSR2_ON_IDLE,			///< SSR2 on when NLT is idle.
+} SSR2_TYPE;
+
+
+
+/**
+ * @brief Global configuration. File /spiffs/config.conf
+ */
+struct strConfig {
+    uint8_t		Version;			///< Record version number for updates.
+    uint8_t		Unit;				///< Celsius or Farenheit (not used yet).
+    float		BoilTemperature;		///< The temperature when water boils.
+    bool		AskAdd;				///< Ask to add the mash.
+    bool		AskRemove;			///< Ask to remove the mash.
+    bool		AskIodine;			///< Ask to confirm the iodine test.
+    uint8_t		IodineTime;			///< Continue after this time if not confirmed.
+    int			EquipmentRec;			///< Equipment record number.
+    char		obsolete1[32];			///< Obsolete.
+    char		hostname[32];			///< Our hostname.
+    char		ap_ssid[32];			///< AP SSID.
+    char		ap_pwd[64];			///< AP password.
+    uint8_t		ap_channel;			///< AP channel.
+    uint8_t		ap_ssid_hidden;			///< AP SSID hidden.
+    wifi_bandwidth_t	ap_bandwidth;			///< AP channel bandwidth.
+    uint16_t		ts_xleft;			///< TS calibration X left.
+    uint16_t		ts_xright;			///< TS calibration X right.
+    uint16_t		ts_ytop;			///< TS calibration Y top.
+    uint16_t		ts_ybottom;			///< TS calibration Y bottom.
+    int			RecipeRec;			///< Current recipe record.
+    char		uuid[37];			///< Sort of uuid code.
+    char		lastSSID[32];			///< Last connected station.
+} config;
+
+/**
+ * @brief Write configuration to disk.
+ */
+void write_config(void);
+
+/** @brief Read configuration file. If it doesn't exist create and
+ *         write a new configuration file with sane defaults.
+ */
+void read_config(void);
+
+
+
+/**
+ * @brief Equipment configuration. File /spiffs/equipments.conf
+ */
+struct strEquipment {
+    int		Version;			///< Record version number for updates.
+    int		Record;				///< Record number.
+    char	Name[32];			///< Equipment name.
+    int		BoilPower;			///< The power percentage needed to keep the wort boiling.
+    int		MashPower;			///< The power percentage needed to heat during mash.
+    int		PumpCycle;			///< Minutes to run the pump during mash rest.
+    int		PumpRest;			///< Minutes to pause the pump during mash rests.
+    bool	PumpPreMash;			///< Pump on before mash-in.
+    bool	PumpOnMash;			///< Pump on during mashing.
+    bool	PumpMashOut;			///< Pump on during mash remove.
+    bool	PumpOnBoil;			///< Pump on during the boil.
+    int		PumpMaxTemp;			///< Turn pump off above this temperature.
+    bool	PIDPipe;			///< Run PID during mash removal.
+    int		SSR2;				///< Use second SSR (HLT) or not.
+    float	TempHLT;			///< The temperature of the sparge water.
+    double	PID_kP;				///< PID P setting.
+    double	PID_kI;				///< PID I setting.
+    double	PID_kD;				///< PID D setting.
+    bool	PID_POn;			///< PID compute on Measurement or Errors.
+    int		SampleTime;			///< PID sample time in seconds.
+} equipment;
+
+/**
+ * @brief Append equipments record to disk.
+ */
+void append_equipment(void);
+
+/** 
+ * @brief Read equipments file. If it doesn't exist create and
+ *        write a new equipment file with sane defaults.
+ *        This is then the first and default brew equipment.
+ * @param RecNo Read the record with Record is RecNo. Start at 1.
+ */
+void read_equipment(int RecNo);
+
+/**
+ * @brief Write rquipment record. It should exist.
+ * @param RecNo The equipment record in memory to write at record. Records start at 1.
+ */
+void write_equipment(int RecNo);
+
+/**
+ * @brief Delete equipment record, renumber remaining records.
+ * @param RecNo The record to remove.
+ */
+void delete_equipment(int RecNo);
+
+
+
+/**
+ * @brief Records with WiFi stations we have succesfully connected.
+ */
+struct strStations {
+    char	SSID[32];			///< Station SSID
+    char	Password[64];			///< Station password
+    bool	hide;				///< Hide from AP scan.
+} wifiStation;
+
+/**
+ * @brief Add a new station record.
+ * @param SSID The SSID
+ * @param Password The password for this SSID
+ * @return The record number, or -1 if error.
+ */
+int add_station(uint8_t *SSID, uint8_t *Password);
+
+/**
+ * @brief Read station info record.
+ * @param SSID Search for the SSID and load the record if found.
+ * @return Return -1 if not found, else the record number and the wifiStation record is filled.
+ */
+int read_station(uint8_t *SSID);
+
+/**
+ * @brief Remove station record.
+ * @param SSID The SSID to remove.
+ */
+void remove_station(uint8_t *SSID);
+
+int blacklist_station(uint8_t *SSID);
+
+
+
+/**
+ * @brief Runtime configuration. File /spiffs/runtime.conf
+ */
+struct strRuntime {
+    uint8_t		Version;			///< Record version number for updates.
+    bool		AutoModeStarted;		///< Running automode.
+    int			StageResume;			///< Current Resume Stage.
+    int			StageTimeLeft;			///< Current Stage Time left.
+    uint8_t		HopAddition;			///< Current Hop Addition.
+    float		ManualMLT;			///< Last used MLT temperature.
+    float		ManualHLT;			///< Last used HLT temperature.
+    time_t		BrewStart;			///< Brew start time.
+    char		Logfile[64];			///< Brew logfile.
+    bool		UseHLT;				///< Use HLT during brew.
+    bool		PumpCooling;			///< Pump during cooling.
+    uint32_t		TimeBrewing;			///< Time we are brewing.
+} runtime;
+
+
+/**
+ * @brief Write runtime information to disk.
+ */
+void write_runtime(void);
+
+/** 
+ * @brief Read runtime information file. If it doesn't exist create and
+ *        write a new runtime file with sane defaults.
+ */
+void read_runtime(void);
+
+
+typedef enum {
+    ADDITION_HOP = 0,
+    ADDITION_FERMENTABLE = 1,
+    ADDITION_SPICE = 2,
+    ADDITION_FINING = 3,
+    ADDITION_WATER_AGENT = 4,
+    ADDITION_HERB = 5,
+    ADDITION_FLAVOR = 6,
+    ADDITION_OTHER = 7,
+} ADDITION_TYPE;
+
+/**
+ * @brief Mash steps
+ */
+typedef struct strMashStep {
+    char		Name[32];			///< Step name.
+    float		Temperature;			///< Step temperature.
+    uint16_t		Resttime;			///< Step resttime.
+    uint16_t		Steptime;			///< Step time to reach temp.
+} mashstep_t;
+
+/**
+ * @brief Hop and other additions
+ */
+typedef struct strAddition {
+    uint16_t		Time;				///< Time for addition.
+    uint8_t		Type;				///< Addition type, Hop, Syrop ...
+    char		Name[64];			///< Addition name.
+} addition_t;
+
+/**
+ * @brief Current recipe
+ */
+struct strRecipe {
+    uint8_t		Version;			///< Record version number.
+    int			Record;
+    char		Name[128];			///< Recipe name.
+    char		Code[32];			///< Recipe code.
+    mashstep_t		MashStep[8];			///< Mash steps.
+    uint16_t		BoilTime;			///< Boil time.
+    uint8_t		Additions;			///< Number of additions.
+    addition_t		Addition[10];			///< Additions
+    float		CoolTemp;			///< Cool temperature.
+    uint16_t		Whirlpool9;			///< Zero or the Hot Whirlpool time 88..100 °
+    uint16_t		Whirlpool7;			///< Zero or the Sub Isomerization Whirlpool time 71..77 °
+    uint16_t		Whirlpool6;			///< Zero or the "Tepid" Whirlpool time 60..66 °
+    uint16_t		Whirlpool2;			///< Zero or the Cold Whirlpool time < 30 °
+    float		SpargeTemp;			///< Sparge water temperature.
+} recipe;
+
+/**
+ * @brief Append a recipe record. Create file if it doesn't exist.
+ */
+void append_recipe(void);
+
+/**
+ * @brief Write current recipe
+ * @param RecNo Record number starting at 1.
+ */
+void write_recipe(int RecNo);
+
+/**
+ * @brief Read current recipe
+ * @param RecNo The record to read, start at 1.
+ */
+void read_recipe(int RecNo);
+
+/**
+ * @brief Delete recipe record, renumber remaining records.
+ * @param RecNo The record to remove.
+ */
+void delete_recipe(int RecNo);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/files.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,92 @@
+/**
+ * @file files.c
+ * @brief Files management.
+ */
+
+#include "config.h"
+
+
+extern sButton                  Buttons[MAXBUTTONS];
+extern int                      Main_Screen;
+
+bool				Refresh = false;
+
+
+
+void Files_Dir(char *path)
+{
+    char		filename[256], tmp[42];
+    uint16_t		y = 28;
+    DIR			*dir;
+    struct dirent	*de;
+    struct stat		st;
+
+    _bg = TFT_BLACK;
+    TFT_fillScreen(_bg);
+    TopMessage(path);
+    _fg = TFT_WHITE;
+    TFT_setFont(DEFAULT_FONT, NULL);
+
+    if ((dir = opendir(path))) {
+	de = readdir(dir);
+	while (de) {
+	    TFT_print(de->d_name, 2, y);
+	    sprintf(filename, "%s/%s", path, de->d_name);
+	    if (stat(filename, &st) == 0) {
+		sprintf(tmp, "%ld", st.st_size);
+		TFT_print(tmp, RIGHT, y);
+	    }
+	    de = readdir(dir);
+	    y += 16;
+	}
+	closedir(dir);
+    }
+
+    Buttons_Add(130, 200, 60, 40, "Ok", 0);
+    Buttons[0].dark = true;
+    Buttons_Show();
+
+    while (true) {
+	if (Buttons_Scan() == 0) {
+	    Buttons_Clear();
+	    return;
+	}
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+}
+
+
+
+/*
+ * Files init function, only runs once a new screen is entered.
+ */
+void Files_Init(void)
+{
+    switch (Main_Screen) {
+	case MAIN_TOOLS_FILES:
+			break;
+
+	default:	break;
+    }
+}
+
+
+
+/*
+ * Recipes management loop, non-blocking.
+ */
+void Files_Loop(void)
+{
+    switch (Main_Screen) {
+
+	case MAIN_TOOLS_FILES:
+			Files_Dir("/sdcard/recipe");
+			Files_Dir("/sdcard/w/log");
+			Main_Screen = MAIN_TOOLS;
+			break;
+
+	default:	break;
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/files.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,18 @@
+/**
+ * @file files.h
+ * @brief Files management.
+ */
+#ifndef _FILESS_H
+#define _FILESS_H
+
+/**
+ *  * @brief Files init fases.
+ *   */
+void Files_Init(void);
+
+/**
+ *  * @brief Files loop screens. Bob-blocking.
+ *   */
+void Files_Loop(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/manual.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,270 @@
+/**
+ * @file manual.c
+ * @brief Manual control functions.
+ */
+
+#include "config.h"
+
+bool 				_ManualHLT = false;		///< Use Hot Liquer Tank
+bool				_ManualMLT = false;		///< Use Mash/Boil kettle
+bool				loop;
+
+extern sButton                  Buttons[MAXBUTTONS];
+extern int                      Main_Screen;
+extern DS18B20_State            *ds18b20_state;
+extern DRIVER_State             *driver_state;
+extern SemaphoreHandle_t        xSemaphoreDS18B20;
+extern SemaphoreHandle_t        xSemaphoreDriver;
+
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+extern float                    Fake_MLT;
+extern float                    Fake_HLT;
+#endif
+
+static const char               *TAG = "manual";
+
+
+/*
+ * Manual init function that only runs once a new screen
+ * is entered.
+ */
+bool Manual_Init(void)
+{
+    switch (Main_Screen) {
+	case MAIN_MANUAL_INIT:
+		ESP_LOGI(TAG, "Start manual mode");
+		if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+		    driver_state->enable = true;
+		    xSemaphoreGive(xSemaphoreDriver);
+		}
+		break;
+
+	case MAIN_MANUAL_MAIN:
+		TopMessage("Handbediening");
+		Buttons_Add(  5, 200, 60, 40, "Stop", 0);
+		Buttons[0].dark = true;
+		uint8_t i = 1;
+		if (_ManualMLT) {
+		    MLT_info(71, 26, false);
+		    Buttons_Add(  5,  26, 60, 40, "+sp"  , i++);
+		    Buttons_Add(255,  26, 60, 40, "-sp"  , i++);
+		    Buttons_Add(  5,  76, 60, 40, "Pomp" , i++);
+		    Buttons_Add(255,  76, 60, 40, "Aan"  , i++);
+		}
+		if (_ManualHLT) {
+		    HLT_info(71,150, false, false);
+		    Buttons_Add(  5, 150, 60, 40, "+sp"  , i++);
+		    Buttons_Add(255, 150, 60, 40, "-sp"  , i++);
+		    Buttons_Add(255, 200, 60, 40, "Aan"  , i);
+		}
+		Buttons_Show();
+		break;
+
+	default:
+		break;
+    }
+
+    return false;
+}
+
+
+
+/*
+ * Manual loop screens, non-blocking.
+ */
+bool Manual_Loop(void)
+{
+    switch (Main_Screen) {
+	case MAIN_MANUAL_INIT:
+		_ManualHLT = _ManualMLT = false;
+		Buttons_Add( 40, 100, 80, 40, "Ja", 0);
+		Buttons_Add(200, 100, 80, 40, "Nee",  1);
+		Buttons_Show();
+
+		if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) {
+		    TopMessage("Spoelwater ketel?");
+		    SoundPlay(SOUND_Prompt);
+		    loop = true;
+		    while (loop) {
+		    	switch (Buttons_Scan()) {
+			    case 0:     loop = false;
+					_ManualHLT = true;
+					break;
+
+			    case 1:     loop = false;
+					_ManualHLT = false;
+					break;
+
+			    default:    break;
+		    	}
+		        vTaskDelay(20 / portTICK_PERIOD_MS);
+		    }
+		}
+
+		TopMessage("Maisch/Kook ketel?");
+		SoundPlay(SOUND_Prompt);
+		loop = true;
+		while (loop) {
+		    switch (Buttons_Scan()) {
+			case 0:         loop = false;
+					_ManualMLT = true;
+					break;
+
+			case 1:         loop = false;
+					_ManualMLT = false;
+					break;
+
+			default:        break;
+		    }
+		    vTaskDelay(20 / portTICK_PERIOD_MS);
+		}
+		Buttons_Clear();
+
+		if (! _ManualHLT && ! _ManualMLT) {
+		    ESP_LOGI(TAG, "End manual mode");
+		    Main_Screen = MAIN_MODE_FREE;
+		    break;
+		}
+		if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+		    /* Set them all, even if not in use now. */
+		    driver_state->hlt_sp = runtime.ManualHLT;
+		    driver_state->mlt_sp = runtime.ManualMLT;
+		    xSemaphoreGive(xSemaphoreDriver);
+		}
+		if (_ManualHLT) {
+		    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			driver_state->hlt_mode = HLT_MODE_OFF;
+			xSemaphoreGive(xSemaphoreDriver);
+		    }
+		}
+		if (_ManualMLT) {
+		    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+			driver_state->mlt_mode = MLT_MODE_OFF;
+			xSemaphoreGive(xSemaphoreDriver);
+		    }
+		}
+		Main_Screen = MAIN_MANUAL_MAIN;
+		break;
+
+	case MAIN_MANUAL_MAIN:
+		if (_ManualMLT) {
+		    MLT_info(71, 26, true);
+		}
+		if (_ManualHLT) {
+		    HLT_info(71, 150, true, false);
+		}
+		switch (Buttons_Scan()) {
+		    case 0:	Main_Screen = MAIN_MODE_FREE;
+				ESP_LOGI(TAG, "End manual mode");
+				if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    runtime.ManualHLT = driver_state->hlt_sp;
+				    runtime.ManualMLT = driver_state->mlt_sp;
+				    driver_state->enable = false;
+				    driver_state->mlt_mode = MLT_MODE_NONE;
+				    driver_state->mlt_sp = 0.0;
+				    driver_state->hlt_mode = HLT_MODE_NONE;
+				    driver_state->hlt_sp = 0.0;
+				    driver_state->pump_run = 0;
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				write_runtime();
+				break;
+
+		    case 1:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    if (_ManualMLT) {
+				    	driver_state->mlt_sp += 0.25;
+				    } else {
+					driver_state->hlt_sp += 0.25;
+				    }
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				break;
+
+		    case 2:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    if (_ManualMLT) {
+					driver_state->mlt_sp -= 0.25;
+				    } else {
+					driver_state->hlt_sp -= 0.25;
+				    }
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				break;
+
+		    case 3:	if (_ManualMLT) {
+				    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+					if (driver_state->pump_run)
+					    driver_state->pump_run = 0;
+					else
+					    driver_state->pump_run = 1;
+					xSemaphoreGive(xSemaphoreDriver);
+				    }
+				} else {
+				    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+					if (driver_state->hlt_mode == HLT_MODE_BANG) {
+					    driver_state->hlt_mode = HLT_MODE_OFF;
+					    Buttons_Add(255, 200, 60, 40, "Aan"  , 3);
+					    ESP_LOGI(TAG, "HLT turned off");
+					} else {
+					    driver_state->hlt_mode = HLT_MODE_BANG;
+					    Buttons_Add(255, 200, 60, 40, "Uit"  , 3);
+					    ESP_LOGI(TAG, "HLT turned on");
+					}
+					xSemaphoreGive(xSemaphoreDriver);
+				    }
+				    Buttons_Show();
+				}
+				break;
+
+		    case 4:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    if (driver_state->mlt_mode == MLT_MODE_PID) {
+					driver_state->mlt_mode = MLT_MODE_OFF;
+					Buttons_Add(255,  76, 60, 40, "Aan"  , 4);
+					ESP_LOGI(TAG, "MLT turned off");
+				    } else {
+					driver_state->mlt_mode = MLT_MODE_PID;
+					Buttons_Add(255,  76, 60, 40, "Uit"  , 4);
+					ESP_LOGI(TAG, "MLT turned on");
+				    }
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				Buttons_Show();
+				break;
+
+		    case 5:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    driver_state->hlt_sp += 0.25;
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				break;
+
+		    case 6:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    driver_state->hlt_sp -= 0.25;
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				break;
+
+		    case 7:	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+				    if (driver_state->hlt_mode == HLT_MODE_BANG) {
+					driver_state->hlt_mode = HLT_MODE_OFF;
+					Buttons_Add(255, 200, 60, 40, "Aan"  , 7);
+					ESP_LOGI(TAG, "HLT turned off");
+				    } else {
+					driver_state->hlt_mode = HLT_MODE_BANG;
+					Buttons_Add(255, 200, 60, 40, "Uit"  , 7);
+					ESP_LOGI(TAG, "HLT turned on");
+				    }
+				    xSemaphoreGive(xSemaphoreDriver);
+				}
+				Buttons_Show();
+				break;
+
+		    default:	break;
+		}
+		break;
+
+	default:
+		break;
+    }
+
+    return false;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/manual.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,20 @@
+/**
+ * @file manual.h
+ * @brief Manual control
+ */
+#ifndef	_MANUAL_H
+#define	_MANUAL_H
+
+/**
+ * @brief Manual init fases.
+ * @return true if should jump to startover.
+ */
+bool Manual_Init(void);
+
+/**
+ * @brief Manual loop screens. Non-blocking.
+ * @return true if should jump to startover.
+ */
+bool Manual_Loop(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/recipes.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,699 @@
+/**
+ * @file recipes.c
+ * @brief Recipes management.
+ */
+
+#include "config.h"
+
+
+extern sButton			Buttons[MAXBUTTONS];
+extern int			Main_Screen;
+
+bool				r_UpdateRec = false;
+int				r_CurrentRec = 1;
+int				r_Records = 1;
+int				r_Imported = 0;
+char                            _xml_element[10][32];
+int                             _xml_depth;
+int				_xml_mashsteps;
+char                            _xml_add_name[64];
+int                             _xml_add_type;
+int                             _xml_add_time;
+int				_xml_add_ramp;
+float				_xml_add_temp;
+float				_xml_tun_temp;
+bool                            _xml_add_valid;
+
+static const char               *TAG = "recipes";
+
+char				char_data_buffer[1024];
+size_t				offs;
+bool				overflow;
+
+
+
+void Addition_Add(char *Name, uint8_t Type, uint16_t Time)
+{
+    printf("Addition_Add(%s, %d, %d)\n", Name, Type, Time);
+    if (! recipe.Additions) {
+	// No entries yet, add the first one.
+    	sprintf(recipe.Addition[recipe.Additions].Name, "%s", Name);
+    	recipe.Addition[recipe.Additions].Type = Type;
+    	recipe.Addition[recipe.Additions].Time = Time;
+    	recipe.Additions++;
+	return;
+    }
+
+    // See if we already got one with the same time.
+    for (int i = 0; i < recipe.Additions; i++) {
+	if (recipe.Addition[i].Time == Time) {
+	    // Yes, update the name.
+	    snprintf(recipe.Addition[i].Name, 63, "%s, %s", recipe.Addition[i].Name, Name);
+	    return;
+	}
+    }
+
+    // A new entry and we already have some. Add it and keep the list sorted.
+    for (int i = 0; i < recipe.Additions; i++) {
+	if (Time > recipe.Addition[i].Time) {
+	    printf("Insert at %d\n", i);
+	    // Nake room
+	    for (int j = i; j < recipe.Additions; j++) {
+		sprintf(recipe.Addition[j+1].Name, "%s", recipe.Addition[j].Name);
+		recipe.Addition[j+1].Type = recipe.Addition[j].Type;
+		recipe.Addition[j+1].Time = recipe.Addition[j].Time;
+	    }
+	    sprintf(recipe.Addition[i].Name, "%s", Name);
+	    recipe.Addition[i].Type = Type;
+	    recipe.Addition[i].Time = Time;
+	    recipe.Additions++;
+	    return;
+	}
+    }
+
+    // Just append.
+    sprintf(recipe.Addition[recipe.Additions].Name, "%s", Name);
+    recipe.Addition[recipe.Additions].Type = Type;
+    recipe.Addition[recipe.Additions].Time = Time;
+    recipe.Additions++;
+}
+
+
+
+void reset_char_data_buffer (void)
+{
+    offs = 0;
+    overflow = false;
+}
+
+
+
+void char_data (void *userData, const XML_Char *s, int len)
+{
+    if (!overflow) {
+        if (len + offs >= sizeof(char_data_buffer) ) {
+            overflow = true;
+        } else {
+            memcpy(char_data_buffer + offs, s, len);
+            offs += len;
+        }
+    }
+}
+
+
+
+void process_char_data_buffer (void)
+{
+    bool        allspace = true;
+
+    if (offs > 0) {
+        char_data_buffer[ offs ] = '\0';
+
+        /*
+         * If all spaces and control characters, the data is invalid.
+         */
+        for (int i = 0; i < strlen(char_data_buffer); i++) {
+            if (char_data_buffer[i] > ' ')
+                allspace = false;
+        }
+        if (allspace)
+            return;
+
+        if ((_xml_depth > 2) && (strcmp(_xml_element[0], "RECIPES") == 0) && (strcmp(_xml_element[1], "RECIPE") == 0)) {
+            /*
+             * We are in a recipe
+             */
+            if ((_xml_depth == 3) && (strcmp("NAME", _xml_element[2]) == 0)) {
+                snprintf(recipe.Name, 127, "%s", char_data_buffer);
+            } else if ((_xml_depth == 3) && (strcmp("BOIL_TIME", _xml_element[2]) == 0)) {
+                recipe.BoilTime = atoi(char_data_buffer);
+            } else if ((_xml_depth == 5) && (strcmp("HOPS", _xml_element[2]) == 0) && (strcmp("HOP", _xml_element[3]) == 0)) {
+		/*
+		 * Hops that are added during the boil.
+		 * But check for whirlpool hops too.
+		 */
+                if (strcmp("NAME", _xml_element[4]) == 0) {
+                    snprintf(_xml_add_name, 63, "%s", char_data_buffer);
+                } else if (strcmp("USE", _xml_element[4]) == 0) {
+                    _xml_add_valid = (strcmp("Boil", char_data_buffer) == 0);   // Only "Boil" is a valid hop
+                    if (strcmp("Aroma", char_data_buffer) == 0) {
+                        recipe.Whirlpool7 = 30;
+                    }
+                } else if (strcmp("TIME", _xml_element[4]) == 0) {
+                    _xml_add_time = atoi(char_data_buffer);
+                }
+            } else if ((_xml_depth >= 3) && (strcmp("STYLE", _xml_element[2]) == 0)) {
+                // Ignore
+            } else if ((_xml_depth >= 3) && (strcmp("EQUIPMENT", _xml_element[2]) == 0)) {
+                // Ignora
+            } else if ((_xml_depth >= 3) && (strcmp("YEASTS", _xml_element[2]) == 0)) {
+                // Ignore
+            } else if ((_xml_depth >= 3) && (strcmp("WATERS", _xml_element[2]) == 0)) {
+                // Ignore
+            } else if ((_xml_depth == 5) && (strcmp("FERMENTABLES", _xml_element[2]) == 0) && (strcmp("FERMENTABLE", _xml_element[3]) == 0)) {
+		/*
+		 * Fermentabes that must be added during the boil.
+		 */
+                if (strcmp("NAME", _xml_element[4]) == 0) {
+                    snprintf(_xml_add_name, 63, "%s", char_data_buffer);
+                } else if (strcmp("TYPE", _xml_element[4]) == 0) {
+                    if ((strcmp("Sugar", char_data_buffer) == 0)/* || (strcmp("Adjunct", char_data_buffer) == 0)*/) {
+                        _xml_add_type = ADDITION_FERMENTABLE;
+                        _xml_add_time = 10;
+                    } else {
+                        _xml_add_type = -1;
+                    }
+                } else if (strcmp("ADD_AFTER_BOIL", _xml_element[4]) == 0) {
+                    _xml_add_valid = (strcmp("FALSE", char_data_buffer) == 0);
+                }
+            } else if ((_xml_depth == 5) && (strcmp("MISCS", _xml_element[2]) == 0) && (strcmp("MISC", _xml_element[3]) == 0)) {
+		/*
+		 * Check for Misc additions to add during the boil.
+		 */
+		if (strcmp("NAME", _xml_element[4]) == 0) {
+		    snprintf(_xml_add_name, 63, "%s", char_data_buffer);
+		} else if (strcmp("USE", _xml_element[4]) == 0) {
+		    _xml_add_valid = (strcmp("Boil", char_data_buffer) == 0);   // Only "Boil" is a valid hop.
+		} else if (strcmp("TYPE", _xml_element[4]) == 0) {
+		    if (strcmp("Spice", char_data_buffer) == 0)
+			_xml_add_type = ADDITION_SPICE;
+		    if (strcmp("Fining", char_data_buffer) == 0)
+			_xml_add_type = ADDITION_FINING;
+		    if (strcmp("Herb", char_data_buffer) == 0)
+			_xml_add_type = ADDITION_HERB;
+		    if (strcmp("Flavor", char_data_buffer) == 0)
+			_xml_add_type = ADDITION_FLAVOR;
+		    if (strcmp("Other", char_data_buffer) == 0)
+			_xml_add_type = ADDITION_OTHER;
+		} else if (strcmp("TIME", _xml_element[4]) == 0) {
+		    _xml_add_time = atoi(char_data_buffer);
+		}
+            } else if ((_xml_depth >= 4) && (strcmp("MASH", _xml_element[2]) == 0)) {
+		if  ((_xml_depth >= 6) && (strcmp("MASH_STEP", _xml_element[4]) == 0)) {
+		    if (strcmp("NAME", _xml_element[5]) == 0) {
+			snprintf(_xml_add_name, 31, "%s", char_data_buffer);
+		    } else if (strcmp("TYPE", _xml_element[5]) == 0) {
+			// Temperature Infusion Decoction
+			_xml_add_valid = (strcmp("Temperature", char_data_buffer) == 0);
+		    } else if (strcmp("STEP_TEMP", _xml_element[5]) == 0) {
+			_xml_add_temp = atof(char_data_buffer);
+		    } else if (strcmp("STEP_TIME", _xml_element[5]) == 0) {
+			_xml_add_time = atoi(char_data_buffer);
+		    } else if (strcmp("RAMP_TIME", _xml_element[5]) == 0) {
+			_xml_add_ramp = atoi(char_data_buffer);
+		    }
+		} else if ((_xml_depth >= 4) && (strcmp("TUN_TEMP", _xml_element[3]) == 0)) {
+		    // Save this and check later if this is the strike temperature.
+		    _xml_tun_temp = atof(char_data_buffer);
+		} else if ((_xml_depth >= 4) && (strcmp("SPARGE_TEMP", _xml_element[3]) == 0)) {
+		    recipe.SpargeTemp = atof(char_data_buffer);
+		}
+            }
+        }
+    }
+}
+
+
+
+void startElement(void *userData, const char *name, const char **attr)
+{
+    sprintf(_xml_element[_xml_depth], "%s", name);
+    _xml_depth++;
+    process_char_data_buffer();
+    reset_char_data_buffer();
+}
+
+
+
+void endElement(void *userData, const char *name)
+{
+    process_char_data_buffer();
+    reset_char_data_buffer();
+    if ((_xml_depth == 4) && (strcmp("HOP", _xml_element[3]) == 0)) {
+        if (_xml_add_valid) {
+	    Addition_Add(_xml_add_name, ADDITION_HOP, _xml_add_time);
+        }
+    }
+    if ((_xml_depth == 4) && (strcmp("FERMENTABLE", _xml_element[3]) == 0)) {
+        if (_xml_add_valid && (_xml_add_type == ADDITION_FERMENTABLE)) {
+	    Addition_Add(_xml_add_name, _xml_add_type, _xml_add_time);
+        }
+    }
+    if ((_xml_depth == 4) && (strcmp("MISC", _xml_element[3]) == 0)) {
+	if (_xml_add_valid) {
+	    Addition_Add(_xml_add_name, _xml_add_type, _xml_add_time);
+	}
+    }
+    if ((_xml_depth == 5) && (strcmp("MASH_STEP", _xml_element[4]) == 0)) {
+//	printf("Flush End MASH_STEP %d %s\n", _xml_depth, _xml_add_name);
+	_xml_mashsteps++;
+	sprintf(recipe.MashStep[_xml_mashsteps].Name, "%s", _xml_add_name);
+	recipe.MashStep[_xml_mashsteps].Temperature = _xml_add_temp;
+	recipe.MashStep[_xml_mashsteps].Resttime = _xml_add_time;
+	recipe.MashStep[_xml_mashsteps].Steptime = _xml_add_ramp;
+    }
+    _xml_depth--;
+}
+
+
+
+int ParseRecipe(char *fn, char *code)
+{
+    char buf[512];
+
+    memset(&recipe, 0, sizeof(recipe));
+    XML_Parser parser = XML_ParserCreate(NULL);
+
+    int done;
+    _xml_depth = 0;
+    _xml_mashsteps = 0;
+    _xml_tun_temp = 0.0;
+
+    XML_SetElementHandler(parser, startElement, endElement);
+    XML_SetCharacterDataHandler(parser, char_data);
+
+    FILE *fp = fopen(fn, "r");
+
+    do {
+        int len = (int)fread(buf, 1, sizeof(buf), fp);
+        done = len < sizeof(buf);
+        if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) {
+            printf( "%s at line %5lu\n", XML_ErrorString(XML_GetErrorCode(parser)), XML_GetCurrentLineNumber(parser));
+            return 1;
+        }
+        vTaskDelay(2 / portTICK_PERIOD_MS);
+    } while (!done);
+
+    XML_ParserFree(parser);
+    fclose(fp);
+
+    /*
+     * Fix missing things.
+     */
+    snprintf(recipe.Code, 31, "%s", code);
+    // The code is created from the recipe filename.
+    for (int i = 0; i < strlen(recipe.Code); i++) {
+	if ((recipe.Code[i] == ' ') || (recipe.Code[i] == '.')) {
+	    recipe.Code[i] = '\0';
+	    break;
+	}
+    }
+    recipe.CoolTemp = 20.0;
+    sprintf(recipe.MashStep[0].Name, "Mash-in");
+    if (_xml_tun_temp > recipe.MashStep[1].Temperature) {
+	recipe.MashStep[0].Temperature = _xml_tun_temp;
+    } else {
+    	recipe.MashStep[0].Temperature = recipe.MashStep[1].Temperature + 1.25;
+    }
+    recipe.MashStep[0].Resttime = 1;
+
+    if (! recipe.MashStep[7].Resttime) {
+	// Move last mash step to position 7.
+	for (int i = 6; i > 1; i--) {
+	    if (recipe.MashStep[i].Resttime) {
+		// Got it, move.
+		sprintf(recipe.MashStep[7].Name, "%s", recipe.MashStep[i].Name);
+		recipe.MashStep[i].Name[0] = '\0';
+		recipe.MashStep[7].Temperature = recipe.MashStep[i].Temperature;
+		recipe.MashStep[i].Temperature = 0.0;
+		recipe.MashStep[7].Resttime = recipe.MashStep[i].Resttime;
+		recipe.MashStep[i].Resttime = 0;
+		recipe.MashStep[7].Steptime = recipe.MashStep[i].Steptime;
+		recipe.MashStep[i].Steptime = 0;
+		break;
+	    }
+	}
+    }
+
+    printf("Recipe: %s\n", recipe.Name);
+    printf("Code  : %s\n", recipe.Code);
+    printf("Boil time %d minutes\n", recipe.BoilTime);
+    printf("n Stepname                         temp time ramp\n");
+    printf("- ------------------------------  ----- ---- ----\n");
+    for (int i = 0; i < 8; i++) {
+        if (recipe.MashStep[i].Resttime) {
+            printf("%d %-31s %5.2f %4d %4d\n", i, recipe.MashStep[i].Name, recipe.MashStep[i].Temperature,
+                            recipe.MashStep[i].Resttime, recipe.MashStep[i].Steptime);
+        }
+    }
+    printf("%d additions\n", recipe.Additions);
+    printf("n Addition name                                                   t Tim\n");
+    printf("- --------------------------------------------------------------- - ---\n");
+    for (int i = 0; i < recipe.Additions; i++) {
+        printf("%d %-63s %d %3d\n", i, recipe.Addition[i].Name, recipe.Addition[i].Type, recipe.Addition[i].Time);
+    }
+    printf("Cooltemp %5.2f\n", recipe.CoolTemp);
+    if (recipe.Whirlpool9)
+    	printf("Whirlpool9 %d\n", recipe.Whirlpool9);
+    if (recipe.Whirlpool7)
+    	printf("Whirlpool7 %d\n", recipe.Whirlpool7);
+    if (recipe.Whirlpool6)
+    	printf("Whirlpool6 %d\n", recipe.Whirlpool6);
+    if (recipe.Whirlpool2)
+    	printf("Whirlpool2 %d\n", recipe.Whirlpool2);
+    printf("SpargeTemp %5.2f\n", recipe.SpargeTemp);
+
+    return 0;
+}
+
+
+
+/*
+ * Recipes init function, only runs once a new screen is entered.
+ */
+void Recipes_Init(void)
+{
+    FILE		*f;
+    DIR			*dir;
+    struct dirent	*de;
+    size_t		bytes;
+    uint8_t		*dst;
+    uint16_t		y;
+    char		filename[256], newname[256];
+    int			rc;
+
+    switch (Main_Screen) {
+	case MAIN_TOOLS_RECIPE:
+			TopMessage("Recepten importeren");
+			r_Imported = 0;
+			TFT_setFont(DEFAULT_FONT, NULL);
+			y = 28;
+			if ((dir = opendir("/sdcard/recipe"))) {
+			    de = readdir(dir);
+			    while (de) {
+				if (strstr(de->d_name, ".xml") || strstr(de->d_name, ".XML")) {
+				    _fg = TFT_YELLOW;
+				    TFT_print(de->d_name, 2, y);
+				    snprintf(filename, 255, "/sdcard/recipe/%s", de->d_name);
+				    snprintf(newname, 255, "/sdcard/recipe/%s", de->d_name);
+				    newname[strlen(newname) -2] = 'o';
+				    newname[strlen(newname) -1] = 'k';
+				    rc = ParseRecipe(filename, de->d_name);
+				    ESP_LOGI(TAG, "Recipe %s parsed, rc=%d", filename, rc);
+				    if (rc == 0) {
+					append_recipe();
+					rename(filename, newname);
+				    	_fg = TFT_GREEN;
+				    	TFT_print(" Ok", LASTX, y);
+				    } else {
+				    	_fg = TFT_RED;
+				    	TFT_print(" Fout", LASTX, y);
+				    }
+				    r_Imported++;	// Count them all
+				    y += 16;
+				}
+				de = readdir(dir);
+			    }
+			    closedir(dir);
+			}
+
+			f = fopen("/spiffs/etc/recipe.conf", "r");
+			dst = (uint8_t*)&recipe;
+			r_Records = 0;
+			while ((bytes = fread(dst, 1, sizeof(recipe), f))) {
+			    r_Records++;
+			}
+			fclose(f);
+			// Load the default record.
+			r_CurrentRec = config.RecipeRec;
+			r_UpdateRec = true;
+			break;
+
+	case MAIN_TOOLS_RECIPE_EDIT:
+			break;
+
+	default:	break;
+    }
+}
+
+
+
+/*
+ * Recipes management loop, non-blocking.
+ */
+void Recipes_Loop(void)
+{
+    uint32_t	crc1, crc2;
+    uint8_t	*dst;
+    int		mashsteps;
+    uint16_t	y;
+    char	tmp[32];
+    float	mintemp;
+
+    switch (Main_Screen) {
+	case MAIN_TOOLS_RECIPE:
+			if (r_Imported) {
+			    Buttons_Clear();
+			    Buttons_Add(135, 210, 50, 30, "Ok"  , 0);
+			    Buttons_Show();
+
+			    while (true) {
+				if (Buttons_Scan() == 0)
+				    break;
+				vTaskDelay(20 / portTICK_PERIOD_MS);
+			    }
+			    Buttons_Clear();
+			    r_Imported = 0;
+			}
+			if (r_UpdateRec) {
+			    _bg = TFT_BLACK;
+			    TFT_fillScreen(_bg);
+			    TFT_resetclipwin();
+			    TopMessage("Recepten");
+			    r_UpdateRec = false;
+			    read_recipe(r_CurrentRec);
+			    TFT_setFont(DEFAULT_FONT, NULL);
+			    ShowText(2, 28, "Naam", recipe.Name);
+			    ShowText(2, 44, "Code", recipe.Code);
+			    ShowInteger(162, 44, "Record", NULL, recipe.Record);
+			    ShowInteger(2, 60, "Kooktijd", " min", recipe.BoilTime);
+			    ShowFloat(162, 60, "Koel tot", " C", recipe.CoolTemp, 2);
+			    ShowFloat(2, 76, "Maisch in", " C", recipe.MashStep[0].Temperature, 2);
+			    ShowFloat(162, 76, "Spoelwater", " C", recipe.SpargeTemp, 2);
+			    y =  92;
+			    _fg = TFT_WHITE;
+			    TFT_print("Maisch stap", 2, y);
+			    TFT_print("Temp.", 200, y);
+			    TFT_print("Rust", 260, y);
+			    _fg = TFT_YELLOW;
+			    y += 16;
+			    for (int i = 1; i < 8; i++) {
+				if (recipe.MashStep[i].Resttime) {
+				    TFT_print(recipe.MashStep[i].Name, 2, y);
+				    sprintf(tmp, "%.2f", recipe.MashStep[i].Temperature);
+				    TFT_print(tmp, 200, y);
+				    sprintf(tmp, "%2d min", recipe.MashStep[i].Resttime);
+				    TFT_print(tmp, 260, y);
+				    y += 16;
+				}
+			    }
+			    if (recipe.Additions) {
+				_fg = TFT_YELLOW;
+				sprintf(tmp, "%d ", recipe.Additions);
+				TFT_print(tmp, 2, y);
+				_fg = TFT_WHITE;
+				TFT_print("toevoegingen om", LASTX, y);
+				_fg = TFT_YELLOW;
+				for (int i = 1; i <= recipe.Additions; i++) {
+				    sprintf(tmp, " %d", recipe.Addition[i-1].Time);
+				    TFT_print(tmp, LASTX, y);
+				}
+				_fg = TFT_WHITE;
+				TFT_print(" minuten", LASTX, y);
+			    } else {
+				_fg = TFT_WHITE;
+				TFT_print("Geen hop toevoegingen.", 2, y);
+			    }
+			    y += 16;
+			    if (recipe.Whirlpool9 || recipe.Whirlpool7 || recipe.Whirlpool6 || recipe.Whirlpool2) {
+				_fg = TFT_WHITE;
+				TFT_print("Whirlpool ", 2, y);
+				_fg = TFT_YELLOW;
+				if (recipe.Whirlpool9)
+				    TFT_print("88+ ", LASTX, y);
+				if (recipe.Whirlpool7)
+				    TFT_print("71+ ", LASTX, y);
+				if (recipe.Whirlpool6)
+				    TFT_print("60+ ", LASTX, y);
+				if (recipe.Whirlpool2)
+				    TFT_print("koud", LASTX, y);
+			    }
+
+			    Buttons_Clear();
+			    Buttons_Add(  0, 210, 45, 30, "Ok"  , 0);
+			    Buttons_Add( 46, 210, 45, 30, "+"   , 1);
+			    if (r_CurrentRec != config.RecipeRec)
+				Buttons_Add( 92, 210, 45, 30, "-", 2);
+			    else
+				Buttons_Add( 92, 210, 45, 30, ""    , 2);
+			    if (r_CurrentRec > 1)
+				Buttons_Add(138, 210, 45, 30, "<", 3);
+			    else
+				Buttons_Add(138, 210, 45, 30, "", 3);
+			    if (r_CurrentRec < r_Records)
+				Buttons_Add(184, 210, 45, 30, ">", 4);
+			    else
+				Buttons_Add(184, 210, 45, 30, "", 4);
+			    if (r_CurrentRec != config.RecipeRec)
+				Buttons_Add(230, 210, 45, 30, "Std", 5);
+			    else
+				Buttons_Add(230, 210, 45, 30, "", 5);
+			    Buttons_Add(276, 210, 45, 30, "Ed"  , 6);
+			    Buttons[0].dark = true;
+			    Buttons_Show();
+			} /* if (r_UpdateRec) */
+			switch (Buttons_Scan()) {
+			    case 0:	Main_Screen = MAIN_TOOLS;
+					break;
+
+			    case 1:	memset(&recipe, 0, sizeof(recipe));
+					recipe.Version = 1;
+					recipe.Record = r_Records + 1;
+					sprintf(recipe.Name, "Recipe %d", r_Records + 1);
+					sprintf(recipe.Code, "00%d", r_Records + 1);
+					sprintf(recipe.MashStep[0].Name, "Mash-in");
+					recipe.MashStep[0].Temperature = 67.5;
+					recipe.MashStep[0].Resttime = 1;
+					sprintf(recipe.MashStep[1].Name, "Mash");
+					recipe.MashStep[1].Temperature = 67.0;
+					recipe.MashStep[1].Resttime = 75;
+					sprintf(recipe.MashStep[7].Name, "Mash-out");
+					recipe.MashStep[7].Temperature = 78.0;
+					recipe.MashStep[7].Resttime = 5;
+					recipe.BoilTime = 60;
+					recipe.Additions = 2;
+					sprintf(recipe.Addition[0].Name, "Hop");
+					recipe.Addition[0].Time = 60;
+					recipe.Addition[0].Type = ADDITION_HOP;
+					sprintf(recipe.Addition[1].Name, "Hop");
+					recipe.Addition[1].Time = 10;
+					recipe.Addition[1].Type = ADDITION_HOP;
+					recipe.CoolTemp = 20.0;
+					recipe.Whirlpool9 = 0;
+					recipe.Whirlpool7 = 0;
+					recipe.Whirlpool6 = 0;
+					recipe.Whirlpool2 = 0;
+					recipe.SpargeTemp = 85.0;
+					append_recipe();
+					r_Records++;
+					r_CurrentRec = r_Records;
+					r_UpdateRec = true;
+					ESP_LOGI(TAG, "New recipe record %d", recipe.Record);
+					break;
+
+			    case 2:	if ((r_CurrentRec != config.RecipeRec) && (r_Records > 1)) {
+					    delete_recipe(r_CurrentRec);
+					    r_Records--;
+					    if (r_CurrentRec > r_Records)
+						r_CurrentRec = r_Records;
+					    r_UpdateRec = true;
+					}
+					break;
+
+			    case 3:	if (r_CurrentRec > 1) {
+					    r_CurrentRec--;
+					    r_UpdateRec = true;
+					}
+					break;
+
+			    case 4:	if (r_CurrentRec < r_Records) {
+				    	    r_CurrentRec++;
+					    r_UpdateRec = true;
+			    		}
+			    		break;
+
+			    case 5:	if (r_CurrentRec != config.RecipeRec) {
+					    config.RecipeRec = r_CurrentRec;
+					    write_config();
+					    r_UpdateRec = true;
+					    ESP_LOGI(TAG, "Recipe %d `%s' set as default", r_CurrentRec, recipe.Name);
+					}
+					break;
+
+			    case 6:	Main_Screen = MAIN_TOOLS_RECIPE_EDIT;
+					break;
+
+			    default:	break;
+			}
+			break;
+
+	case MAIN_TOOLS_RECIPE_EDIT:
+			dst = (uint8_t*)&recipe;
+			crc1 = crc32_le(0, dst, sizeof(recipe));
+
+			EditText("Naam", recipe.Name, 31);
+			EditText("Code", recipe.Code, 31);
+			mashsteps = 0;
+			for (int i = 1; i < 7; i++) {
+			    if (recipe.MashStep[i].Resttime)
+				mashsteps++;
+			}
+			EditInt("Maisch stappen", &mashsteps, 1, 6);
+			EditFloat("Inmaisch temperatuur", &recipe.MashStep[0].Temperature, 38, 74, 2);
+			// Round to 0.25 values
+			recipe.MashStep[0].Temperature = ((int)(recipe.MashStep[0].Temperature * 4)) / 4.0;
+			for (int i = 1; i <= mashsteps; i++) {
+			    sprintf(tmp, "Maisch stap %d naam", i);
+			    EditText(tmp, recipe.MashStep[i].Name, 31);
+			    sprintf(tmp, "Maisch stap %d temperatuur", i);
+			    if (i == 1)
+				mintemp = recipe.MashStep[0].Temperature - 5;
+			    else
+				mintemp = recipe.MashStep[i - 1].Temperature;
+			    EditFloat("Inmaisch temperatuur", &recipe.MashStep[i].Temperature, mintemp, 74, 2);
+			    recipe.MashStep[i].Temperature = ((int)(recipe.MashStep[i].Temperature * 4)) / 4.0;
+			    sprintf(tmp, "Maisch stap %d rusttijd in minuten", i);
+			    EditUint16(tmp, &recipe.MashStep[i].Resttime, 1, 480);
+			    if (i == 1) {
+				recipe.MashStep[i].Steptime = 1;
+			    } else {
+				recipe.MashStep[i].Steptime = (int)(recipe.MashStep[i].Temperature - recipe.MashStep[i - 1].Temperature);
+			    }
+			}
+			mintemp = recipe.MashStep[mashsteps].Temperature;
+			EditFloat("Uitmaischen temperatuur", &recipe.MashStep[7].Temperature, mintemp, 80, 2);
+			recipe.MashStep[7].Temperature = ((int)(recipe.MashStep[7].Temperature * 4)) / 4.0;
+			EditUint16("Uitmaischen tijd in minuten", &recipe.MashStep[7].Resttime, 1, 15);
+			recipe.MashStep[7].Steptime = (int)(recipe.MashStep[7].Temperature - recipe.MashStep[mashsteps].Temperature);
+			// Zero any higher steps to diable them.
+			for (int i = (mashsteps + 1); i < 7; i++)
+			    recipe.MashStep[i].Resttime = 0;
+
+			EditUint16("Kook tijd in minuten", &recipe.BoilTime, 3, 480);
+			EditUint8("Hop/kruiden toevoegingen", &recipe.Additions, 1, 10);
+			for (uint8_t i = 0; i < recipe.Additions; i++) {
+			    sprintf(tmp, "Toevoeging %d maam", i+1);
+			    if (strlen(recipe.Addition[i].Name) == 00) {
+				sprintf(recipe.Addition[i].Name, "Hop %d", (int)i+1);
+			    }
+			    EditText(tmp, recipe.Addition[i].Name, 40);
+			    sprintf(tmp, "Toevoeging %d tijd", i+1);
+			    if (i == 0) {
+			    	EditUint16(tmp, &recipe.Addition[i].Time, 0, recipe.BoilTime);
+			    } else {
+				EditUint16(tmp, &recipe.Addition[i].Time, 0, recipe.Addition[i-1].Time - 1);
+			    }
+			    recipe.Addition[i].Type = ADDITION_HOP;
+			}
+
+			EditFloat("Koel temperatuur", &recipe.CoolTemp, 10, 30, 2);
+			recipe.CoolTemp = ((int)(recipe.CoolTemp * 4)) / 4.0;
+			EditFloat("Spoelwater temperatuur", &recipe.SpargeTemp, 75, 98, 2);
+			recipe.SpargeTemp = ((int)(recipe.SpargeTemp * 4)) / 4.0;
+			EditUint16("Whirlpool 88..100 graden, 0 = niet", &recipe.Whirlpool9, 0, 120);
+			EditUint16("Whirlpool 71..77 graden, 0 = niet", &recipe.Whirlpool7, 0, 120);
+			EditUint16("Whirlpool 60..66 graden, 0 = niet", &recipe.Whirlpool6, 0, 120);
+			EditUint16("Whirlpool koud, 0 = niet", &recipe.Whirlpool2, 0, 120);
+
+			crc2 = crc32_le(0, dst, sizeof(recipe));
+			if ((crc1 != crc2) && Confirm("Gewijzigd, opslaan?", "Ja", "Nee")) {
+			    write_recipe(recipe.Record);
+			}
+			Main_Screen = MAIN_TOOLS_RECIPE;
+			break;
+
+	default:	break;
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/recipes.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,26 @@
+/**
+ * @file recipes.h
+ * @brief Recipes management.
+ */
+
+#ifndef	_RECIPES_H
+#define	_RECIPES_H
+
+
+/**
+ * @brief Parse a Beerxml file and add the recipe.
+ * @return Zero if no errors were found.
+ */
+int ParseRecipe(char *fn, char *code);
+
+/**
+ * @brief Recipes init fases.
+ */
+void Recipes_Init(void);
+
+/**
+ * @brief Revipes loop screens. Bob-blocking.
+ */
+void Recipes_Loop(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/setup.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,329 @@
+/**
+ * @file setup.c
+ * @brief Setup functions.
+ */
+
+#include "config.h"
+
+
+extern sButton                  Buttons[MAXBUTTONS];
+extern int                      Main_Screen;
+
+bool UpdateRec = false;
+int  CurrentRec = 1;
+int  Records = 1;
+
+
+static const char               *TAG = "setup";
+
+
+
+/*
+ * Setup init function that only runs once a new screen
+ * is entered.
+ */
+void Setup_Init(void)
+{
+    FILE	*f;
+    size_t	bytes;
+    uint8_t	*dst;
+
+    switch (Main_Screen) {
+
+	case MAIN_TOOLS_SETUP:
+			TopMessage("Setup menu");
+			Buttons_Add( 20, 40,120, 40, "Config", 0);
+			Buttons_Add(180, 40,120, 40, "Apparatuur", 1);
+			Buttons_Add( 20,120,120, 40, "WiFi", 2);
+			Buttons_Add(180,120,120, 40, "Calibratie", 3);
+			Buttons_Add(130, 200, 60, 40, "Ok", 4);
+			Buttons[4].dark = true;
+			Buttons_Show();
+			break;
+
+	case MAIN_TOOLS_SETUP_CONFIG:
+			_bg = TFT_BLACK;
+			TopMessage("Configuratie");
+			TFT_setFont(DEFAULT_FONT, NULL);
+			ShowFloat(2, 28, "Kook temp.", "C", config.BoilTemperature, 2);
+			ShowBool(2, 44, "Vraag mout storten", config.AskAdd);
+			ShowBool(2, 60, "Vraag mout verwijderen", config.AskRemove);
+			ShowBool(2, 76, "Vraag Jodium proef", config.AskIodine);
+			ShowInteger(2, 92, "Jodiumtest wachttijd", " min.", config.IodineTime);
+			ShowText(2, 108, "Hostnaam", config.hostname);
+			ShowText(2, 124, "AP SSID", config.ap_ssid);
+			ShowText(2, 140, "AP pwd", config.ap_pwd);
+			ShowInteger(2, 156, "AP kanaal", NULL, config.ap_channel);
+			ShowBool(2, 172, "AP SSID verborgen", config.ap_ssid_hidden);
+			Buttons_Clear();
+			Buttons_Add(  0, 210, 45, 30, "Ok"  , 0);
+			Buttons_Add(276, 210, 45, 30, "Ed"  , 1);
+			Buttons[0].dark = true;
+			Buttons_Show();
+			break;
+
+	case MAIN_TOOLS_SETUP_CO_EDIT:
+			break;
+
+	case MAIN_TOOLS_SETUP_EQUIPMENT:
+			TopMessage("Apparatuur");
+			f = fopen("/spiffs/etc/equipments.conf", "r");
+			dst = (uint8_t*)&equipment;
+			Records = 0;
+			while ((bytes = fread(dst, 1, sizeof(equipment), f))) {
+			    Records++;
+			}
+			fclose(f);
+			// Load the default record.
+			CurrentRec = config.EquipmentRec;
+			UpdateRec = true;
+			break;
+
+	case MAIN_TOOLS_SETUP_EQ_EDIT:
+			break;
+
+	case MAIN_TOOLS_SETUP_CALIBRATION:
+			Calibration_Init();
+			break;
+
+	default:
+			break;
+    }
+}
+
+
+
+/*
+ * Setup loop screens, non-blocking.
+ */
+void Setup_Loop(void)
+{
+    uint32_t	crc1, crc2;
+    uint8_t	*dst;
+
+    switch (Main_Screen) {
+
+	case MAIN_TOOLS_SETUP:
+			switch (Buttons_Scan()) {
+			    case 0:     Main_Screen = MAIN_TOOLS_SETUP_CONFIG;
+					break;
+
+			    case 1:	Main_Screen = MAIN_TOOLS_SETUP_EQUIPMENT;
+					break;
+
+			    case 2:	Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+					break;
+
+			    case 3:     Main_Screen = MAIN_TOOLS_SETUP_CALIBRATION;
+					break;
+
+			    case 4:     Main_Screen = MAIN_TOOLS;
+					break;
+
+			    default:    break;
+			}
+			break;
+
+	case MAIN_TOOLS_SETUP_CONFIG:
+			switch (Buttons_Scan()) {
+			    case 0:     Main_Screen = MAIN_TOOLS_SETUP;
+					break;
+
+			    case 1:	Main_Screen = MAIN_TOOLS_SETUP_CO_EDIT;
+					break;
+
+			    default:	break;
+			}
+			break;
+
+	case MAIN_TOOLS_SETUP_CO_EDIT:
+			dst = (uint8_t*)&config;
+			crc1 = crc32_le(0, dst, sizeof(config));
+			EditFloat("Kook temperatuur", &config.BoilTemperature, 80, 105, 2);
+			// Round to 0.25 degrees
+			config.BoilTemperature = ((int)(config.BoilTemperature * 4)) / 4.0;
+			EditBool("Vraag mout storten", &config.AskAdd);
+			EditBool("Vraag mout verwijderen", &config.AskRemove);
+			EditBool("Vraag Jodium proef", &config.AskIodine);
+			EditUint8("Jodium test wachttijd", &config.IodineTime, 0, 120);
+			EditText("DHCP hostnaam", config.hostname, 31);
+			EditText("AP SSID", config.ap_ssid, 31);
+			EditTextMin("AP password", config.ap_pwd, 40, 8);
+			EditUint8("AP kanaal", &config.ap_channel, 1, 14);
+			bool hidden = config.ap_ssid_hidden;
+			EditBool("AP SSID verborgen", &hidden);
+			config.ap_ssid_hidden = (uint8_t)hidden;
+
+			crc2 = crc32_le(0, dst, sizeof(config));
+			if ((crc1 != crc2) && Confirm("Gewijzigd, opslaan?", "Ja", "Nee")) {
+			    write_config();
+			} else {
+			    read_config();
+			}
+			Main_Screen = MAIN_TOOLS_SETUP_CONFIG;
+			break;
+
+	case MAIN_TOOLS_SETUP_EQUIPMENT:
+			if (UpdateRec) {
+			    _bg = TFT_BLACK;
+			    TFT_fillScreen(_bg);
+			    TFT_resetclipwin();
+			    TopMessage("Apparatuur");
+			    UpdateRec = false;
+			    read_equipment(CurrentRec);
+			    TFT_setFont(DEFAULT_FONT, NULL);
+			    ShowText(2, 28, "Naam", equipment.Name);
+			    ShowInteger(  2, 44, "Kook vermogen", "%", equipment.BoilPower);
+			    ShowInteger(161, 44, "Maisch verm.", "%", equipment.MashPower);
+			    ShowInteger(  2, 60, "Pomp cyclus", "m", equipment.PumpCycle);
+			    ShowInteger(161, 60, "Pomp rust", "m", equipment.PumpRest);
+			    ShowBool(  2,  76, "Pomp opwarmen", equipment.PumpPreMash);
+			    ShowBool(161,  76, "Pomp maischen", equipment.PumpOnMash);
+			    ShowBool(  2,  92, "Pomp uitmaischen", equipment.PumpMashOut);
+			    ShowBool(161,  92, "Pomp bij koken", equipment.PumpOnBoil);
+			    ShowInteger(2, 108, "Pomp maxtemp.", "C", equipment.PumpMaxTemp);
+			    ShowBool(161, 108, "PID bij mout weg", equipment.PIDPipe);
+			    ShowSSR2(2, 124, equipment.SSR2);
+			    ShowFloat(161, 124, "Spoelwater", NULL, equipment.TempHLT, 2);
+			    ShowDouble(2, 140, "PID P", NULL, equipment.PID_kP, 3);
+			    ShowBool(161, 140, "PID klassiek", equipment.PID_POn);
+			    ShowDouble(2, 156, "PID I", NULL, equipment.PID_kI, 3);
+			    ShowInteger(161, 156, "Sample tijd", "mS", equipment.SampleTime);
+			    ShowDouble(2, 172, "PID D", NULL, equipment.PID_kD, 3);
+			    Buttons_Clear();
+			    Buttons_Add(  0, 210, 45, 30, "Ok"  , 0);
+			    Buttons_Add( 46, 210, 45, 30, "+"   , 1);
+			    if (CurrentRec != config.EquipmentRec)
+			        Buttons_Add( 92, 210, 45, 30, "-", 2);
+			    else
+				Buttons_Add( 92, 210, 45, 30, ""    , 2);
+			    if (CurrentRec > 1)
+			    	Buttons_Add(138, 210, 45, 30, "<", 3);
+			    else
+				Buttons_Add(138, 210, 45, 30, "", 3);
+			    if (CurrentRec < Records)
+			    	Buttons_Add(184, 210, 45, 30, ">", 4);
+			    else
+				Buttons_Add(184, 210, 45, 30, "", 4);
+			    if (CurrentRec != config.EquipmentRec)
+			    	Buttons_Add(230, 210, 45, 30, "Std", 5);
+			    else
+				Buttons_Add(230, 210, 45, 30, "", 5);
+			    Buttons_Add(276, 210, 45, 30, "Ed"  , 6);
+			    Buttons[0].dark = true;
+			    Buttons_Show();
+			}
+			switch (Buttons_Scan()) {
+			    case 0:	Main_Screen = MAIN_TOOLS_SETUP;
+					break;
+
+			    case 1:	memset(&equipment, 0, sizeof(equipment));
+					equipment.Version = 1;
+					equipment.Record = Records + 1;
+					sprintf(equipment.Name, "new eq %d", Records + 1);
+					equipment.BoilPower = 80;
+					equipment.MashPower = 100;
+					equipment.PumpCycle = 8;
+					equipment.PumpRest = 2;
+					equipment.PumpPreMash = true;
+					equipment.PumpOnMash = true;
+					equipment.PumpMashOut = true;
+					equipment.PumpOnBoil = false;
+					equipment.PumpMaxTemp = 80;
+					equipment.PIDPipe = true;
+					equipment.SSR2 = 0;
+					equipment.TempHLT = 85.0;
+					equipment.PID_kP = 150.0;
+					equipment.PID_kI = 1.5;
+					equipment.PID_kD = 15000.0;
+					equipment.PID_POn = PID_P_ON_E;
+					equipment.SampleTime = 5000;
+					append_equipment();
+					Records++;
+					CurrentRec = Records;
+					UpdateRec = true;
+					ESP_LOGI(TAG, "New equipment record %d", equipment.Record);
+					break;
+
+			    case 2:	if ((CurrentRec != config.EquipmentRec) && (Records > 1)) {
+					    delete_equipment(CurrentRec);
+					    Records--;
+					    if (CurrentRec > Records)
+						CurrentRec = Records;
+					    UpdateRec = true;
+					}
+					break;
+
+			    case 3:	if (CurrentRec > 1) {
+					    CurrentRec--;
+					    UpdateRec = true;
+					}
+					break;
+
+			    case 4:	if (CurrentRec < Records) {
+					    CurrentRec++;
+					    UpdateRec = true;
+					}
+					break;
+
+			    case 5:	if (CurrentRec != config.EquipmentRec) {
+					    config.EquipmentRec = CurrentRec;
+					    write_config();
+					    UpdateRec = true;
+					    ESP_LOGI(TAG, "Equipment %d `%s' set as default", CurrentRec, equipment.Name);
+					}
+					break;
+
+			    case 6:	Main_Screen = MAIN_TOOLS_SETUP_EQ_EDIT;
+					break;
+
+			    default:	break;
+			}		
+			break;
+
+	case MAIN_TOOLS_SETUP_EQ_EDIT:
+			dst = (uint8_t*)&equipment;
+			crc1 = crc32_le(0, dst, sizeof(equipment));
+			EditText("Naam", equipment.Name, 31);
+			EditInt("Kook vermogen in %", &equipment.BoilPower, 0, 100);
+			EditInt("Maisch vermogen in %", &equipment.MashPower, 0, 100);
+			EditInt("Pomp cyclus minuten", &equipment.PumpCycle, 5, 15);
+			EditInt("Pomp rust minuten", &equipment.PumpRest, 0, 5);
+			EditBool("Pomp bij opwarmen", &equipment.PumpPreMash);
+			EditBool("Pomp bij maischen", &equipment.PumpOnMash);
+			EditBool("Pomp bij uitmasichen", &equipment.PumpMashOut);
+			EditBool("Pomp tijdens koken", &equipment.PumpOnBoil);
+			EditInt("Pomp max. temperatuur", &equipment.PumpMaxTemp, 60, 105);
+			EditBool("PID by mout verwijderen", &equipment.PIDPipe);
+			EditSSR2(&equipment.SSR2);
+			EditFloat("Spoelwater temp", &equipment.TempHLT, 75, 98, 2);
+			// Round to 0.25 values.
+			equipment.TempHLT = ((int)(equipment.TempHLT * 4)) / 4.0;
+			EditBool("PID klassiek", &equipment.PID_POn);
+			EditDouble("PID P",  &equipment.PID_kP, 20, 2000, 3);
+			EditDouble("PID I",  &equipment.PID_kI, 0, 100, 3);
+			EditDouble("PID D",  &equipment.PID_kD, 0, 50000, 3);
+			EditInt("Sample tijd in mS", &equipment.SampleTime, 1000, 20000);
+			// Round to 250 mSec units.
+			equipment.SampleTime = ((int)(equipment.SampleTime / 250)) * 250;
+
+			crc2 = crc32_le(0, dst, sizeof(equipment));
+			if ((crc1 != crc2) && Confirm("Gewijzigd, opslaan?", "Ja", "Nee")) {
+			    write_equipment(equipment.Record);
+			}
+			Main_Screen = MAIN_TOOLS_SETUP_EQUIPMENT;
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI:
+			break;
+
+	case MAIN_TOOLS_SETUP_CALIBRATION:
+			Calibration_Loop();
+			Main_Screen = MAIN_TOOLS_SETUP;
+			break;
+
+	default:
+			break;
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/setup.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,18 @@
+/**
+ * @file setup.h
+ * @brief Setup screens.
+ */
+#ifndef	_SETUP_H
+#define	_SETUP_H
+
+/**
+ * @brief Setup init fases.
+ */
+void Setup_Init(void);
+
+/**
+ * @brief Setup loop screens. Non-blocking.
+ */
+void Setup_Loop(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_driver.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,307 @@
+/**
+ * @file task_driver.c
+ * @brief BrewBoard relays driver. Control the hardware outputs with the
+ *        Solid State relays for the Mash/Boil kettle (MLT, the Hot
+ *        Liquer Tank (HLT) and the pump. The MLT has a PID controller
+ *        during mashing, and a simple bang on/off control during the
+ *        boil.
+ *        The HLT output can be off, bang on/off, or just on if the MLT
+ *        is off, depending on the configuration.
+ *        Use SSR modules that switch during zero crossing of the mains
+ *        power, so that when one is turned on and on the same time the
+ *        other is turned off, they won't be active at the same time.
+ */
+
+#include "config.h"
+
+
+#define	SSR_MLT		CONFIG_SSR_MLT_GPIO
+#define	SSR_HLT		CONFIG_SSR_HLT_GPIO
+#define SSR_PUMP	CONFIG_SSR_PUMP_GPIO
+
+
+bool			outEnable = false;
+DRIVER_State		* driver_state;
+SemaphoreHandle_t	xSemaphoreDriver  = NULL;
+int			MLT_pin = 0;
+int			HLT_pin = 0;
+int			Pump_pin = 0;
+double			Input = 0, Output = 0, Setpoint = 0;
+int			MLT_Mode = MLT_MODE_NONE;
+double			HLT_Input = 0, HLT_Setpoint = 0;
+int			HLT_Output = 0;
+int			HLT_Mode = HLT_MODE_NONE;
+
+static const char       *TAG = "task_driver";
+
+extern SemaphoreHandle_t xSemaphoreDS18B20;
+extern DS18B20_State * ds18b20_state;
+extern unsigned long lastTime;
+
+
+/**
+ * @brief Turn the MLT SSR on or off.
+ */
+void MLT(int onoff);
+
+/**
+ * @brief Turn the HLT SSR on or off.
+ */
+void HLT(int onoff);
+
+/**
+ * @brief Turn the Pump on or off.
+ */
+void Pump(int onoff);
+
+
+
+void MLT(int onoff) {
+
+    if (onoff && outEnable) {
+	gpio_set_level(SSR_MLT, 1);
+	MLT_pin = 1;
+    } else {
+	gpio_set_level(SSR_MLT, 0);
+	MLT_pin = 0;
+    }
+}
+
+
+
+void HLT(int onoff) {
+
+    if (onoff && outEnable) {
+	gpio_set_level(SSR_HLT, 1);
+	HLT_pin = 1;
+    } else {
+	gpio_set_level(SSR_HLT, 0);
+	HLT_pin = 0;
+    }
+}
+
+
+
+void Pump(int onoff) {
+
+    if (onoff && outEnable) {
+	gpio_set_level(SSR_PUMP, 1);
+	Pump_pin = 1;
+    } else {
+	gpio_set_level(SSR_PUMP, 0);
+	Pump_pin = 0;
+    }
+}
+
+
+
+void LoadPIDsettings() {
+    PID_SetTunings(equipment.PID_kP, equipment.PID_kI, equipment.PID_kD, equipment.PID_POn);
+    PID_SetSampleTime(equipment.SampleTime);
+
+    /*
+     * Initialize the PID
+     */
+    Output = 0.0;   // Reset internal Iterm.
+    PID_SetMode(PID_MANUAL);
+    PID_SetMode(PID_AUTOMATIC);
+}
+
+
+
+void task_driver(void *pvParameter)
+{
+    TickType_t		wait_ticks, last_tick, now_tick;
+    bool		rc;
+    unsigned long	now, RealTime, w_StartTime = 0;
+
+    ESP_LOGI(TAG, "Starting output drivers");
+
+    /*
+     * Configure IOMUX register.
+     */
+    gpio_pad_select_gpio(SSR_MLT);
+    gpio_set_direction(SSR_MLT, GPIO_MODE_OUTPUT);
+    gpio_pad_select_gpio(SSR_HLT);
+    gpio_set_direction(SSR_HLT, GPIO_MODE_OUTPUT);
+    gpio_pad_select_gpio(SSR_PUMP);
+    gpio_set_direction(SSR_PUMP, GPIO_MODE_OUTPUT);
+
+    /*
+     * Initialize state
+     */
+    driver_state = malloc(sizeof(DRIVER_State));
+    driver_state->enable = outEnable = false;
+    driver_state->mlt_gpio = SSR_MLT;
+    driver_state->mlt_mode = MLT_MODE_NONE;
+    driver_state->mlt_sp = driver_state->mlt_pv = 0.0;
+    driver_state->mlt_power = 0;
+    driver_state->hlt_gpio = SSR_HLT;
+    driver_state->hlt_mode = HLT_MODE_NONE;
+    driver_state->hlt_sp = driver_state->hlt_pv = 0.0;
+    driver_state->hlt_power = 0;
+    driver_state->hlt_and_mlt = false;
+    driver_state->pump_gpio = SSR_PUMP;
+    driver_state->pump_run = 0;
+
+    PID(&Input, &Output, &Setpoint, 150, 1.5, 15000, PID_P_ON_E, PID_DIRECT);
+    
+    /*
+     * One loop must complete in 20 mSecs, that is one mains
+     * frequency period cycle in 50 Hz countries.
+     */
+    while (1) {
+
+	last_tick = xTaskGetTickCount();
+
+	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	    /*
+	     * Get the current temperature readings
+	     */
+	    if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) {
+		if (ds18b20_state->mlt_valid)
+		    driver_state->mlt_pv = ds18b20_state->mlt_temperature;
+		if (ds18b20_state->hlt_valid)
+		    driver_state->hlt_pv = ds18b20_state->hlt_temperature;
+		xSemaphoreGive(xSemaphoreDS18B20);
+	    }
+
+	    /*
+	     * Other values that we need
+	     */
+	    Input = driver_state->mlt_pv;
+	    Setpoint = driver_state->mlt_sp;
+	    if (driver_state->mlt_mode != MLT_Mode) {
+		if (driver_state->mlt_mode == MLT_MODE_BANG) {
+		    PID_SetMode(PID_MANUAL);
+		} else if (driver_state->mlt_mode == MLT_MODE_PID) {
+		    LoadPIDsettings();
+		}
+		MLT_Mode = driver_state->mlt_mode;
+		ESP_LOGI(TAG, "MLT mode set to %d", MLT_Mode);
+	    }
+	    if (driver_state->hlt_mode != HLT_Mode) {
+		HLT_Mode = driver_state->hlt_mode;
+		ESP_LOGI(TAG, "HLT mode set to %d", HLT_Mode);
+	    }
+	    outEnable = driver_state->enable;
+	    HLT_Input = driver_state->hlt_pv;
+	    HLT_Setpoint = driver_state->hlt_sp;
+	    xSemaphoreGive(xSemaphoreDriver);
+	}
+
+	rc = false;
+	now = xTaskGetTickCount() * portTICK_PERIOD_MS;
+
+	if ((PID_GetMode() == PID_AUTOMATIC) && (MLT_Mode == MLT_MODE_PID)) {
+	    rc = PID_Compute();
+	    RealTime = (equipment.SampleTime * equipment.MashPower) / 100;
+	} else {
+	    /*
+	     * Schedule the loop ourself.
+	     */
+	    unsigned long timeChange = (now - lastTime);
+	    if (timeChange >= equipment.SampleTime) {
+		lastTime = now;
+		rc = true;
+	    }
+	    RealTime = equipment.SampleTime;
+	    if (driver_state->mlt_mode == MLT_MODE_BANG) {
+	        Output = (Input < Setpoint) ? 255:0;
+	    }
+	    if (driver_state->mlt_mode == MLT_MODE_NONE || driver_state->mlt_mode == MLT_MODE_OFF) {
+		Output = 0;
+	    }
+	}
+
+	if (rc) {
+	    w_StartTime = now;
+	}
+
+	if ((int)((Output / 255.0) * RealTime) > (now - w_StartTime)) {
+	    MLT(1);
+	    if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_ON_IDLE)) {
+	    	HLT(0);
+		HLT_Output = 0;
+	    }
+	} else {
+	    MLT(0);
+	    if (equipment.SSR2 == SSR2_HLT_SHARE) {
+	    	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+		    HLT_Output = 0;
+	    	    if (driver_state->hlt_mode == HLT_MODE_BANG) {
+		    	HLT_Output = (HLT_Input < HLT_Setpoint) ? 1:0;
+	    	    } else if (driver_state->hlt_mode == HLT_MODE_ON) {
+		    	HLT_Output = 1;
+	    	    }
+		    xSemaphoreGive(xSemaphoreDriver);
+	    	}
+	    	HLT(HLT_Output);
+	    } else if (equipment.SSR2 == SSR2_ON_IDLE) {
+		HLT_Output = 1;
+		HLT(1);
+	    }
+	}
+
+	/*
+	 * Independant HLT temperature control
+	 */
+	if  (equipment.SSR2 == SSR2_HLT_IND) {
+	    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+		HLT_Output = 0;
+		if (driver_state->hlt_mode == HLT_MODE_BANG) {
+		    HLT_Output = (HLT_Input < HLT_Setpoint) ? 1:0;
+		} else if (driver_state->hlt_mode == HLT_MODE_ON) {
+		    HLT_Output = 1;
+		}
+		xSemaphoreGive(xSemaphoreDriver);
+	    }
+	    HLT(HLT_Output);
+	}
+
+	/*
+	 * Update the driver results.
+	 */
+	if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	    driver_state->mlt_power = (int)((Output * 100) / 255.0);
+	    if (HLT_Output) {
+		if (equipment.SSR2 == SSR2_HLT_SHARE) {
+	    	    driver_state->hlt_power = 100 - driver_state->mlt_power;
+		} else if  (equipment.SSR2 == SSR2_HLT_IND) {
+		    driver_state->hlt_power = 100;
+		}
+	    } else {
+	    	driver_state->hlt_power = 0;
+	    }
+	    if (driver_state->pump_run != Pump_pin)
+		Pump(driver_state->pump_run);
+	    xSemaphoreGive(xSemaphoreDriver);
+	}
+
+#if 0
+	if (rc) {
+	    printf("ST: %s MLT[In: %7.3f Out: %3.0f Sp: %6.2f %s RT: %lu]  HLT[In: %7.3f Out: %d Sp: %5.1f]\n", outEnable ? "E":"D",
+		    Input, Output, Setpoint, PID_GetMode() ? "AUTOMATIC" : "MANUAL   ", RealTime,
+		    HLT_Input, HLT_Output, HLT_Setpoint);
+	}
+#endif
+
+	// Not reliable, so do it manually.
+	//vTaskDelayUntil(&last_wake_time, (1000 / 50) / portTICK_PERIOD_MS);
+	now_tick = xTaskGetTickCount();
+	if ((now_tick - last_tick) > (1000 / 50)) {
+	    // This happens one or two times during a brew.
+	    wait_ticks = (1000 / 50);
+	} else {
+	    wait_ticks = (1000 / 50) - (now_tick - last_tick);
+	}
+	if (wait_ticks == 0) {
+	    // This is rare, but it happens.
+	    wait_ticks = 1;
+	}
+
+	vTaskDelay(wait_ticks);
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_driver.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,59 @@
+#ifndef	_TASK_DRIVER_H
+#define	_TASK_DRIVER_H
+
+/**
+ * @file task_driver.h
+ * @brief The interface to the FreeRTOS driver task. This task drives the Solid
+ *        State Relays (SSR) to power the Mash Lauter Tun (MLT) and the Hot
+ *        Liquer Tank (HLT) heating elements. It also controls the pump relay.
+ *
+ * It takes the temperature readings from the DS18B20 task.
+ */
+
+
+typedef enum {
+    MLT_MODE_NONE = 0,			///< Not active
+    MLT_MODE_BANG = 1,			///< Bang on/off mode
+    MLT_MODE_PID = 2,			///< PID mode
+    MLT_MODE_OFF = 3,			///< Off but show setpoint
+    MLT_MODE_EXT = 4,			///< External regulation
+} MLT_MODE_TYPE;
+
+typedef enum {
+    HLT_MODE_NONE = 0,			///< Not active
+    HLT_MODE_BANG = 1,			///< Bang on/off mode
+    HLT_MODE_OFF = 2,			///< Off but show setpoint
+    HLT_MODE_ON = 3,			///< On if free
+} HLT_MODE_TYPE;
+
+
+/**
+ * @brief Structure containing the information of the driver task.
+ */
+typedef struct {
+    bool   enable;			///< Enable outputs
+    int    mlt_gpio;			///< MLT SSR GPIO pin
+    int    mlt_mode;			///< MLT Mode: 0 = off, 1 = bang on/off, 2 = PID, 3 = cooling
+    double mlt_sp;			///< Setpoint MLT temperature
+    double mlt_pv;			///< Current MLT temperature
+    int    mlt_power;			///< Current MLT drive power %
+    int    hlt_gpio;			///< HLT SSR  GPIO pin
+    int    hlt_mode;			///< HLT Mode:  0 = off, 1 = bang on/off, 2 = always on
+    double hlt_sp;			///< Setpoint HLT temperature
+    double hlt_pv;			///< Current HLT temperature
+    int    hlt_power;			///< Current HLT drive power %
+    bool   hlt_and_mlt;			///< True if both MLT and HLT are allowed at the same time.
+    int	   pump_gpio;			///< Pump SSR GPIO pin
+    int	   pump_run;			///< Pump on/off
+} DRIVER_State;
+
+
+/**
+ * @brief FreeRTOS driver task. This task drives the Solid
+ *        State Relays (SSR) to power the Mash Lauter Tun (MLT) and the Hot
+ *        Liquer Tank (HLT) heating elements. It also controls the pump relay.
+ */
+void task_driver(void *);
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_ds18b20.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,252 @@
+/**
+ * @file task_ds18b20.c
+ * @brief Task that reads the DS18B20 sensors on two one-wire busses.
+ *        The task can also be compiled with simulated sensors.
+ */
+
+
+#include "owb.h"
+#include "owb_rmt.h"
+#include "ds18b20.h"
+#include "config.h"
+
+
+#ifdef CONFIG_TEMP_SENSORS_ONEWIRE
+#define GPIO_DS18B20_MLT     (CONFIG_ONE_WIRE_MLT)
+#define GPIO_DS18B20_HLT     (CONFIG_ONE_WIRE_HLT)
+#define MAX_DEVICES          (8)
+#define DS18B20_RESOLUTION   (DS18B20_RESOLUTION_12_BIT)
+#define SAMPLE_PERIOD        (2000)   // milliseconds
+#endif
+
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+#define SAMPLE_PERIOD        (750)    // milliseconds
+
+float               	Fake_MLT = 18.90;
+float			Fake_HLT = 18.70;
+#endif
+
+static const char	*TAG = "task_ds18b20";
+
+SemaphoreHandle_t	xSemaphoreDS18B20 = NULL;
+DS18B20_State		*ds18b20_state;
+
+
+/*
+ * Task to read temperature sensors.
+ */
+void task_ds18b20(void *pvParameter)
+{
+#ifdef CONFIG_TEMP_SENSORS_ONEWIRE
+    int			num_devices = 0;
+    float		readings = 0;
+    bool		found = false;
+    DS18B20_ERROR	errors = 0;
+    TickType_t		last_wake_time = xTaskGetTickCount();
+
+    ESP_LOGI(TAG, "Starting DS18B20 sensors");
+    ds18b20_state = malloc(sizeof(DS18B20_State));
+    ds18b20_state->mlt_valid = ds18b20_state->hlt_valid = false;
+    ds18b20_state->mlt_temperature = ds18b20_state->hlt_temperature = 0.0;
+    ds18b20_state->mlt_error = ds18b20_state->hlt_error = DS18B20_ERR_NOSENSOR;
+
+    /*
+     * Initialize the MLT and HLT one-wire busses.
+     */
+    OneWireBus *owb_mlt;
+    OneWireBus *owb_hlt;
+    owb_rmt_driver_info rmt_driver_info_mlt;
+    owb_rmt_driver_info rmt_driver_info_hlt;
+    owb_mlt = owb_rmt_initialize(&rmt_driver_info_mlt, GPIO_DS18B20_MLT, RMT_CHANNEL_1, RMT_CHANNEL_0);
+    owb_hlt = owb_rmt_initialize(&rmt_driver_info_hlt, GPIO_DS18B20_HLT, RMT_CHANNEL_3, RMT_CHANNEL_2);
+    owb_use_crc(owb_mlt, true);                     // enable CRC check for ROM code
+    owb_use_crc(owb_hlt, true); 
+
+    DS18B20_Info * ds18b20_info = ds18b20_malloc();
+
+    /*
+     * Task loop forever.
+     */
+    while (1) {
+
+	last_wake_time = xTaskGetTickCount();
+
+	num_devices = 0;
+	OneWireBus_SearchState mlt_search_state = {0};
+	found = false;
+	owb_search_first(owb_mlt, &mlt_search_state, &found);
+	while (found) {
+	    ++num_devices;
+	    owb_search_next(owb_mlt, &mlt_search_state, &found);
+	}
+
+	if (num_devices == 1) {
+	    ds18b20_init_solo(ds18b20_info, owb_mlt);			// only one device on bus
+	    ds18b20_use_crc(ds18b20_info, true);			// enable CRC check for temperature readings
+	    ds18b20_set_resolution(ds18b20_info, DS18B20_RESOLUTION);	// returns true if ok.
+
+	    // Read temperatures more efficiently by starting conversions on all devices at the same time
+	    ds18b20_convert_all(owb_mlt);
+	    ds18b20_wait_for_conversion(ds18b20_info);
+
+	    errors = ds18b20_read_temp(ds18b20_info, &readings);
+
+	    if (xSemaphoreTake(xSemaphoreDS18B20, 25) == pdTRUE) {
+	    	if (errors == DS18B20_OK) {
+		    ds18b20_state->mlt_error = DS18B20_ERR_NONE;
+		    ds18b20_state->mlt_valid = true;
+		    ds18b20_state->mlt_temperature = readings;
+	    	} else {
+		    if (errors == DS18B20_ERROR_CRC)
+		    	ds18b20_state->mlt_error = DS18B20_ERR_CRC;
+		    if (errors == DS18B20_ERROR_OWB)
+		    	ds18b20_state->mlt_error = DS18B20_ERR_READ;
+		    if (errors == DS18B20_ERROR_DEVICE)
+		    	ds18b20_state->mlt_error = DS18B20_ERR_READ;
+		    ds18b20_state->mlt_valid = false;
+		    ds18b20_state->mlt_temperature = 0.0;
+	    	}
+		xSemaphoreGive(xSemaphoreDS18B20);
+	    }
+
+	} else {
+	    /*
+	     * Zero or more then one device, this is an error.
+	     */
+	    if (xSemaphoreTake(xSemaphoreDS18B20, 25) == pdTRUE) {
+	    	if (num_devices == 0)
+		    ds18b20_state->mlt_error = DS18B20_ERR_NOSENSOR;
+	    	else
+		    ds18b20_state->mlt_error = DS18B20_ERR_TOOMANY;
+	    	ds18b20_state->mlt_valid = false;
+	    	ds18b20_state->mlt_temperature = 0.0;
+		xSemaphoreGive(xSemaphoreDS18B20);
+	    }
+	} // if num_devices == 1
+
+	num_devices = 0;
+	OneWireBus_SearchState hlt_search_state = {0};
+	found = false;
+	owb_search_first(owb_hlt, &hlt_search_state, &found);
+	while (found) {
+	    ++num_devices;
+	    owb_search_next(owb_hlt, &hlt_search_state, &found);
+	}
+
+	if (num_devices == 1) {
+	    ds18b20_init_solo(ds18b20_info, owb_hlt);                   // only one device on bus
+	    ds18b20_use_crc(ds18b20_info, true);                        // enable CRC check for temperature readings
+	    ds18b20_set_resolution(ds18b20_info, DS18B20_RESOLUTION);
+
+	    // Read temperatures more efficiently by starting conversions on all devices at the same time
+	    ds18b20_convert_all(owb_hlt);
+	    ds18b20_wait_for_conversion(ds18b20_info);
+	
+	    errors = ds18b20_read_temp(ds18b20_info, &readings);
+	
+	    if (xSemaphoreTake(xSemaphoreDS18B20, 25) == pdTRUE) {
+	    	if (errors == DS18B20_OK) {
+		    ds18b20_state->hlt_error = DS18B20_ERR_NONE;
+		    ds18b20_state->hlt_valid = true;
+		    ds18b20_state->hlt_temperature = readings;
+	    	} else {
+		    if (errors == DS18B20_ERROR_CRC)
+		    	ds18b20_state->hlt_error = DS18B20_ERR_CRC;
+		    if (errors == DS18B20_ERROR_OWB)
+		    	ds18b20_state->hlt_error = DS18B20_ERR_READ;
+		    if (errors == DS18B20_ERROR_DEVICE)
+		    	ds18b20_state->hlt_error = DS18B20_ERR_READ;
+		    ds18b20_state->hlt_valid = false;
+		    ds18b20_state->hlt_temperature = 0.0;
+	    	}
+		xSemaphoreGive(xSemaphoreDS18B20);
+	    }
+	} else {
+	    /*
+	     * Zero or more then one device, this is an error.
+	     */
+	    if (xSemaphoreTake(xSemaphoreDS18B20, 25) == pdTRUE) {
+	    	if (num_devices == 0)
+		    ds18b20_state->hlt_error = DS18B20_ERR_NOSENSOR;
+	    	else
+		    ds18b20_state->hlt_error = DS18B20_ERR_TOOMANY;
+	    	ds18b20_state->hlt_valid = false;
+	    	ds18b20_state->hlt_temperature = 0.0;
+		xSemaphoreGive(xSemaphoreDS18B20);
+	    }
+	} // if num_devices == 1
+
+#if 0
+	printf("MLT  %.3f  %d err %s, HLT  %.3f,  %d err %s\n",
+		ds18b20_state->mlt_temperature, ds18b20_state->mlt_error, (ds18b20_state->mlt_valid) ? "valid":" N/A ",
+		ds18b20_state->hlt_temperature, ds18b20_state->hlt_error, (ds18b20_state->hlt_valid) ? "valid":" N/A ");
+#endif
+	vTaskDelayUntil(&last_wake_time, SAMPLE_PERIOD / portTICK_PERIOD_MS);
+    }
+#endif
+
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+
+    float		Plate_MLT = 18.90;
+    extern int		MLT_pin, HLT_pin;
+
+    ESP_LOGI(TAG, "Starting Fake sensors");
+    ds18b20_state = malloc(sizeof(DS18B20_State));
+    ds18b20_state->mlt_valid = ds18b20_state->hlt_valid = true;
+    ds18b20_state->mlt_temperature = 18.90;
+    ds18b20_state->hlt_temperature = 18.70;
+    ds18b20_state->mlt_error = ds18b20_state->hlt_error = DS18B20_ERR_NONE;
+
+    /*
+     * Task loop forever. Update the temperatures each 750 mSeconds.
+     */
+    while (1) {
+
+	/*
+	 * Make this fake heater a bit more real by using a simulated heatplate.
+	 * We heatup that plate and then transfer the heat to the water.
+	 * That way we get a nice overshoot like in real life.
+	 */
+	if (MLT_pin) {
+	    if (Plate_MLT < 250.0)
+		Plate_MLT += SAMPLE_PERIOD * 0.001;   // Simulate plate upto 250 degrees
+	} else {
+	    if (Plate_MLT > Fake_MLT)
+		Plate_MLT -= SAMPLE_PERIOD * 0.00002 * (Plate_MLT - Fake_MLT);
+	}
+	// If plate is hotter then the water with a offset so that cooling later works.
+	if (Plate_MLT > (Fake_MLT + 5.0)) {
+	    if (Fake_MLT < 100.05)
+		Fake_MLT += SAMPLE_PERIOD * 0.000001 * (Plate_MLT - Fake_MLT);
+	}
+	// Allways loose heat to the air
+	if (Fake_MLT > 16.0) {
+	    Fake_MLT -= SAMPLE_PERIOD * 0.00000010 * (Fake_MLT - 16.0);
+	}
+
+	if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) {
+	    /*
+	     * There is a HLT function configured.
+	     */
+	    if (HLT_pin) {
+	    	if (Fake_HLT < 100.05)
+		    Fake_HLT += SAMPLE_PERIOD * 0.000055;
+	    } else {
+	    	if (Fake_HLT > 16.0)
+		    Fake_HLT -= SAMPLE_PERIOD * 0.00000006 * (Fake_HLT - 16.0);
+	    }
+	}
+
+	if (xSemaphoreTake(xSemaphoreDS18B20, 25) == pdTRUE) {
+	    ds18b20_state->mlt_temperature = ((int)(Fake_MLT * 16)) / 16.0;
+	    ds18b20_state->hlt_temperature = ((int)(Fake_HLT * 16)) / 16.0;
+	    xSemaphoreGive(xSemaphoreDS18B20);
+	}
+
+	vTaskDelay(SAMPLE_PERIOD / portTICK_PERIOD_MS);
+    }
+
+#endif
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_ds18b20.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,48 @@
+
+/**
+ * @file task_ds18b20.h
+ * @brief The FreeRTOS task to query the DS18B20 sensors on one or two
+ *        one-wire busses. Each bus must have only one sensor. That way
+ *        we don't need to care about the DS18B20 internal ROM address.
+ *        The task will update the DS18B20_State structure.
+ *
+ * Using a compile setting with make menuconfig it is possible to compile
+ * a simulator instead of real hardware sensors. The simulator includes
+ * fake kettles so that the temperature actually changes with the 
+ * activation of the kettle Solid State Relays. Only usefull for developers.
+ */
+
+#ifndef	_TASK_DS18B20_H
+#define	_TASK_DS18B20_H
+
+/*
+ * Error codes in this task
+ */
+#define	DS18B20_ERR_NONE		0
+#define DS18B20_ERR_NOSENSOR		1
+#define	DS18B20_ERR_TOOMANY		2
+#define	DS18B20_ERR_CRC			3
+#define	DS18B20_ERR_READ		4
+
+
+/**
+ * @brief Structure containing the variables for the DS18B20 task.
+ */
+typedef struct {
+    bool   mlt_valid;			///< MLT sensor valid reading.
+    double mlt_temperature;		///< Current MLT temperature.
+    int    mlt_error;			///< MLT error number.
+    bool   hlt_valid;			///< HLT sensor valid reading.
+    double hlt_temperature;		///< Current HLT temperature.
+    int    hlt_error;			///< HLT error number.
+} DS18B20_State;
+
+
+/**
+ * @brief The FreeRTOS task to update the DS18B20 temperature sensors.
+ */
+void task_ds18b20(void *);
+
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_http.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,529 @@
+/**
+ * @file task_http.c
+ * @brief HTTP and Websocket server functions.
+ *        This uses some ESP32 Websocket code written by Blake Felt - blake.w.felt@gmail.com
+ */
+#include "config.h"
+#include "mbedtls/base64.h"
+#include "mbedtls/sha1.h"
+
+
+const char			*TAG = "task_http";
+static QueueHandle_t		client_queue;
+const static int		client_queue_size = 10;
+static TaskHandle_t             xTaskHTTP  = NULL;
+static TaskHandle_t             xTaskQueue = NULL;
+
+
+
+void dump_buf(char *buf, uint16_t buflen);
+
+
+/**
+ * @brief Send HTTP error and message
+ * @param conn The socket to send to.
+ * @param error The HTTP error code.
+ * @param message Yhe human readable explanation.
+ * @paeam body The human readable explanation.
+ */
+static void http_error(struct netconn *conn, int error, char *message, char *body)
+{
+    char	*tmp = malloc(200);
+
+    ESP_LOGI(TAG, "Http response %d - %s", error, message);
+    if (strlen(body)) {
+	snprintf(tmp, 199, "HTTP/1.1 %d %s\r\nContent-type: text/plain\r\n\r\n%s\n", error, message, body);
+    } else {
+        snprintf(tmp, 199, "HTTP/1.1 %d %s\r\n\r\n", error, message);
+    }
+    netconn_write(conn, tmp, strlen(tmp), NETCONN_NOCOPY);
+    free(tmp);
+}
+
+
+
+/**
+ * @brief Send HTTP file from the filesystem.
+ * @param conn The network connection.
+ * @param url The requested url.
+ * @param mod_since The date/time of the file, or NULL.
+ * @param ipstr The IP address of the remote.
+ */
+static void http_sendfile(struct netconn *conn, char *url, char *mod_since, char *ipstr)
+{
+    char	temp_url[128], temp_url_gz[128], header[128], c_type[32];
+    struct stat	st;
+    off_t	filesize;
+    size_t	sentsize;
+    char	strftime_buf[64];
+    err_t	err;
+    bool	send_gz;
+    FILE	*f;
+
+    if (url[strlen(url) - 1] == '/') {
+	// If no filename given, use index.html
+	sprintf(temp_url, "/spiffs/w%sindex.html", url);
+    } else if (strncmp(url, "/log/", 5) == 0) {
+	// Logfiles are on the SD card.
+	sprintf(temp_url, "/sdcard/w%s", url);
+    } else {
+	sprintf(temp_url, "/spiffs/w%s", url);
+	for (int i = 0; i < 127; i++) {
+	    if (temp_url[i] == '?')
+		temp_url[i] = '\0';
+	    if (temp_url[i] == '\0')
+		break;
+	}
+    }
+    sprintf(temp_url_gz, "%s.gz", temp_url);
+
+    /*
+     * Get filesize and date for the response headers.
+     * Note that the /spiffs filesystem doesn't support file timestamps
+     * and returns the epoch for each file. Therefore we cannot use
+     * the cache based on timestamps.
+     */
+    filesize = 0;
+    strftime_buf[0] = '\0';
+    send_gz = false;
+    if (stat(temp_url_gz, &st) == 0) {
+	filesize = st.st_size;
+	strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime)));
+	send_gz = true;
+    } else if (stat(temp_url, &st) == 0) {
+	filesize = st.st_size;
+	strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime)));
+    }
+
+    /*
+     * If we have a If-Modified-Since parameter, compare that with the current
+     * filedate on disk. If It's the same send a 304 response.
+     * Cannot work on /spiffs.
+     */
+#if 0
+    time_t    Now;
+    struct tm timeInfo;
+    if (mod_since && strlen(strftime_buf)) {
+	time(&Now);
+	localtime_r(&Now, &timeInfo);
+	strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", &timeInfo);
+	sprintf(header, "HTTP/1.1 304 Not Modified\r\nDate: %s\r\n\r\n", strftime_buf);
+	netconn_write(conn, header, strlen(header), NETCONN_NOCOPY);
+	ESP_LOGI(TAG, "http_sendfile %s Not Modified, ok", temp_url);
+	return;
+    }
+#endif
+
+    if (send_gz) {
+	f = fopen(temp_url_gz, "r");
+    } else {
+    	f = fopen(temp_url, "r");
+    }
+    if (f == NULL) {
+	ESP_LOGI(TAG, "%s url \'%s\' file \'%s\' not found", ipstr, url, temp_url);
+	http_error(conn, 404, "Not found", "Not found");
+	return;
+    }
+
+    if (strcmp(".html", &temp_url[strlen(temp_url) - 5]) == 0) {
+	sprintf(c_type, "text/html");
+    } else if (strcmp(".css", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "text/css");
+    } else if (strcmp(".js", &temp_url[strlen(temp_url) - 3]) == 0) {
+	sprintf(c_type, "text/javascript");
+    } else if (strcmp(".json", &temp_url[strlen(temp_url) - 5]) == 0) {
+	sprintf(c_type, "text/json");
+    } else if (strcmp(".gz", &temp_url[strlen(temp_url) - 3]) == 0) {
+	sprintf(c_type, "application/x-gzip");
+    } else if (strcmp(".png", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "image/png");
+    } else if (strcmp(".svg", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "image/svg+xml");
+    } else if (strcmp(".oga", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "audio/ogg");
+    } else if (strcmp(".ico", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "image/x-icon");
+    } else if (strcmp(".xml", &temp_url[strlen(temp_url) - 4]) == 0) {
+	sprintf(c_type, "text/xml");
+    } else {
+	sprintf(c_type, "application/octet-stream");
+	printf("Unknown content type for %s\n", temp_url);
+    }
+
+    vTaskDelay(2 / portTICK_PERIOD_MS);
+    //  httpdHeader(connData, "Cache-Control", "max-age=3600, must-revalidate");
+    if (filesize) {
+	sprintf(header, "HTTP/1.1 200 OK\r\nLast-Modified: %s\r\nContent-Length: %ld\r\nContent-type: %s\r\n", strftime_buf, filesize,  c_type);
+    } else {
+	sprintf(header, "HTTP/1.1 200 OK\r\nContent-type: %s\r\n", c_type);
+    }
+    if (send_gz) {
+	sprintf(header, "%sContent-Encoding: gzip\r\n", header);
+    }
+    sprintf(header, "%s\r\n", header);	// Add last empty line.
+    err = netconn_write(conn, header, strlen(header), NETCONN_NOCOPY);
+    if (err != ERR_OK) {
+	ESP_LOGE(TAG, "%s sendfile %s%s err=%d on header write", ipstr, temp_url, (send_gz) ? ".gz":"", err);
+	fclose(f);
+	return;
+    }
+    // if (strstr(acceptEncodingBuffer, "gzip") == NULL)
+    // http_error(conn, 501, "Not implemented", "Your browser does not accept gzip-compressed data.");
+
+    sentsize = 0;
+    uint8_t	*buff = malloc(1024);
+    size_t      bytes;
+    int		pause = 0;
+
+    for (;;) {
+	bytes = fread(buff, 1, 1024, f);
+	if (bytes == 0)
+		break;
+
+	err = netconn_write(conn, buff, bytes, NETCONN_NOCOPY);
+	if (err != ERR_OK) {
+	    ESP_LOGE(TAG, "%s sendfile %s%s err=%d send %u bytes of %ld bytes", ipstr, temp_url,  (send_gz) ? ".gz":"", err, sentsize, filesize);
+	    break;
+	}
+	vTaskDelay(2 / portTICK_PERIOD_MS);
+	sentsize += bytes;
+	pause++;
+	if (pause > 50) { // 50 K
+	    pause = 0;
+	    vTaskDelay(50 / portTICK_PERIOD_MS);
+	}
+    }
+    fclose(f);
+    free(buff);
+	
+    if (sentsize == filesize) {
+	ESP_LOGI(TAG, "%s sendfile %s%s sent %u bytes, ok (%s)", ipstr, temp_url,  (send_gz) ? ".gz":"", sentsize, url);
+    }
+}
+
+
+
+/**
+ * @brief Handle VNC websocket events.
+ */
+void websockify_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len)
+{
+    switch(type) {
+	case WEBSOCKET_CONNECT:
+			ESP_LOGI(TAG,"Websockify client %i connected!",num);
+			break;
+
+	case WEBSOCKET_DISCONNECT_EXTERNAL:
+			ESP_LOGI(TAG,"Websockify client %i sent a disconnect message",num);
+			VncStopWS(num);
+			break;
+
+	case WEBSOCKET_DISCONNECT_INTERNAL:
+			ESP_LOGI(TAG,"Websockify client %i was disconnected",num);
+			VncStopWS(num);
+			break;
+
+	case WEBSOCKET_DISCONNECT_ERROR:
+			ESP_LOGI(TAG,"Websockify client %i was disconnected due to an error",num);
+			VncStopWS(num);
+			break;
+	
+	case WEBSOCKET_TEXT:
+			ESP_LOGI(TAG,"Websockiify client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg);
+			break;
+
+	case WEBSOCKET_BIN:
+			VncGetWSmessage(msg, len);
+			//dump_buf(msg, len);	
+			break;
+
+	case WEBSOCKET_PING:
+			ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(uint32_t)len,msg);
+			break;
+
+	case WEBSOCKET_PONG:
+			ESP_LOGI(TAG,"client %i responded to the ping",num);
+			break;
+    }
+}
+
+
+
+/**
+ * @brief Handle web ui websocket events.
+ */
+void websock_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len)
+{
+    switch(type) {
+	case WEBSOCKET_CONNECT:
+			ESP_LOGI(TAG,"Websocket client %i connected!",num);
+			break;
+										            
+	case WEBSOCKET_DISCONNECT_EXTERNAL:
+			ESP_LOGI(TAG,"Websocket client %i sent a disconnect message",num);
+			break;
+
+	case WEBSOCKET_DISCONNECT_INTERNAL:
+			ESP_LOGI(TAG,"Websocket client %i was disconnected",num);
+			break;
+
+	case WEBSOCKET_DISCONNECT_ERROR:
+			ESP_LOGI(TAG,"Websocket client %i was disconnected due to an error",num);
+			break;
+
+	case WEBSOCKET_TEXT:
+			ESP_LOGI(TAG,"Websocket client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg);
+			dump_buf(msg, len);
+			break;
+
+	case WEBSOCKET_BIN:
+			ESP_LOGI(TAG,"Websocket client %i sent bin message of size %i:\n",num,(uint32_t)len);
+			dump_buf(msg, len);
+			break;
+
+	case WEBSOCKET_PING:
+			ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(uint32_t)len,msg);
+			break;
+
+	case WEBSOCKET_PONG:
+			ESP_LOGI(TAG,"client %i responded to the ping",num);
+			break;
+    }
+}
+
+
+
+void dump_buf(char *buf, uint16_t buflen)
+{
+    int i = 0, l = 1;
+
+    printf("request length %d\n00: ", buflen);
+    for (;;) {
+	if (i >= buflen)
+	    break;
+	if ((buf[i] < ' ') || (buf[i] > 126)) {
+	    if (buf[i] == '\n') {
+		printf("\\n\n%02d: ", l);
+		l++;
+	    } else if (buf[i] == '\r') {
+		printf("\\r");
+	    } else {
+		printf("\\%02x", buf[i]);
+	    }
+	} else {
+	    printf("%c", buf[i]);
+	}
+	i++;
+    }
+    printf("\n");
+}
+
+
+
+/**
+ * @brief Serve any client.
+ * @param http_client_sock The socket on which the client is connected.
+ */
+static void http_serve(struct netconn *conn)
+{
+    char		ipstr[IPADDR_STRLEN_MAX];
+    struct netbuf	*inbuf;
+    static char		*buf;
+    static uint16_t	buflen;
+    static err_t	err;
+    char		url[128], *p, *mod_since = NULL;
+    ip_addr_t		remote_addr;
+    uint16_t		remote_port;
+
+    if (netconn_getaddr(conn, &remote_addr, &remote_port, 0) == ERR_OK) {
+	strcpy(ipstr, ip4addr_ntoa(ip_2_ip4(&remote_addr)));
+    } else {
+	ipstr[0] = '\0';
+    }
+
+    netconn_set_recvtimeout(conn,1000); // allow a connection timeout of 1 second
+    err = netconn_recv(conn, &inbuf);
+
+    if (err != ERR_OK) {
+	if (err != ERR_TIMEOUT) {	// Ignore timeout
+	    ESP_LOGI(TAG,"%s error %d on read", ipstr, err);
+	}
+	netconn_close(conn);
+	netconn_delete(conn);
+	netbuf_delete(inbuf);
+	return;
+    }
+
+    netbuf_data(inbuf, (void**)&buf, &buflen);
+
+    if (buf) {
+	/*
+	 * Build url string from the data buffer.
+	 * It looks like: GET /app/localization.js HTTP/1.1
+	 */
+	for (int i = 0; i < 10; i++) {
+	    if (buf[i] == ' ') {
+		i++;
+		for (int j = i; j < 128; j++) {
+		    url[j-i] = buf[j];
+		    if (url[j-i] == ' ') {
+			url[j-i] = '\0';
+			break;
+		    }
+		}
+		break;
+	    }
+	}
+
+	if (strstr(buf, "GET /logfiles.json")) {
+	    char temp[64];
+	    FILE *dest = fopen("/spiffs/w/logfiles.json", "w");
+	    if (dest) {
+	    	fprintf(dest, "{\"Dir\":[{\"Folder\":\"/log\",\"Files\":[");
+	    	DIR *dir = opendir("/sdcard/w/log");
+	    	if (dir) {
+		    struct dirent* de = readdir(dir);
+		    struct stat st;
+		    bool comma = false;
+		    while (de) {
+		    	sprintf(temp, "/sdcard/w/log/%s", de->d_name);
+		    	if (stat(temp, &st) == ESP_OK) {
+			    fprintf(dest, "%s{\"File\":\"%s\",\"Size\":%ld,\"Date\":%ld}", (comma)?",":"", de->d_name, st.st_size, st.st_mtime);
+			    comma = true;
+		    	}
+		    	de = readdir(dir);
+		    	vTaskDelay(5 / portTICK_PERIOD_MS);
+		    }
+		    closedir(dir);
+	    	}
+	    	fprintf(dest, "]}]}");
+	    	fclose(dest);
+	    }
+	    http_sendfile(conn, "/logfiles.json", NULL, ipstr);
+	    netconn_close(conn);
+	    netconn_delete(conn);
+	    netbuf_delete(inbuf);
+	    unlink("/spiffs/w/logfiles.json");
+	    return;
+	}
+
+	// http requests
+	if (! strstr(buf,"Upgrade: websocket")) {	// Not websocket
+	    p = strstr(buf, "If-Modified-Since:");	// May be cached
+	    if (p) {
+		size_t mod_len = strcspn(p, " ");
+		p += (int)(mod_len + 1);
+		mod_len = strcspn(p, "\r\n");
+		mod_since = malloc(mod_len + 2);
+		memcpy(mod_since, p, mod_len);
+		mod_since[mod_len] = '\0';
+	    }
+	    http_sendfile(conn, url, mod_since, ipstr);
+	    if (mod_since)
+	        free(mod_since);
+	    mod_since = NULL;
+	    netconn_close(conn);
+	    netconn_delete(conn);
+	    netbuf_delete(inbuf);
+	    return;
+	}
+
+	// websocket for noVNC.
+	if ((strstr(buf,"GET /websockify ") && strstr(buf,"Upgrade: websocket"))) {
+	    int nr = ws_server_add_client_protocol(conn, buf, buflen, "/websockify", "binary", websockify_callback);
+	    ESP_LOGI(TAG, "%s new websocket on /websockify client: %d", ipstr, nr);
+	    netbuf_delete(inbuf);
+	    VncStartWS(nr);
+	    return;
+	}
+
+	// websocket for web UI.
+	if ((strstr(buf,"GET /ws ") && strstr(buf,"Upgrade: websocket"))) {
+	    int nr = ws_server_add_client_protocol(conn, buf, buflen, "/ws", "binary", websock_callback);
+	    ESP_LOGI(TAG, "%s new websocket on /ws client: %d", ipstr, nr);
+	    netbuf_delete(inbuf);
+	    TFTstartWS(nr);
+	    // Startup something? Init webscreen?
+	    return;
+	}
+
+	dump_buf(buf, buflen);
+
+	if (strstr(buf, "GET /")) {
+		ESP_LOGI(TAG, "%s request: %s", ipstr, buf);
+		http_error(conn, 404, "Not found", "Not found");
+	} else {
+		http_error(conn, 405, "Invalid method", "Invalid method");
+	}
+    }
+
+    netconn_close(conn);
+    netconn_delete(conn);
+    netbuf_delete(inbuf);
+}
+
+
+
+/**
+ * @brief Handles clients when they first connect. passes to a queue
+ */
+static void task_HTTPserver(void* pvParameters)
+{
+    struct netconn	*conn, *newconn;
+    static err_t	err;
+
+    ESP_LOGI(TAG, "Starting http server_task");
+    client_queue = xQueueCreate(client_queue_size,sizeof(struct netconn*));
+
+    conn = netconn_new(NETCONN_TCP);
+    netconn_bind(conn,NULL,80);
+    netconn_listen(conn);
+
+    do {
+	err = netconn_accept(conn, &newconn);
+	if (err == ERR_OK) {
+	    if (xQueueSendToBack(client_queue,&newconn,portMAX_DELAY) != pdTRUE) {
+		ESP_LOGE(TAG, "xQueueSendToBack() queue full");	    
+	    };
+	}
+	vTaskDelay(5 / portTICK_PERIOD_MS);
+    } while (err == ERR_OK);
+
+    ESP_LOGE(TAG, "Stopping http server_task");
+    netconn_close(conn);
+    netconn_delete(conn);
+    vTaskDelete(NULL);
+}
+
+
+
+/**
+ * @brief Receives clients from queue and handle them.
+ */
+static void task_Queue(void* pvParameters)
+{
+    struct netconn* conn;
+
+    ESP_LOGI(TAG, "Starting Queue task");
+    for(;;) {
+	xQueueReceive(client_queue, &conn, portMAX_DELAY);
+	if (!conn)
+	    continue;
+	http_serve(conn);
+	vTaskDelay(2 / portTICK_PERIOD_MS);
+    }
+    ESP_LOGE(TAG, "Stopping Queue task");
+    vTaskDelete(NULL);
+}
+
+
+
+void start_http_websocket(void)
+{
+    ESP_LOGI(TAG, "Starting HTTP/Websocket server");
+
+    ws_server_start();
+    xTaskCreate(&task_HTTPserver, "HTTPserver", 3000, NULL, 9, &xTaskHTTP);
+    xTaskCreate(&task_Queue,      "Queue",      4000, NULL, 6, &xTaskQueue);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_http.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,15 @@
+/**
+ * @file task_http.h
+ * @brief HTTP and Websocket server functions.
+ */
+
+#ifndef	_TASK_HTTP_H
+#define	_TASK_HTTP_H
+
+
+/**
+ * @brief Start the HTTP/Websocket server.
+ */
+void start_http_websocket(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_sdcard.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,588 @@
+/**
+ * @file task_sdcard.c
+ * @brief SD/MMC driver. This driver is for a slot for the user on
+ *        the front panel. It will detect an inserted card and then
+ *        mount it. But, it cannot detect the removal of a card!!
+ *        Also, brew logging is handled here and finished brewlogs
+ *        are copied to the SD card if it is mounted.
+ *        Recipes to import must go into the /sdcard/recipe folder
+ *        and have extension .xml and the contents be a beerxml file.
+ */
+
+
+#include "vfs_fat_internal.h"
+#include "driver/sdmmc_host.h"
+#include "driver/sdspi_host.h"
+#include "sdmmc_cmd.h"
+#include "diskio.h"
+
+#include "config.h"
+
+
+
+SDCARD_State		* sdcard_state;
+JSON_log		* json_log;
+EventGroupHandle_t	xEventGroupSDcard;		///< SD card events.
+
+static const char	*TAG = "task_sdcard";
+static sdmmc_card_t* 	s_card = NULL;
+static uint8_t		s_pdrv = 0;
+static char		* s_base_path = NULL;
+
+BYTE pdrv = 0xFF;
+
+
+#define SDCARD_HOST_SLOT	VSPI_HOST		///< HSPI_HOST is used by the TFT
+#define SDCARD_PIN_NUM_MISO	2
+#define SDCARD_PIN_NUM_MOSI	15
+#define SDCARD_PIN_NUM_CLK	14
+#define SDCARD_PIN_NUM_CS	13
+#define	SDCARD_DMA_CHANNEL	2			///< Channel 1 is used by the TFT
+
+
+const int TASK_SDCARD_LOG_CLEAN = BIT0;			///< Clean spiffs logfile directory
+const int TASK_SDCARD_LOG_CLOSE = BIT1;			///< Close the logfile.
+
+extern time_t		now;
+extern struct tm	timeinfo;
+
+
+
+void log_begin(time_t t)
+{
+    struct tm	timeinfo;
+
+    /*
+     * If there is an open logfile from an crashed brew, open that one.
+     */
+    if (! strlen(sdcard_state->logfile) && strlen(runtime.Logfile)) {
+	sprintf(sdcard_state->logfile, "%s", runtime.Logfile);
+	ESP_LOGI(TAG, "Resumed %s", sdcard_state->logfile);
+    } else {
+    	localtime_r(&t, &timeinfo);
+    	if (timeinfo.tm_year > (2016 - 1900)) {
+	    // Valid time, construct the filename.
+	    strftime(sdcard_state->logfile, sizeof(sdcard_state->logfile), "br%y%m%d%H%M", &timeinfo);
+    	} else {
+	    sprintf(sdcard_state->logfile, "brewlog");
+    	}
+	ESP_LOGI(TAG, "Create %s", sdcard_state->logfile);
+    	sprintf(runtime.Logfile, "%s", sdcard_state->logfile);
+    }
+}
+
+
+
+void log_close(void)
+{
+    xEventGroupSetBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLOSE);
+}
+
+
+
+void log_clean(void)
+{
+    xEventGroupSetBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLEAN);
+}
+
+
+
+void log_json(void)
+{
+    char        filename[32], strftime_buf[64];
+    FILE	*f;
+    bool	addcomma = true;
+
+    if (strlen(sdcard_state->logfile)) {
+	sprintf(filename, "/spiffs/log/%s.json", sdcard_state->logfile);
+	f = fopen(filename, "r");
+	if (f == NULL) {
+	    // Create the file and add the header
+	    f = fopen(filename, "a");
+	    if (f) {
+		strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
+		fprintf(f, "{\n");
+		fprintf(f, " \"brew\": [{\n");
+		fprintf(f, "  \"Recipe\":\"%s\",\n", recipe.Name);
+		fprintf(f, "  \"Date\":\"%s\",\n", strftime_buf);
+		fprintf(f, "  \"brewdata\":[\n");
+		addcomma = false;
+		fclose(f);
+	    } else {
+		ESP_LOGE(TAG, "Failed to create %s error %d", filename, errno);
+		return;
+	    }
+	} else {
+	    fclose(f);	// Was open for reading.
+	}
+
+	f = fopen(filename, "a");
+	if (f) {
+	    if (addcomma) {
+		fprintf(f, ",\n");
+	    }
+	    addcomma = true;
+	    fprintf(f, "   {\"MLT_sp\":\"%.3f\",\"MLT_pv\":\"%.3f\",\"MLT_pwm\":\"%d\",\"MLT_tr\":\"%d\",\"Pump\":\"%d\",", 
+			    json_log->mlt_sp, json_log->mlt_pv, json_log->mlt_power, json_log->mlt_tempreached, json_log->pump_run);
+	    if (json_log->hlt_sp > 0.0) {
+	    	fprintf(f, "\"HLT_sp\":\"%.3f\",\"HLT_pv\":\"%.3f\",\"HLT_pwm\":\"%d\",", json_log->hlt_sp, json_log->hlt_pv, json_log->hlt_power);
+	    }
+	    fprintf(f, "\"Label\":\"%s\"}", json_log->time);
+	    printf("{\"MLT_sp\":\"%.3f\",\"MLT_pv\":\"%.3f\",\"MLT_pwm\":\"%d\",\"MLT_tr\":\"%d\",\"Pump\":\"%d\",",
+			    json_log->mlt_sp, json_log->mlt_pv, json_log->mlt_power, json_log->mlt_tempreached, json_log->pump_run);
+	    if (json_log->hlt_sp > 0.0) {
+	    	printf("\"HLT_sp\":\"%.3f\",\"HLT_pv\":\"%.3f\",\"HLT_pwm\":\"%d\",", json_log->hlt_sp, json_log->hlt_pv, json_log->hlt_power);
+	    }
+	    printf("\"Label\":\"%s\"}\n", json_log->time);
+	    fclose(f);
+	}
+    }
+}
+
+
+
+void log_annotation(int annotation_type, char *label)
+{
+    char        filename[32];
+    char	bordercolor[9], color[9], pos[8];
+    FILE        *f;
+    bool        addcomma = true;
+
+    if (strlen(sdcard_state->logfile)) {
+	sprintf(filename, "/spiffs/log/%s.anno", sdcard_state->logfile);
+
+	switch (annotation_type) {
+	    case ANNOTATION_STAGE:	snprintf(bordercolor, 8, "#8942f4");
+					snprintf(color, 8, "#00215b");
+					snprintf(pos, 7, "bottom");
+		    			break;
+
+	    case ANNOTATION_EVENT:	snprintf(bordercolor, 8, "#42f445");
+					snprintf(color, 8, "#00215b");
+					snprintf(pos, 7, "top");
+					break;
+
+	    case ANNOTATION_SYSTEM:	snprintf(bordercolor, 8, "black");
+					snprintf(color, 8, "red");
+					snprintf(pos, 7, "center");
+	 				break;
+	}
+
+	// Check if the file exists to see if we need to insert a comma.
+	f = fopen(filename, "r");
+	if (f == NULL) {
+	    addcomma = false;
+	} else {
+	    fclose(f);
+	}
+
+	f = fopen(filename, "a");
+	if (f) {
+	    if (addcomma) {
+		fprintf(f, ",\n");
+	    }
+	    addcomma = true;
+	    fprintf(f, "{\"type\":\"line\",\"mode\":\"vertical\",\"scaleID\":\"x-axis-0\",\"value\":\"%s\",\"borderColor\":\"%s\",\"borderWidth\":2,\"label\":{\"backgroundColor\":\"%s\",\"content\":\"%s\",\"position\":\"%s\",\"enabled\":true}}", json_log->time, bordercolor, color, label, pos);
+	    fclose(f);
+	    printf(    "{\"type\":\"line\",\"mode\":\"vertical\",\"scaleID\":\"x-axis-0\",\"value\":\"%s\",\"borderColor\":\"%s\",\"borderWidth\":2,\"label\":{\"backgroundColor\":\"%s\",\"content\":\"%s\",\"position\":\"%s\",\"enabled\":true}}\n", json_log->time, bordercolor, color, label, pos);
+	}
+    }
+}
+
+
+
+/*
+ * This is a local modified version of the esp_vfs_fat_sdmmc_mount() function in
+ * the FreeRTOS components library. It is here so we can better handle errors
+ * for our application.
+ */
+esp_err_t my_vfs_fat_sdmmc_init(const char* base_path, const sdmmc_host_t* host_config, const void* slot_config)
+{
+    if (s_card != NULL) {
+	return ESP_ERR_INVALID_STATE;
+    }
+
+    // connect SDMMC driver to FATFS
+    pdrv = 0xFF;
+    if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == 0xFF) {
+	ESP_LOGI(TAG, "the maximum count of volumes is already mounted");
+	return ESP_ERR_NO_MEM;
+    }
+
+    s_base_path = strdup(base_path);
+    if (!s_base_path) {
+	ESP_LOGI(TAG, "could not copy base_path");
+	return ESP_ERR_NO_MEM;
+    }
+    esp_err_t err = ESP_OK;
+    s_card = malloc(sizeof(sdmmc_card_t));
+    if (s_card == NULL) {
+	err = ESP_ERR_NO_MEM;
+	goto fail;
+    }
+
+    err = (*host_config->init)();
+    if (err != ESP_OK) {
+	ESP_LOGI(TAG, "host init returned rc=0x%x", err);
+	goto fail;
+    }
+
+    // configure SD slot
+    if (host_config->flags == SDMMC_HOST_FLAG_SPI) {
+	err = sdspi_host_init_slot(host_config->slot, (const sdspi_slot_config_t*) slot_config);
+    } else {
+	err = sdmmc_host_init_slot(host_config->slot, (const sdmmc_slot_config_t*) slot_config);
+    }
+    if (err != ESP_OK) {
+	ESP_LOGI(TAG, "slot_config returned rc=0x%x", err);
+	goto fail;
+    }
+    return ESP_OK;
+
+fail:
+    host_config->deinit();
+    free(s_card);
+    s_card = NULL;
+    return err;
+}
+
+
+
+esp_err_t my_esp_vfs_fat_sdmmc_mount(const char* base_path,
+	const sdmmc_host_t* host_config,
+	const void* slot_config,
+	const esp_vfs_fat_mount_config_t* mount_config, 
+	sdmmc_card_t** out_card)
+{
+    FATFS* fs = NULL;
+    esp_err_t err = ESP_OK;
+
+    if (s_card == NULL) {
+	return ESP_ERR_INVALID_STATE;
+    }
+
+    // probe and initialize card
+    err = sdmmc_card_init(host_config, s_card);
+    if (err != ESP_OK) {
+	if (err != ESP_ERR_INVALID_RESPONSE) {  // No card present, do not log
+	    ESP_LOGI(TAG, "sdmmc_card_init failed 0x(%x)", err);
+	}
+	goto fail;
+    }
+    if (out_card != NULL) {
+	*out_card = s_card;
+    }
+
+    ff_diskio_register_sdmmc(pdrv, s_card);
+    s_pdrv = pdrv;
+    ESP_LOGD(TAG, "using pdrv=%i", pdrv);
+    char drv[3] = {(char)('0' + pdrv), ':', 0};
+
+    // connect FATFS to VFS
+    err = esp_vfs_fat_register(base_path, drv, mount_config->max_files, &fs);
+    if (err == ESP_ERR_INVALID_STATE) {
+    	// it's okay, already registered with VFS
+    } else if (err != ESP_OK) {
+    	ESP_LOGI(TAG, "esp_vfs_fat_register failed 0x(%x)", err);
+    	goto fail;
+    }
+
+    // Try to mount partition
+    FRESULT res = f_mount(fs, drv, 1);
+    if (res != FR_OK) {
+	err = ESP_FAIL;
+	ESP_LOGD(TAG, "f_mount failed (%d)", res);
+	goto fail;
+    }
+    return ESP_OK;
+
+fail:
+    if (fs) {
+	f_mount(NULL, drv, 0);
+    }
+    esp_vfs_fat_unregister_path(base_path);
+    ff_diskio_unregister(pdrv);
+    return err;
+}
+
+
+
+esp_err_t my_esp_vfs_fat_sdmmc_unmount()
+{
+    if (s_card == NULL) {
+	return ESP_ERR_INVALID_STATE;
+    }
+    // unmount
+    char drv[3] = {(char)('0' + s_pdrv), ':', 0};
+    f_mount(0, drv, 0);
+    return ESP_OK;
+}
+
+
+
+int FileCopy(char *ff, char *tf)
+{
+    FILE	*f, *t;
+    uint8_t	buf[512];
+    size_t	bytes;
+
+    f = fopen(ff, "r");
+    if (f == NULL) {
+	ESP_LOGE(TAG, "FileCopy cannot open %s for read, error %d", ff, errno);
+	return 1;
+    }
+
+    t = fopen(tf, "w+");
+    if (t == NULL) {
+	ESP_LOGE(TAG, "FileCopy cannot open %s for create/write, error %d", tf, errno);
+	fclose(f);
+	return 1;
+    }
+
+    while ((bytes = fread(&buf, 1, 512, f))) {
+	fwrite(&buf, 1, bytes, t);
+	vTaskDelay(10 / portTICK_PERIOD_MS);
+    }
+
+    fclose(f);
+    fclose(t);
+    return 0;
+}
+
+
+
+void SyncDirs(char *fromdir, char *todir)
+{
+    char	ff[64], tf[64];
+    struct stat	fs, ts;
+    int		rc;
+
+    ESP_LOGI(TAG, "SyncDirs(%s, %s)", fromdir, todir);
+
+    DIR *dir = opendir(fromdir);
+    struct dirent* de = readdir(dir);
+    while (de) {
+	if (de->d_type == DT_REG) {
+	    sprintf(ff, "%s/%s", fromdir, de->d_name);
+	    if (stat(ff, &fs) == ESP_OK) {
+
+	    	sprintf(tf, "%s/%s", todir, de->d_name);
+		if (stat(tf, &ts) != ESP_OK) {
+		    ts.st_size = 0;
+		}
+
+		if (fs.st_size && (fs.st_size != ts.st_size)) {
+		    rc = FileCopy(ff, tf);
+		    ESP_LOGI(TAG, "Copy %s to %s, %ld bytes, rc=%d", ff, todir, fs.st_size, rc);
+		}
+	    }
+	}
+	de = readdir(dir);
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+    closedir(dir);
+
+    /*
+     * Now see if we need to remove files.
+     */
+    dir = opendir(todir);
+    de = readdir(dir);
+    while (de) {
+	sprintf(tf, "%s/%s", todir, de->d_name);
+	sprintf(ff, "%s/%s", fromdir, de->d_name);
+
+	if (stat(ff, &fs) != ESP_OK) {
+	    if (unlink(tf) == ESP_OK) {
+		ESP_LOGI(TAG, "Removed %s", tf);
+	    }
+	}
+
+	de = readdir(dir);
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+    closedir(dir);
+}
+
+
+
+void task_sdcard(void *pvParameter)
+{
+    sdmmc_card_t*	card;
+    esp_err_t		ret;
+    EventBits_t		uxBits;
+    char		filename[64];
+
+    sdcard_state = malloc(sizeof(SDCARD_State));
+    sdcard_state->host_ok = false;
+    sdcard_state->card_present = false;
+    sdcard_state->logfile[0] = '\0';
+
+    json_log = malloc(sizeof(JSON_log));
+
+    ESP_LOGI(TAG, "Starting SD card");
+    sdmmc_host_t host = SDSPI_HOST_DEFAULT();
+    host.slot = SDCARD_HOST_SLOT;	// HSPI_HOST is in use by the TFT.
+    sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT();
+    slot_config.gpio_miso   = SDCARD_PIN_NUM_MISO;
+    slot_config.gpio_mosi   = SDCARD_PIN_NUM_MOSI;
+    slot_config.gpio_sck    = SDCARD_PIN_NUM_CLK;
+    slot_config.gpio_cs     = SDCARD_PIN_NUM_CS;
+    slot_config.dma_channel = SDCARD_DMA_CHANNEL;
+    // This initializes the slot without card detect (CD) and write protect (WP) signals.
+    // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
+
+    /*
+     * No errors from the sdmmc_cmd driver.
+     */
+    esp_log_level_set("sdmmc_cmd", ESP_LOG_NONE);
+
+    /*
+     * Options for mounting the filesystem.
+     * If format_if_mount_failed is set to true, SD card will be partitioned and
+     * formatted in case when mounting fails.
+     */
+    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
+	.format_if_mount_failed = false,
+	.max_files = 5,
+	.allocation_unit_size = 16 * 1024
+    };
+
+    ret = my_vfs_fat_sdmmc_init("/sdcard", &host, &slot_config);
+    if (ret == ESP_OK) {
+	ESP_LOGI(TAG, "SPI card interface ready");
+	sdcard_state->host_ok = true;
+    } else {
+	ESP_LOGE(TAG, "SPI card interface failed, abort task");
+	vTaskDelete(NULL);
+	return;
+    }
+
+    xEventGroupSDcard = xEventGroupCreate();
+
+    /*
+     * Task loop, continues check of the inserted cards.
+     */
+    while (1) {
+
+	if (sdcard_state->card_present == false) {
+	    /*
+	     * If the card is not mounted, try it.
+	     */
+	    ret = my_esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);
+	    if (ret == ESP_OK) {
+		ESP_LOGI(TAG, "SD card mounted on /sdcard");
+		sdcard_state->card_present = true;
+
+		DIR* dir = opendir("/sdcard/w/log");
+		if (dir == NULL) {
+		    ret = mkdir("/sdcard/w/log", 0755);
+		    printf("Dir created ret=%d\n", ret);
+		} else {
+		    closedir(dir);
+		}
+
+
+		SyncDirs("/sdcard/w",            "/spiffs/w");
+		SyncDirs("/sdcard/w/js",         "/spiffs/w/js");
+		SyncDirs("/sdcard/w/css",         "/spiffs/w/css");
+//		SyncDirs("/sdcard/w/js/modl" ,   "/spiffs/w/js/modl"); //
+//		SyncDirs("/sdcard/w/js/utils",   "/spiffs/w/js/utils");
+//		SyncDirs("/sdcard/w/js/zlib",    "/spiffs/w/js/zlib");
+//		SyncDirs("/sdcard/w/core",       "/spiffs/w/core");
+//		SyncDirs("/sdcard/w/core/input", "/spiffs/w/core/input");
+//		SyncDirs("/sdcard/w/core/util",  "/spiffs/w/core/util");
+//		SyncDirs("/sdcard/w/app",        "/spiffs/w/app");
+//		SyncDirs("/sdcard/w/app/images", "/spiffs/w/app/images");
+//		SyncDirs("/sdcard/w/app/locale", "/spiffs/w/app/locale");
+//		SyncDirs("/sdcard/w/app/sounds", "/spiffs/w/app/sounds");  //
+		SyncDirs("/sdcard/w/app/styles", "/spiffs/w/app/styles");
+//		SyncDirs("/sdcard/fonts",        "/spiffs/fonts"); //
+
+	    }
+	} else {
+	    /*
+	     * Check if the mounted card is still in the slot.
+	     */
+	    DIR* dir = opendir("/sdcard/w/log");
+	    if (dir == NULL) {
+		ESP_LOGI(TAG, "SD card missing, unmount");
+		my_esp_vfs_fat_sdmmc_unmount();
+		sdcard_state->card_present = false;
+	    } else {
+		closedir(dir);
+	    }
+	}
+	
+	uxBits = xEventGroupWaitBits(xEventGroupSDcard, 
+			TASK_SDCARD_LOG_CLEAN | TASK_SDCARD_LOG_CLOSE, pdFALSE, pdFALSE, 1000 / portTICK_PERIOD_MS);
+	
+	if (uxBits & TASK_SDCARD_LOG_CLEAN) {
+	    DIR   *dir = opendir("/spiffs/log");
+	    char  lf[32];
+
+	    if (dir != NULL) {
+		struct dirent   *de = readdir(dir);
+	    	while (de) {
+		    sprintf(lf, "/spiffs/log/%s", de->d_name);
+		    if (unlink(lf) == ESP_OK) {
+		    	ESP_LOGI(TAG, "Removed old %s", lf);
+		    }
+		    de = readdir(dir);
+		    vTaskDelay(2 / portTICK_PERIOD_MS);
+	    	}
+	    	closedir(dir);
+	    }
+	    xEventGroupClearBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLEAN);
+	}
+
+	if (uxBits & TASK_SDCARD_LOG_CLOSE) {
+	    // Close the logfile.
+	    if (strlen(sdcard_state->logfile) && (sdcard_state->card_present == true)) {
+		ESP_LOGI(TAG, "Closing logfile");
+		char	destname[64];
+		sprintf(destname, "/sdcard/w/log");
+		int rc = mkdir(destname, 0755);
+		if (rc && (errno != EEXIST)) {
+		    ESP_LOGE(TAG, "Cannot create %s error %d", destname, errno);
+		} else {
+		    sprintf(filename, "/spiffs/log/%s.json", sdcard_state->logfile);
+		    // First close the JSON data records
+		    FILE *f = fopen(filename, "a+");
+		    if (f) {
+			fprintf(f, "  ],\n");	// End of brewdata
+			fprintf(f, "  \"annotations\":[\n");
+			// Insert annotation records
+			sprintf(destname, "/spiffs/log/%s.anno", sdcard_state->logfile);
+			FILE *a = fopen(destname, "r");
+			char buf[256];
+			if (a) {
+			    while(true) {
+				if (fgets(buf, sizeof(buf), a)) {
+				    fprintf(f, "%s", buf);
+				} else {
+				    break;
+				}
+			    }
+			    fclose(a);
+			    unlink(destname);
+			}
+			fprintf(f, "  ]\n");	// End of annotations
+			fprintf(f, " }]\n");	// End of brew
+			fprintf(f, "}\n");
+			fclose(f);
+		    }
+		    sprintf(destname, "/sdcard/w/log/%s.json", sdcard_state->logfile);
+		    if (FileCopy(filename, destname) == 0) {
+			ESP_LOGI(TAG, "JSON file copied to %s", destname);
+			unlink(filename);
+		    }
+		}
+	    }
+	    sdcard_state->logfile[0] = '\0';		// Clear logfile name
+	    runtime.Logfile[0] = '\0';
+	    xEventGroupClearBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLOSE);
+	}
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_sdcard.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,80 @@
+#ifndef	_TASK_SDCARD_H
+#define	_TASK_SDCARD_H
+
+/**
+ * @file task_sdcard.h
+ * @brief The interface to the FreeRTOS SD/MMC card task.
+ */
+
+
+typedef enum
+{
+    ANNOTATION_STAGE = 0,		///< Annotate stage event
+    ANNOTATION_EVENT,			///< Annotate an event
+    ANNOTATION_SYSTEM,			///< Annotate a system event.
+} ANNOTATION_TYPES;
+
+/**
+ * @brief Structure containing the information of the SD cards task.
+ */
+typedef struct {
+    bool	host_ok;		///< SPI host is Ok.
+    bool	card_present;		///< If card is present in the slot.
+    char	logfile[32];		///< Current logfile name.
+} SDCARD_State;
+
+/**
+ * @brief JSON log data
+ */
+typedef struct {
+    char	time[9];		///< Time in the brew.
+    float	mlt_sp;			///< MLT setpoint.
+    float	mlt_pv;			///< MLT temperature.
+    int		mlt_power;		///< MLT power in %
+    int		mlt_tempreached;	///< MLT temperature reached (0 or 1).
+    int		pump_run;		///< Pump run (0 or 1).
+    float	hlt_sp;			///< HLT setpoint.
+    float	hlt_pv;			///< HLT temperature.
+    int		hlt_power;		///< HLT power in %.
+    char	event[64];		///< Event message.
+} JSON_log;
+
+/**
+ * @brief Begin a new logfile. The logfile is created and written to on
+ *        the spiffs filesystem.
+ * @param t The time in seconds since the Epoch, to create the filename.
+ */
+void log_begin(time_t t);
+
+/**
+ * @brief Close an open logfile. The logfile is moved to the SD card if
+ *        the card is present and mounted. Then, the temporary logfile
+ *        is removed.
+ */
+void log_close(void);
+
+/**
+ * @brief Clean logfile directory /spiffs/log/
+ */
+void log_clean(void);
+
+/**
+ * @brief Log to JSON logfile.
+ */
+void log_json(void);
+
+/**
+ * @brief Log annotations for the chart. These are stored in a temporary 
+ *        file and later when the logfile is closed they are merged in 
+ *        the main json logfile.
+ * @param annotation_type The type of annotation, stage/event/system.
+ * @param label The label text that will be seen in the chart.
+ */
+void log_annotation(int annotation_type, char *label);
+
+/**
+ * @brief FreeRTOS sd card task. 
+ */
+void task_sdcard(void *);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_sound.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,139 @@
+/**
+ * @file task_sound.c
+ * @brief BrewBoard Sound driver. Plays WAVE audio, Microsoft PCM, 8 bit, mono 16000 Hz
+ *        files from the /spiffs/sound/ directory.
+ */
+
+#include "config.h"
+
+
+#define	PIEZO_BUZZER	CONFIG_BUZZER_GPIO
+
+EventGroupHandle_t	xEventGroupSound;
+
+
+static const char       *TAG = "task_sound";
+
+const int SOUND_STARTUP = BIT0;			///< When set, play startup sound.
+const int SOUND_PROMPT = BIT1;			///< When set, play prompt sound.
+const int SOUND_TEMPREACHED = BIT2;		///< When set, play temperature reached sound.
+const int SOUND_TIMEOUT = BIT3;			///< When set, play time out sound.
+const int SOUND_ADDHOP = BIT4;			///< When set, play add hop sound.
+const int SOUND_END = BIT5;			///< When set, play finished sound.
+const int SOUND_WARN = BIT6;			///< When set, play warning sound.
+
+
+const uint16_t		_sound_Startup[]	= { 50, 10, 10, 10, 10, 10, 10, 10, 10, 10, 200, 
+							10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 
+							10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 
+							10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 
+							10, 10, 10, 10, 10, 10, 10, 10, 10, 10 };
+const uint16_t		_sound_Prompt[]		= { 2, 50, 125 };
+const uint16_t		_sound_TempReached[]	= { 6, 250, 75, 250, 75, 250, 75};
+const uint16_t		_sound_TimeOut[]	= {10, 25, 975, 25, 975, 25, 975, 25, 975, 500, 500 }; // 5 seconds
+const uint16_t		_sound_AddHop[]		= {18, 50, 50, 50, 50, 50, 225, 150, 50, 150, 50, 150, 225, 50, 50, 50 , 50 , 50, 50 };
+const uint16_t		_sound_End[]		= { 2, 1000, 75 };
+const uint16_t		_sound_Warn[]		= { 6, 100, 75, 100, 75, 100, 50 };
+
+
+
+void BuzzerPlay(const uint16_t *sound)
+{
+    for (int i = 1; i <= sound[0]; i += 2) {
+	gpio_set_level(PIEZO_BUZZER, 1);
+	vTaskDelay(sound[i] / portTICK_PERIOD_MS);
+	gpio_set_level(PIEZO_BUZZER, 0);
+	vTaskDelay(sound[i+1] / portTICK_PERIOD_MS);
+    }
+
+    gpio_set_level(PIEZO_BUZZER, 0);	// Just make sure the sound is off.
+}
+
+
+
+void SoundPlay(int id)
+{
+    switch (id) {
+	case SOUND_StartUp:	xEventGroupSetBits(xEventGroupSound, SOUND_STARTUP);
+				break;
+
+	case SOUND_Prompt:	xEventGroupSetBits(xEventGroupSound, SOUND_PROMPT);
+				break;
+
+	case SOUND_TempReached:	xEventGroupSetBits(xEventGroupSound, SOUND_TEMPREACHED);
+				break;
+
+	case SOUND_TimeOut:	xEventGroupSetBits(xEventGroupSound, SOUND_TIMEOUT);
+				break;
+
+	case SOUND_AddHop:	xEventGroupSetBits(xEventGroupSound, SOUND_ADDHOP);
+				break;
+
+	case SOUND_End:		xEventGroupSetBits(xEventGroupSound, SOUND_END);
+				break;
+	
+	case SOUND_Warn:	xEventGroupSetBits(xEventGroupSound, SOUND_WARN);
+				break;
+    }
+}
+
+
+
+void task_sound(void *pvParameter)
+{
+    EventBits_t uxBits;
+
+    ESP_LOGI(TAG, "Starting sound");
+
+    gpio_pad_select_gpio(PIEZO_BUZZER);
+    gpio_set_direction(PIEZO_BUZZER, GPIO_MODE_OUTPUT);
+
+    xEventGroupSound = xEventGroupCreate();
+
+    while (1) {
+	uxBits = xEventGroupWaitBits(xEventGroupSound,
+			SOUND_STARTUP | SOUND_PROMPT | SOUND_TEMPREACHED | SOUND_TIMEOUT | SOUND_ADDHOP | SOUND_END | SOUND_WARN,
+			pdFALSE, pdFALSE, portMAX_DELAY );
+
+	if (uxBits & SOUND_STARTUP) {
+	    BuzzerPlay(_sound_Startup);
+	    xEventGroupClearBits(xEventGroupSound, SOUND_STARTUP);
+	}
+	if (uxBits & SOUND_PROMPT) {
+	    VncSoundBell();
+	    BuzzerPlay(_sound_Prompt);
+	    xEventGroupClearBits(xEventGroupSound, SOUND_PROMPT);
+	}
+	if (uxBits & SOUND_TEMPREACHED) {
+	    VncSoundBell();
+	    BuzzerPlay(_sound_TempReached);
+	    VncSoundBell();
+	    xEventGroupClearBits(xEventGroupSound, SOUND_TEMPREACHED);
+	}
+	if (uxBits & SOUND_TIMEOUT) {
+	    BuzzerPlay(_sound_TimeOut);
+	    xEventGroupClearBits(xEventGroupSound, SOUND_TIMEOUT);
+	}
+	if (uxBits & SOUND_ADDHOP) {
+	    VncSoundBell();
+	    VncSoundBell();
+	    BuzzerPlay(_sound_AddHop);
+	    VncSoundBell();
+	    VncSoundBell();
+	    xEventGroupClearBits(xEventGroupSound, SOUND_ADDHOP);
+	}
+	if (uxBits & SOUND_END) {
+	    BuzzerPlay(_sound_End);
+	    xEventGroupClearBits(xEventGroupSound, SOUND_END);
+	}
+	if (uxBits & SOUND_WARN) {
+	    VncSoundBell();
+	    BuzzerPlay(_sound_Warn);
+	    xEventGroupClearBits(xEventGroupSound, SOUND_WARN);
+	}
+
+	vTaskDelay(10 / portTICK_PERIOD_MS);
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_sound.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,31 @@
+#ifndef	_TASK_SOUND_H
+#define	_TASK_SOUND_H
+
+/**
+ * @file task_sound.h
+ * @brief Sound player using the ESP32 internal DAC port.
+ */
+
+typedef enum {
+    SOUND_StartUp = 0,			///< Startup sound.
+    SOUND_Prompt,                   	///< Prompt sound
+    SOUND_TempReached,                  ///< Temperature Reached
+    SOUND_TimeOut,                      ///< Time out.
+    SOUND_AddHop,                       ///< Add hop sound
+    SOUND_End,                          ///< End sound
+    SOUND_Warn,                         ///< Warning sound
+} SOUND_TYPE;
+
+/**
+ * @brief Play sound
+ * @param id Sound ID
+ */
+void SoundPlay(int id);
+
+/**
+ * @brief FreeRTOS sound task.
+ */
+void task_sound(void *);
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_tft.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,795 @@
+/**
+ * @file task_tft.c
+ * @brief BrewBoard TFT and Touch screen driver. 
+ *        But because the application is controlled using the touch screen,
+ *        all the processing of menus is also found here.
+ *        It's the first started task, but it does nothing until the
+ *        Main_Screen variable is set.
+ *
+ */
+
+#include "config.h"
+
+spi_lobo_device_handle_t	spi;
+spi_lobo_device_handle_t	tsspi = NULL;
+extern sButton			Buttons[MAXBUTTONS];	// 40 buttons on a screen.
+time_t				now;
+struct tm			timeinfo;
+char				s_timer[10];
+char				s_top_msg[64];
+
+extern float			stageTemp;
+extern uint16_t			stageTime;
+extern uint16_t			TimeWhirlPool;
+extern uint32_t			TimeLeft;
+extern uint32_t			TimeSpent;
+extern uint32_t			SecsCount;
+extern uint32_t			pumpTime;
+extern uint32_t			TimeBrewing;
+extern uint16_t			Steady;
+esp_timer_handle_t		timerHandle;
+extern bool			_NewMinute;
+extern bool			_UseHLT;
+extern bool			System_TimeOk;
+
+static const char		*TAG = "task_tft";
+
+// Define which spi bus to use TFT_VSPI_HOST or TFT_HSPI_HOST
+#define SPI_BUS			TFT_HSPI_HOST
+
+extern int			Main_Screen;
+extern int			Old_Screen;
+extern int			MLT_pin;
+extern int			HLT_pin;
+extern int			Pump_pin;
+extern DS18B20_State		*ds18b20_state;
+extern DRIVER_State		*driver_state;
+extern JSON_log			*json_log;
+extern SemaphoreHandle_t	xSemaphoreDS18B20;
+extern SemaphoreHandle_t	xSemaphoreDriver;
+extern SemaphoreHandle_t        xSemaphoreWiFi;
+extern WIFI_State               *wifi_state;
+extern double			Output;
+extern sButton                  Buttons[MAXBUTTONS];
+
+extern int			BoilPower, LastMashStep;
+extern char			temp_buf[], logline[], strftime_buf[64];
+extern bool			loop, CoolBeep, Resume, pumpRest, updateRuntime;
+extern bool			NewMinute, TempReached;
+extern uint8_t			MashState;
+extern float			temp_MLT, MinMash, MaxMash;
+extern uint32_t			power_MLT, power_HLT, counts;
+
+
+#ifdef CONFIG_TEMP_SENSORS_SIMULATOR
+extern float			Fake_MLT;
+extern float			Fake_HLT;
+#endif
+
+
+
+/**
+ * @brief Seconds timer callback.
+ */
+void TimerCallback(void *arg);
+
+
+/***************************************************************************/
+
+
+
+int init_tft_display(void)
+{
+    esp_err_t ret;
+    esp_timer_create_args_t timerSecond = {
+	.callback = &TimerCallback,
+	.name = "SecondsTimer"
+    };
+
+    ESP_LOGI(TAG, "Initialize TFT");
+
+    max_rdclock = 8000000;
+    TFT_PinsInit();
+
+    spi_lobo_bus_config_t buscfg = {
+	.miso_io_num=PIN_NUM_MISO,		// set SPI MISO pin
+	.mosi_io_num=PIN_NUM_MOSI,		// set SPI MOSI pin
+	.sclk_io_num=PIN_NUM_CLK,		// set SPI CLK pin
+	.quadwp_io_num=-1,
+	.quadhd_io_num=-1,
+	.max_transfer_sz = 6*1024,
+    };
+    spi_lobo_device_interface_config_t devcfg={
+	.clock_speed_hz=8000000,                // Initial clock out at 8 MHz
+	.mode=0,                                // SPI mode 0
+	.spics_io_num=-1,                       // we will use external CS pin
+	.spics_ext_io_num=PIN_NUM_CS,           // external CS pin
+	.flags=LB_SPI_DEVICE_HALFDUPLEX,        // ALWAYS SET  to HALF DUPLEX MODE!! for display spi
+    };
+    spi_lobo_device_interface_config_t tsdevcfg={
+	.clock_speed_hz=2500000,                //Clock out at 2.5 MHz
+	.mode=0,                                //SPI mode 0
+	.spics_io_num=PIN_NUM_TCS,              //Touch CS pin
+	.spics_ext_io_num=-1,                   //Not using the external CS
+    };
+
+    ESP_LOGI(TAG, "TFT pins: miso=%d, mosi=%d, sck=%d, cs=%d", PIN_NUM_MISO, PIN_NUM_MOSI, PIN_NUM_CLK, PIN_NUM_CS);
+
+    ret = spi_lobo_bus_add_device(SPI_BUS, &buscfg, &devcfg, &spi);
+    assert(ret == ESP_OK);
+    disp_spi = spi;
+
+    // ==== Test select/deselect ====
+    ret = spi_lobo_device_select(spi, 1);
+    assert(ret == ESP_OK);
+    ret = spi_lobo_device_deselect(spi);
+    assert(ret == ESP_OK);
+
+    ESP_LOGI(TAG, "SPI: attached display, spi bus: %d, speed: %u, bus uses native pins: %s", 
+		    SPI_BUS, spi_lobo_get_speed(spi), spi_lobo_uses_native_pins(spi) ? "true" : "false");
+
+    ESP_LOGI(TAG, "TS pins : miso=%d, mosi=%d, sck=%d, cs=%d", PIN_NUM_MISO, PIN_NUM_MOSI, PIN_NUM_CLK, PIN_NUM_TCS);
+
+    ret=spi_lobo_bus_add_device(SPI_BUS, &buscfg, &tsdevcfg, &tsspi);
+    assert(ret == ESP_OK);
+    ts_spi = tsspi;
+
+    // ==== Test select/deselect ====
+    ret = spi_lobo_device_select(tsspi, 1);
+    assert(ret == ESP_OK);
+    ret = spi_lobo_device_deselect(tsspi);
+    assert(ret == ESP_OK);
+
+    ESP_LOGI(TAG, "SPI: attached TS device, spi bus: %d, speed: %u", SPI_BUS, spi_lobo_get_speed(tsspi));
+
+    // ==== Initialize the Display ====
+    TFT_display_init();
+
+    // ---- Detect maximum read speed ----
+    max_rdclock = find_rd_speed();
+
+    // ==== Set SPI clock used for display operations ====
+    spi_lobo_set_speed(spi, DEFAULT_SPI_CLOCK);
+    ESP_LOGI(TAG, "SPI: Max rd speed: %u, changed speed to %u", max_rdclock, spi_lobo_get_speed(spi));
+
+    font_rotate = 0;
+    text_wrap = 0;
+    font_transparent = 0;
+    font_forceFixed = 0;
+    gray_scale = 0;
+    TFT_setGammaCurve(DEFAULT_GAMMA_CURVE);
+    TFT_setRotation(LANDSCAPE);
+    TFT_setFont(DEFAULT_FONT, NULL);
+    TFT_resetclipwin();
+
+    /*
+     * Create a one second periodic timer.
+     */
+    ret = esp_timer_create(&timerSecond, &timerHandle);
+    assert(ret == ESP_OK);
+    ret = esp_timer_start_periodic(timerHandle, 1000000);
+    assert(ret == ESP_OK);
+
+    return ret;
+}
+
+
+
+void TimerCallback(void *arg) 
+{
+    TimeSpent++;
+    SecsCount++;
+    Steady++;
+    TimeBrewing++;
+    runtime.TimeBrewing++;
+    if ((SecsCount % 60) == 0)
+	_NewMinute = true;
+
+    if (TimeLeft) {
+	TimeLeft--;
+	if (TimeLeft == 5) {
+	    SoundPlay(SOUND_TimeOut);
+	}
+	if ((TimeLeft % 60) == 0) {
+	    pumpTime++;
+	}
+    }
+}
+
+
+
+void TimerSet(uint32_t seconds)
+{
+    Steady = TimeSpent = SecsCount = 0;
+    TimeLeft = seconds;
+}
+
+
+
+void TimerShow(uint32_t Time, int X, int Y)
+{
+    uint8_t		Hours   = (uint8_t)(Time / 3600);
+    uint8_t		Minutes = (uint8_t)((Time % 3600) / 60);
+    uint8_t		Seconds = (uint8_t)(Time % 60);
+    char		msg[32];
+    static uint32_t	_oldTime = 0;
+
+    if (Time != _oldTime) {
+    	_fg = TFT_GREEN;
+    	TFT_setFont(FONT_7SEG, NULL);
+    	set_7seg_font_atrib(12, 2, 1, TFT_DARKGREY);
+    	snprintf(s_timer, 9, "%02d:%02d:%02d", Hours, Minutes, Seconds);
+    	TFT_print(s_timer, X, Y);
+	_oldTime = Time;
+	snprintf(msg, 31, "{\"timer\":\"%s\"}", s_timer);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+}
+
+
+
+void TopMessage(char *text)
+{
+    char	msg[64];
+
+    snprintf(s_top_msg, 63, "%s", text);
+    _fg = TFT_YELLOW;
+    font_transparent = 1;
+    TFT_setFont(DEJAVU24_FONT, NULL);
+    TFT_fillRect(0, 0, 319, 25, TFT_NAVY);
+    TFT_print(s_top_msg, CENTER, 2);
+    font_transparent = 0;
+    snprintf(msg, 63, "{\"top_msg\":\"%s\"}", s_top_msg);
+    ws_server_send_text_clients("/ws", msg, strlen(msg));
+}
+
+
+
+void MLT_info(int x, int y, bool update)
+{
+    char	ctemp[16], csp[16], cpower[16], msg[32];
+    static char	ltemp[16], lsp[16], lpower[16];
+    bool	con, cpwr, cpump = false;
+    static bool	lon, lpwr, lpump;
+
+    _bg = (color_t){ 48, 48, 48 };
+    _fg = TFT_WHITE;
+    color_t _led  = { 31,255, 31};
+    color_t _pump = {127,175,255};
+    color_t _pwr  = {255, 47, 47};
+
+    if (! update) {
+	TFT_fillRect(x, y, 178, 90, _bg);
+	TFT_drawRect(x, y, 178, 90, _fg);
+	TFT_drawFastHLine(x, y + 21, 178, _fg);
+	TFT_setFont(DEJAVU18_FONT, NULL);
+	TFT_print("MLT", x + 67, y + 3);
+    }
+
+    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	sprintf(ctemp,  "%7.3f",    driver_state->mlt_pv);
+	if (driver_state->mlt_mode) {
+	    sprintf(csp,    "%6.2f sp", driver_state->mlt_sp);
+	} else {
+	    csp[0] = '\0';
+	}
+	if ((driver_state->mlt_mode == MLT_MODE_BANG) || (driver_state->mlt_mode == MLT_MODE_PID) || (driver_state->mlt_mode == MLT_MODE_EXT)) {
+	    sprintf(cpower, "%3d%%",    driver_state->mlt_power);
+	} else {
+	    cpower[0] = '\0';
+	}
+	xSemaphoreGive(xSemaphoreDriver);
+    }
+
+    con = (MLT_pin) ? true : false;
+    if ((con != lon) || (! update)) {
+	if (con) {
+	    TFT_fillCircle(x + 166, y + 11, 8, _led);
+	} else {
+	    TFT_fillCircle(x + 166, y + 11, 8, _bg);
+	}
+	lon = con;
+	snprintf(msg, 31, "{\"mlt_led\":\"%s\"}", con ? "1":"0");
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+
+    cpump = (Pump_pin) ? true : false;
+    if ((cpump != lpump) || (! update)) {
+	if (cpump) {
+	    TFT_fillCircle(x + 146, y + 11, 8, _pump);
+	} else {
+	    TFT_fillCircle(x + 146, y + 11, 8, _bg);
+	}
+	lpump = cpump;
+	snprintf(msg, 31, "{\"pump_led\":\"%s\"}", cpump ? "1":"0");
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+
+    if (equipment.SSR2 == SSR2_ON_IDLE) {
+    	cpwr = (HLT_pin) ? true : false;
+        if ((cpwr != lpwr) || (! update)) {
+	    if (cpwr) {
+		TFT_fillCircle(x + 126, y + 11, 8, _pwr);
+	    } else {
+		TFT_fillCircle(x + 126, y + 11, 8, _bg);
+	    }
+	    lpwr = cpwr;
+	    snprintf(msg, 31, "{\"hlt_led\":\"%s\"}", cpwr ? "1":"0");
+	    ws_server_send_text_clients("/ws", msg, strlen(msg));
+	}
+    }
+
+    if (strcmp(ctemp, ltemp) || (! update)) {
+    	TFT_setFont(USER_FONT, "/spiffs/fonts/Grotesk24x48.fon");
+    	TFT_print(ctemp, x + 5, y + 23);
+	strncpy(ltemp, ctemp, 16);
+	snprintf(msg, 31, "{\"mlt_pv\":\"%s\"}", ctemp);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    if (strcmp(csp, lsp) || (! update)) {
+    	TFT_clearStringRect(x + 5, y + 70, "123.45 sp");
+    	TFT_print(csp, x + 5, y + 70);
+	strncpy(lsp, csp, 16);
+	snprintf(msg, 31, "{\"mlt_sp\":\"%s\"}", csp);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+    if (strcmp(cpower, lpower) || (! update)) {
+    	TFT_clearStringRect(x + 120, y + 70, "100%");
+    	TFT_print(cpower, x + 120, y + 70);
+	strncpy(lpower, cpower, 16);
+	snprintf(msg, 31, "{\"mlt_power\":\"%s\"}", cpower);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+}
+
+
+
+void HLT_info(int x, int y, bool update, bool small)
+{
+    char        ctemp[16], csp[16], cpower[16], msg[32];
+    static char	ltemp[16], lsp[16], lpower[16];
+    bool        con = false;
+    static bool lon;
+    uint8_t	H;
+
+    _bg = (color_t){ 63, 63, 64 };
+    _fg = TFT_YELLOW;
+    color_t _led = {255, 47, 47};
+    H = (small) ? 70 : 90;
+
+    if (! update) {
+	TFT_fillRect(x, y, 178, H, _bg);
+	TFT_drawRect(x, y, 178, H, _fg);
+	TFT_drawFastHLine(x, y + 21, 178, _fg);
+	TFT_setFont(DEJAVU18_FONT, NULL);
+	TFT_print("HLT", x + 67, y + 3);
+    }
+
+    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	sprintf(ctemp, "%7.3f", driver_state->hlt_pv);
+	if (driver_state->hlt_mode == HLT_MODE_BANG) {
+	    sprintf(cpower, "%3d%%", driver_state->hlt_power);
+	} else {
+	    cpower[0] = '\0';
+	}
+	if (driver_state->hlt_mode == HLT_MODE_BANG || driver_state->hlt_mode == HLT_MODE_OFF) {
+	    sprintf(csp, "%6.2f sp", driver_state->hlt_sp);
+	} else {
+	    csp[0] = '\0';
+	}
+	xSemaphoreGive(xSemaphoreDriver);
+    }
+
+    con = (HLT_pin) ? true : false;
+    if ((con != lon) || (! update)) {
+	if (con) {
+	    TFT_fillCircle(x + 166, y + 11, 8, _led);
+	} else {
+	    TFT_fillCircle(x + 166, y + 11, 8, _bg);
+	}
+	lon = con;
+	snprintf(msg, 31, "{\"hlt_led\":\"%s\"}", con ? "1":"0");
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+
+    if (strcmp(ltemp, ctemp) || (! update)) {
+	if (small) {
+    	    TFT_setFont(USER_FONT, "/spiffs/fonts/DejaVuSans24.fon");
+    	    TFT_print(ctemp, x + 40,  y + 25);
+	} else {
+	    TFT_setFont(USER_FONT, "/spiffs/fonts/Grotesk24x48.fon");
+	    TFT_print(ctemp, x + 5,  y + 23);
+	}
+	strncpy(ltemp, ctemp, 16);
+	snprintf(msg, 31, "{\"hlt_pv\":\"%s\"}", ctemp);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+
+    H = (small) ? 50 : 70;
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    if (strcmp(csp, lsp) || (! update)) {
+	TFT_clearStringRect(x + 5, y + H, "123.45 sp");
+	TFT_print(csp, x + 5, y + H);
+	strncpy(lsp, csp, 16);
+	snprintf(msg, 31, "{\"hlt_sp\":\"%s\"}", csp);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+    if (strcmp(cpower, lpower) || (! update)) {
+	TFT_clearStringRect(x + 120, y + H, "100%");
+	TFT_print(cpower, x + 120, y + H);
+	strncpy(lpower, cpower, 16);
+	snprintf(msg, 31, "{\"hlt_power\":\"%s\"}", cpower);
+	ws_server_send_text_clients("/ws", msg, strlen(msg));
+    }
+}
+
+
+
+void update_json(void)
+{
+    int Hour   = (TimeBrewing / 3600); 
+    int Minute = ((TimeBrewing % 3600) / 60);
+
+    if (counts == 0)
+	counts = 1;	// Prevent division by zero.
+
+    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	snprintf(json_log->time, 8, "%02d:%02d", Hour, Minute);
+	json_log->mlt_sp = driver_state->mlt_sp;
+	json_log->mlt_pv = driver_state->mlt_pv;
+	json_log->mlt_power = power_MLT / counts;
+	json_log->mlt_tempreached = TempReached ? 1:0;
+	json_log->pump_run = driver_state->pump_run;
+	json_log->hlt_sp = driver_state->hlt_sp;
+	json_log->hlt_pv = driver_state->hlt_pv;
+	json_log->hlt_power = power_HLT / counts;
+	json_log->event[0] = '\0';
+	xSemaphoreGive(xSemaphoreDriver);
+    }
+}
+
+
+
+void TFTstartWS(int client)
+{
+    char	msg[1024];
+    char	mlt_sp[16], mlt_power[16], hlt_sp[16], hlt_power[16];
+
+    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+	if (driver_state->mlt_sp) {
+	    snprintf(mlt_sp, 15, "%6.2f sp", driver_state->mlt_sp);
+	    snprintf(mlt_power, 15, "%3d%%", driver_state->mlt_power);
+	} else {
+	    mlt_sp[0] = '\0';
+	    mlt_power[0] = '\0';
+	}
+	if (driver_state->hlt_sp) {
+	    snprintf(hlt_sp, 15, "%6.2f sp", driver_state->hlt_sp);
+	    snprintf(hlt_power, 15, "%3d%%", driver_state->hlt_power);
+	} else {
+	    hlt_sp[0] = '\0';
+	    hlt_power[0] = '\0';
+	}
+    	snprintf(msg, 1023, "{\"main\":\"%d\",\"mlt_led\":\"%d\",\"mlt_pv\":\"%7.3f\",\"mlt_sp\":\"%s\",\"mlt_power\":\"%s\"" \
+		    ",\"pump_led\":\"%d\",\"hlt_led\":\"%d\",\"hlt_pv\":\"%7.3f\",\"hlt_sp\":\"%s\",\"hlt_power\":\"%s\"" \
+		    ",\"timer\":\"%s\",\"top_msg\":\"%s\"}",
+		    Main_Screen, (MLT_pin) ? 1:0, driver_state->mlt_pv, mlt_sp, mlt_power,
+		    (Pump_pin) ? 1:0, (HLT_pin) ? 1:0, driver_state->hlt_pv, hlt_sp, hlt_power,
+		    s_timer, s_top_msg);
+
+    	xSemaphoreGive(xSemaphoreDriver);
+	ws_server_send_text_client(client, msg, strlen(msg));
+    }
+
+}
+
+
+
+void task_tft(void *pvParameter)
+{
+    char	msg[32];
+
+    ESP_LOGI(TAG, "Initialize TFT/Touch task");
+
+    /*
+     * Task loop. Read touchscreen events.
+     */
+    while (1) {
+	/*
+	 * Build new screen.
+	 */
+startover:
+
+	updateRuntime = false;
+	if (_NewMinute) {
+	    _NewMinute = false;
+	    NewMinute = true;
+	}
+
+	/*
+	 * Timekeeping. In the WiFi task sntp is started if there
+	 * is a valid internet connection.
+	 */
+	time(&now);
+	localtime_r(&now, &timeinfo);
+	// Is time set? If not, tm_year will be (1970 - 1900).
+	if ((timeinfo.tm_year > (2016 - 1900)) && (! System_TimeOk)) {
+	    System_TimeOk = true;
+	    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
+	    ESP_LOGI(TAG, "System time is set: %s", strftime_buf);
+	}
+
+	if (Old_Screen != Main_Screen) {
+
+	    if ((Main_Screen == MAIN_MODE_FREE) && ((config.ts_xleft == 0) || (config.ts_ybottom == 0))) {
+		Main_Screen = MAIN_MODE_CALIBRATION;
+	    }
+
+	    /*
+	     * With each screenchange, remove the timer too.
+	     */
+	    snprintf(msg, 31, "{\"main\":\"%d\",\"timer\":\"\"}", Main_Screen);
+	    ws_server_send_text_clients("/ws", msg, strlen(msg));
+
+	    ESP_LOGI(TAG, "Change screen %d to %d", Old_Screen, Main_Screen);
+	    _bg = TFT_BLACK;
+	    TFT_fillScreen(_bg);
+	    TFT_resetclipwin();
+	    Buttons_Clear();
+	    Old_Screen = Main_Screen;
+
+	    switch (Main_Screen) {
+		case MAIN_MODE_FREE:
+			TopMessage("Hoofdmenu");
+			MLT_info(71, 26, false);
+			if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) {
+			    HLT_info(71,150, false, false);
+			}
+			Buttons_Add(  5,  26, 60, 40, "Hand",  0);
+			Buttons_Add(255,  26, 60, 40, "Auto",  1);
+			Buttons_Add(  5, 200, 60, 40, "Info",  2);
+			Buttons_Add(255, 200, 60, 40, "Tools", 3);
+			Buttons_Show();
+			break;
+
+		case MAIN_MODE_CALIBRATION:
+			Calibration_Init();
+			break;
+
+		case MAIN_INFO:
+			sprintf(temp_buf, "BrewBoard %s", VERSION);
+			TopMessage(temp_buf);
+			_fg = TFT_YELLOW;
+			TFT_setFont(UBUNTU16_FONT, NULL);
+			TFT_print("Written by Michiel Broek (C) 2018\r\n\n", 0, 50);
+			//         -------------------------------------
+			_fg = TFT_ORANGE;
+			TFT_print("Parts are written by Chris Morgan,\r\n", 0, LASTY);
+			TFT_print("Brett Beauregard, Chris Garry, LoBo,\r\n", 0, LASTY);
+			TFT_print("and David Antliff.\r\n", 0, LASTY);
+			ShowInteger(1,140, "Free memory", " bytes", esp_get_free_heap_size());
+			ShowText(1,158, "IDF version", (char *)esp_get_idf_version());
+			Buttons_Add(130, 200, 60, 40, "Ok", 0);
+			Buttons[0].dark = true;
+			Buttons_Show();
+			break;
+
+		case MAIN_TOOLS:
+			TopMessage("Tools menu");
+			Buttons_Add( 20, 40,120, 40, "Setup", 0);
+			Buttons_Add( 20,120,120, 40, "Bestanden", 1);
+			Buttons_Add(180, 40,120, 40, "Recepten", 2);
+			Buttons_Add(180,120,120, 40, "Updates", 3);
+			Buttons_Add(130, 200, 60, 40, "Ok", 4);
+			Buttons[4].dark = true;
+			Buttons_Show();
+			break;
+
+		case MAIN_TOOLS_SETUP:
+		case MAIN_TOOLS_SETUP_CONFIG:
+		case MAIN_TOOLS_SETUP_CO_EDIT:
+		case MAIN_TOOLS_SETUP_EQUIPMENT:
+		case MAIN_TOOLS_SETUP_EQ_EDIT:
+		case MAIN_TOOLS_SETUP_CALIBRATION:
+			Setup_Init();
+			break;
+
+		case MAIN_TOOLS_SETUP_WIFI:
+		case MAIN_TOOLS_SETUP_WIFI_CUR:
+		case MAIN_TOOLS_SETUP_WIFI_CON:
+		case MAIN_TOOLS_SETUP_WIFI_NEW:
+			if (WiFi_Init())
+			    goto startover;
+			break;
+
+		case MAIN_TOOLS_RECIPE:
+		case MAIN_TOOLS_RECIPE_EDIT:
+			Recipes_Init();
+			break;
+
+		case MAIN_TOOLS_FILES:
+			Files_Init();
+			break;
+
+		case MAIN_TOOLS_UPDATES:
+			Updates_Init();
+			break;
+
+		case MAIN_AUTO_INIT:
+		case MAIN_AUTO_DELAYSTART:
+		case MAIN_AUTO_HEATUP:
+		case MAIN_AUTO_MASH_IN:
+		case MAIN_AUTO_MASH_1:
+		case MAIN_AUTO_MASH_2:
+		case MAIN_AUTO_MASH_3:
+		case MAIN_AUTO_MASH_4:
+		case MAIN_AUTO_MASH_5:
+		case MAIN_AUTO_MASH_6:
+		case MAIN_AUTO_MASH_OUT:
+		case MAIN_AUTO_TOBOIL:
+		case MAIN_AUTO_BOILING:
+		case MAIN_AUTO_COOLING_H:
+		case MAIN_AUTO_COOLING_M:
+		case MAIN_AUTO_COOLING_C:
+		case MAIN_AUTO_WHIRLPOOL9:
+		case MAIN_AUTO_WHIRLPOOL7:
+		case MAIN_AUTO_WHIRLPOOL6:
+		case MAIN_AUTO_WHIRLPOOL2:
+		case MAIN_AUTO_DONE:
+		case MAIN_AUTO_ABORT:
+			if (Automation_Init())
+			    goto startover;
+			break;
+
+		case MAIN_MANUAL_INIT:
+		case MAIN_MANUAL_MAIN:
+			if (Manual_Init())
+			    goto startover;
+			break;
+
+		default:
+			break;
+	    }
+	}
+
+	/*
+	 * Update screen
+	 */
+	switch (Main_Screen) {
+	    case MAIN_MODE_FREE:
+		MLT_info(71, 26, true);
+		if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) {
+		    HLT_info(71, 150, true, false);
+		}
+		switch (Buttons_Scan()) {
+		    case 0:	Main_Screen = MAIN_MANUAL_INIT;	break;
+		    case 1:	Main_Screen = MAIN_AUTO_INIT;	break;
+		    case 2:	Main_Screen = MAIN_INFO;		break;
+		    case 3:	Main_Screen = MAIN_TOOLS;		break;
+		    default:	break;
+		}
+		break;
+
+	    case MAIN_MODE_CALIBRATION:
+		Calibration_Loop();
+		Main_Screen = MAIN_MODE_FREE;
+		break;
+
+	    case MAIN_TOOLS:
+		switch (Buttons_Scan()) {
+		    case 0:	Main_Screen = MAIN_TOOLS_SETUP;		break;
+		    case 1:	Main_Screen = MAIN_TOOLS_FILES;		break;
+		    case 2:	Main_Screen = MAIN_TOOLS_RECIPE;	break;
+		    case 3:	Main_Screen = MAIN_TOOLS_UPDATES;	break;
+		    case 4:	Main_Screen = MAIN_MODE_FREE;		break;
+		    default:	break;
+		}
+		break;
+
+	    case MAIN_TOOLS_SETUP:
+	    case MAIN_TOOLS_SETUP_CONFIG:
+	    case MAIN_TOOLS_SETUP_CO_EDIT:
+	    case MAIN_TOOLS_SETUP_EQUIPMENT:
+	    case MAIN_TOOLS_SETUP_EQ_EDIT:
+	    case MAIN_TOOLS_SETUP_CALIBRATION:
+		Setup_Loop();
+		break;
+
+	    case MAIN_TOOLS_SETUP_WIFI:
+	    case MAIN_TOOLS_SETUP_WIFI_CUR:
+	    case MAIN_TOOLS_SETUP_WIFI_CON:
+	    case MAIN_TOOLS_SETUP_WIFI_NEW:
+		if (WiFi_Loop())
+		    goto startover;
+		break;
+
+	    case MAIN_TOOLS_RECIPE:
+	    case MAIN_TOOLS_RECIPE_EDIT:
+		Recipes_Loop();
+		break;
+
+	    case MAIN_TOOLS_FILES:
+		Files_Loop();
+		break;
+
+	    case MAIN_TOOLS_UPDATES:
+		Updates_Loop();
+		break;
+
+	    case MAIN_INFO:
+		if (Buttons_Scan() == 0) {
+		    Main_Screen = MAIN_MODE_FREE;
+		}
+		break;
+
+	    case MAIN_AUTO_INIT:
+	    case MAIN_AUTO_DELAYSTART:
+	    case MAIN_AUTO_HEATUP:
+	    case MAIN_AUTO_MASH_IN:
+	    case MAIN_AUTO_MASH_1:
+	    case MAIN_AUTO_MASH_2:
+	    case MAIN_AUTO_MASH_3:
+	    case MAIN_AUTO_MASH_4:
+	    case MAIN_AUTO_MASH_5:
+	    case MAIN_AUTO_MASH_6:
+	    case MAIN_AUTO_MASH_OUT:
+	    case MAIN_AUTO_TOBOIL:
+	    case MAIN_AUTO_BOILING:
+	    case MAIN_AUTO_COOLING_H:
+	    case MAIN_AUTO_COOLING_M:
+	    case MAIN_AUTO_COOLING_C:
+	    case MAIN_AUTO_WHIRLPOOL9:
+	    case MAIN_AUTO_WHIRLPOOL7:
+	    case MAIN_AUTO_WHIRLPOOL6:
+	    case MAIN_AUTO_WHIRLPOOL2:
+	    case MAIN_AUTO_DONE:
+	    case MAIN_AUTO_ABORT:
+		if (Automation_Loop())
+		    goto startover;
+		break;
+
+	    case MAIN_MANUAL_INIT:
+	    case MAIN_MANUAL_MAIN:
+		if (Manual_Loop())
+		    goto startover;
+		break;
+
+	    default:
+		break;
+	}
+
+	if (updateRuntime) {
+	    write_runtime();
+	}
+
+	/*
+	 * Count power average during brewing.
+	 */
+	if ((Main_Screen >= MAIN_AUTO_MASH_IN) && (Main_Screen < MAIN_AUTO_DONE)) {
+	    if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) {
+		power_MLT += driver_state->mlt_power;
+		power_HLT += driver_state->hlt_power;
+		counts++;
+		xSemaphoreGive(xSemaphoreDriver);
+	    }
+	}
+	
+    	if (NewMinute) {
+	    /*
+	     * Brew logging.
+	     */
+	    if ((Main_Screen >= MAIN_AUTO_MASH_IN) && (Main_Screen < MAIN_AUTO_DONE)) {
+		update_json();
+		log_json();
+		power_MLT = power_HLT = counts = 0;
+	    }
+	}
+
+	NewMinute = false;
+	vTaskDelay(50 / portTICK_PERIOD_MS);
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_tft.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,66 @@
+#ifndef	_TASK_TFT_H
+#define	_TASK_TFT_H
+
+/**
+ * @file task_tft.h
+ * @brief The interface to the TFT with touch module.
+ */
+
+/**
+ * @brief Update the JSON structure with the logdata.
+ */
+void update_json(void);
+
+/**
+ * @brief Initialize the TFT display
+ */
+int init_tft_display(void);
+
+/**
+ * @brief Write a big message at the center top of the screen.
+ * @param text The text to display
+ */
+void TopMessage(char *text);
+
+/**
+ * @brief Draw Mash Lauter Tun info
+ * @param x X startpoint
+ * @param y Y startpoint
+ * @param update If update or initial draw.
+ */
+void MLT_info(int x, int y, bool update);
+
+/**
+ * @brief Draw Hot Liquer Tank info
+ * @param x X startpoint
+ * @param y Y startpoint
+ * @param update If update or initial draw.
+ * @param small To display a smaller box.
+ */
+void HLT_info(int x, int y, bool update, bool small);
+
+/**
+ * @brief Set the timer.
+ * @param seconds The number of seconds in the timer.
+ */
+void TimerSet(uint32_t seconds);
+
+/**
+ * @brief Show the timer.
+ * @param Time the time to display.
+ * @param X the X position to display the time
+ * @param Y the Y position to display the time
+ */
+void TimerShow(uint32_t Time, int X, int Y);
+
+/**
+ * @brief Initialize all values for the webui.
+ */
+void TFTstartWS(int client);
+
+/**
+ * @brief FreeRTOS TFT/Touch task. 
+ */
+void task_tft(void *);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_wifi.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,806 @@
+/**
+ * @file task_wifi.c
+ * @brief WiFi task. Connects to a known Access Point. If we know more then
+ *        one AP, try to connect all of them until it succeeds (Not yet written).
+ */
+
+
+#include "config.h"
+
+
+static const char *TAG = "task_wifi";
+
+
+SemaphoreHandle_t       	xSemaphoreWiFi = NULL;		///< Semaphore WiFi task.
+EventGroupHandle_t		xEventGroupWifi;		///< Events WiFi task.
+uint16_t			ap_num = MAX_AP_NUM;		///< Scan counter.
+wifi_ap_record_t		*accessp_records;		///< [MAX_AP_NUM] records array with scan results.
+wifi_config_t			*task_wifi_ConfigSTA = NULL;	///< Current STA configuration.
+WIFI_State			*wifi_state = NULL;		///< Public state for other tasks.
+
+
+uint8_t				_wifi_ssid[33];
+bool				_wifi_ScanAPs = false;
+bool				_wifi_ScanDone = false;
+uint16_t			_wifi_Scanned = 0;
+
+extern int			Main_Screen;			///< Current Screen number.
+extern sButton			Buttons[MAXBUTTONS];		///< Buttons definitions.
+extern uint32_t			TimeSpent;			///< Counter that is increased each second.
+
+const int TASK_WIFI_REQUEST_WIFI_SCAN = BIT0;			///< When set, means a client requested to scan wireless networks.
+const int TASK_WIFI_REQUEST_STA_DISCONNECT = BIT1;		///< When set, means a client requested to disconnect from currently connected AP.
+const int TASK_WIFI_REQUEST_STA_CONNECT = BIT2;			///< When set, means a client requested to connect to an access point.
+
+const int TASK_WIFI_HAS_IP = BIT3;				///< Indicate that we have an IP address
+const int TASK_WIFI_AP_STARTED = BIT4;				///< Indicate that the SoftAP is started
+const int TASK_WIFI_STA_FAILED = BIT5;				///< Indicate that we could not get a connection to AP as station.
+const int TASK_WIFI_STA_DISCONNECTED = BIT6;			///* Indicate that we are disconnected from an ap station.
+const int TASK_WIFI_STA_CONNECTED = BIT7;			///< Indicate that we are connected to AP as station, flip of BIT6.
+
+
+/**
+ * @brief Local function, save station configuration.
+ * @return Esp error code.
+ */
+esp_err_t SaveStaConfig(void);
+
+/**
+ * @brief Local function, fetch last connected station configuration.
+ * @return True if there was a last connection, false if there was not.
+ */
+bool FetchStaConfig(void);
+
+/**
+ * @brief Local function, WiFi event handler.
+ * @return Esp error code.
+ */
+esp_err_t task_wifi_EventHandler(void *ctx, system_event_t *event);
+
+
+/****************************************************************************/
+
+
+
+esp_err_t SaveStaConfig(void)
+{
+    int		record;
+
+    if (task_wifi_ConfigSTA && strlen((char *)task_wifi_ConfigSTA->sta.ssid)) {
+	/*
+	 * Store in /spiffs/stations.conf if it's a new station.
+	 */
+	record = read_station(task_wifi_ConfigSTA->sta.ssid);
+	if (record == -1) {
+	    add_station(task_wifi_ConfigSTA->sta.ssid, task_wifi_ConfigSTA->sta.password);
+	}
+
+	/*
+	 * Update main configuration if needed.
+	 */
+	if (strcmp(config.lastSSID, (char *)task_wifi_ConfigSTA->sta.ssid)) {
+	    sprintf(config.lastSSID, "%s", task_wifi_ConfigSTA->sta.ssid);
+	    write_config();
+	}
+	ESP_LOGI(TAG, "SaveStaConfig %s, record %d", wifiStation.SSID, record);
+    }
+
+    return ESP_OK;
+}
+
+
+
+bool FetchStaConfig(void)
+{
+    if (task_wifi_ConfigSTA == NULL) {
+	task_wifi_ConfigSTA = (wifi_config_t*)malloc(sizeof(wifi_config_t));
+    }
+    memset(task_wifi_ConfigSTA, 0x00, sizeof(wifi_config_t));
+
+    /*
+     * Search last connected AP as station.
+     */
+    if (strlen(config.lastSSID)) {
+	read_station((uint8_t *)config.lastSSID);
+
+	/* ssid */
+	size_t sz = sizeof(task_wifi_ConfigSTA->sta.ssid);
+	memcpy(task_wifi_ConfigSTA->sta.ssid, wifiStation.SSID, sz);
+
+	/* password */
+	sz = sizeof(task_wifi_ConfigSTA->sta.password);
+	memcpy(task_wifi_ConfigSTA->sta.password, wifiStation.Password, sz);
+
+	ESP_LOGI(TAG, "FetchStaConfig: last connected to ssid: %s", task_wifi_ConfigSTA->sta.ssid);
+	return task_wifi_ConfigSTA->sta.ssid[0] != '\0';
+    }
+
+    ESP_LOGI(TAG, "FetchStaConfig: no last connection found");
+    return false;
+}
+
+
+
+esp_err_t task_wifi_EventHandler(void *ctx, system_event_t *event)
+{
+    switch(event->event_id) {
+	case SYSTEM_EVENT_SCAN_DONE:
+		// Get the results so the memory used is freed.
+		ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, accessp_records));
+		_wifi_Scanned = ap_num;
+		_wifi_ScanDone = true;
+		break;
+
+	case SYSTEM_EVENT_STA_START:
+		ESP_LOGD(TAG, "Event STA started");
+		// Set the configured hostname for the dhcp client.
+		ESP_ERROR_CHECK(tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, config.hostname));   
+		esp_wifi_connect();
+		break;
+
+	//   SYSTEM_EVENT_STA_STOP		3
+	case SYSTEM_EVENT_STA_CONNECTED:
+		ESP_LOGI(TAG, "Event STA connected, turn off AP");
+		wifi_ap_record_t ap_info;
+		esp_wifi_sta_get_ap_info(&ap_info);
+		if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+		    wifi_state->STA_connected = true;
+		    wifi_state->STA_rssi = ap_info.rssi;
+		    sprintf(wifi_state->STA_ssid, "%s", ap_info.ssid);
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		// Turn off AP
+		ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+		xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_STA_CONNECTED);
+		xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED);
+		break;
+
+	case SYSTEM_EVENT_STA_DISCONNECTED:
+		ESP_LOGI(TAG, "Event STA disconnected, turn on AP");
+		if (xSemaphoreTake(xSemaphoreWiFi, 10) == pdTRUE) {
+		    wifi_state->STA_connected = false;
+		    wifi_state->STA_rssi = 0;
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		// Turn on AP
+		ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
+		xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_STA_CONNECTED);
+		xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED);
+
+		/*
+		 * Meanwhile, try to reconnect.
+		 */
+		if (FetchStaConfig()) {
+		    xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_CONNECT);
+		}
+		break;
+
+	//   SYSTEM_EVENT_STA_AUTHMODE_CHANGE	6
+	case SYSTEM_EVENT_STA_GOT_IP:
+//		printf("SYSTEM_EVENT_STA_GOT_IP\n");
+		xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_HAS_IP);
+		tcpip_adapter_ip_info_t ip;
+		memset(&ip, 0, sizeof(tcpip_adapter_ip_info_t));
+		if (tcpip_adapter_get_ip_info(ESP_IF_WIFI_STA, &ip) == 0) {
+		    if (xSemaphoreTake(xSemaphoreWiFi, 10) == pdTRUE) {
+		    	snprintf(wifi_state->STA_ip, 15, IPSTR, IP2STR(&ip.ip));
+			snprintf(wifi_state->STA_nm, 15, IPSTR, IP2STR(&ip.netmask));
+			snprintf(wifi_state->STA_gw, 15, IPSTR, IP2STR(&ip.gw));
+		    	xSemaphoreGive(xSemaphoreWiFi);
+		    }
+		}
+		break;
+
+	case SYSTEM_EVENT_STA_LOST_IP:
+		ESP_LOGI(TAG, "Lost IP address");
+		xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_HAS_IP);
+		if (xSemaphoreTake(xSemaphoreWiFi, 10) == pdTRUE) {
+		    wifi_state->STA_ip[0] = '\0';
+		    wifi_state->STA_nm[0] = '\0';
+		    wifi_state->STA_gw[0] = '\0';
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		break;
+
+	//   SYSTEM_EVENT_STA_WPS_ER_SUCCESS	9
+	//   SYSTEM_EVENT_STA_WPS_ER_FAILED	10
+	//   SYSTEM_EVENT_STA_WPS_ER_TIMEOUT	11
+	//   SYSTEM_EVENT_STA_WPS_ER_PIN	12
+
+    	case SYSTEM_EVENT_AP_START:
+		ESP_LOGD(TAG, "Event AP started");
+    		xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_AP_STARTED);
+		if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+		    wifi_state->AP_active = true;
+		    wifi_state->AP_clients = 0;
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		break;
+
+	case SYSTEM_EVENT_AP_STOP:
+		ESP_LOGD(TAG, "Event AP stopped");
+		xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_AP_STARTED);
+		if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+		    wifi_state->AP_active = false;
+		    wifi_state->AP_clients = 0;
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		break;
+
+    	case SYSTEM_EVENT_AP_STACONNECTED:
+		if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+		    wifi_state->AP_clients++;
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		ESP_LOGI(TAG, "Event AP new client, %d connections", wifi_state->AP_clients);
+		break;
+
+    	case SYSTEM_EVENT_AP_STADISCONNECTED:
+		if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+		    if (wifi_state->AP_clients > 0)
+		    	wifi_state->AP_clients--;
+		    else
+		    	wifi_state->AP_clients = 0;
+		    xSemaphoreGive(xSemaphoreWiFi);
+		}
+		ESP_LOGI(TAG, "Event AP client disconnect, %d connections", wifi_state->AP_clients);
+		break;
+
+	//   SYSTEM_EVENT_AP_PROBEREQRECVED	17
+	//   SYSTEM_EVENT_GOT_IP6		18
+	//   SYSTEM_EVENT_ETH_START		19
+	//   SYSTEM_EVENT_ETH_STOP		20
+	//   SYSTEM_EVENT_ETH_CONNECTED		21
+	//   SYSTEM_EVENT_ETH_DISCONNECTED	22
+	//   SYSTEM_EVENT_ETH_GOT_IP		23
+
+	default:
+		printf("Unknown event %d\n", event->event_id);
+        	break;
+    }
+    return ESP_OK;
+}
+
+
+
+/**
+ * @brief WiFi manager task
+ */
+void task_wifi( void * pvParameters )
+{
+    esp_err_t		ret;
+
+    ESP_LOGI(TAG, "Starting WiFi driver");
+
+    /*
+     * Initialize NVS
+     */
+    ret = nvs_flash_init();
+    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
+	ESP_ERROR_CHECK(nvs_flash_erase());
+	ret = nvs_flash_init();
+    }
+    ESP_ERROR_CHECK(ret);
+
+    /* event handler and event group for the wifi driver */
+    xEventGroupWifi = xEventGroupCreate();
+    /* initialize the tcp stack */
+    tcpip_adapter_init();
+    ESP_ERROR_CHECK(esp_event_loop_init(task_wifi_EventHandler, NULL));
+
+    /*
+     * memory allocation of objects used by the task 
+     */
+    accessp_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * MAX_AP_NUM);
+    task_wifi_ConfigSTA = (wifi_config_t*)malloc(sizeof(wifi_config_t));
+    memset(task_wifi_ConfigSTA, 0x00, sizeof(wifi_config_t));
+
+    xSemaphoreWiFi = xSemaphoreCreateMutex();
+    wifi_state = malloc(sizeof(WIFI_State));
+    wifi_state->AP_clients = 0;
+    wifi_state->AP_active = false;
+    wifi_state->STA_connected = false;
+    wifi_state->STA_rssi = 0;
+
+    /* wifi scanner config */
+    wifi_scan_config_t scan_config = {
+	.ssid = 0,
+	.bssid = 0,
+	.channel = 0,
+	.show_hidden = false
+    };
+
+    /*
+     * start the softAP access point
+     * stop DHCP server
+     */
+    ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP));
+
+    /*
+     * Assign a static IP to the AP network interface
+     */
+    tcpip_adapter_ip_info_t info;
+    memset(&info, 0x00, sizeof(info));
+    IP4_ADDR(&info.ip, 192, 168, 1, 1);
+    IP4_ADDR(&info.gw, 192, 168, 1, 1);
+    IP4_ADDR(&info.netmask, 255, 255, 255, 0);
+    ESP_ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info));
+
+    /* start dhcp server */
+    ESP_ERROR_CHECK(tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP));
+    ESP_LOGI(TAG, "AP start dhcps ip: 192.168.1.1 nm: 255.255.255.0 gw: 192.168.1.1");
+
+    /* start dhcp client */
+    ESP_ERROR_CHECK(tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA));
+    ESP_LOGI(TAG, "Start DHCP client for STA interface.");
+
+    /*
+     * init wifi as station + access point
+     */
+    wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
+    ESP_ERROR_CHECK(esp_wifi_init(&wifi_init_config));
+    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
+    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
+    ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_AP, config.ap_bandwidth));
+
+    // configure the softAP and start it */
+    wifi_config_t ap_config = {
+	.ap = {
+	    .ssid_len = 0,
+	    .channel = config.ap_channel,
+	    .authmode = WIFI_AUTH_WPA2_PSK,
+	    .ssid_hidden = config.ap_ssid_hidden,
+	    .max_connection = AP_MAX_CONNECTIONS,
+	    .beacon_interval = 100,
+	},
+    };
+    memcpy(ap_config.ap.ssid, config.ap_ssid , sizeof(config.ap_ssid));
+    memcpy(ap_config.ap.password, config.ap_pwd, sizeof(config.ap_pwd));
+    ret = esp_wifi_set_config(WIFI_IF_AP, &ap_config);
+    if (ret != ESP_OK) {
+	ESP_LOGE(TAG, "esp_wifi_set_config(WIFI_IF_AP, nnn) rc=%d", ret);
+    }
+    ESP_ERROR_CHECK(esp_wifi_start());
+
+    ESP_LOGI(TAG, "SoftAP start ssid: `%s' pwd: `%s' channel: %d, hidden: %s", 
+		    ap_config.ap.ssid, ap_config.ap.password, ap_config.ap.channel, ap_config.ap.ssid_hidden ? "yes":"no");
+
+    /*
+     * try to get access to previously saved wifi
+     */
+    if (FetchStaConfig()) {
+	xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_CONNECT);
+    }
+
+    /* 
+     * Wait for access point to start
+     */
+    xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED);
+    xEventGroupWaitBits(xEventGroupWifi, TASK_WIFI_AP_STARTED, pdFALSE, pdTRUE, portMAX_DELAY );
+    EventBits_t uxBits;
+
+    for(;;) {
+
+	/* actions that can trigger: request a connection, a scan, or a disconnection */
+	uxBits = xEventGroupWaitBits(xEventGroupWifi, 
+		                     TASK_WIFI_REQUEST_STA_CONNECT | TASK_WIFI_REQUEST_WIFI_SCAN | TASK_WIFI_REQUEST_STA_DISCONNECT,
+		                     pdFALSE, pdFALSE, portMAX_DELAY );
+
+	if (uxBits & TASK_WIFI_REQUEST_STA_DISCONNECT) {
+	    /*
+	     * user requested a disconnect, this will in effect disconnect the wifi but also erase NVS memory
+	     */
+	    ESP_LOGI(TAG, "Request disconnect");
+	    sntp_stop();
+	    ESP_ERROR_CHECK(esp_wifi_disconnect());
+	    xEventGroupWaitBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED, pdFALSE, pdTRUE, portMAX_DELAY );
+	    
+	    /*
+	     * erase configuration
+	     */
+	    if (task_wifi_ConfigSTA) {
+		memset(task_wifi_ConfigSTA, 0x00, sizeof(wifi_config_t));
+	    }
+	    config.lastSSID[0] = '\0';
+	    write_config();
+
+	    /* finally: release the scan request bit */
+	    xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_DISCONNECT);
+	    ESP_LOGI(TAG, "Request disconnect is finished.");
+
+	} else if (uxBits & TASK_WIFI_REQUEST_STA_CONNECT) {
+
+	    //someone requested a connection!
+	    ESP_LOGI(TAG, "Request STA connect `%s'", task_wifi_ConfigSTA->sta.ssid);
+	    /* set the new config and connect - reset the failed bit first as it is later tested */
+	    xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_STA_FAILED);
+	    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, task_wifi_ConfigSTA));
+
+            esp_err_t wifierror = esp_wifi_connect();
+	    if (wifierror != ESP_OK) {
+	    	ESP_LOGE(TAG, "esp_wifi_connect() rc=%04x", (int)wifierror);
+		xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_STA_FAILED);
+	    }
+
+	    /* 
+	     * 2 scenarios here: connection is successful and TASK_WIFI_HAS_IP will be posted
+	     * or it's a failure and we get a TASK_WIFI_STA_FAILED with a reason code.
+	     * Note that the reason code is not exploited. For all intent and purposes a failure is a failure.
+	     */
+	    uxBits = xEventGroupWaitBits(xEventGroupWifi, TASK_WIFI_HAS_IP | TASK_WIFI_STA_FAILED, pdFALSE, pdFALSE, portMAX_DELAY );
+
+	    if (uxBits & (TASK_WIFI_HAS_IP | TASK_WIFI_STA_FAILED)) {
+		/* 
+		 * only save the config if the connection was successful! 
+		 */
+		if(uxBits & TASK_WIFI_HAS_IP) {
+		    /* save wifi config */
+		    SaveStaConfig();
+		    sntp_setservername(0, "nl.pool.ntp.org");
+		    sntp_init();
+		    ESP_LOGI(TAG, "Initialized SNTP %s", sntp_getservername(0));
+		} else {
+		    ESP_LOGI(TAG, "Connection failed");	// TODO: Scan other SSID's for known networks.
+		    /* failed attempt to connect regardles of the reason */
+
+		    /* otherwise: reset the config */
+		    memset(task_wifi_ConfigSTA, 0x00, sizeof(wifi_config_t));
+		}
+	    } else {
+		/* hit portMAX_DELAY limit ? */
+		abort();
+	    }
+
+	    /* finally: release the connection request bit */
+	    xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_CONNECT);
+
+	} else if (uxBits & TASK_WIFI_REQUEST_WIFI_SCAN) {
+
+	    /* safe guard against overflow */
+	    ap_num = MAX_AP_NUM;
+	    ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false));
+
+	    /* finally: release the scan request bit */
+	    xEventGroupClearBits(xEventGroupWifi, TASK_WIFI_REQUEST_WIFI_SCAN);
+	}
+
+	/*
+	 * Here we should check for reconnect actions.
+	 */
+
+    } /* for(;;) */
+    vTaskDelay( (TickType_t)10);
+}
+
+
+const char *apsec[] = { "Open", "WEP", "WPA", "WPA2", "WPA WPA2", "Enterprise" };
+
+
+/**
+ * @brief Show an AP station as a button. The buttons are already defined.
+ * @param idx The index position on the display, 1 to 7.
+ * @param ap The AP information from the WiFi scan.
+ * @param show, How to display. 0 is blank, 1 is unknown, 2 is known, 3 is a connected AP.
+ */
+void Show_AP(uint8_t idx, wifi_ap_record_t ap, int show)
+{
+    char	tmp[33];
+
+    if ((idx > 7) || (idx < 1))
+	return;
+
+    if (show == 0) {
+	_bg = TFT_BLACK;
+    } else {
+    	_bg = (color_t){ 63, 63, 64 };
+    }
+
+    TFT_fillRect(Buttons[idx].x, Buttons[idx].y, Buttons[idx].w, Buttons[idx].h, _bg);
+    if (show == 0)
+	return;
+
+    TFT_drawRect(Buttons[idx].x, Buttons[idx].y, Buttons[idx].w, Buttons[idx].h, TFT_NAVY);
+    if (show == 3) {
+	_fg = TFT_WHITE;
+    } else if (show == 1) {
+    	_fg = TFT_YELLOW;
+    } else {
+	_fg = TFT_CYAN;
+    }
+
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    sprintf(tmp, "%s", ap.ssid);
+    TFT_print(tmp, Buttons[idx].x + 5, Buttons[idx].y + 6);
+    sprintf(tmp, "%d", ap.rssi);
+    TFT_setFont(DEF_SMALL_FONT, NULL);
+    TFT_print(tmp, Buttons[idx].x + Buttons[idx].w - (TFT_getStringWidth(tmp) + 5), Buttons[idx].y + 4);
+    sprintf(tmp, "%s", apsec[ap.authmode]);
+    TFT_print(tmp, Buttons[idx].x + Buttons[idx].w - (TFT_getStringWidth(tmp) + 5), Buttons[idx].y + 15);
+}
+
+
+
+bool WiFi_Init(void)
+{
+    char	pwd[65], pmpt[32];
+
+    switch (Main_Screen) {
+	case MAIN_TOOLS_SETUP_WIFI:
+			TopMessage("WiFi");
+			TFT_setFont(DEJAVU24_FONT, NULL);
+			_fg = TFT_WHITE;
+			TFT_print("Momentje ..", CENTER, CENTER);
+			_wifi_ScanAPs = true;
+			_wifi_ScanDone = false;
+			Buttons_Add(260, 200, 60, 40, "Ok", 0);
+			Buttons[0].dark = true;
+			Buttons_Show();
+			// Now add the buttons we draw manually.
+			Buttons_Add(  0, 30, 250, 30, "", 1);
+			Buttons_Add(  0, 60, 250, 30, "", 2);
+			Buttons_Add(  0, 90, 250, 30, "", 3);
+			Buttons_Add(  0,120, 250, 30, "", 4);
+			Buttons_Add(  0,150, 250, 30, "", 5);
+			Buttons_Add(  0,180, 250, 30, "", 6);
+			Buttons_Add(  0,210, 250, 30, "", 7);
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI_CUR:
+			TopMessage("WiFi verbinding");
+			// Get extra information.
+			wifi_ap_record_t ap_info;
+			esp_wifi_sta_get_ap_info(&ap_info);
+
+			wifi_config_t *wconfig = task_wifi_ConfigSTA /*task_wifi_GetWifiStaConfig( ) */;
+			if (wconfig) {
+
+			    tcpip_adapter_ip_info_t ip_info;
+			    ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info));
+			    char ip[IP4ADDR_STRLEN_MAX];
+			    char gw[IP4ADDR_STRLEN_MAX];
+			    char netmask[IP4ADDR_STRLEN_MAX];
+			    strcpy(ip, ip4addr_ntoa(&ip_info.ip));
+			    strcpy(netmask, ip4addr_ntoa(&ip_info.netmask));
+			    strcpy(gw, ip4addr_ntoa(&ip_info.gw));
+			    TFT_setFont(DEFAULT_FONT, NULL);
+			    _fg = TFT_WHITE;
+			    TFT_print("SSID", 155 - TFT_getStringWidth("SSID"), 40);
+			    TFT_print("Kanaal", 155 - TFT_getStringWidth("Kanaal"), 60);
+			    TFT_print("Rssi", 155 - TFT_getStringWidth("Rssi"), 80);
+			    TFT_print("Mode", 155 - TFT_getStringWidth("Mode"), 100);
+			    TFT_print("IP adres", 155 - TFT_getStringWidth("IP adres"), 120);
+			    TFT_print("Netmask", 155 - TFT_getStringWidth("Netmask"), 140);
+			    TFT_print("Gateway", 155 - TFT_getStringWidth("Gateway"), 160);
+			    _fg = TFT_YELLOW;
+			    TFT_print((char*)wconfig->sta.ssid, 165, 40);
+			    sprintf(pmpt, "%d", ap_info.primary);
+			    TFT_print(pmpt, 165, 60);
+			    sprintf(pmpt, "%d", ap_info.rssi);
+			    TFT_print(pmpt, 165, 80);
+			    sprintf(pmpt, "%s%s%s", ap_info.phy_11b ? "b":"", ap_info.phy_11g ? "g":"", ap_info.phy_11n ? "n":"");
+			    TFT_print(pmpt, 165, 100);
+			    TFT_print((char*)ip, 165, 120);
+			    TFT_print((char*)netmask, 165, 140);
+			    TFT_print((char*)gw, 165, 160);
+			}
+			Buttons_Add(130, 200, 60, 40, "Ok", 0);
+			Buttons[0].dark = true;
+			Buttons_Show();
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI_CON:
+			TopMessage("WiFi verbinden");
+			TFT_setFont(DEJAVU18_FONT, NULL);
+			_fg = TFT_WHITE;
+			TFT_print("SSID", 155 - TFT_getStringWidth("SSID"), 70);
+			_fg = TFT_YELLOW;
+			TFT_print((char*)_wifi_ssid, 165, 70);
+			Buttons_Add(  0, 200, 100, 40, "Annuleer", 0);
+			Buttons_Add(110, 200, 100, 40, "Vergeet",  1);
+			Buttons_Add(220, 200, 100, 40, "Verbind",  2);
+			Buttons_Show();
+			Buttons[0].dark = true;
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI_NEW:
+			TopMessage("WiFi nieuw");
+			snprintf(pmpt, 32, "Password for %s", _wifi_ssid);
+			pwd[0] = '\0';
+			EditTextMin(pmpt, pwd, 64, 8);
+			/*
+			 * Disconnect first
+			 */
+			_bg = TFT_BLACK;
+			TFT_fillScreen(_bg);
+			TFT_setFont(DEJAVU24_FONT, NULL);
+			_fg = TFT_WHITE;
+			TFT_print("Momentje ..", CENTER, CENTER);
+			xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_DISCONNECT);
+			vTaskDelay(100 / portTICK_PERIOD_MS);
+			xEventGroupWaitBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED, pdFALSE, pdTRUE, portMAX_DELAY );
+
+			/*
+			 * Setup new connection
+			 */
+			if (strlen(pwd)) {
+			    wifi_config_t* config = task_wifi_ConfigSTA;
+			    memset(config, 0x00, sizeof(wifi_config_t));
+			    memcpy(config->sta.ssid, _wifi_ssid, strlen((char*)_wifi_ssid));
+			    memcpy(config->sta.password, pwd, strlen(pwd));
+			    ESP_LOGI(TAG, "new AP %s %s", _wifi_ssid, pwd);
+			    xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_CONNECT);
+			} else {
+			    // TODO: what about WPS, it works but how to insert it in this app.
+			    return true;
+			}
+			// We must wait here for the result.
+			break;
+
+	default:
+			break;
+    }
+
+    return false;
+}
+
+
+bool WiFi_Loop(void)
+{
+    uint8_t		idx;
+    int			Choice;
+    static int		AP[8];
+    wifi_ap_record_t	ap;
+
+    switch (Main_Screen) {
+	case MAIN_TOOLS_SETUP_WIFI:
+			if (_wifi_ScanAPs) {
+			    xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_WIFI_SCAN);
+			    _wifi_ScanAPs = false;
+			    TimeSpent = 0;
+			}
+			if (_wifi_ScanDone) {
+			    /*
+			     * Show scan results. There is room for 7 entries. If we have a connection,
+			     * the first one is that connection followed by available Access Points.
+			     * If there is no connection yet, there is only a iist of available Access
+			     * points.
+			     * The list is sorted by signal strength and is filled by the eventhandler.
+			     */
+			    idx = 1;
+			    _wifi_ScanDone = false;
+
+			    if ((xEventGroupGetBits(xEventGroupWifi) & TASK_WIFI_STA_CONNECTED) == TASK_WIFI_STA_CONNECTED) {
+				// We are connected. Search and display this one.
+				if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+				    for (int i = 0; i < _wifi_Scanned; i++) {
+					ap = accessp_records[i];
+					if (strcmp(wifi_state->STA_ssid, (char *)ap.ssid) == 0) {
+					    AP[idx] = i;
+					    Show_AP(idx, ap, 3);
+					    idx++;
+					    break;
+					}
+				    }
+				    xSemaphoreGive(xSemaphoreWiFi);
+				}
+			    }
+
+			    // Display available Access Points.
+			    for (int i = 0; i < _wifi_Scanned; i++) {
+				// The connected AP can be somewhere in this list, don't display it again.
+				ap = accessp_records[i];
+				if (xSemaphoreTake(xSemaphoreWiFi, 25) == pdTRUE) {
+				    if (strcmp(wifi_state->STA_ssid, (char *)ap.ssid) == 0) {
+					xSemaphoreGive(xSemaphoreWiFi);
+					continue;	// Skip connected AP, already on top of the list.
+				    }
+				    xSemaphoreGive(xSemaphoreWiFi);
+				}
+				// Check if we know this AP in the database.
+				if ((read_station(ap.ssid) == -1)) {
+				    Show_AP(idx, ap, 2);	// Unknown
+				} else {
+				    if (wifiStation.hide) {
+					continue;		// Blacklisted.
+				    }
+				    Show_AP(idx, ap, 1);	// We know this one.
+				}
+				AP[idx] = i;
+				idx++;
+				if (idx ==  8)
+				    break;
+			    }
+			    if (idx < 7) {
+				for (int i = idx; i < 8; i++) {
+				    Show_AP(i, ap, 0);
+				    AP[i] = 0;
+				}
+			    }
+			}
+
+			Choice = Buttons_Scan();
+			if ((Choice >= 1) && (Choice <= 7)) {
+			    ap = accessp_records[AP[Choice]];
+			    sprintf((char *)_wifi_ssid, "%s", ap.ssid);		// Save selected SSID.
+			}
+			if ((Choice == 1) && ((xEventGroupGetBits(xEventGroupWifi) & TASK_WIFI_STA_CONNECTED) == TASK_WIFI_STA_CONNECTED)) {
+			    Main_Screen = MAIN_TOOLS_SETUP_WIFI_CUR;
+			    // Cancel a possible scan.
+			    ESP_ERROR_CHECK(esp_wifi_scan_stop());
+			} else if ((Choice >= 1) && (Choice <= 7)) {
+			    if ((read_station(_wifi_ssid) != -1)) {
+				Main_Screen = MAIN_TOOLS_SETUP_WIFI_CON;
+			    } else {
+				Main_Screen = MAIN_TOOLS_SETUP_WIFI_NEW;
+			    }
+			    ESP_ERROR_CHECK(esp_wifi_scan_stop());
+			} else if (Choice == 0) {
+			    Main_Screen = MAIN_TOOLS_SETUP;
+			    ESP_ERROR_CHECK(esp_wifi_scan_stop());
+			}
+			if (TimeSpent > 10) {
+			     _wifi_ScanAPs = true;
+			}
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI_CUR:
+
+			if (Buttons_Scan() == 0) {
+			    Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+			}
+			break;
+
+	case MAIN_TOOLS_SETUP_WIFI_CON:
+			switch (Buttons_Scan()) {
+			    case 0:	// Cancel choice
+				    	Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+					break;
+
+			    case 1:	// Forget connection
+					remove_station(_wifi_ssid);
+					Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+					break;
+
+			    case 2:	// Connect
+					_bg = TFT_BLACK;
+					TFT_fillScreen(_bg);
+					TFT_setFont(DEJAVU24_FONT, NULL);
+					_fg = TFT_WHITE;
+					TFT_print("Momentje ..", CENTER, CENTER);
+					/*
+					 * Disconnect old connections and wait until it's gone.
+					 */
+					xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_DISCONNECT);
+					vTaskDelay(100 / portTICK_PERIOD_MS);
+					xEventGroupWaitBits(xEventGroupWifi, TASK_WIFI_STA_DISCONNECTED, pdFALSE, pdTRUE, portMAX_DELAY );
+					/*
+					 * Setup new connection.
+					 */
+					if ((read_station(_wifi_ssid) != -1)) {
+					    wifi_config_t* config = task_wifi_ConfigSTA;
+					    memset(config, 0x00, sizeof(wifi_config_t));
+					    memcpy(config->sta.ssid, wifiStation.SSID, strlen(wifiStation.SSID));
+					    memcpy(config->sta.password, wifiStation.Password, strlen(wifiStation.Password));
+					    xEventGroupSetBits(xEventGroupWifi, TASK_WIFI_REQUEST_STA_CONNECT);
+					    vTaskDelay(1000 / portTICK_PERIOD_MS);
+					}
+					Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+					break;
+
+			    default:	break;
+			}
+			break;
+												         
+	case MAIN_TOOLS_SETUP_WIFI_NEW:
+			// All work is already done, jump to the base screen.
+			Main_Screen = MAIN_TOOLS_SETUP_WIFI;
+			break;
+
+	default:
+			break;
+    }
+
+    return false;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/task_wifi.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,73 @@
+/**
+ * @file task_wifi.h
+ * @brief WiFi task. Connects to a known Access Point. If we know more then
+ *        one AP, try to connect all of them until it succeeds (Not yet written).
+ */
+
+#ifndef _TASK_WIFI_H
+#define _TASK_WIFI_H
+
+
+/**
+ * @brief Defines the maximum number of access points that can be scanned.
+ *
+ * To save memory and avoid nasty out of memory errors,
+ * we can limit the number of APs detected in a wifi scan.
+ * The display can handle 7 entries, allow a few more.
+ */
+#define MAX_AP_NUM 			10
+
+/** 
+ * @brief Defines access point's maximum number of clients. 
+ */
+#define AP_MAX_CONNECTIONS 		4
+
+
+/**
+ * @brief Structure containing the information of the driver task.
+ */
+typedef struct {
+    bool		AP_active;		///< Is the AP active.
+    uint8_t		AP_clients;		///< Connected AP clients.
+    bool		STA_connected;		///< Connected to AP as station.
+    char		STA_ssid[33];		///< Connected to this SSID.
+    int8_t		STA_rssi;		///< Signal strength.
+    char		STA_ip[16];		///< IP address.
+    char		STA_nm[16];		///< IP netmask.
+    char		STA_gw[16];		///< IP gateway.
+} WIFI_State;
+
+
+
+typedef enum update_reason_code_t {
+    UPDATE_CONNECTION_OK = 0,
+    UPDATE_FAILED_ATTEMPT = 1,
+    UPDATE_USER_DISCONNECT = 2,
+    UPDATE_LOST_CONNECTION = 3
+} update_reason_code_t;
+
+
+
+/**
+ * @brief Get WiFi STA config.
+ * #return wifi_config_t structure.
+ */
+//wifi_config_t* task_wifi_GetWifiStaConfig(void);
+
+/**
+ * @brief Main task for the wifi_manager
+ */
+void task_wifi( void * pvParameters );
+
+/**
+ * @brief WiFi setup, init screens.
+ */
+bool WiFi_Init(void);
+
+/**
+ * @brief WiFi setup, loop screens.
+ */
+bool WiFi_Loop(void);
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/updates.c	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,174 @@
+/**
+ * @file updates.c
+ * @brief Updates management.
+ */
+
+#include "config.h"
+
+
+#define BUFFSIZE 1024
+static char ota_write_data[BUFFSIZE + 1] = { 0 };
+
+static const char               *TAG = "update";
+
+extern sButton                  Buttons[MAXBUTTONS];
+extern int                      Main_Screen;
+
+
+static void http_cleanup(esp_http_client_handle_t client)
+{
+    esp_http_client_close(client);
+    esp_http_client_cleanup(client);
+}
+
+
+
+void run_update(void)
+{
+    char                        temp[64];
+    esp_err_t                   err;
+    const esp_partition_t       *update_partition = NULL;
+    esp_ota_handle_t            update_handle = 0;
+
+    TFT_setFont(DEJAVU18_FONT, NULL);
+    _fg = TFT_CYAN;
+    const esp_partition_t       *running = esp_ota_get_running_partition();
+    snprintf(temp, 63, "Running part.type %d sub %d,\r\nat offset 0x%08x\r\n",
+		running->type, running->subtype, running->address);
+    TFT_print(temp, 0, 25);
+
+    /*
+     * Don't use https because it costs more then 100K memory.
+     */
+    esp_http_client_config_t update = {
+	.url = "http://seaport.mbse.ym/update/brewboard.bin",
+    };
+
+    esp_http_client_handle_t client = esp_http_client_init(&update);
+    if (client == NULL) {
+	ESP_LOGI(TAG, "Failed to init HTTP connection");
+	goto updateerr;
+    }
+
+    err = esp_http_client_open(client, 0);
+    if (err != ESP_OK) {
+	ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
+	esp_http_client_cleanup(client);
+	goto updateerr;
+    }
+
+    esp_http_client_fetch_headers(client);
+    update_partition = esp_ota_get_next_update_partition(NULL);
+    if (update_partition == NULL) {
+	ESP_LOGE(TAG, "No update partition");
+	esp_http_client_cleanup(client);
+	goto updateerr;
+    }
+    ESP_LOGI(TAG, "Update to partition subtype %d at offset 0x%x", update_partition->subtype, update_partition->address);
+
+    err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
+    if (err != ESP_OK) {
+	ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
+	http_cleanup(client);
+	goto updateerr;
+    }
+
+    TFT_print("Begin download.\r\n", 0, LASTY);
+    ESP_LOGI(TAG, "Download update %s", update.url);
+    int binary_file_length = 0;
+    /*deal with all receive packet*/
+    while (1) {
+	int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE);
+	if (data_read < 0) {
+	    ESP_LOGE(TAG, "Error: data read error");
+	    http_cleanup(client);
+	    goto updateerr;;
+	} else if (data_read > 0) {
+	    err = esp_ota_write( update_handle, (const void *)ota_write_data, data_read);
+	    if (err != ESP_OK) {
+		http_cleanup(client);
+		goto updateerr;;
+	    }
+	    binary_file_length += data_read;
+//	    ESP_LOGD(TAG, "Written image length %d", binary_file_length);
+	} else if (data_read == 0) {
+	    break;
+	}
+    }
+
+    ESP_LOGI(TAG, "Download complete, binary data length: %d", binary_file_length);
+    http_cleanup(client);
+
+    if (esp_ota_end(update_handle) != ESP_OK) {
+	ESP_LOGE(TAG, "esp_ota_end failed!");
+	goto updateerr;
+    }
+    snprintf(temp, 63, "Received image %d bytes\r\n", binary_file_length);
+    TFT_print(temp, 0, LASTY);
+
+    if (esp_partition_check_identity(esp_ota_get_running_partition(), update_partition) == true) {
+	ESP_LOGI(TAG, "Already the latest version");
+	TFT_print("Already the latest version.\r\n", LASTX, LASTY);
+	goto updateok;
+    }
+
+    /*
+     * Here we have a different and hopefully newer version, install and boot it.
+     */
+    err = esp_ota_set_boot_partition(update_partition);
+    if (err != ESP_OK) {
+	ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
+	goto updateerr;
+    }
+
+    ESP_LOGI(TAG, "Prepare to restart system!");
+    TFT_print("Rebooting ...", 0, LASTY);
+    vTaskDelay(1000 / portTICK_PERIOD_MS);
+    esp_restart();
+    return ;
+
+updateerr:
+    _fg = TFT_RED;
+    TFT_print("Error\r\n", 0, LASTY);
+
+updateok:
+    vTaskDelay(3000 / portTICK_PERIOD_MS);
+}
+
+
+
+/*
+ * Files init function, only runs once a new screen is entered.
+ */
+void Updates_Init(void)
+{
+    switch (Main_Screen) {
+	case MAIN_TOOLS_UPDATES:
+			_bg = TFT_BLACK;
+			TFT_fillScreen(_bg);
+			TopMessage("Update");
+			break;
+
+	default:	break;
+    }
+}
+
+
+
+/*
+ * Updates management loop, non-blocking.
+ */
+void Updates_Loop(void)
+{
+    switch (Main_Screen) {
+
+	case MAIN_TOOLS_UPDATES:
+			run_update();
+			Main_Screen = MAIN_TOOLS;
+			break;
+
+	default:	break;
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/updates.h	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,18 @@
+/**
+ * @file updates.h
+ * @brief Updates management.
+ */
+#ifndef _UPDATES_H
+#define _UPDATES_H
+
+/**
+ *  * @brief Updates init fases.
+ *   */
+void Updates_Init(void);
+
+/**
+ *  * @brief Updates loop screens. Non-blocking.
+ *   */
+void Updates_Loop(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/partitions.csv	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,9 @@
+# Name,   Type, SubType, Offset,  Size, Flags
+# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
+nvs,      data, nvs,     0x9000,  0x4000,
+otadata,  data, ota,     0xd000,  0x2000
+phy_init, data, phy,     0xf000,  0x1000,
+factory,  app,  factory, 0x10000, 1M,
+ota_0,    0,    ota_0,   0x110000, 1M,
+ota_1,    0,    ota_1,	 0x210000, 1M,
+storage,  data, spiffs,  0x310000, 0xf0000,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sdkconfig	Sat Oct 20 13:23:15 2018 +0200
@@ -0,0 +1,660 @@
+#
+# Automatically generated file; DO NOT EDIT.
+# Espressif IoT Development Framework Configuration
+#
+
+#
+# SDK tool configuration
+#
+CONFIG_TOOLPREFIX="xtensa-esp32-elf-"
+CONFIG_PYTHON="python"
+CONFIG_MAKE_WARN_UNDEFINED_VARIABLES=y
+
+#
+# Bootloader config
+#
+CONFIG_LOG_BOOTLOADER_LEVEL_NONE=
+CONFIG_LOG_BOOTLOADER_LEVEL_ERROR=
+CONFIG_LOG_BOOTLOADER_LEVEL_WARN=
+CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y
+CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG=
+CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE=
+CONFIG_LOG_BOOTLOADER_LEVEL=3
+CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V=
+CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y
+CONFIG_BOOTLOADER_FACTORY_RESET=
+CONFIG_BOOTLOADER_APP_TEST=
+CONFIG_BOOTLOADER_WDT_ENABLE=
+
+#
+# Security features
+#
+CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=
+CONFIG_SECURE_BOOT_ENABLED=
+CONFIG_FLASH_ENCRYPTION_ENABLED=
+
+#
+# Serial flasher config
+#
+CONFIG_ESPTOOLPY_PORT="/dev/ttyUSB0"
+CONFIG_ESPTOOLPY_BAUD_115200B=y
+CONFIG_ESPTOOLPY_BAUD_230400B=
+CONFIG_ESPTOOLPY_BAUD_921600B=
+CONFIG_ESPTOOLPY_BAUD_2MB=
+CONFIG_ESPTOOLPY_BAUD_OTHER=
+CONFIG_ESPTOOLPY_BAUD_OTHER_VAL=115200
+CONFIG_ESPTOOLPY_BAUD=115200
+CONFIG_ESPTOOLPY_COMPRESSED=y
+CONFIG_FLASHMODE_QIO=
+CONFIG_FLASHMODE_QOUT=
+CONFIG_FLASHMODE_DIO=y
+CONFIG_FLASHMODE_DOUT=
+CONFIG_ESPTOOLPY_FLASHMODE="dio"
+CONFIG_ESPTOOLPY_FLASHFREQ_80M=
+CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
+CONFIG_ESPTOOLPY_FLASHFREQ_26M=
+CONFIG_ESPTOOLPY_FLASHFREQ_20M=
+CONFIG_ESPTOOLPY_FLASHFREQ="40m"
+CONFIG_ESPTOOLPY_FLASHSIZE_1MB=
+CONFIG_ESPTOOLPY_FLASHSIZE_2MB=
+CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=
+CONFIG_ESPTOOLPY_FLASHSIZE_16MB=
+CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
+CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y
+CONFIG_ESPTOOLPY_BEFORE_RESET=y
+CONFIG_ESPTOOLPY_BEFORE_NORESET=
+CONFIG_ESPTOOLPY_BEFORE="default_reset"
+CONFIG_ESPTOOLPY_AFTER_RESET=y
+CONFIG_ESPTOOLPY_AFTER_NORESET=
+CONFIG_ESPTOOLPY_AFTER="hard_reset"
+CONFIG_MONITOR_BAUD_9600B=
+CONFIG_MONITOR_BAUD_57600B=
+CONFIG_MONITOR_BAUD_115200B=y
+CONFIG_MONITOR_BAUD_230400B=
+CONFIG_MONITOR_BAUD_921600B=
+CONFIG_MONITOR_BAUD_2MB=
+CONFIG_MONITOR_BAUD_OTHER=
+CONFIG_MONITOR_BAUD_OTHER_VAL=115200
+CONFIG_MONITOR_BAUD=115200
+
+#
+# BrewBoard Configuration
+#
+CONFIG_TEMP_SENSORS_ONEWIRE=
+CONFIG_TEMP_SENSORS_SIMULATOR=y
+CONFIG_SSR_MLT_GPIO=33
+CONFIG_SSR_HLT_GPIO=32
+CONFIG_SSR_PUMP_GPIO=12
+CONFIG_BUZZER_GPIO=25
+
+#
+# Partition Table
+#
+CONFIG_PARTITION_TABLE_SINGLE_APP=
+CONFIG_PARTITION_TABLE_TWO_OTA=
+CONFIG_PARTITION_TABLE_CUSTOM=y
+CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
+CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
+CONFIG_PARTITION_TABLE_OFFSET=0x8000
+CONFIG_PARTITION_TABLE_MD5=y
+
+#
+# Compiler options
+#
+CONFIG_OPTIMIZATION_LEVEL_DEBUG=y
+CONFIG_OPTIMIZATION_LEVEL_RELEASE=
+CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y
+CONFIG_OPTIMIZATION_ASSERTIONS_SILENT=
+CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED=
+CONFIG_CXX_EXCEPTIONS=
+CONFIG_STACK_CHECK_NONE=y
+CONFIG_STACK_CHECK_NORM=
+CONFIG_STACK_CHECK_STRONG=
+CONFIG_STACK_CHECK_ALL=
+CONFIG_STACK_CHECK=
+CONFIG_WARN_WRITE_STRINGS=
+CONFIG_DISABLE_GCC8_WARNINGS=
+
+#
+# Component config
+#
+
+#
+# Application Level Tracing
+#
+CONFIG_ESP32_APPTRACE_DEST_TRAX=
+CONFIG_ESP32_APPTRACE_DEST_NONE=y
+CONFIG_ESP32_APPTRACE_ENABLE=
+CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
+CONFIG_AWS_IOT_SDK=
+
+#
+# Bluetooth
+#
+CONFIG_BT_ENABLED=
+CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=0
+CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=0
+CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0
+CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0
+CONFIG_BT_RESERVE_DRAM=0
+
+#
+# Driver configurations
+#
+
+#
+# ADC configuration
+#
+CONFIG_ADC_FORCE_XPD_FSM=
+CONFIG_ADC2_DISABLE_DAC=y
+
+#
+# SPI configuration
+#
+CONFIG_SPI_MASTER_IN_IRAM=
+CONFIG_SPI_MASTER_ISR_IN_IRAM=y
+CONFIG_SPI_SLAVE_IN_IRAM=
+CONFIG_SPI_SLAVE_ISR_IN_IRAM=y
+
+#
+# ESP32-specific
+#
+CONFIG_ESP32_DEFAULT_CPU_FREQ_80=
+CONFIG_ESP32_DEFAULT_CPU_FREQ_160=
+CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
+CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=240
+CONFIG_SPIRAM_SUPPORT=
+CONFIG_MEMMAP_TRACEMEM=
+CONFIG_MEMMAP_TRACEMEM_TWOBANKS=
+CONFIG_ESP32_TRAX=
+CONFIG_TRACEMEM_RESERVE_DRAM=0x0
+CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH=
+CONFIG_ESP32_ENABLE_COREDUMP_TO_UART=
+CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y
+CONFIG_ESP32_ENABLE_COREDUMP=
+CONFIG_TWO_UNIVERSAL_MAC_ADDRESS=
+CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y
+CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4
+CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
+CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2048
+CONFIG_MAIN_TASK_STACK_SIZE=3584
+CONFIG_IPC_TASK_STACK_SIZE=1024
+CONFIG_TIMER_TASK_STACK_SIZE=3584
+CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y
+CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF=
+CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR=
+CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF=
+CONFIG_NEWLIB_STDIN_LINE_ENDING_LF=
+CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y
+CONFIG_NEWLIB_NANO_FORMAT=
+CONFIG_CONSOLE_UART_DEFAULT=y
+CONFIG_CONSOLE_UART_CUSTOM=
+CONFIG_CONSOLE_UART_NONE=
+CONFIG_CONSOLE_UART_NUM=0
+CONFIG_CONSOLE_UART_BAUDRATE=115200
+CONFIG_ULP_COPROC_ENABLED=
+CONFIG_ULP_COPROC_RESERVE_MEM=0
+CONFIG_ESP32_PANIC_PRINT_HALT=
+CONFIG_ESP32_PANIC_PRINT_REBOOT=y
+CONFIG_ESP32_PANIC_SILENT_REBOOT=
+CONFIG_ESP32_PANIC_GDBSTUB=
+CONFIG_ESP32_DEBUG_OCDAWARE=y
+CONFIG_ESP32_DEBUG_STUBS_ENABLE=y
+CONFIG_INT_WDT=y
+CONFIG_INT_WDT_TIMEOUT_MS=300
+CONFIG_INT_WDT_CHECK_CPU1=y
+CONFIG_TASK_WDT=y
+CONFIG_TASK_WDT_PANIC=
+CONFIG_TASK_WDT_TIMEOUT_S=5
+CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
+CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
+CONFIG_BROWNOUT_DET=y
+CONFIG_BROWNOUT_DET_LVL_SEL_0=y
+CONFIG_BROWNOUT_DET_LVL_SEL_1=
+CONFIG_BROWNOUT_DET_LVL_SEL_2=
+CONFIG_BROWNOUT_DET_LVL_SEL_3=
+CONFIG_BROWNOUT_DET_LVL_SEL_4=
+CONFIG_BROWNOUT_DET_LVL_SEL_5=
+CONFIG_BROWNOUT_DET_LVL_SEL_6=
+CONFIG_BROWNOUT_DET_LVL_SEL_7=
+CONFIG_BROWNOUT_DET_LVL=0
+CONFIG_REDUCE_PHY_TX_POWER=y
+CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y
+CONFIG_ESP32_TIME_SYSCALL_USE_RTC=
+CONFIG_ESP32_TIME_SYSCALL_USE_FRC1=
+CONFIG_ESP32_TIME_SYSCALL_USE_NONE=
+CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y
+CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL=
+CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC=
+CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256=
+CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024
+CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000
+CONFIG_ESP32_XTAL_FREQ_40=y
+CONFIG_ESP32_XTAL_FREQ_26=
+CONFIG_ESP32_XTAL_FREQ_AUTO=
+CONFIG_ESP32_XTAL_FREQ=40
+CONFIG_DISABLE_BASIC_ROM_CONSOLE=
+CONFIG_NO_BLOBS=
+CONFIG_ESP_TIMER_PROFILING=
+CONFIG_COMPATIBLE_PRE_V2_1_BOOTLOADERS=
+CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
+
+#
+# Wi-Fi
+#
+CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
+CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
+CONFIG_ESP32_WIFI_STATIC_TX_BUFFER=
+CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y
+CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1
+CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
+CONFIG_ESP32_WIFI_CSI_ENABLED=
+CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y
+CONFIG_ESP32_WIFI_TX_BA_WIN=6
+CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y
+CONFIG_ESP32_WIFI_RX_BA_WIN=6
+CONFIG_ESP32_WIFI_NVS_ENABLED=y
+CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y
+CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1=
+CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752
+
+#
+# PHY
+#
+CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y
+CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION=
+CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20
+CONFIG_ESP32_PHY_MAX_TX_POWER=20
+
+#
+# Power Management
+#
+CONFIG_PM_ENABLE=
+
+#
+# ADC-Calibration
+#
+CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y
+CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y
+CONFIG_ADC_CAL_LUT_ENABLE=y
+
+#
+# ESP HTTP client
+#
+CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=
+
+#
+# Ethernet
+#
+CONFIG_DMA_RX_BUF_NUM=10
+CONFIG_DMA_TX_BUF_NUM=10
+CONFIG_EMAC_L2_TO_L3_RX_BUF_MODE=
+CONFIG_EMAC_CHECK_LINK_PERIOD_MS=2000
+CONFIG_EMAC_TASK_PRIORITY=20
+CONFIG_EMAC_TASK_STACK_SIZE=3072
+
+#
+# FAT Filesystem support
+#
+CONFIG_FATFS_CODEPAGE_DYNAMIC=
+CONFIG_FATFS_CODEPAGE_437=y
+CONFIG_FATFS_CODEPAGE_720=
+CONFIG_FATFS_CODEPAGE_737=
+CONFIG_FATFS_CODEPAGE_771=
+CONFIG_FATFS_CODEPAGE_775=
+CONFIG_FATFS_CODEPAGE_850=
+CONFIG_FATFS_CODEPAGE_852=
+CONFIG_FATFS_CODEPAGE_855=
+CONFIG_FATFS_CODEPAGE_857=
+CONFIG_FATFS_CODEPAGE_860=
+CONFIG_FATFS_CODEPAGE_861=
+CONFIG_FATFS_CODEPAGE_862=
+CONFIG_FATFS_CODEPAGE_863=
+CONFIG_FATFS_CODEPAGE_864=
+CONFIG_FATFS_CODEPAGE_865=
+CONFIG_FATFS_CODEPAGE_866=
+CONFIG_FATFS_CODEPAGE_869=
+CONFIG_FATFS_CODEPAGE_932=
+CONFIG_FATFS_CODEPAGE_936=
+CONFIG_FATFS_CODEPAGE_949=
+CONFIG_FATFS_CODEPAGE_950=
+CONFIG_FATFS_CODEPAGE=437
+CONFIG_FATFS_LFN_NONE=
+CONFIG_FATFS_LFN_HEAP=y
+CONFIG_FATFS_LFN_STACK=
+CONFIG_FATFS_MAX_LFN=255
+CONFIG_FATFS_API_ENCODING_ANSI_OEM=y
+CONFIG_FATFS_API_ENCODING_UTF_16=
+CONFIG_FATFS_API_ENCODING_UTF_8=
+CONFIG_FATFS_FS_LOCK=0
+CONFIG_FATFS_TIMEOUT_MS=10000
+CONFIG_FATFS_PER_FILE_CACHE=y
+
+#
+# FreeRTOS
+#
+CONFIG_FREERTOS_UNICORE=
+CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF
+CONFIG_FREERTOS_CORETIMER_0=y
+CONFIG_FREERTOS_CORETIMER_1=
+CONFIG_FREERTOS_HZ=1000
+CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y
+CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE=
+CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL=
+CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y
+CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=
+CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y
+CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1
+CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y
+CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE=
+CONFIG_FREERTOS_ASSERT_DISABLE=
+CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1024
+CONFIG_FREERTOS_ISR_STACKSIZE=1536
+CONFIG_FREERTOS_LEGACY_HOOKS=
+CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
+CONFIG_SUPPORT_STATIC_ALLOCATION=
+CONFIG_TIMER_TASK_PRIORITY=1
+CONFIG_TIMER_TASK_STACK_DEPTH=2048
+CONFIG_TIMER_QUEUE_LENGTH=10
+CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
+CONFIG_FREERTOS_USE_TRACE_FACILITY=y
+CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
+CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=
+CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=
+CONFIG_FREERTOS_DEBUG_INTERNALS=
+CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y
+
+#
+# Heap memory debugging
+#
+CONFIG_HEAP_POISONING_DISABLED=
+CONFIG_HEAP_POISONING_LIGHT=y
+CONFIG_HEAP_POISONING_COMPREHENSIVE=
+CONFIG_HEAP_TRACING=y
+CONFIG_HEAP_TRACING_STACK_DEPTH=2
+CONFIG_HEAP_TASK_TRACKING=y
+
+#
+# HTTP Server
+#
+CONFIG_HTTPD_MAX_REQ_HDR_LEN=512
+CONFIG_HTTPD_MAX_URI_LEN=512
+
+#
+# libsodium
+#
+CONFIG_LIBSODIUM_USE_MBEDTLS_SHA=y
+
+#
+# Log output
+#
+CONFIG_LOG_DEFAULT_LEVEL_NONE=
+CONFIG_LOG_DEFAULT_LEVEL_ERROR=
+CONFIG_LOG_DEFAULT_LEVEL_WARN=
+CONFIG_LOG_DEFAULT_LEVEL_INFO=y
+CONFIG_LOG_DEFAULT_LEVEL_DEBUG=
+CONFIG_LOG_DEFAULT_LEVEL_VERBOSE=
+CONFIG_LOG_DEFAULT_LEVEL=3
+CONFIG_LOG_COLORS=y
+
+#
+# LWIP
+#
+CONFIG_L2_TO_L3_COPY=
+CONFIG_LWIP_IRAM_OPTIMIZATION=
+CONFIG_LWIP_MAX_SOCKETS=10
+CONFIG_USE_ONLY_LWIP_SELECT=
+CONFIG_LWIP_SO_REUSE=y
+CONFIG_LWIP_SO_REUSE_RXTOALL=y
+CONFIG_LWIP_SO_RCVBUF=
+CONFIG_LWIP_DHCP_MAX_NTP_SERVERS=1
+CONFIG_LWIP_IP_FRAG=
+CONFIG_LWIP_IP_REASSEMBLY=
+CONFIG_LWIP_STATS=
+CONFIG_LWIP_ETHARP_TRUST_IP_MAC=y
+CONFIG_ESP_GRATUITOUS_ARP=y
+CONFIG_GARP_TMR_INTERVAL=60
+CONFIG_TCPIP_RECVMBOX_SIZE=32
+CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y
+CONFIG_LWIP_DHCP_RESTORE_LAST_IP=
+
+#
+# DHCP server
+#
+CONFIG_LWIP_DHCPS_LEASE_UNIT=60
+CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8
+CONFIG_LWIP_AUTOIP=
+CONFIG_LWIP_NETIF_LOOPBACK=y
+CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8
+
+#
+# TCP
+#
+CONFIG_LWIP_MAX_ACTIVE_TCP=16
+CONFIG_LWIP_MAX_LISTENING_TCP=16
+CONFIG_TCP_MAXRTX=12
+CONFIG_TCP_SYNMAXRTX=6
+CONFIG_TCP_MSS=1436
+CONFIG_TCP_MSL=60000
+CONFIG_TCP_SND_BUF_DEFAULT=5744
+CONFIG_TCP_WND_DEFAULT=5744
+CONFIG_TCP_RECVMBOX_SIZE=6
+CONFIG_TCP_QUEUE_OOSEQ=y
+CONFIG_ESP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES=
+CONFIG_TCP_OVERSIZE_MSS=y
+CONFIG_TCP_OVERSIZE_QUARTER_MSS=
+CONFIG_TCP_OVERSIZE_DISABLE=
+
+#
+# UDP
+#
+CONFIG_LWIP_MAX_UDP_PCBS=16
+CONFIG_UDP_RECVMBOX_SIZE=6
+CONFIG_TCPIP_TASK_STACK_SIZE=2560
+CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y
+CONFIG_TCPIP_TASK_AFFINITY_CPU0=
+CONFIG_TCPIP_TASK_AFFINITY_CPU1=
+CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF
+CONFIG_PPP_SUPPORT=
+
+#
+# ICMP
+#
+CONFIG_LWIP_MULTICAST_PING=
+CONFIG_LWIP_BROADCAST_PING=
+
+#
+# LWIP RAW API
+#
+CONFIG_LWIP_MAX_RAW_PCBS=16
+
+#
+# mbedTLS
+#
+CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
+CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC=
+CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC=
+CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
+CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=
+CONFIG_MBEDTLS_DEBUG=
+CONFIG_MBEDTLS_HARDWARE_AES=y
+CONFIG_MBEDTLS_HARDWARE_MPI=
+CONFIG_MBEDTLS_HARDWARE_SHA=
+CONFIG_MBEDTLS_HAVE_TIME=y
+CONFIG_MBEDTLS_HAVE_TIME_DATE=
+CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y
+CONFIG_MBEDTLS_TLS_SERVER_ONLY=
+CONFIG_MBEDTLS_TLS_CLIENT_ONLY=
+CONFIG_MBEDTLS_TLS_DISABLED=
+CONFIG_MBEDTLS_TLS_SERVER=y
+CONFIG_MBEDTLS_TLS_CLIENT=y
+CONFIG_MBEDTLS_TLS_ENABLED=y
+
+#
+# TLS Key Exchange Methods
+#
+CONFIG_MBEDTLS_PSK_MODES=
+CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y
+CONFIG_MBEDTLS_SSL_RENEGOTIATION=y
+CONFIG_MBEDTLS_SSL_PROTO_SSL3=
+CONFIG_MBEDTLS_SSL_PROTO_TLS1=y
+CONFIG_MBEDTLS_SSL_PROTO_TLS1_1=y
+CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
+CONFIG_MBEDTLS_SSL_PROTO_DTLS=
+CONFIG_MBEDTLS_SSL_ALPN=y
+CONFIG_MBEDTLS_SSL_SESSION_TICKETS=y
+
+#
+# Symmetric Ciphers
+#
+CONFIG_MBEDTLS_AES_C=y
+CONFIG_MBEDTLS_CAMELLIA_C=
+CONFIG_MBEDTLS_DES_C=
+CONFIG_MBEDTLS_RC4_DISABLED=y
+CONFIG_MBEDTLS_RC4_ENABLED_NO_DEFAULT=
+CONFIG_MBEDTLS_RC4_ENABLED=
+CONFIG_MBEDTLS_BLOWFISH_C=
+CONFIG_MBEDTLS_XTEA_C=
+CONFIG_MBEDTLS_CCM_C=y
+CONFIG_MBEDTLS_GCM_C=y
+CONFIG_MBEDTLS_RIPEMD160_C=
+
+#
+# Certificates
+#
+CONFIG_MBEDTLS_PEM_PARSE_C=y
+CONFIG_MBEDTLS_PEM_WRITE_C=y
+CONFIG_MBEDTLS_X509_CRL_PARSE_C=y
+CONFIG_MBEDTLS_X509_CSR_PARSE_C=y
+CONFIG_MBEDTLS_ECP_C=y
+CONFIG_MBEDTLS_ECDH_C=y
+CONFIG_MBEDTLS_ECDSA_C=y
+CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
+CONFIG_MBEDTLS_ECP_NIST_OPTIM=y
+
+#
+# mDNS
+#
+CONFIG_MDNS_MAX_SERVICES=10
+
+#
+# ESP-MQTT Configurations
+#
+CONFIG_MQTT_PROTOCOL_311=y
+CONFIG_MQTT_TRANSPORT_SSL=
+CONFIG_MQTT_TRANSPORT_WEBSOCKET=
+CONFIG_MQTT_USE_CUSTOM_CONFIG=
+CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED=
+CONFIG_MQTT_CUSTOM_OUTBOX=
+
+#
+# NVS
+#
+
+#
+# OpenSSL
+#
+CONFIG_OPENSSL_DEBUG=
+CONFIG_OPENSSL_ASSERT_DO_NOTHING=y
+CONFIG_OPENSSL_ASSERT_EXIT=
+
+#
+# PThreads
+#
+CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5
+CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072
+CONFIG_PTHREAD_STACK_MIN=768
+
+#
+# SPI Flash driver
+#
+CONFIG_SPI_FLASH_VERIFY_WRITE=
+CONFIG_SPI_FLASH_ENABLE_COUNTERS=
+CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y
+CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y
+CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS=
+CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED=
+
+#
+# SPIFFS Configuration
+#
+CONFIG_SPIFFS_MAX_PARTITIONS=3
+
+#
+# SPIFFS Cache Configuration
+#
+CONFIG_SPIFFS_CACHE=y
+CONFIG_SPIFFS_CACHE_WR=y
+CONFIG_SPIFFS_CACHE_STATS=
+CONFIG_SPIFFS_PAGE_CHECK=y
+CONFIG_SPIFFS_GC_MAX_RUNS=10
+CONFIG_SPIFFS_GC_STATS=
+CONFIG_SPIFFS_PAGE_SIZE=256
+CONFIG_SPIFFS_OBJ_NAME_LEN=32
+CONFIG_SPIFFS_USE_MAGIC=y
+CONFIG_SPIFFS_USE_MAGIC_LENGTH=y
+CONFIG_SPIFFS_META_LENGTH=4
+CONFIG_SPIFFS_USE_MTIME=y
+
+#
+# Debug Configuration
+#
+CONFIG_SPIFFS_DBG=
+CONFIG_SPIFFS_API_DBG=
+CONFIG_SPIFFS_GC_DBG=
+CONFIG_SPIFFS_CACHE_DBG=
+CONFIG_SPIFFS_CHECK_DBG=
+CONFIG_SPIFFS_TEST_VISUALISATION=
+
+#
+# TCP/IP Adapter
+#
+CONFIG_IP_LOST_TIMER_INTERVAL=120
+CONFIG_TCPIP_LWIP=y
+
+#
+# Virtual file system
+#
+CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y
+CONFIG_SUPPORT_TERMIOS=
+
+#
+# VNC Server
+#
+CONFIG_VNC_SERVER_PORT=5900
+CONFIG_VNC_SERVER_FRAME_WIDTH=320
+CONFIG_VNC_SERVER_FRAME_HEIGHT=240
+
+#
+# Wear Levelling
+#
+CONFIG_WL_SECTOR_SIZE_512=
+CONFIG_WL_SECTOR_SIZE_4096=y
+CONFIG_WL_SECTOR_SIZE=4096
+
+#
+# WebSocket Server
+#
+CONFIG_WEBSOCKET_SERVER_MAX_CLIENTS=10
+CONFIG_WEBSOCKET_SERVER_QUEUE_SIZE=10
+CONFIG_WEBSOCKET_SERVER_QUEUE_TIMEOUT=30
+CONFIG_WEBSOCKET_SERVER_TASK_STACK_DEPTH=6000
+CONFIG_WEBSOCKET_SERVER_TASK_PRIORITY=5

mercurial