Rotary Menu for input of SSID and Password

(edits: new video URL and text additions)

My first attempt of a piece of code to input wifi SSID and password via a menu option with use of a rotary encoder.
I had so much fun coding this although, as most of you can tell, it's a not-so-experienced-programming-level of coding. BUT I learned so much! I almost never ask help because doing it yourself is much better for learning. It took me 2 weeks... I also lost a lot of hair due to scratching my head so much when getting stuck so often...
(uhm... Yes I can hard-code the wifi credentials but I just wanted a new challenge...)

I used a Arduino Nano 33 IoT and VScode/PlatformIO.

Here a link to the somewhat long demo video: CLICK

The sketch of which the only part working is the password input menu.
I love to learn... I must be possible to do this better (with an excisting library)

Oh one thing: The 1,44" TFT screen I used has a terribly narrow viewing angle... Any suggestions for a better one? has to be 1" x 1.5" max.

Thanks.

BTW1: This menu option will be part of the v2 of my Bathroom Fan Controller of which version 1 performs so very well.

#include <Arduino.h>
// Wifi
#include <WiFiNINA.h>
#include <WiFiUdp.h>
#include "arduino_secrets.h"
// Includes for Time
#include <NTPClient.h>
#include <TimeLib.h>
#include <Timezone.h>
#include "timeRules.h"
// Buttons
#include <AceButton.h>
// TFT screen
#include <TFT_eSPI.h> // Hardware-specific library
//
#include <SPI.h>
#include <Wire.h>
// Encoder
#include <SimpleRotary.h>

// Pin A, Pin B, Button Pin
SimpleRotary enc(A0, A1, A2);
// TFT 1,44" ST7735 Green tab3
TFT_eSPI tft = TFT_eSPI();

// Additional font colors beside the ones of the TFT_eSPI library
#define TFT_DARKGREY1 0x5ACB // Box Backgound
#define TFT_DEEPGREY 0x2945
#define TFT_DARKBLUE 0x0006
#define TFT_TURQUOISE 0x471A
#define TFT_GOLDENROD 0xDD24
#define TFT_SLATEBLUE 0x6AD9
#define TFT_DARKGREEN1 0x0140
#define TFT_DARKRED 0x5000
#define TFT_DEEPRED 0x3000
#define TFT_NICEGREEN 0x5C4C
#define TFT_ORANGE1 0xFBE0
#define TFT_LOWBRIGHTBLUE 0x3C33

// Do not include "" around the array name!
#define AA_FONT_SMALL NotoSansBold15
#define AA_FONT_LARGE NotoSansBold36

// Custom Graphics
// Ventilation Fan icon:
#define fan_width 16
#define fan_height 16
const unsigned char fan_icon[] PROGMEM = {
    // fan, 16x16px
    0x0f, 0x00, 0x1f, 0x80, 0x1f, 0xc0, 0x0f, 0xc6, 0x07, 0xcf, 0x03, 0x9f, 0x3b, 0xff, 0x7e, 0x7f,
    0xfe, 0x7e, 0xff, 0xdc, 0xf9, 0xc0, 0xf3, 0xe0, 0x63, 0xf0, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0xf0};

//Wifi
int status = WL_AP_CONNECTED;

//Button
using namespace ace_button;
// The pin numbers attached to the buttons.
const int MODE_BUTTON_PIN = 2;
const int UP_BUTTON_PIN = 3;
const int DOWN_BUTTON_PIN = 4;
// Two buttons, automatically sharing the default System ButtonConfig.
AceButton modeButton(MODE_BUTTON_PIN);
AceButton upButton(UP_BUTTON_PIN);
AceButton downButton(DOWN_BUTTON_PIN);

//please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = WIFI_SSID;     // your network SSID (name)
char pass[] = WIFI_PASSWORD; // your network password (use for WPA, or use as key for WEP)

// A UDP instance to let us send and receive packets over UDP
// Wifi Objects
WiFiUDP ntpUDP;

// Time Objects
NTPClient timeClient(ntpUDP);
// Function prototypes
time_t syncNTPTime();

//                            d8b          888      888
//                            Y8P          888      888
//                                         888      888
//  888  888  8888b.  888d888 888  8888b.  88888b.  888  .d88b.  .d8888b
//  888  888     "88b 888P"   888     "88b 888 "88b 888 d8P  Y8b 88K
//  Y88  88P .d888888 888     888 .d888888 888  888 888 88888888 "Y8888b.
//   Y8bd8P  888  888 888     888 888  888 888 d88P 888 Y8b.          X88
//    Y88P   "Y888888 888     888 "Y888888 88888P"  888  "Y8888   88888P'
//
//
//
// custom ASCII (lookup) table. If entering characters it's easier to know
// roughly where to find the character you need.
// You can change the it to your own liking... I used a spreadsheet to quickly
// put this together and then copied it here with some editing afterwards.
const int asciiVal[][2] = {
    {0, 65},   // ASCII: A
    {1, 66},   // ASCII: B
    {2, 67},   // ASCII: C
    {3, 68},   // ASCII: D
    {4, 69},   // ASCII: E
    {5, 70},   // ASCII: F
    {6, 71},   // ASCII: G
    {7, 72},   // ASCII: H
    {8, 73},   // ASCII: I
    {9, 74},   // ASCII: J
    {10, 75},  // ASCII: K
    {11, 76},  // ASCII: L
    {12, 77},  // ASCII: M
    {13, 78},  // ASCII: N
    {14, 79},  // ASCII: O
    {15, 80},  // ASCII: P
    {16, 81},  // ASCII: Q
    {17, 82},  // ASCII: R
    {18, 83},  // ASCII: S
    {19, 84},  // ASCII: T
    {20, 85},  // ASCII: U
    {21, 86},  // ASCII: V
    {22, 87},  // ASCII: W
    {23, 88},  // ASCII: X
    {24, 89},  // ASCII: Y
    {25, 90},  // ASCII: Z
    {26, 97},  // ASCII: a
    {27, 98},  // ASCII: b
    {28, 99},  // ASCII: c
    {29, 100}, // ASCII: d
    {30, 101}, // ASCII: e
    {31, 102}, // ASCII: f
    {32, 103}, // ASCII: g
    {33, 104}, // ASCII: h
    {34, 105}, // ASCII: i
    {35, 106}, // ASCII: j
    {36, 107}, // ASCII: k
    {37, 108}, // ASCII: l
    {38, 109}, // ASCII: m
    {39, 110}, // ASCII: n
    {40, 111}, // ASCII: o
    {41, 112}, // ASCII: p
    {42, 113}, // ASCII: q
    {43, 114}, // ASCII: r
    {44, 115}, // ASCII: s
    {45, 116}, // ASCII: t
    {46, 117}, // ASCII: u
    {47, 118}, // ASCII: v
    {48, 119}, // ASCII: w
    {49, 120}, // ASCII: x
    {50, 121}, // ASCII: y
    {51, 122}, // ASCII: z
    {52, 49},  // ASCII: 1
    {53, 50},  // ASCII: 2
    {54, 51},  // ASCII: 3
    {55, 52},  // ASCII: 4
    {56, 53},  // ASCII: 5
    {57, 54},  // ASCII: 6
    {58, 55},  // ASCII: 7
    {59, 56},  // ASCII: 8
    {60, 57},  // ASCII: 9
    {61, 48},  // ASCII: 0
    {62, 33},  // ASCII: !
    {63, 63},  // ASCII: ?
    {64, 64},  // ASCII: @
    {65, 35},  // ASCII: #
    {66, 36},  // ASCII: $
    {67, 37},  // ASCII: %
    {68, 94},  // ASCII: ^
    {69, 38},  // ASCII: &
    {70, 42},  // ASCII: *
    {71, 45},  // ASCII: -
    {72, 43},  // ASCII: +
    {73, 61},  // ASCII: =
    {74, 95},  // ASCII: _
    {75, 40},  // ASCII: (
    {76, 41},  // ASCII: )
    {77, 123}, // ASCII: {
    {78, 125}, // ASCII: }
    {79, 91},  // ASCII: [
    {80, 93},  // ASCII: ]
    {81, 47},  // ASCII: slash
    {82, 92},  // ASCII: backslash
    {83, 124}, // ASCII: |
    {84, 60},  // ASCII: <
    {85, 62},  // ASCII: >
    {86, 58},  // ASCII: :
    {87, 59},  // ASCII: ;
    {88, 46},  // ASCII: .
    {89, 44},  // ASCII: ,
    {90, 34},  // ASCII: "
    {91, 39},  // ASCII: '
    {92, 126}, // ASCII: ~
    {93, 96},  // ASCII: `
};

// define the desired layout of the display of the 30 character string here.
// character width: if you change this you need to do a lot of manual adjusting
// in the code because the cursor with and erase before write on screen is all
// tied to this 'width' value.
#define CHAR_WIDTH 12
#define START_POS_PW 2
// the 3-line string positions with 17px height
#define PW_LINE_1 20
#define PW_LINE_2 37
#define PW_LINE_3 54
const int passwCursor[][2] = {
    {(0 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(1 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(2 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(3 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(4 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(5 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(6 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(7 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(8 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(9 * CHAR_WIDTH) + START_POS_PW, PW_LINE_1},
    {(0 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(1 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(2 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(3 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(4 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(5 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(6 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(7 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(8 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(9 * CHAR_WIDTH) + START_POS_PW, PW_LINE_2},
    {(0 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(1 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(2 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(3 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(4 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(5 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(6 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(7 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(8 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3},
    {(9 * CHAR_WIDTH) + START_POS_PW, PW_LINE_3}};

// variable for the 'options menu', see wState 0
#define PW_OPT_X 57
#define PW_OPT_Y1 97
#define PW_OPT_Y2 106
#define PW_OPT_Y3 116
const int pwOptCursor[][2] = {
    {PW_OPT_X, PW_OPT_Y1},
    {PW_OPT_X, PW_OPT_Y2},
    {PW_OPT_X, PW_OPT_Y3}};

// Main state machine states
enum FSM
{
  FSM_IDLE_FAN_OFF,
  FSM_FAN_ON,
  FSM_MANUAL_ON,
  FSM_MANUAL_OFF,
  FSM_DISABLE,
  FSM_FAN_OFF_DELAY,
  FSM_SET_THRESHOLD,
  FSM_SET_HYSTERESIS,
  FSM_SET_SWITCH_OFF_DELAY,
  FSM_SET_WIFI_SSID,
  FSM_SET_WIFI_CODE,
};
// set default Main state
FSM state = FSM_IDLE_FAN_OFF; // no action when starting

// set wifi Password menu Sub state
int wState = 0;
int wState_prev = 0; //not used!

// variables for button function demo - not used
bool displayRefresh = false;
// Three Button states: 0, 1, and 2.
int bState = 0;
// Change button increments this counter in Edit mode.
int counter = 0;
// LongPress on ModeButton will go into "edit" mode.
bool isEditting = false;
// In edit mode, the "field" is blinking. But when the Change button is
// Pressed or LongPressed, the blinking temporarily stops.
bool isBlinking = false;

// Menu
int menu_selection = 0; // For menu selection
bool in_menu = false;
bool enc_button = false;

// Rotary encoder
int enc_asc = 0;
int enc_val = 0;
int enc_pos = 0;
int enc_pos_prev = 0;
int enc_pos_curr = 0;

// Time in seconds to Sync NTP Time
const int sync_time = 60;
// Loop timer
unsigned long loop_interval = 1000L;
unsigned long main_prev_millis = 0;

// default wifi password if desired
// in this case I set one to debug and test
char wifiPass[31] = "1234567890ABCDFTYKNH&^%$FD@#(";
int wifiPassLen = 0;
int wifiPassLenMax = 30;

//  8888888b.                    888 8888888888
//  888  "Y88b                   888 888
//  888    888                   888 888
//  888    888  .d88b.   .d8888b 888 8888888 888  888 88888b.   .d8888b
//  888    888 d8P  Y8b d88P"    888 888     888  888 888 "88b d88P"
//  888    888 88888888 888      888 888     888  888 888  888 888
//  888  .d88P Y8b.     Y88b.    888 888     Y88b 888 888  888 Y88b.
//  8888888P"   "Y8888   "Y8888P 888 888      "Y88888 888  888  "Y8888P
//
//
// declare functions. In the Arduino IDE this is done also
// but in the background.
void stateMachine();
void checkEncoder();
void printWifiStatus();
void wifiStatus();
void wifiConnect();
void handleEvent(AceButton *button, uint8_t eventType, uint8_t /*buttonState*/);
time_t syncNTPTime();

//   .d8888b.           888                        .d88 88b.
//  d88P  Y88b          888                       d88P" "Y88b
//  Y88b.               888                      d88P     Y88b
//   "Y888b.    .d88b.  888888 888  888 88888b.  888       888
//      "Y88b. d8P  Y8b 888    888  888 888 "88b 888       888
//        "888 88888888 888    888  888 888  888 Y88b     d88P
//  Y88b  d88P Y8b.     Y88b.  Y88b 888 888 d88P  Y88b. .d88P
//   "Y8888P"   "Y8888   "Y888  "Y88888 88888P"    "Y88 88P"
//                                      888
//                                      888
//                                      888

void setup()
{

  // Open serial communications and wait for port to open:
  Serial.begin(115200);
  Serial.println(F("setup(): begin"));

  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);

  // Button uses the built-in pull up register.
  pinMode(MODE_BUTTON_PIN, INPUT_PULLUP);
  pinMode(UP_BUTTON_PIN, INPUT_PULLUP);
  pinMode(UP_BUTTON_PIN, INPUT_PULLUP);

  // Configure the ButtonConfig with the event handler.
  ButtonConfig *buttonConfig = ButtonConfig::getSystemButtonConfig();
  buttonConfig->setEventHandler(handleEvent);
  buttonConfig->setFeature(ButtonConfig::kFeatureLongPress);
  buttonConfig->setFeature(ButtonConfig::kFeatureRepeatPress);
  buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterLongPress);

  // disable functions to test only password string edit menu
  // Wifi
  //wifiConnect();
  //wifiStatus();

  // Time Setup
  //timeClient.begin(0);
  //setSyncProvider(syncNTPTime);
  //setSyncInterval(sync_time);

  // End of Setup
  Serial.println(F("setup(): ready"));
}

//  888                                  .d88 88b.
//  888                                 d88P" "Y88b
//  888                                d88P     Y88b
//  888      .d88b.   .d88b.  88888b.  888       888
//  888     d88""88b d88""88b 888 "88b 888       888
//  888     888  888 888  888 888  888 Y88b     d88P
//  888     Y88..88P Y88..88P 888 d88P  Y88b. .d88P
//  88888888 "Y88P"   "Y88P"  88888P"    "Y88 88P"
//                            888
//                            888
//                            888

void loop()
{
  //===> FAST TIMER <=========================================================
  // Should be called every 4-5ms or faster, for the default debouncing time
  // of ~20ms.
  modeButton.check();
  upButton.check();
  downButton.check();
  checkEncoder();
  stateMachine();

  //===> MAIN TIMER <=========================================================
  unsigned long main_current_millis = millis();
  if (main_current_millis - main_prev_millis >= 500)
  {
    main_prev_millis = main_current_millis;
    // run everything here that needs to happen on
    // 1 second timer

    // DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG
    //
    // display several variables on the TFT display
    // some only appear when needed
    tft.fillRect(0, 0, 127, 17, TFT_BLACK);
    tft.setCursor(0, 0, 1);
    // print wifipassword array lenght
    tft.setTextColor(TFT_MAGENTA);
    tft.print("len");
    tft.print(wifiPassLen);
    //print enc_pos_prev
    tft.setTextColor(TFT_ORANGE);
    tft.print(" e");
    tft.print(enc_pos);
    //print enc_pos
    tft.setTextColor(TFT_ORANGE1);
    tft.print("|");
    tft.print(enc_pos_prev);
    //print wStatePrevious
    tft.setTextColor(TFT_CYAN);
    tft.print(" s");
    tft.print(wState);
    //print wStateCurrent
    tft.setTextColor(TFT_DARKCYAN);
    tft.print("|");
    tft.print(wState_prev);
    tft.setCursor(0, 9, 1);
    // wifipassword array end position
    if (wifiPassLen > 0)
    {
      tft.setTextColor(TFT_MAGENTA);
      tft.print("end");
      tft.print(wifiPassLen - 1);
    }
    if (wState == 1)
    {
      tft.setTextColor(TFT_RED);
      tft.print(" dec");
      tft.print(wifiPass[enc_pos], DEC);
    }
    if (wState == 2)
    {
      // print ASCII table -corresponding DEC.ASCII value
      tft.setTextColor(TFT_BLUE);
      tft.print(" dec");
      tft.print((asciiVal[enc_val][1]));
      // print ASCII table -position
      tft.setTextColor(TFT_BLUE);
      tft.print("|pos");
      tft.print(enc_val);
    }
    //DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG
  }
}

//  8888888888 .d8888b.  888b     d888
//  888       d88P  Y88b 8888b   d8888
//  888       Y88b.      88888b.d88888
//  8888888    "Y888b.   888Y88888P888
//  888           "Y88b. 888 Y888P 888
//  888             "888 888  Y8P  888
//  888       Y88b  d88P 888   "   888
//  888        "Y8888P"  888       888
//
//
//
// Main state machine
void stateMachine()
{
  switch (state)
  {
  //##########################################################################
  case FSM_IDLE_FAN_OFF:
  {
    // check actions
    if (enc_button == true)
    {
      // reset enc button state
      enc_button = false;
    }

    // DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG
    // straight to the wifi password edit state
    // set edit menu state
    wState = 0;
    // clear screen
    tft.fillScreen(TFT_BLACK);
    // set font color
    tft.setTextColor(TFT_GREEN);
    // go to next state
    state = FSM_SET_WIFI_SSID;
  }
  break;

  //##########################################################################
  case FSM_SET_WIFI_SSID:
  {
    // go through the editing steps via a seperate state machine
    switch (wState)
    {

      //  ##      ##  ######  ########    ###    ######## ########      #####
      //  ##  ##  ## ##    ##    ##      ## ##      ##    ##           ##   ##
      //  ##  ##  ## ##          ##     ##   ##     ##    ##          ##     ##
      //  ##  ##  ##  ######     ##    ##     ##    ##    ######      ##     ##
      //  ##  ##  ##       ##    ##    #########    ##    ##          ##     ##
      //  ##  ##  ## ##    ##    ##    ##     ##    ##    ##           ##   ##
      //   ###  ###   ######     ##    ##     ##    ##    ########      #####

    case 0: // wifi password string - SETUP DISPLAY TEXT
    {       // * <--- extra 'code block' to allow for local variables
            //
      // determine the lenght of the current array
      wifiPassLen = (strlen(wifiPass));

      // display current wifi password code string
      for (int p = 0; p <= wifiPassLen; p++)
      {
        int x = passwCursor[p][0];
        int y = passwCursor[p][1];
        tft.setTextColor(TFT_DARKGREEN);
        tft.setCursor(x + 2, y, 2); // font 2: 15px
        tft.print(wifiPass[p]);
      }
      // display editing options text
      tft.setTextColor(TFT_DARKGREEN);
      tft.setCursor(PW_OPT_X + 10, PW_OPT_Y1, 1);
      tft.print("BACKSPACE");
      tft.setCursor(PW_OPT_X + 10, PW_OPT_Y2, 1);
      tft.print("CLEAR ALL");
      tft.setCursor(PW_OPT_X + 10, PW_OPT_Y3, 1);
      tft.print("SAVE+EXIT");
      // draw option menu cursor outlines
      tft.drawCircle(PW_OPT_X, PW_OPT_Y1 + 3, 3, TFT_DARKGREEN1);
      tft.drawCircle(PW_OPT_X, PW_OPT_Y2 + 3, 3, TFT_DARKGREEN1);
      tft.drawCircle(PW_OPT_X, PW_OPT_Y3 + 3, 3, TFT_DARKGREEN1);

      // selection cursor to default position
      enc_pos = 0;
      // previous position is needed to erase the cursor at that point
      enc_pos_prev = 0;

      // draw green selection cursor at the start position of the string
      int x = passwCursor[enc_pos][0];
      int y = passwCursor[enc_pos][1];
      tft.drawRect(x, y, 12, 17, TFT_GREEN);

      // jump to next step
      wState_prev = 0;
      wState = 1;
    }
    break;
      //##########################################################################
      //
      //  ##      ##  ######  ########    ###    ######## ########       ##
      //  ##  ##  ## ##    ##    ##      ## ##      ##    ##           ####
      //  ##  ##  ## ##          ##     ##   ##     ##    ##             ##
      //  ##  ##  ##  ######     ##    ##     ##    ##    ######         ##
      //  ##  ##  ##       ##    ##    #########    ##    ##             ##
      //  ##  ##  ## ##    ##    ##    ##     ##    ##    ##             ##
      //   ###  ###   ######     ##    ##     ##    ##    ########     ######

    case 1: // wifi password string - CHARACTER SELECTION MODE
    {
      // only erase 'previous' cursor if needed, else you'll get erase/write
      // on the same position resulting in flickering text.
      if (enc_pos_prev != enc_pos)
      {
        // remove previous rectangle
        int x = passwCursor[enc_pos_prev][0];
        int y = passwCursor[enc_pos_prev][1];
        tft.drawRect(x, y, 12, 17, TFT_BLACK);
      }
      // draw character edit rectangle
      int x = passwCursor[enc_pos][0];
      int y = passwCursor[enc_pos][1];
      tft.drawRect(x, y, 12, 17, TFT_GREEN);

      // check encoder rotation
      byte i;
      // get encoder direction value (no direction = '0', CW = '1', CCW = '2')
      i = enc.rotate();
      if (i == 1) //CW
      {
        // save previous cursor position in variable so we know at which position
        // we need to erase the cursur when moving it
        enc_pos_prev = enc_pos;
        // increment cursor position counter
        enc_pos++;
      }
      else if (i == 2) //CCW
      {
        // save previous cursor position in variable so we know at which position
        // we need to erase the cursur when moving it
        enc_pos_prev = enc_pos;
        // decrement cursor position counter
        enc_pos--;
      }

      // after rotation, check the new cursor position value
      // prevent lower values that minimum
      if (enc_pos < 0)
        enc_pos = 0;

      // handle maximum value overflow
      if ((enc_pos == wifiPassLenMax) || (enc_pos > wifiPassLen))
      {
        // cursor position value greater than the wifiPassword string length.
        // overflow to options menu
        // but first remove selection cursor
        int x = passwCursor[enc_pos_prev][0];
        int y = passwCursor[enc_pos_prev][1];
        tft.drawRect(x, y, 12, 17, TFT_BLACK);
        // store the cursor position before it triggered this 'if' statement
        enc_pos_curr = enc_pos_prev;
        enc_pos = 0;
        // go to options menu
        wState_prev = 1;
        wState = 3;
      }

      // check actions
      if (enc_button == true)
      {
        // encoder button was pressed
        // reset button state
        enc_button = false;

        // before jump to 'edit' state, set initial ASCII code to the current array value
        // else an edit mistake would cuase us to search again for the right character.
        // now if we selected for example 'N' instead of 'M', it's easy to correct
        // by one turn of the rotary encoder.
        //
        // NOTE: if we want to add a new character at the END position of the
        // wifi password string (which is not empty but has the 'null' character
        // there) then we want to set the start value of the ASCII code range
        // at the beginning of the ASCII table array.
        //
        // if we are adding a new character to the string:
        if (enc_pos == wifiPassLen)
          enc_val = 0;
        // here we edit an existing string character:
        else
        {
          // we want to find the ASCII value of the character currently
          // selected from the wifi password string.
          // bij searching the ASCII code array for the character value
          // we can determine the corresponding position counter value:
          for (int s = 0; s <= 93; s++)
          {
            if (wifiPass[enc_pos] == asciiVal[s][1])
            {
              enc_val = s;
            }
          }
        }
        // set edit state
        wState_prev = 1;
        wState = 2;
      }
    }
    break;
      //##########################################################################
      //
      //  ##      ##  ######  ########    ###    ######## ########     #######
      //  ##  ##  ## ##    ##    ##      ## ##      ##    ##          ##     ##
      //  ##  ##  ## ##          ##     ##   ##     ##    ##                 ##
      //  ##  ##  ##  ######     ##    ##     ##    ##    ######       #######
      //  ##  ##  ##       ##    ##    #########    ##    ##          ##
      //  ##  ##  ## ##    ##    ##    ##     ##    ##    ##          ##
      //   ###  ###   ######     ##    ##     ##    ##    ########    #########

    case 2: // wifi password string - EDIT MODE
    {
      // x and y values are set to the current cursor position
      int x = passwCursor[enc_pos][0];
      int y = passwCursor[enc_pos][1];
      // make cursor RED
      tft.drawRect(x, y, 12, 17, TFT_RED);

      // if we want to ADD to the wifi password string then the edit
      // cursor will be empty because we edit the existing 'null'
      // character present at the end of the wifi password string.
      // So if that is the case we need to display a starting ASCII
      // character at the edit cursor position
      if (wifiPass[enc_pos] == '\0')
      {
        // draw new ASCII character
        tft.setCursor(x + 2, y, 2); // font 2: 15px
        // display the ASCII code corresponding to the enc_val
        tft.setTextColor(TFT_DARKGREEN);
        tft.print((char)asciiVal[enc_val][1]);
      }

      // check encoder rotation
      byte i;
      i = enc.rotate();
      if (i == 1) //CW
      {
        enc_val++;
        //check for overflow of ASCII code range 33-126
        if (enc_val > 93)
          enc_val = 0;
        // clear display area before displaying new character
        tft.fillRect(x + 1, y + 1, 10, 15, TFT_BLACK);
        // draw new ASCII character
        tft.setCursor(x + 2, y, 2); // font 2: 15px
        // display the ASCII code corresponding to the enc_val
        tft.setTextColor(TFT_DARKGREEN);
        tft.print((char)asciiVal[enc_val][1]);
      }
      else if (i == 2) //CCW
      {
        enc_val--;
        // check for overflow of ASCII code range 33-126
        if (enc_val < 0)
          enc_val = 93;
        // clear display area before displaying new character
        tft.fillRect(x + 1, y + 1, 10, 15, TFT_BLACK);
        // draw new ASCII character
        tft.setCursor(x + 2, y, 2); // font 2: 15px
        // display the ASCII code corresponding to the enc_val
        tft.setTextColor(TFT_DARKGREEN);
        tft.print((char)asciiVal[enc_val][1]);
      }

      // check actions
      if (enc_button == true)
      {
        // reset enc button state
        enc_button = false;

        // write the current ASCII code to the wifi password string
        // at the current cursor position
        // * NOTE: enc_pos = character position of the wifi password string,
        // *       enc_val = the selected ASCII value to be written there.
        wifiPass[enc_pos] = (char)asciiVal[enc_val][1];

        // it is possible to add to an excisting wifi password string so check
        // if a new character has been added to the end of the wifi password
        if (enc_pos == wifiPassLen)
        {
          // the last 'null' character of the wifi password string has been
          // overwritten so we need to properly end the wifi password string
          // again with a new 'null' character.
          wifiPass[enc_pos + 1] = '\0';
          // update the lenght of the current wifi password string
          wifiPassLen = (strlen(wifiPass));
        }

        // DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG
        Serial.print("---");
        Serial.print(wifiPass);
        Serial.println("---");
        // DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG--DEBUG

        // move cursor 1 position forwar but only if the end of the wifi password
        // string is not reached
        if ((enc_pos + 1) != wifiPassLenMax)
        {
          // remove previous rectangle
          int x = passwCursor[enc_pos][0];
          int y = passwCursor[enc_pos][1];
          tft.drawRect(x, y, 12, 17, TFT_BLACK);

          enc_pos++;
          enc_pos_prev++;
        }
        wState_prev = 2;
        wState = 1;
      }
    }
    break;
      //##########################################################################
      //
      //  ##      ##  ######  ########    ###    ######## ########     #######
      //  ##  ##  ## ##    ##    ##      ## ##      ##    ##          ##     ##
      //  ##  ##  ## ##          ##     ##   ##     ##    ##                 ##
      //  ##  ##  ##  ######     ##    ##     ##    ##    ######       #######
      //  ##  ##  ##       ##    ##    #########    ##    ##                 ##
      //  ##  ##  ## ##    ##    ##    ##     ##    ##    ##          ##     ##
      //   ###  ###   ######     ##    ##     ##    ##    ########     #######

    case 3:
    { // wifi password string - OPTIONS MENU

      // we have arrived here after the cursor position counter went over the
      // maximum size of the wifi password string or after it went over the max
      // lenght of the current password.
      // Draw a cursor at the first menu option:

      // only erase 'previous' cursor if needed, else you'll get erase/write
      // on the same position resulting in flickering text.
      if (enc_pos_prev != enc_pos)
      {
        // remove previous cursor
        int x = pwOptCursor[enc_pos_prev][0];
        int y = pwOptCursor[enc_pos_prev][1];
        tft.fillCircle(x, y + 3, 2, TFT_BLACK);
        // tft.drawRect(x, y, 7, 7, TFT_BLACK);
      }
      // draw edit cursor
      int x = pwOptCursor[enc_pos][0];
      int y = pwOptCursor[enc_pos][1];
      // make the cursor red if on the 'CLEAR ALL' menu option because is
      // is irreversable although not desasterous of course...
      if (enc_pos == 1)
        tft.fillCircle(x, y + 3, 2, TFT_RED);
      else
        tft.fillCircle(x, y + 3, 2, TFT_GREEN);

      // check encoder rotation
      byte i;
      i = enc.rotate();
      if (i == 1) //CW
      {
        enc_pos_prev = enc_pos;
        enc_pos++;
      }
      else if (i == 2) //CCW
      {
        enc_pos_prev = enc_pos;
        enc_pos--;
      }

      // deal with menu options overflow
      if (enc_pos > 2)
        enc_pos = 2;
      // exit cursor selection mode
      else if (enc_pos < 0)
      {
        // return to the wifi password string selection mode 'state 1'
        // remove cursor
        int x = pwOptCursor[enc_pos_prev][0];
        int y = pwOptCursor[enc_pos_prev][1];
        tft.fillCircle(x, y + 3, 2, TFT_BLACK);
        // restore the cursor position before going back to the wifi password
        // string selection mode and draw the green cursor.
        if (wifiPassLen == wifiPassLenMax)
          enc_pos = wifiPassLen - 1;
        else
          enc_pos = wifiPassLen;
        // redraw the green selection cursor at the last position
        x = passwCursor[enc_pos][0];
        y = passwCursor[enc_pos][1];
        tft.drawRect(x, y, 12, 17, TFT_DARKGREEN);

        // jump back
        wState_prev = 3;
        wState = 1;
      }

      // check actions
      if (enc_button == true)
      {
        // reset enc button state
        enc_button = false;

        //======================================================================
        // menu option: DELETE LAST CHARACTER
        if (enc_pos == 0)
        {
          // erase the last character of the wifi password string
          wifiPass[wifiPassLen - 1] = '\0';
          // recalculate the new string lenght
          wifiPassLen = (strlen(wifiPass));
          // redraw the display without returning to the cursor selection mode
          // so we can delete multiple characters if needed
          // determine the lenght of the current array
          tft.fillRect(0, 15, 127, 70, TFT_BLACK);
          for (int p = 0; p <= wifiPassLen; p++)
          {
            int x = passwCursor[p][0];
            int y = passwCursor[p][1];
            tft.setTextColor(TFT_DARKGREEN);
            tft.setCursor(x + 2, y, 2); // font 2: 15px
            tft.print(wifiPass[p]);     // start at '0' with the wifiPassw array
          }
        }
        //======================================================================
        // menu option: CLEAR ALL
        else if (enc_pos == 1)
        {
          wifiPass[0] = '\0';
          // clear screen
          tft.fillScreen(TFT_BLACK);
          wState = 0;
        }
        //======================================================================
        // menu option: EXIT
        else if (enc_pos == 2)
        {
          state = FSM_IDLE_FAN_OFF;
        }
      }
    }
    break;
      //##########################################################################
    case 4:

      break;
      //##########################################################################
    case 5:

      break;
      //##########################################################################
    default:
      break;
    }
  }
  break;
  default:
    break;
  }
}

//           888                        888      8888888888                                888
//           888                        888      888                                       888
//           888                        888      888                                       888
//   .d8888b 88888b.   .d88b.   .d8888b 888  888 8888888    88888b.   .d8888b .d88b.   .d88888  .d88b.  888d888
//  d88P"    888 "88b d8P  Y8b d88P"    888 .88P 888        888 "88b d88P"   d88""88b d88" 888 d8P  Y8b 888P"
//  888      888  888 88888888 888      888888K  888        888  888 888     888  888 888  888 88888888 888
//  Y88b.    888  888 Y8b.     Y88b.    888 "88b 888        888  888 Y88b.   Y88..88P Y88b 888 Y8b.     888
//   "Y8888P 888  888  "Y8888   "Y8888P 888  888 8888888888 888  888  "Y8888P "Y88P"   "Y88888  "Y8888  888
//
//
//
void checkEncoder()
{
}

//  8888888b.          888    888       888 d8b  .d888 d8b  .d8888b.  888
//  888   Y88b         888    888   o   888 Y8P d88P"  Y8P d88P  Y88b 888
//  888    888         888    888  d8b  888     888        Y88b.      888
//  888   d88P 888d888 888888 888 d888b 888 888 888888 888  "Y888b.   888888
//  8888888P"  888P"   888    888d88888b888 888 888    888     "Y88b. 888
//  888        888     888    88888P Y88888 888 888    888       "888 888
//  888        888     Y88b.  8888P   Y8888 888 888    888 Y88b  d88P Y88b.
//  888        888      "Y888 888P     Y888 888 888    888  "Y8888P"   "Y888
//
//
//
void printWifiStatus()
{
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

//  888       888 d8b  .d888 d8b  .d8888b.                                               888
//  888   o   888 Y8P d88P"  Y8P d88P  Y88b                                              888
//  888  d8b  888     888        888    888                                              888
//  888 d888b 888 888 888888 888 888         .d88b.  88888b.  88888b.   .d88b.   .d8888b 888888
//  888d88888b888 888 888    888 888        d88""88b 888 "88b 888 "88b d8P  Y8b d88P"    888
//  88888P Y88888 888 888    888 888    888 888  888 888  888 888  888 88888888 888      888
//  8888P   Y8888 888 888    888 Y88b  d88P Y88..88P 888  888 888  888 Y8b.     Y88b.    Y88b.
//  888P     Y888 888 888    888  "Y8888P"   "Y88P"  888  888 888  888  "Y8888   "Y8888P  "Y888
//
//
//
void wifiConnect()
{
  // check for the WiFi module:
  if (WiFi.status() == WL_NO_MODULE)
  {
    Serial.println("Communication with WiFi module failed!");
    // don't continue
    while (true)
      ;
  }

  String firmwareCurrent = WiFi.firmwareVersion();
  String firmwareLatest = WIFI_FIRMWARE_LATEST_VERSION;

  Serial.println("Current Wifi firmware: " + firmwareCurrent);
  Serial.println("Latest Wifi firmware:  " + firmwareLatest);

  if (firmwareCurrent < firmwareLatest)
  {
    Serial.println("*** Please upgrade the firmware ***");
  }

  // attempt to connect to Wifi network:
  while (status != WL_CONNECTED)
  {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);
  }
}

//  888       888 d8b  .d888 d8b  .d8888b.  888             888
//  888   o   888 Y8P d88P"  Y8P d88P  Y88b 888             888
//  888  d8b  888     888        Y88b.      888             888
//  888 d888b 888 888 888888 888  "Y888b.   888888  8888b.  888888 888  888 .d8888b
//  888d88888b888 888 888    888     "Y88b. 888        "88b 888    888  888 88K
//  88888P Y88888 888 888    888       "888 888    .d888888 888    888  888 "Y8888b.
//  8888P   Y8888 888 888    888 Y88b  d88P Y88b.  888  888 Y88b.  Y88b 888      X88
//  888P     Y888 888 888    888  "Y8888P"   "Y888 "Y888888  "Y888  "Y88888  88888P'
//
//
//
void wifiStatus()
{
  Serial.println("Connected to wifi");
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

//  888               888    888                     8888888888                           888
//  888               888    888                     888                                  888
//  888               888    888                     888                                  888
//  88888b.  888  888 888888 888888 .d88b.  88888b.  8888888   888  888  .d88b.  88888b.  888888 .d8888b
//  888 "88b 888  888 888    888   d88""88b 888 "88b 888       888  888 d8P  Y8b 888 "88b 888    88K
//  888  888 888  888 888    888   888  888 888  888 888       Y88  88P 88888888 888  888 888    "Y8888b.
//  888 d88P Y88b 888 Y88b.  Y88b. Y88..88P 888  888 888        Y8bd8P  Y8b.     888  888 Y88b.       X88
//  88888P"   "Y88888  "Y888  "Y888 "Y88P"  888  888 8888888888  Y88P    "Y8888  888  888  "Y888  88888P'
//
//
//
// The event handler for the buttons.
void handleEvent(AceButton *button, uint8_t eventType, uint8_t /*buttonState*/)

{
  uint8_t pin = button->getPin();

  //====== MODE ==========================================================
  if (pin == MODE_BUTTON_PIN)
  {
    switch (eventType)
    {
    // Interpret a Released event as a Pressed event, to distiguish it
    // from a LongPressed event.
    case AceButton::kEventReleased:
      if (!isEditting)
      {
        enc_button = true;
      }
      break;

    // LongPressed goes in and out of edit mode.
    case AceButton::kEventLongPressed:
      isEditting = !isEditting;
      if (isEditting)
      {
        isBlinking = true;
      }
      Serial.println(F("Mode Button: Long Pressed"));
      Serial.print(F("Editting: "));
      Serial.print(isEditting ? F("true") : F("false"));
      Serial.print(F("; blinking: "));
      Serial.println(isBlinking ? F("true") : F("false"));
      break;
    }
  }
  //====== UP ==========================================================
  else if (pin == UP_BUTTON_PIN)
  {
    switch (eventType)
    {
    case AceButton::kEventPressed:
    case AceButton::kEventRepeatPressed:
      isBlinking = false;
      if (isEditting)
      {
        if (eventType == AceButton::kEventPressed)
        {
          Serial.println(F("Change Button: Pressed"));
        }
        else
        {
          Serial.println(F("Change Button: Repeat Pressed"));
        }
        counter++;
        Serial.print(F("Counter: "));
        Serial.print(counter);
        Serial.print(F("; blinking: "));
        Serial.println(isBlinking ? F("true") : F("false"));
      }
      break;

    case AceButton::kEventReleased:
    case AceButton::kEventLongReleased:
      isBlinking = true;
      if (isEditting)
      {
        Serial.println(F("Change Button: Released"));
        Serial.print(F("Counter: "));
        Serial.print(counter);
        Serial.print(F("; blinking: "));
        Serial.println(isBlinking ? F("true") : F("false"));
      }
      break;
    }
  }
}

/*
 * syncNTPTime();
 * 
 * Called as a callback on a timer every sync_time seconds.
 * ========================================================================= */
time_t syncNTPTime()
{
  unsigned long cur_time, update_time;
  unsigned int drift_time;
  cur_time = timeClient.getEpochTime();
  timeClient.update();
  update_time = timeClient.getEpochTime();
  drift_time = (update_time - cur_time);
  Serial.println("NTP Time Sync <=====================================");
  Serial.print("NTP Epoch: ");
  Serial.println(timeClient.getEpochTime());
  Serial.print("NTP Time : ");
  Serial.println(timeClient.getFormattedTime());
  Serial.print("Epoch Pre Sync:  ");
  Serial.println(cur_time);
  Serial.print("Epoch Post Sync: ");
  Serial.println(update_time);
  Serial.print("Drift Correct:   ");
  Serial.println(drift_time);

  return timeClient.getEpochTime();
}