/*
 * Sketch for counting impulses in a defined interval
 * e.g. for power meters with an s0 interface that can be 
 * connected to an input of an arduino or esp8266 board 
 *
 * the sketch uses pin change interrupts which can be anabled 
 * for any of the inputs on e.g. an arduino uno, jeenode, wemos d1 etc.
 *
 * the pin change Interrupt handling for arduinos used here 
 * is based on the arduino playground example on PCINT:
 * http://playground.arduino.cc/Main/PcInt which is outdated.
 *
 * see https://github.com/GreyGnome/EnableInterrupt for a newer library (not used here)
 * and also 
 * https://playground.arduino.cc/Main/PinChangeInterrupt
 * http://www.avrfreaks.net/forum/difference-between-signal-and-isr
 *
 * Refer to avr-gcc header files, arduino source and atmega datasheet.
 */

/* Arduino Uno / Nano Pin to interrupt map:
 * D0-D7 =           PCINT 16-23 = PCIR2 = PD = PCIE2 = pcmsk2
 * D8-D13 =          PCINT 0-5 =   PCIR0 = PB = PCIE0 = pcmsk0
 * A0-A5 (D14-D19) = PCINT 8-13 =  PCIR1 = PC = PCIE1 = pcmsk1
 */

/* test cmds 

    Nano analog
    21,2,0,50,2,98,115  A7=21, falling, no pullup, min 50, ir-out 2, thresholds 98/115

    analog ESP 8266:
    20v               Verbose
    17,3,0,50a        A0, rising, no Pullup, MinLen 50
    15,25t            Level Diff Thresholds
 
    for ESP8266 with D5 falling pullup 30
    5,2,1,30a
    20v
    10,20,1,1i
 
    for ESP32 pin 23
    23,2,1,50a
    10,20,1,1i
 
    for ESP32 with A0 = 36
    36,3,0,50a
    25v
 
    TTGO T-Display has right button at GPIO 35
    35,2,0,50a
    36,3,0,50,27a

*/

/*
    Changes:
        V1.2
        27.10.16 - use noInterrupts in report()
                 - avoid reporting very short timeDiff in case of very slow impulses after a report
                 - now reporting is delayed if impulses happened only within in intervalSml
                 - reporting is also delayed if less than countMin pulses counted
                 - extend command "int" for optional intervalSml and countMin
        29.10.16 - allow interval Min >= Max or Sml > Min 
                   which changes behavior to take fixed calculation interval instead of timeDiff between pulses
                   -> if intervalMin = intervalMax, counting will allways follow the reporting interval
        3.11.16  - more noInterrupt blocks when accessing the non uint8_t volatiles in report
        V1.3    
        4.11.16  - check min pulse width and add more output,
                 - prefix show output with M
        V1.4
        10.11.16 - restructure add Cmd
                 - change syntax for specifying minPulseLengh
             - res (reset) command
        V1.6
        13.12.16 - new startup message logic?, newline before first communication?
        18.12.16 - replace all code containing Strings, new communication syntax and parsing from Jeelink code
        V1.7
        2.1.17 - change message syntax again, report time as well, first and last impulse are reported 
                 relative to start of intervall not start of reporting intervall
        V1.8
        4.1.17 - fixed a missing break in the case statement for pin definition
        5.1.17 - cleanup debug logging
        14.10.17 - fix a bug where last port state was not initialized after interrupt attached but this is necessary there
        23.11.17 - beautify code, add comments, more debugging for users with problematic pulse creation devices
        28.12.17 - better reportung of first pulse (even if only one pulse and countdiff is 0 but realdiff is 1)
        30.12.17 - rewrite PCInt, new handling of min pulse length, pulse history ring
        1.1.18   - check len in add command, allow pin 8 and 13
        2.1.18   - add history per pin to report line, show negative starting times in show history
        3.1.18   - little reporting fix (start pos of history report)
        
        V2.0
        17.1.18  - rewrite many things - use pin number instead of pcIntPinNumber as index, split interrupt handler for easier porting to ESP8266, ...
        V2.23
        10.2.18  - new commands for check alive and quit, send setup message after reboot also over tcp
                    remove reporting time of first pulse (now we hava history)
                    remove pcIntMode (is always change now)
                    pulse min interval is now always checked and defaults to 2 if not set
        march 2018  many changes more to support ESP8266 
        7.3.18  - change pin config output, fix pullup (V2.26), store config in eeprom and read it back after boot
        22.4.18 - many changes, delay report if tcp mode and disconnected, verbose levels, ...
        13.5.18 - V2.36 Keepalive also on Arduino side
        9.12.18 - V3.0 start implementing analog input for old ferraris counters
        6.1.19  - V3.1 printIntervals in hello
        19.1.19 - V3.12 support for ESP with analog
        24.2.19 - V3.13 fix internal pin to GPIO mapping (must match ISR functions) when ESP8266 and analog support       
                - V3.14 added return of devVerbose upon startup
        27.6.19 - V3.20 replace timeNextReport with lastReportCall to avoid problem with data tyoes on ESP
                        fix a bug with analog counting on the ESP 
        20.7.19 -       nicer debug output for analog leves
        21.7.19 - V3.30 replace delay during analog read with millis() logic, optimize waiting times for analog read
        10.8.19 - V3.32 add ICACHE_RAM_ATTR for ISRs and remove remaining long casts (bug) when handling time
        12.8.19 - V3.33 fix handling of keepalive timeouts when millis wraps
                  V3.34 add RSSI output when devVerbose >= 5 in kealive responses
        16.8.19 - V3.35 fix a bug when analog support was disabled and a warning with an unused variable
        19.8.19 - V4.00 start porting to ESP32.
        21.12.19 - V4.10 Support for TTGO Lilygo Board (T-Display) with ST7789V 1,14 Zoll Display (240x135) see https://github.com/Xinyuan-LilyGO/TTGO-T-Display
                        or https://de.aliexpress.com/item/33048962331.html?spm=a2g0o.store_home.hotSpots_212315783.0
        30.12.19 - V4.20 started to make analog support user defineable at runtime
            reconnect when wifi is lost 
        20.1.2020 - rewrite many things ...
        2.2.2020 integrate ESPOTA - see https://raw.githubusercontent.com/esp8266/Arduino/master/tools/espota.py
        30.4.2020 V4.25 - show if compiled with display support, debug tcp disconnects
        12.5.2020 V4.26 - restore fixes after a crash (resetWifi, displayMode 3, change commandData to uint32_t)

    ToDo / Ideas:
        max 10 printData entries, displayMode to cycle through pins to display in different views        
        detect analog Thresholds automatically and adjust over time
        printPinHistory could be called independent of report to avoid of losing history data
*/ 

#include <Arduino.h>

#if defined(TFT_DISPLAY)
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <Button2.h>
#include "esp_adc_cal.h"
/*#include "bmp.h"*/
TFT_eSPI tft = TFT_eSPI();  // Invoke library, pins defined in User_Setup.h or as -D in platformio.ini
uint8_t lineCount;          // initialized in each report call

#define TFT_BUTTON 0
#define displayModeMax 3
Button2 buttonA = Button2(TFT_BUTTON);
uint8_t displayMode = 1;
#endif

#include "pins_arduino.h"
#include <EEPROM.h>

const char versionStr[] PROGMEM = "ArduCounter V4.26";

// even with 38400 one report takes less than 1ms. However printing the report on the tft takes 2ms!
// but if analog interval is set to 10 or less and we output a lot to serial (devVerbose 50) then 
//     background tasks (serial?) eat up 5ms per round...
//     this is much better at 115200! (obviously serial io is done interrupt driven at same core)
// TCP io also creates delays of 15-20ms between loop calls
// show cmd also takes > 10 ms 
// serial output of hello msg takes long because the serial buffer is too small so Serial.print takes longer ...

//#define SERIAL_SPEED 38400
#define SERIAL_SPEED 115200
#define MAX_INPUT_NUM 16

#define pin2GPIO(P) ( pgm_read_byte( digital_pin_to_gpio_PGM + (P) ) )
#define FF 255

#if defined(ESP8266) || defined(ESP32)  // Wifi stuff
#define WifiSupport 1

#if defined(ESP8266)
#include <ESP8266WiFi.h>          
#elif defined(ESP32)
#include <WiFi.h>          
#endif
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#if defined(STATIC_WIFI)
#include "ArduCounterTestConfig.h"
#else
#include <WiFiManager.h>
WiFiManager wifiManager;
#endif

WiFiServer Server(80);              // For ESP WiFi connection
WiFiClient Client1;                 // active TCP connection
WiFiClient Client2;                 // secound TCP connection to send reject message
boolean serverStarted;              // to show the status once


boolean TCPconnected;               // remember state of TCP connection so loss can be reported
boolean tcpMode = false;            // remember if we had a tcp connection so we can delay report if disconnected
uint8_t delayedTcpReports = 0;      // how often did we already delay reporting because tcp disconnected
uint32_t lastDelayedTcpReports = 0; // last time we delayed

uint16_t keepAliveTimeout = 200;
uint32_t lastKeepAlive;
uint32_t lastReconnectTry;
int reconnects = 0;

#endif


// function declaraions
void clearInput();
void restoreFromEEPROM();
void CmdSaveToEEPROM();
void CmdInterval();
void CmdThreshold();
void CmdAdd ();
void CmdRemove();
void CmdShow();
void CmdHello();
void CmdWait();
void CmdDevVerbose();
void CmdKeepAlive();
void CmdQuit();
void printWifiState(Print *Out);
void handleInput(char c);


/* ESP8266 pins that are typically ok to use 
 * (some might be set to FF (disallowed) because they are used 
 * as reset, serial, led or other things on most boards) 
 * maps printed pin numbers (aPin) to gpio pin numbers (via their macros if available)
 
 * Wemos / NodeMCU Pins 3,4 and 8 (GPIO 0,2 and 15) define boot mode and therefore
 * can not be used to connect to signal */
 
#if defined(ESP8266)                // ESP 8266 variables and definitions
#define MAX_PIN 7                   // max number of pins that can be defined
#define MAX_HIST 20                 // 20 history entries for ESP boards (can be increased)
#define MAX_APIN 18
const uint8_t PROGMEM digital_pin_to_gpio_PGM[] = {
	D0, D1, D2, FF,                 // D0=16, D1=5, D2=4
    FF, D5, D6, D7,                 // D5=14, D6 is my preferred pin for IR out, D7 for LED out
    FF, FF, FF, FF,
    FF, FF, FF, FF,
    FF, A0                          // A0 is gpio pin 17
}; 


/* ESP32 pins that are typically ok to use 
 * (some might be set to -1 (disallowed) because they are used 
 * as reset, serial, led or other things on most boards) 
 * maps printed pin numbers (aPin) to sketch internal index numbers */
 
#elif defined(ESP32)                // ESP32 variables and definitions
#define MAX_PIN 12
#define MAX_HIST 100                // 100 history entries for ESP boards (can be increased)
#define MAX_APIN 40
#if defined(TFT_DISPLAY)            // TTGO T-Display
const uint8_t PROGMEM digital_pin_to_gpio_PGM[] = {
    FF, FF, FF, FF,                 // pwm at boot, debug, LED, high at boot
    04, FF, FF, FF,                 // 4 is ok, pwm at boot, 
    FF, FF, FF, FF,                 // 6-11 is flash
    FF, FF, FF, FF,                 // 12-15 are used for JTAG
    FF, 17, FF, FF,                 // only 17 is free. 16,18 and 19 for display
    FF, 21, 22, FF,                 // 21-22 avaliable, 23 for display
    FF, 25, 26, 27,                 // 25-26 avaliable, use 27 as irOut
    FF, FF, FF, FF,
    32, 33, 34, 35,                 // 32-35 avaliable (34/35 input only, 35 is right button)
    36, FF, FF, 39};                // 36 is A0, is 39 avaliable but also input only

#else                               // normal ESP32 Devboard
const uint8_t PROGMEM digital_pin_to_gpio_PGM[] = {
    FF, FF, FF, FF,                 // pwm at boot, debug, LED, high at boot
    04, FF, FF, FF,                 // 4 is ok, pwm at boot, 
    FF, FF, FF, FF,                 // 6-11 is flash
    FF, FF, FF, FF,                 // 12 is used at boot, 12-15 for JTAG, otherwise 13 is ok, 14/15 output pwm
    16, 17, 18, 19,
    FF, 21, 22, 23,                 // 21-23 avaliable   
    FF, 25, 26, 27,                 // 25-26 avaliable, use 27 as irOut
    FF, FF, FF, FF,
    32, 33, 34, 35,                 // 32-35 avaliable (34/35 input only)
    36, FF, FF, 39};                // 36 is A0, 39 is avaliable but also input only
#endif


#elif defined(__AVR_ATmega328P__)   // Arduino Nano
#define MAX_HIST 20                 // 20 history entries for arduino boards
#define MAX_PIN 12                  // max 20 counting pins at the same time
#define MAX_APIN 22
const uint8_t PROGMEM digital_pin_to_gpio_PGM[] = {
    FF, FF,  2,  3,                 // 2 is typically ir out for analog,
     4,  5,  6,  7,
     8,  9, 10, 11,
    12, 13, A0, A1,                 // 12 often is led out
    A2, A3, A4, A5,
    A6, A7                          // A7 is typically analog in for ir
};
  
uint8_t pinIndexMap[MAX_APIN];      // map needed by 328p isr to map back from aPin to pinIndex in pinData
uint8_t firstPin[] = {8, 14, 0};    // first and last pin at port PB, PC and PD for arduino uno/nano
uint8_t lastPin[]  = {13, 19, 7};   // not really needed. Instead check bit position (or bit != 0)
volatile uint8_t *port_to_pcmask[] = {&PCMSK0, &PCMSK1, &PCMSK2};    // Pin change mask for each port on arduino
volatile uint8_t PCintLast[3];      // last PIN States at io port to detect pin changes in arduino ISR
#endif                              // end of Nano / Uno specific stuff


Print *Output;                      // Pointer to output device (Serial / TCP connection with ESP8266)
uint32_t bootTime;                  
uint16_t bootWraps;                 // counter for millis wraps at last reset
uint16_t millisWraps;               // counter to track when millis counter wraps 
uint32_t lastMillis;                // milis at last main loop iteration - initialized in setup()
uint32_t lastTimeMillis;            // millis at last show time

uint8_t enableHistory;              // new flag to control collecting and reporting pin history
uint8_t enableSerialEcho;           // echo tcp output on serial
uint8_t enablePinDebug;             // show digital pin changes
uint8_t enableAnalogDebug;          // show analog level sampling
uint8_t enableDevTime;              // show device Time every hour

uint32_t intervalMin = 30000;       // default 30 sec - report after this time if nothing else delays it
uint32_t intervalMax = 60000;       // default 60 sec - report after this time if it didin't happen before
uint32_t intervalSml =  2000;       // default 2 secs - continue if timeDiff is less and intervalMax not over
uint16_t countMin    =     2;       // continue counting if count is less than this and intervalMax not over

uint8_t ledOutPin;                  // todo: not implemented yet

uint32_t lastReportCall;
#if defined(TFT_DISPLAY)
uint32_t lastPrintFlowCall;
#endif


typedef struct pinData {
    // pin configuration data
    uint8_t pinName;                            // printed pin Number for user input / output
                                                
    uint8_t pulseWidthMin;                      // minimal pulse length in millis for filtering
    uint8_t pulseLevel;                         // rising (1)/ falling (0)          // only one bit needed
    uint8_t pullupFlag;                         // 1 for pullup                     // only one bit needed
    uint8_t analogFlag;                         // 1 for analog                     // only one bit needed

    // counting data
    volatile uint32_t counter;                  // counter for pulses
    volatile uint32_t rejectCounter;            // counter for rejected pulses (width too small)
    volatile uint32_t intervalStart;            // time of first impulse in interval
    volatile uint32_t intervalEnd;              // time of last impulse in interval
    volatile uint32_t pulseWidthSum;            // sum of all pulse widthes during interval (for average)
    volatile uint8_t counterIgn;                // counts first pulse that marks begin of the very first interval

    // isr internal states
    volatile uint8_t initialized;               // set if first pulse has ben seen to start interval
    volatile uint32_t lastChange;               // millis at last level change (for measuring pulse length)
    volatile uint8_t lastLevel;                 // level of input at last interrupt     // only one bit needed
    volatile uint8_t lastLongLevel;             // last level that was longer than pulseWidthMin    // only bit

    // reporting data
    uint32_t lastCount;                         // counter at last report (to get the delta count)
    uint16_t lastRejCount;                      // reject counter at last report (to get the delta count)
    uint32_t lastReport;                        // millis at last report to find out when maxInterval is over
    uint8_t reportSequence;                     // sequence number for reports
    uint8_t lastDebugLevel;                          // for debug output when a pin state changes
} pinData_t;

pinData_t pinData[MAX_PIN];
uint8_t maxPinIndex = 0;                        // the next available index (= number of indices used)

typedef struct analogData {
    pinData_t *inPinData;                       // pointer to pinData structure for input pin (to call doCount)
    uint8_t inPinName;                          // printed pin Number for user input / output (optinal here?)
    uint8_t outPinName;                         // printed pin number to use for ir (convert using our macro)
    uint16_t thresholdMin;                      // measurement for 0 level
    uint16_t thresholdMax;                      // measurement for 1 
    uint8_t triggerState;                       // which level was it so far
    uint16_t sumOff = 0;                        // sum of measured values during light off
    uint16_t sumOn  = 0;                        // sum of measured values during light on
    uint16_t avgCnt = 0;                        // counter for average during one level
    uint32_t avgSum = 0;                        // sum for average during one level
} analogData_t;

#define MAX_ANALOG 2
analogData_t analogData[MAX_ANALOG];
uint8_t maxAnalogIndex = 0;                     // the next available index

typedef struct histData {
    volatile uint16_t seq;                      // history sequence number
    volatile uint8_t pin;                       // pin for this entry
    volatile uint8_t level;                     // gap/signal level for this entry 
    volatile uint16_t aLvl;                     // average analog level for this entry
    volatile uint32_t time;                     // time for this entry
    volatile uint32_t len;                      // time that this level was held
    volatile char act;                          // action (count, reject, ...) as one char
} histData_t;

histData_t histData[MAX_HIST];
volatile uint8_t histIndex;                     // pointer to next entry in history ring
volatile uint16_t histNextSeq;                  // next seq number to use
uint16_t histLastOut;                           // seqnuence of last entry already reported

uint32_t commandData[MAX_INPUT_NUM];            // input data over serial port or network
uint8_t  commandDataPointer = 0;                // index pointer to next input value
uint8_t  commandDataSize = 0;                   // number of input values specified in commandData array
char     commandLetter;                         // the actual command letter
uint32_t commandValue = 0;                      // the current value for input function


uint32_t analogReadLast;            // millis() at last analog read
uint32_t analogReadWait;            // millis() during state machine 
uint16_t analogReadInterval = 50;   // interval at which to read analog values (miliseconds)
uint8_t analogReadState = 0;        // to keep track of switching LED on/off, measuring etc.
uint8_t analogReadAmp = 3;          // amplification for display
uint8_t analogReadSamples = 4;      // samples to take with the light off - max 16 so sum can be an int 16
uint8_t analogReadCount = 0;        // counter for sampling

uint32_t analogCallLast = 0;        // last analog read call for debugging delays

#define MAX_UNIT 5                  // 4 characters and a trailing zero
typedef struct printData {
    uint8_t pin;                    // pin number that this unit information is for
    uint32_t pulsesPerUnit;         // number of pulses counted per unit
    uint32_t pulsesPerUnitDiv;      // divisor for ppu
    char unit[MAX_UNIT];            // unit e.g. "l" or "kWh"
    uint32_t flowUnitFactor;        // to get from secounds to minutes or hours as desired
    char flowUnit[MAX_UNIT];        // flow unit e.g. "7/h" or "W"
    uint32_t intervalStart;
    uint32_t lastCount;
} printData_t;
printData_t printData;


void initPinVars(pinData_t *pd, uint32_t now) {
    uint8_t level = 0;
    pd->pinName        = FF;        // inactive
    pd->initialized    = false;     // no pulse seen yet
    pd->pulseWidthMin  = 0;         // min pulse length
    pd->counter        = 0;         // counters to 0
    pd->counterIgn     = 0;    
    pd->lastCount      = 0;
    pd->rejectCounter  = 0;        
    pd->lastRejCount   = 0;
    pd->intervalStart  = now;       // time vars
    pd->intervalEnd    = now;
    pd->lastChange     = now;
    pd->lastReport     = now;
    pd->reportSequence = 0;
    if (!pd->analogFlag)
        level = digitalRead(pin2GPIO(pd->pinName));
    pd->lastLevel = level;
    pd->lastLongLevel = level;
    pd->lastDebugLevel = level;     // for debug output
}


void initHistVars() {
    histIndex = 0;
    histNextSeq = 1;
    for (uint8_t hIdx=0; hIdx < MAX_HIST; hIdx++) {
        histData_t *hd = &histData[hIdx];
        hd->seq = 0;
        hd->pin = FF;
        hd->level = 0;
        hd->time = 0;
        hd->len = 0;
        hd->act = ' ';
    }
}

void initialize() {
    uint32_t now = millis();
    bootTime = now;             // with boot / reset time
    bootWraps = millisWraps;    
    lastReportCall = now;       // time for first output after intervalMin from now
    analogReadLast = now;
    histLastOut = 0;
    lastTimeMillis = now;

    enableHistory = 0;          // pin change history
    enableSerialEcho = 0;       // echo tcp output on serial
    enablePinDebug = 0;         // show digital pin changes
    enableAnalogDebug = 0;      // analog changes
    enableDevTime = 0;          // show device time 


    maxPinIndex = 0;
    maxAnalogIndex = 0;
    analogReadState = 0;
    initHistVars();
    for (uint8_t pinIndex=0; pinIndex < MAX_PIN; pinIndex++)
        initPinVars(&pinData[pinIndex], now);                   // not necessary but initialize anyway
#if defined TFT_DISPLAY        
    displayMode = 1;
    printData.intervalStart = now;
    printData.lastCount = 0;
    lastPrintFlowCall = now;    
#endif    
#if defined(__AVR_ATmega328P__)        
    for (uint8_t aPin=0; aPin < MAX_APIN; aPin++)
        pinIndexMap[aPin] = FF;
#endif        
#if defined(WifiSupport)
    lastKeepAlive = now;
    serverStarted = false;
    lastReconnectTry = now;
    delayedTcpReports = 0;     
    lastDelayedTcpReports = 0; 
    reconnects = 0;
#endif      
    restoreFromEEPROM();
    clearInput();
}


// search pinData for aPin and return the pinIndex
uint8_t findInPin (uint8_t aPin) {
    for (uint8_t pinIndex = 0; pinIndex < maxPinIndex; pinIndex++) {
        if (pinData[pinIndex].pinName == aPin) return pinIndex;
    }
    return FF;
}

// search analogData for oPin and return the analogIndex
uint8_t findOutPin (uint8_t oPin) {
    for (uint8_t analogIndex = 0; analogIndex < maxAnalogIndex; analogIndex++) {
        if (analogData[analogIndex].outPinName == oPin) return analogIndex;
    }
    return FF;
}

// check if pin is allowed
bool checkPin (uint8_t aPin) {
    if (aPin >= MAX_APIN) return false;                     // pin number too big
    if (pin2GPIO(aPin) == FF) return false;      // pin is not allowed at all
    return true;
}


// find analogData for given pinData
uint8_t findAnalogData(pinData_t *pd) {
    for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
        if (analogData[aIdx].inPinData == pd) 
            return aIdx;
    }
    return FF;
}


void clearInput() {
        commandDataPointer = 0;
        commandDataSize = 0;
        commandValue = 0;
        for (uint8_t i=0; i < MAX_INPUT_NUM; i++)
            commandData[i] = 0;     
}


void PrintErrorMsg() {
    Output->print(F("Error: "));
    Output->print(F("command "));
    Output->print(commandLetter);
    Output->print(F(" "));
}

void PrintPinErrorMsg(uint8_t aPin) {
    PrintErrorMsg(); 
    Output->print(F("Illegal pin specification "));
    Output->println(aPin);
}    


void printVersionMsg() {  
    uint8_t len = strlen_P(versionStr);
    char myChar;
    for (unsigned char k = 0; k < len; k++) {
        myChar = pgm_read_byte_near(versionStr + k);
        Output->print(myChar);
    }
    Output->print(F(" on "));
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
#if defined (ARDUINO_AVR_NANO)
    Output->print(F("NANO"));
#else 
    Output->print(F("UNO"));
#endif
#elif defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega16U4__)
    Output->print(F("Leonardo"));
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    Output->print(F("Mega"));
#elif defined(ESP8266)
    Output->print(F("ESP8266"));
#elif defined(ESP32)
    Output->print(F("ESP32"));
#else
    Output->print(F("UNKNOWN"));
#endif
#if defined (ARDUINO_BOARD)
    Output->print(F(" "));
    Output->print(F(ARDUINO_BOARD));
#endif
#if defined (TFT_DISPLAY)
    Output->print(F(" with display"));
#endif
    Output->print(F(" compiled "));
    Output->print(F(__DATE__ " " __TIME__));
#if defined(ESP8266)
#if defined (ARDUINO_ESP8266_RELEASE)
    Output->print(F(" with core version "));
    Output->print(F(ARDUINO_ESP8266_RELEASE));
#endif
#if defined (ARDUINO_ESP32_RELEASE)
    Output->print(F(" with core version "));
    Output->print(F(ARDUINO_ESP32_RELEASE));
#endif
#endif    
}


void printPinChangeDebug(pinData_t *pd, uint8_t pinState) {
    pd->lastDebugLevel = pinState;
    Output->print(F("M pin "));       Output->print(pd->pinName);
    Output->print(F(" changed to ")); Output->print(pinState);
    Output->print(F(", histIdx "));   Output->print(histIndex);
    Output->print(F(" seq "));        Output->print(histData[histIndex].seq);
    Output->print(F(", count "));     Output->print(pd->counter);
    Output->print(F(", reject "));    Output->print(pd->rejectCounter);
    Output->println();
}


bool checkVal(uint8_t index, uint16_t min, uint16_t max = 0, bool doErr = true) {
    if (index >= commandDataSize) {
        if (doErr) {
            PrintErrorMsg();
            Output->print(F("missing parameter number ")); Output->println(index+1);
        }
        return false;
    }
    if (commandData[index] < min || (max > 0 && commandData[index] > max)) {
        PrintErrorMsg();
        Output->print(F("parameter number ")); Output->print(index+1);
        Output->print(F(" (value "));          Output->print(commandData[index]);
        Output->println(F(") is out of bounds"));
        return false;
    }
    return true;
}


/*
   do counting and set start / end time of interval.
   reporting is not triggered from here.
   
   only here counter[] is modified
   intervalEnd[] is set here and in report
   intervalStart[] is set in case a pin was not initialized yet and in report
*/
static void doCount(pinData_t *pd, uint8_t level, uint16_t analogLevel=0) {
    uint32_t now = millis();
    uint32_t len = now - pd->lastChange;
    char act = ' ';
    if (len < pd->pulseWidthMin) {                      // len is too short
        if (pd->lastLevel == pd->pulseLevel) {          // if change to gap level (we just had a too short pulse)
            act = 'R';                                  // -> reject
            pd->rejectCounter++;                       
        } else {                                        // change to pulse level (we just had a too short gap)
            act = 'X';                                  // -> reject gap / set action to X (gap too short)
        }
    } else {    // len is big enough
        if (pd->lastLevel != pd->pulseLevel) {          // edge fits pulse start, level is pulse, before was gap
            act = 'G';                                  // -> gap (even if betw. was a spike that we ignored)
        } else {                                        // edge is a change to gap, level is now gap
            if (pd->lastLongLevel != pd->pulseLevel) {  // last valid level was gap, now pulse 
                act = 'C';                              // -> count
                pd->counter++;                          
                pd->intervalEnd = now;                  // remember time in case pulse is last in the interval
                if (!pd->initialized) {
                    pd->intervalStart = now;            // if first impulse on this pin -> start interval now
                    printData.intervalStart = now;
                    pd->initialized = true;             // and start counting the next impulse (counter is 0)
                    pd->counterIgn++;                   // count ignored for diff because defines start of intv
                }
                pd->pulseWidthSum += len;               // for average calculation
            } else {                                    // last valid level was pulse -> now another valid pulse
                act = 'P';                              // -> pulse was already counted, only short drop inbetween
                pd->pulseWidthSum += len;               // for average calculation
            }
        }       
        pd->lastLongLevel = pd->lastLevel;              // remember this valid level as lastLongLevel
    }
    if (enableHistory) {
        if (++histIndex >= MAX_HIST) histIndex = 0;
        histData_t *hd = &histData[histIndex];              // write pin history
        hd->seq   = histNextSeq++;                          // fhem side detects wrapping
        hd->pin   = pd->pinName;
        hd->time  = pd->lastChange;
        hd->level = pd->lastLevel;
        hd->aLvl  = analogLevel;
        hd->len   = len;
        hd->act   = act;
    }
    pd->lastChange = now;
    pd->lastLevel  = level;
}


/* Interrupt handlers and their installation  */


#if defined(ESP8266) || defined(ESP32)
void IRAM_ATTR ESPISR(void* arg) {                      // common ISR for all Pins on ESP
    // ESP32 now also defines IRAM_ATTR as ICACHE_RAM_ATTR
    pinData_t *pd = (pinData_t *)arg;
    doCount(pd, digitalRead(pin2GPIO(pd->pinName)));
}
#endif


/* Add a pin to be handled */
uint8_t AddPinChangeInterrupt(uint8_t pinIndex) {
    uint8_t aPin = pinData[pinIndex].pinName;
    uint8_t rPin = pin2GPIO(aPin);
#if defined(ESP8266) || defined(ESP32)
    attachInterruptArg(digitalPinToInterrupt(rPin), ESPISR, &pinData[pinIndex], CHANGE);
#elif defined(__AVR_ATmega328P__)
    volatile uint8_t *pcmask;                       // pointer to PCMSK0 or 1 or 2 depending on the port corresponding to the pin
    uint8_t bitM = digitalPinToBitMask(rPin);       // mask to bit in PCMSK to enable pin change interrupt for this arduino pin 
    uint8_t port = digitalPinToPort(rPin);          // port that this arduno pin belongs to for enabling interrupts
    if (port == NOT_A_PORT) 
        return 0;
    pinIndexMap[aPin] = pinIndex;
    port -= 2;                                      // from port (PB, PC, PD) to index in our array
    PCintLast[port] = *portInputRegister(port+2);   // save current inut state to detect changes in isr
    pcmask = port_to_pcmask[port];                  // point to PCMSK0 or 1 or 2 depending on the port corresponding to the pin
    *pcmask |= bitM;                                // set the pin change interrupt mask through a pointer to PCMSK0 or 1 or 2 
    PCICR |= 0x01 << port;                          // enable the interrupt
#endif    
    return 1;
}


/* Remove a pin to be handled */
uint8_t RemovePinChangeInterrupt(uint8_t pinIndex) {
    uint8_t aPin = pinData[pinIndex].pinName;
    uint8_t rPin = pin2GPIO(aPin);
#if defined(ESP8266) || defined(ESP32)
    detachInterrupt(digitalPinToInterrupt(rPin));
#elif defined(__AVR_ATmega328P__)
    volatile uint8_t *pcmask;
    uint8_t bitM = digitalPinToBitMask(rPin);
    uint8_t port = digitalPinToPort(rPin);
    if (port == NOT_A_PORT)
        return 0;
    pinIndexMap[aPin] = FF;
    port -= 2;                                  // from port (PB, PC, PD) to index in our array
    pcmask = port_to_pcmask[port];          
    *pcmask &= ~bitM;                           // clear the bit in the mask.
    if (*pcmask == 0) {                         // if that's the last one, disable the interrupt.
        PCICR &= ~(0x01 << port);
    }
#endif    
    return 1;
}


#if defined(__AVR_ATmega328P__)
/* 
   common function for arduino pin change interrupt handlers. 
   "port" is the PCINT port index (0-2) as passed from above, 
   not PB, PC or PD which are mapped to 2-4
*/
static void PCint(uint8_t port) {
    uint8_t bit;
    uint8_t curr;
    uint8_t delta;
    short pinIndex;

    // get the pin states for the indicated port.
    curr  = *portInputRegister(port+2);                         // current pin states at port (add 2 to get from index to PB, PC or PD)
    delta = (curr ^ PCintLast[port]) & *port_to_pcmask[port];   // xor gets bits that are different and & screens out non pcint pins
    PCintLast[port] = curr;                                     // store new pin state for next interrupt
    if (delta == 0) return;                                     // no handled pin changed 
    
    // the printed pin numbers for the ports are sequential, starting with 8, 14 and 0 which we keep in an array
    bit = 0x01;                                                 // start mit rightmost (least significant) bit in a port
    for (uint8_t aPin = firstPin[port]; aPin <= lastPin[port]; aPin++) { // loop over each pin on the given port that changed - todo: until bit == 0 ??
        if (delta & bit) {                                      // did this pin change?
            pinIndex = pinIndexMap[aPin];
            if (pinIndex != FF)                                 // shound not be necessary but test anyway
                doCount (&pinData[pinIndex], ((curr & bit) > 0));    // do the counting, history and so on
        }
        bit = bit << 1;                                         // shift mask to go to next bit
    } 
}


ISR(PCINT0_vect) {
    PCint(0);
}
ISR(PCINT1_vect) {
    PCint(1);
}
ISR(PCINT2_vect) {
    PCint(2);
}

#endif


void printAvailablePins() {
    Output->print(F("C"));
    boolean first = true;
    for (uint8_t aPin=0; aPin < MAX_APIN; aPin++)
        if (pin2GPIO(aPin) != FF) {
            if (!first)
                Output->print(F(","));
            first = false;
            Output->print(aPin);            // show available pins
        }
    Output->println();
}


void printTime(uint32_t now) {
    Output->print(F("N")); Output->print(now);
    Output->print(F(",")); Output->print(millisWraps);
    Output->print(F("B")); Output->print(bootTime);
    Output->print(F(",")); Output->print(bootWraps);    
    Output->println();
}
    

void printIntervals() {
    Output->print(F("I"));  Output->print(intervalMin / 1000);
    Output->print(F(","));  Output->print(intervalMax / 1000);
    Output->print(F(","));  Output->print(intervalSml / 1000);
    Output->print(F(","));  Output->print(countMin);
    Output->print(F(","));  Output->print(analogReadInterval);
    Output->print(F(","));  Output->print(analogReadSamples);
    Output->println();
}


void printVerboseFlags() {
    Output->print(F("V")); Output->print(enableHistory);     
    Output->print(F(",")); Output->print(enableSerialEcho);  
    Output->print(F(",")); Output->print(enablePinDebug);
    Output->print(F(",")); Output->print(enableAnalogDebug);
    Output->print(F(",")); Output->print(enableDevTime);
    Output->println();
}
    

#ifdef TFT_DISPLAY
void printUnitConfig() {
    Output->print(F("U")); Output->print(printData.pin);     
    Output->print(F(",")); Output->print(printData.pulsesPerUnit);  
    Output->print(F(",")); Output->print(printData.pulsesPerUnitDiv);  
    Output->print(F(",")); Output->print(printData.unit);
    Output->print(F(",")); Output->print(printData.flowUnitFactor);  
    Output->print(F(",")); Output->print(printData.flowUnit);
    Output->println();
}
#endif


void printPinConfig(pinData_t *pd) {
    Output->print(F("P"));  Output->print(pd->pinName);
    switch (pd->pulseLevel) {
        case 1:  Output->print(F("r")); break;
        case 0: Output->print(F("f")); break;
        default: Output->print(F("-")); break;
    }        
    if (pd->pullupFlag) 
        Output->print(F("p"));
    Output->print(F(" m"));  Output->print(pd->pulseWidthMin);
    if (pd->analogFlag) {
        uint8_t analogIndex = findAnalogData(pd);           // reuse or create analog entry for aPin with oPin
        if (analogIndex != FF) {
            analogData_t *ad = &analogData[analogIndex];
            Output->print(F("out"));  Output->print(ad->outPinName);
            Output->print(F("t"));  Output->print(ad->thresholdMin);
            Output->print(F("/"));  Output->print(ad->thresholdMax);
        }
    }
}


#if defined(TFT_DISPLAY)
void printTFTFlow(uint32_t timeDiff, uint32_t countDiff) {
    char fs[10];
    float flow;
    if (timeDiff != 0 && printData.pulsesPerUnit != 0 && printData.pulsesPerUnitDiv != 0) {
        flow = ((float)countDiff * 1000 * printData.flowUnitFactor) 
                        / (timeDiff * (printData.pulsesPerUnit / printData.pulsesPerUnitDiv));
    } else {
        flow = 0;
    }
    tft.fillRect(0,64,TFT_HEIGHT, TFT_WIDTH-64, TFT_BLACK);
    tft.setCursor(0, 64);
    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    if (flow < 10000 && flow > -1000) {
        sprintf (fs, "%07.2f", flow);
        tft.setTextFont(7);
        tft.setTextSize(1);
        tft.print(fs);
        tft.setTextFont(2);
        tft.setTextSize(1);
        tft.print(printData.flowUnit);
    } else {
        tft.setTextFont(4);
        tft.setTextSize(1);
        tft.print(F("flow too big"));
    }
}
#endif


void printPinHistory(pinData_t *pd, uint32_t now) {
    histData_t *hd;
    uint8_t start = (histIndex + 2) % MAX_HIST;                             // start two after current slot in ring buffer
    uint8_t count = 0;
    uint8_t first = 0;
    uint8_t pinName = pd->pinName;

    for (uint8_t i = 0; i < MAX_HIST; i++) {
        hd = &histData[(start + i) % MAX_HIST];                             // entry relative to start in ring
        if (hd->pin == pinName && ((long int)hd->seq - histLastOut) > 0) {  // are there entries for this pin at all?
            count++;
            first = i;
            break;
        }
    }
    if (!count) { 
      // Output->println (F("M No Pin History"));            
      return;
    }    
    Output->print (F("H"));  Output->print(pinName);                        // printed pin number
    Output->print (F(" "));
    for (uint8_t i = first; i < MAX_HIST; i++) {
        hd = &histData[(start + i) % MAX_HIST];
        if (hd->pin == pinName && ((long int)hd->seq - histLastOut) > 0) {  // include ignored drops / spikes
            if (i != first) Output->print (F(", "));
            Output->print (hd->seq);                                        // sequence
            Output->print (F(","));  Output->print ((long) (hd->time - now)); // time when level started
            Output->print (F(":"));  Output->print (hd->len);               // length 
            Output->print (F("@"));  Output->print (hd->level);             // level (0/1)
            if (pd->analogFlag) {
                Output->print (F("/"));  Output->print (hd->aLvl);          // analog level
            }
            Output->print (hd->act);                                        // action
            histLastOut = hd->seq;
        }
    }        
    Output->println();    
}


/*
   lastCount and lastRejCount are only modified here (counters at time of last reporting)
   intervalEnd is modified here and in ISR - disable interrupts in critcal moments to avoid garbage in var
   intervalStart is modified only here or for very first Interrupt in ISR
*/
void printPinCounter(pinData_t *pd, boolean showOnly, uint32_t now) {
    uint32_t count, countDiff, realDiff;
    uint32_t startT, endT, timeDiff, widthSum;
    uint16_t rejCount, rejDiff;
    uint8_t countIgn;
    
    noInterrupts();                             // copy counters while they cant be changed in isr
    startT   = pd->intervalStart;               // start of interval (typically first pulse)
    endT     = pd->intervalEnd;                 // end of interval (last unless not enough)
    count    = pd->counter;                     // get current counter (counts all pulses
    rejCount = pd->rejectCounter;
    countIgn = pd->counterIgn;                  // pulses that mark the beginning of an interval
    widthSum = pd->pulseWidthSum;
    interrupts();
        
    timeDiff  = endT - startT;                  // time between first and last impulse
    realDiff  = count - pd->lastCount;          // pulses during intervall
    countDiff = realDiff - countIgn;            // ignore forst pulse after device restart
    rejDiff   = rejCount - pd->lastRejCount;
    
    if (!showOnly) {                            // real reporting sets the interval borders new
        if((now - pd->lastReport) > intervalMax) { 
            // intervalMax is over
            if ((countDiff >= countMin) && (timeDiff > intervalSml) && (intervalMin != intervalMax)) {
                // normal procedure
                noInterrupts();                 // vars could be modified in ISR as well
                pd->intervalStart = endT;       // time of last impulse becomes first in next
                interrupts();
            } else {
                // nothing counted or counts happened during a fraction of intervalMin only
                noInterrupts();                 // vars could be modified in ISR as well
                pd->intervalStart = now;        // start a new interval for next report now
                pd->intervalEnd   = now;        // no last impulse, use now instead
                interrupts();
                timeDiff  = now - startT;       // special handling - calculation ends now
            }        
        } else if (((now - pd->lastReport) > intervalMin)   
                    && (countDiff >= countMin) && (timeDiff > intervalSml)) {
            // minInterval has elapsed and other conditions are ok
            noInterrupts();                     // vars could be modified in ISR as well
            pd->intervalStart = endT;           // time of last also time of first in next
            interrupts();
        } else {
          return;                               // intervalMin and Max not over - dont report yet
        }
        noInterrupts(); 
        pd->counterIgn    = 0;
        pd->pulseWidthSum = 0;
        interrupts();
        pd->lastCount    = count;               // remember current count for next interval
        pd->lastRejCount = rejCount;
        pd->lastReport   = now;                 // remember when we reported
#if defined(WifiSupport)
        delayedTcpReports      = 0;
#endif
        pd->reportSequence++;
    } else {
        Output->print(F("D"));                  // prefix with "D" if showOnly so it is not parsed as report line
    }
    Output->print(F("R")); Output->print(pd->pinName);
    Output->print(F("C")); Output->print(count);
    Output->print(F("D")); Output->print(countDiff);
    Output->print(F("/")); Output->print(realDiff);
    Output->print(F("T")); Output->print(timeDiff);  
    
    //Output->print(F("N")); Output->print(now);          // moved to its own line N... dependent on new flag that time should be reported at all
    //Output->print(F(",")); Output->print(millisWraps);    
    
    Output->print(F("X")); Output->print(rejDiff);  
    
    if (!showOnly) {
        Output->print(F("S")); Output->print(pd->reportSequence);  
    }
    if (countDiff > 0) {
        Output->print(F("A")); Output->print(widthSum / countDiff);
    }
    Output->println();    
#if defined(WifiSupport)
    if (enableSerialEcho && tcpMode && !showOnly) {
        Serial.print(F("D reported pin "));  Serial.print(pd->pinName);
        Serial.print(F(" sequence "));       Serial.print(pd->reportSequence);  
        Serial.println(F(" over tcp "));  
    }
#endif  
#if defined(TFT_DISPLAY)
    if (displayMode == 1) {
        if (lineCount < 4) {
            tft.setTextFont(2);
            tft.setTextSize(1);
            tft.setTextColor(TFT_GREEN, TFT_BLACK);
            tft.setCursor(0,48+16*lineCount);               // todo: how can this be separated from report? (timing)
            tft.print(F("R"));  tft.print(pd->pinName);
            tft.print(F(" C")); tft.print(count);
            tft.print(F(" D")); tft.print(countDiff);
            tft.print(F("/"));  tft.print(realDiff);
            tft.print(F(" T")); tft.print(timeDiff);  
            tft.print(F(" X")); tft.print(rejDiff);  
            lineCount++;
        }
    } else if (displayMode == 3) {
        if (pd->pinName == printData.pin) 
            printTFTFlow(timeDiff, countDiff);
    }
#endif  
}


#if defined(WifiSupport)
// called from show and new connectinn
void printWifiState(Print *Out) {
    if (WiFi.status() == WL_CONNECTED) {
        Out->print(F("D Connected to "));   Out->print(WiFi.SSID());    
        Out->print(F(" with IP "));         Out->print(WiFi.localIP());    
        Out->print(F(" RSSI "));            Out->print(WiFi.RSSI());
        Out->println();
    } else {
        Out->println(F("D Wifi not connected"));
    }
#if defined(TFT_DISPLAY)
    if (displayMode > 0) {
        //tft.fillRect(0, 0, TFT_HEIGHT, 31, TFT_BLACK);       // oberen Teil löschen
        tft.fillScreen(TFT_BLACK);                              // clear Wifi manager output as well
        tft.setTextFont(2);
        tft.setTextSize(1);
        tft.setTextColor(TFT_GREEN, TFT_BLACK);
        tft.setCursor(0, 0);
        if (WiFi.status() == WL_CONNECTED) {
            tft.print(F("Connected to ")); tft.print(WiFi.SSID());
            tft.setCursor(0, 16);
            tft.print("IP ");       tft.print(WiFi.localIP());
            tft.print(F(" RSSI ")); tft.print(WiFi.RSSI());
        } else {
            tft.print(F("Wifi not Connected"));
            tft.setCursor(0, 16);
            tft.print(F("Retry "));
            tft.print(reconnects);
        }
    }
#endif
}
#endif


/* 
   report count and time for pins that are between min and max interval    
*/

bool reportDue(uint32_t now) {
    if((now - lastReportCall) > intervalMin)            // works fine when millis wraps.
        return true;                                    // intervalMin is over 
    else 
        for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++)  
            if((now - pinData[pinIndex].lastReport) > intervalMax)
                return true;                            // active pin has not been reported for langer than intervalMax
    return false;
}


bool delayIfDisconnected(uint32_t now) {
#if defined(WifiSupport)
    if (tcpMode && !TCPconnected && (delayedTcpReports < 3)) {
        if(delayedTcpReports == 0 || ((now - lastDelayedTcpReports) > 30000)) {
            if (enableSerialEcho) {
                Serial.print(F("D report called but tcp is disconnected - delaying ("));
                Serial.print(delayedTcpReports); Serial.print(F(")"));
                Serial.print(F(" now "));  Serial.print(now);
                Serial.print(F(" last ")); Serial.print(lastDelayedTcpReports);
                Serial.print(F(" diff ")); Serial.print(now - lastDelayedTcpReports);
                Serial.println();        
            }
            delayedTcpReports++;
            lastDelayedTcpReports = now;
            return true;        // continue delaying after message
        } else 
            return true;        // another 30 secs not over yet, just continue delaying without message
    }
#endif    
    return false;               // not TCP mode -> no delays anyway
}


#if defined(TFT_DISPLAY)
void handleTFTReport(uint32_t now) {
    lineCount = 0;              // for mode 0 where lines of pin reports are printed
    if (displayMode == 2 && ((now - lastPrintFlowCall) > 5000)) {
        lastPrintFlowCall = now;

        uint8_t pinIndex = findInPin(printData.pin); 
        if (pinIndex == FF)                         // not used so far
            return;
        pinData_t *pd = &pinData[pinIndex];         // pinData entry to work with

        uint32_t countDiff = pd->counter - printData.lastCount - pd->counterIgn;      // ignore first pulse after device restart
        uint32_t timeDiff = pd->intervalEnd - printData.intervalStart;                // time between first and last impulse
        printData.lastCount = pd->counter;
        printData.intervalStart = pd->intervalEnd;
        printTFTFlow(timeDiff, countDiff);
    }
}
#endif


void report() {
    uint32_t now = millis();
#if defined(TFT_DISPLAY)
    handleTFTReport(now);
#endif
    if (!reportDue(now)) return;
    if (delayIfDisconnected(now)) return;
    for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++) {  // go through all observed pins as pinIndex
        pinData_t *pd = &pinData[pinIndex];
        printPinCounter (pd, false, now);                           // report pin counters if necessary
        if (enableHistory) 
            printPinHistory(pd, now);                               // show pin history todo: call outside of report
    }
    lastReportCall = now;                                           // check again after intervalMin or if intervalMax is over for a pin
}


void updateEEPROM(int &address, char value) {
    if( EEPROM.read(address) != value){
        EEPROM.write(address, value);
    }
    address++;
}


void save(int &address, uint16_t n, bool komma=true) {
    char b[6];
    sprintf(b, "%u", n);
    uint8_t len = strlen(b);
    if (len < 6)
        for (uint8_t i=0; i<len; i++) 
            updateEEPROM(address, b[i]);
    if(komma)
        updateEEPROM(address, ',');
}

// store a string as sequence of text coded integers
void saveStr(int &address, char *s, uint8_t komma=1) {
    uint8_t cCount = 0;
    uint8_t byteNum = 0;
    uint16_t val;
    bool first = true;
    while (cCount < MAX_UNIT && *(s+cCount) != 0) {
        if (byteNum) {
            val = (*(s+cCount) << 8) + val;         // second char -> add as high byte
            if (!first) updateEEPROM(address, ',');
            save(address, val, false);
            first = false;
            byteNum = 0;
        } else {
            val = *(s+cCount);                      // first char
            byteNum++;
        }
        cCount++;
    }
    if (byteNum) {
        if (!first) updateEEPROM(address, ',');
        save(address, val, false);
        if (!val) {
            updateEEPROM(address, ',');
            save(address, 0, false);
        }
    }
    if (komma)
        updateEEPROM(address, ',');
}


void CmdSaveToEEPROM() {
    int a = 0;
    updateEEPROM(a, 'C');
    updateEEPROM(a, 'f');
    updateEEPROM(a, '2');
    save(a,intervalMin / 1000); save(a,intervalMax / 1000);
    save(a,intervalSml / 1000); save(a,countMin); 
    save(a,analogReadInterval); save(a,analogReadSamples,false); 
    updateEEPROM(a, 'i');
    
    save(a,enableHistory); save(a,enableSerialEcho); 
    save(a,enablePinDebug); save(a,enableAnalogDebug); 
    save(a,enableDevTime,false); 
    updateEEPROM(a, 'v');

#if defined TFT_DISPLAY
    save(a,printData.pin);  
    save(a,printData.pulsesPerUnit); save(a,printData.pulsesPerUnitDiv); 
    saveStr(a,printData.unit);           
    save(a,printData.flowUnitFactor); saveStr(a,printData.flowUnit,false); 
    updateEEPROM(a, 'u');
#endif    

    for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++) {
        pinData_t *pd = &pinData[pinIndex];
        uint8_t analogIndex = findAnalogData(pd);       // find analog entry (if exists)                    
        if (pd->analogFlag && analogIndex != FF) {
            analogData_t *ad = &analogData[analogIndex];
            save(a,pd->pinName);    save(a,pd->pulseLevel ? 3:2);
            save(a,pd->pullupFlag); save(a,pd->pulseWidthMin);
            save(a,ad->outPinName); save(a,ad->thresholdMin); save(a,ad->thresholdMax,false);
            updateEEPROM(a, 'a');
        } else {
            save(a,pd->pinName);    save(a,pd->pulseLevel ? 3:2);
            save(a,pd->pullupFlag); save(a,pd->pulseWidthMin,false);
            updateEEPROM(a, 'a');
        }
    }
    updateEEPROM(a, 0);
#if defined(ESP8266) || defined(ESP32)
    EEPROM.commit();               
#endif  
    Output->println(F("D config saved"));
}


void printEEPROM() {
    int address = 0;
    char c; 
    if (EEPROM.read(address) != 'C' || EEPROM.read(address+1) != 'f' || EEPROM.read(address+2) != '2') {
        Output->println(F("D no config in EEPROM"));
        return;
    }
    address = 3;
    Output->print(F("D EEPROM Config: "));
    while (address < 512 && (c = EEPROM.read(address++)) != 0) {
        Output->print(c);        
    }
    Output->println();
}


void restoreFromEEPROM() {
    int address = 0;
    char c;
    if (EEPROM.read(address) != 'C' || EEPROM.read(address+1) != 'f' || EEPROM.read(address+2) != '2') {
        Output->println(F("M no config in EEPROM"));
        return;
    }
    address = 3;
    Output->println(F("M restoring config from EEPROM: "));
    while (address < 512 && (c = EEPROM.read(address++)) != 0) {
        handleInput(c);
        Output->print(c);
    }
    Output->println();
#if defined(TFT_DISPLAY) 
    if (displayMode == 1) {
        tft.setTextFont(2);
        tft.setTextSize(1);
        tft.setTextColor(TFT_GREEN, TFT_BLACK);
        tft.setCursor(0, 32);
        tft.print(F("Pin config loaded"));
    }
#endif
}


void CmdInterval() {
    if (!checkVal(0,1,3600)) return;                    // index 0 is interval min, min 1, max 3600 (1h)
    if (!checkVal(1,1,3600)) return;                    // index 1 is interval max, min 1, max 3600 (1h)
    if (!checkVal(2,0,3600)) return;                    // index 2 is interval small, max 3600 (1h)
    if (!checkVal(3,0,100))  return;                    // index 3 is count min, max 100

    intervalMin = commandData[0] * 1000;      // convert to miliseconds
    intervalMax = commandData[1] * 1000;
    intervalSml = commandData[2] * 1000;
    countMin    = commandData[3];
        
    if (checkVal(4,0,10000,false)) {                          // index 4 is optional analog read interval
        analogReadInterval = (int)commandData[4];
    }
    if (checkVal(5,1,100,false)) {                      // index 5 is optional analog read samples
        analogReadSamples = (uint8_t)commandData[5];
    }
    printIntervals();
}


// for backward compatibility - set thresholds for all analog pins at the same time
void CmdThreshold() {    
    if (!checkVal(0,1,1023)) return;                            // analog threshold min, min 1, max 1023
    if (!checkVal(1,1,1023)) return;                            // analog threshold max, min 1, max 1023
    for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++)
        analogData[aIdx].thresholdMin = commandData[0]; 
    for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++)
        analogData[aIdx].thresholdMax = commandData[1];

    Output->print(F("D analog thresholds set to ")); Output->print(commandData[0]);
    Output->print(F(" ")); Output->println(commandData[1]);
}


/*
    handle add command.
*/
void CmdAdd () {
    uint8_t aPin = commandData[0];              // commandData[0] is pin number
    if ( commandData[0] > 255                   // more than one byte?
         || !checkPin(aPin)                     // is pin allowed?
         || (ledOutPin && aPin == ledOutPin)    // is pin used as led out?
         || findOutPin(aPin) != FF)  {          // is pin used as output so far?
        PrintPinErrorMsg(aPin);
        return;
    }
    uint8_t rPin = pin2GPIO(aPin);              // get gpio pin number for later use
    uint8_t pinIndex = findInPin(aPin);         // is pin already in use counting?
    if (pinIndex == FF) {                       // not used so far
        pinIndex = maxPinIndex;                 // use next available index
        initPinVars(&pinData[pinIndex], millis());
    }
    pinData_t *pd = &pinData[pinIndex];         // pinData entry to work with

    if (!checkVal(1,2,3)) return;               // index 1 is pulse level, min 2, max 3, std error
    pd->pulseLevel = (commandData[1] == 3);     // 2 = falling -> pulseLevel 0, 3 = rising -> pulseLevel 1
    pd->pinName = aPin;                         // save printed pin number for reporting

    if (checkVal(2,0,1, false))                 // index 2 is pullup, optional, no error message if omitted
        pd->pullupFlag = commandData[2];        // as defined
    else pd->pullupFlag = 0;                    // default to no pullup

    if (!checkVal(3,1,1000, false)) 
        commandData[3] = 2;                     // value 3 is min length, optional. Assume default 2 if invalid
    pd->pulseWidthMin = commandData[3];
    
    if (checkVal(4,1,MAX_APIN, false)) {        // 4 - analog out pin number given
        uint8_t oPin = commandData[4];
        if (!checkPin(oPin) || (ledOutPin && oPin == ledOutPin) || findInPin(oPin) != FF)  {          
            PrintPinErrorMsg(oPin);             // pin alreday used as input or ledout (analog out would be ok)
            return;
        }
        uint8_t analogIndex = findAnalogData(pd);           // reuse or create analog entry for aPin with oPin
        if (analogIndex == FF) {
            if (maxAnalogIndex < MAX_ANALOG) {
                analogIndex = maxAnalogIndex++;             // new entry -> initialize, inc used analog pins 
                analogData_t *ad = &analogData[analogIndex];
                ad->thresholdMin = 0;
                ad->thresholdMax = 0;
                ad->inPinData = pd;
            } else {
                PrintErrorMsg();
                Output->println(F("too many analog pins"));
                return;
            }
        }
        pd->analogFlag = 1;                                 // set analog flag
        analogData_t *ad = &analogData[analogIndex];
        ad->inPinName = aPin;
        ad->outPinName = oPin;
        pinMode (rPin, INPUT);
        pinMode (pin2GPIO(oPin), OUTPUT);
        
        if (checkVal(5,1,1023))                             // analog threshold min, min 1, max 1023
            ad->thresholdMin = commandData[5];
        if (checkVal(6,1,1023))                             // analog threshold max, min 1, max 1023
            ad->thresholdMax = commandData[6];
    } else {
        if (pd->pullupFlag) pinMode (rPin, INPUT_PULLUP);
        else pinMode (rPin, INPUT); 
        AddPinChangeInterrupt(pinIndex);
    }
    pd->pinName = aPin;
    if (pinIndex == maxPinIndex) maxPinIndex++;             // increment used entries in pinData
    Output->print(F("D defined ")); printPinConfig(pd); Output->println();
}


/*
    handle remove command.
*/
void CmdRemove() {
    uint8_t aPin = commandData[0];              // commandData[0] is pin number
    uint8_t pinIndex = findInPin(aPin);
    if (commandData[0] > 255                    // too big
         || pinIndex == FF)                     // pin is currently not used as input
         return;                                
    pinData_t *pd = &pinData[pinIndex];         // config entry to work with

    if (!pd->analogFlag) {                      
        RemovePinChangeInterrupt(pinIndex);
    } else {
        // find analog data entry
        uint8_t analogIndex = FF;
        for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
            if (analogData[aIdx].inPinData == pd) {
                analogIndex = aIdx;
                break;
            }
        }
        if (analogIndex != FF) {
            // copy idx +1 and following up and clear the last
            // then dec max
            for (uint8_t rIdx = analogIndex; (rIdx + 1) < maxAnalogIndex; rIdx++)
                analogData[rIdx] = analogData[rIdx + 1];
            analogData_t *lastAd = &analogData[--maxAnalogIndex];
            lastAd->inPinData = 0;
            lastAd->inPinName = 0;
            lastAd->outPinName = 0;
        }
    }
    
    for (uint8_t rIdx = pinIndex; (rIdx + 1) < maxPinIndex; rIdx++) {
        if (!pinData[rIdx].analogFlag) {
            RemovePinChangeInterrupt(rIdx+1);
        }
        pinData[rIdx] = pinData[rIdx + 1];                              // move pinData entries after pinIndex up
        if (!pinData[rIdx].analogFlag) {
            AddPinChangeInterrupt(rIdx);
        }
        
        if (pd->analogFlag) { 
            for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {                    
                if (analogData[aIdx].inPinData == &pinData[rIdx + 1]) 
                    analogData[aIdx].inPinData = &pinData[rIdx];
            }
        }
    }
    pinData_t *lastPd = &pinData[--maxPinIndex];                        // clear the last one that is now no longer needed
    initPinVars(lastPd, 0);    
    Output->print(F("D removed ")); Output->println(aPin);
}


void CmdClear () {
    uint8_t aPin = commandData[0];              // commandData[0] is pin number
    uint8_t pinIndex = findInPin(aPin);         // is pin in use?
    if (commandData[0] > 255                    // too big
         || pinIndex == FF) {                   // not found
        PrintPinErrorMsg(aPin);                 // no active pin
        return;
    }
    pinData_t *pd = &pinData[pinIndex];

    uint32_t now = millis();  
    pd->lastReport     = now;
    pd->lastCount      = 0;
    pd->lastRejCount   = 0;
    
    noInterrupts();
    pd->intervalStart  = pd->intervalEnd;       // time of last pulse is now time of first pulse in this new interval
    pd->counter        = 0;                     // counters to 0
    pd->counterIgn     = 0;
    pd->rejectCounter  = 0;        
    pd->pulseWidthSum  = 0;
    interrupts();
    Output->print(F("M cleared ")); Output->println(aPin);
}


/* give status report in between if requested over serial input */
void CmdShow() {
    uint32_t now = millis();  
    Output->println();
    Output->print(F("D Status: ")); printVersionMsg();
    Output->println();
    
#if defined(WifiSupport)
    printWifiState(Output);                            // print line with IP and RSSI
#endif    
    printIntervals();
    printVerboseFlags();
#if defined(TFT_DISPLAY)
    printUnitConfig();
#endif
    
    for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++) {
        pinData_t *pd = &pinData[pinIndex];
        printPinConfig(pd);
        Output->print(F(", "));
        printPinCounter(pd, true, now);
    }
    printEEPROM();
    Output->print(F("D Next report in "));              // Fhem side recognizes this as end of show command
    Output->print(lastReportCall + intervalMin - millis());
    Output->println(F(" milliseconds"));
}


void CmdHello() {
    uint32_t now = millis();
    Output->println(); 
    printVersionMsg(); Output->println(F(" Hello"));
    printTime(now);                  // print line with device time,                 e.g. N456,2 B789,3
    printAvailablePins();            // print line with available pins,              e.g. C2,3,4 ...
    printIntervals();                // print line with configured intervals,        e.g. I30,60,2,2 ...
    printVerboseFlags();             // print line with configured verbose flags,    e.g. V0,1,...
    
    for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++) { // go through all observed pins as pinIndex
        printPinConfig(&pinData[pinIndex]);
        Output->println();
    }
}


void CmdLED() {
    // set monitor ouput LED and a max of 5 pins to monitor
    // 12,2,3l would set pin 12 as LED and pins 2 and 3 to create LED switches when their level changes
    // dont allow 0, check if already used as led, if not checkPin
    uint8_t aPin = commandData[0];                   // commandData[0] is led pin number
    
    if (commandData[0] > 255 || !aPin || !checkPin(aPin) 
         || findOutPin(aPin) != FF || findInPin(aPin) != FF) {
        PrintPinErrorMsg(aPin);                 // illegal pin or already used for in/other output
        return;
    }
    // save pin and validate other params / pins to monitor
}


uint8_t stuffString(char* str, uint8_t offset) {
    uint8_t sIndex = 0;
    uint8_t byteVal;
    uint8_t cIndex; 
    for (cIndex = offset; (cIndex < MAX_INPUT_NUM) && (sIndex < MAX_UNIT-1); cIndex++) {
        uint16_t value = commandData[cIndex];
        for (uint8_t byteNum = 0; (byteNum < 2) && (sIndex < MAX_UNIT-1); byteNum++) {
            byteVal = value & 0xFF;
            if (!byteVal) break;
            value = value >> 8;
            str[sIndex++] = byteVal;
        }
        if (!byteVal) break;
    }
    str[sIndex] = 0;
    return cIndex + 1;
}


void CmdUnits() {
#if defined(TFT_DISPLAY)
    // set pulses per unit for a pin and a unit for printing consumption per minute / per hour
    uint8_t next = 1;
    uint8_t aPin = commandData[0];                          // commandData[0] is pin number to display
    if (commandData[0] > 255 
         || !aPin || !checkPin(aPin)) {                     // is pin allowed?
        PrintPinErrorMsg(aPin);
        return;
    }
    printData.pin = aPin;

    if (!checkVal(next,1))  return;                         // index 1 is pulsesPerUnit
    printData.pulsesPerUnit = commandData[next++];

    if (!checkVal(next,1))  return;                         // index 2 is pulsesPerUnitDiv
    printData.pulsesPerUnitDiv = commandData[next++];

    next = stuffString(printData.unit, next);               // unit string starting at commandData[3]

    if (!checkVal(next,1))  return;                         // index after first strng is flowUnitFactor
    printData.flowUnitFactor = commandData[next++];

    next = stuffString(printData.flowUnit, next);           // unit string starting at commandData[2]
    printUnitConfig();
#endif    
}


void CmdDevVerbose() {    
    if (!checkVal(0,0,1))  return;                  // index 0 is enableHistory
    if (!checkVal(1,0,10)) return;                  // index 1 is enableSerialEcho
    if (!checkVal(2,0,1))  return;                  // index 2 is enablePinDebug
    if (!checkVal(3,0,10)) return;                  // index 3 is enableAnalogDebug
    if (!checkVal(4,0,1))  return;                  // index 3 is enableDevTime

    enableHistory     = (uint8_t)commandData[0];
    enableSerialEcho  = (uint8_t)commandData[1];
    enablePinDebug    = (uint8_t)commandData[2];
    enableAnalogDebug = (uint8_t)commandData[3];
    enableDevTime     = (uint8_t)commandData[4];
    printVerboseFlags();
}
            
            
void CmdKeepAlive() {
    if (commandData[0] == 1 && commandDataSize > 0) {
        Output->print(F("A"));
#if defined(WifiSupport)
        uint32_t now = millis();
        Output->print(F("R")); Output->print(WiFi.RSSI());
        if (commandData[0] == 1 && commandDataSize > 0 && commandDataSize < 3 && Client1.connected()) {
            tcpMode = true;
            if (commandDataSize == 2) {
                keepAliveTimeout = commandData[1];  // timeout in seconds (on ESP side we use it times 3)
            } else {
                keepAliveTimeout = 200;             // *3 gives 10 minutes if nothing sent (should not happen)
            }
        }  
        lastKeepAlive = now;
#endif
        Output->println();
    }
}

 
void CmdQuit() {
#if defined(WifiSupport)
    if (Client1.connected()) {
        Client1.println(F("closing connection"));
        Client1.stop();
        tcpMode =  false;
        if (enableSerialEcho) 
            Serial.println(F("D TCP connection closed after Q command"));
        return;
    } 
#endif
    Serial.println(F("D TCP not connected"));
}


void CmdRestart() {
#if defined(ESP8266) || defined(ESP32)
    ESP.restart();
#endif
    // beim uno / nano ohne wlan einfach nur die Variablen zurücksetzen.
    initialize();
}


void CmdWifiReset() {
#if defined(WifiSupport)
    wifiManager.resetSettings();
#endif
}


/* theoretical issue: when connected via Wifi and serial at the same time, input might overlap */
void handleInput(char c) {
    if (c == ',') {                       // Komma input, last value is finished
        if (commandDataPointer < (MAX_INPUT_NUM - 1)) {
            commandData[commandDataPointer++] = commandValue;
            commandValue = 0;
        }
    }
    else if ('0' <= c && c <= '9') {      // digit input
        commandValue = 10 * commandValue + c - '0';
        commandDataSize = commandDataPointer + 1;
    }
    else if ('a' <= c && c <= 'z') {      // letter input is command
        commandLetter = c;
        if (commandDataPointer < (MAX_INPUT_NUM - 1)) {
            commandData[commandDataPointer] = commandValue;    
        }
    
        if (enableSerialEcho > 1) {
            Serial.print(F("D got "));
            for (short v = 0; v <= commandDataPointer; v++) {          
                if (v > 0) Serial.print(F(","));
                Serial.print(commandData[v]);
            }
            Serial.print(c);
            Serial.print(F(" size ")); Serial.println(commandDataSize);
        }

        switch (c) {
        case 'a':                       // add a pin
            CmdAdd(); break;
        case 'c':
            CmdClear(); break;          // clear a counter for pin specifid
        case 'd':                       // delete a pin
            CmdRemove(); break;
        case 'e':                       // save to EEPROM
            CmdSaveToEEPROM(); break; 

        case 'h':                       // hello
            CmdHello(); break;
        case 'i':                       // interval
            CmdInterval(); break;
        case 'k':                       // keep alive
            CmdKeepAlive(); break;   
        case 'l'    :                   // led feedback
            CmdLED(); break;   

        case 'q':                       // quit
            CmdQuit(); break; 
        case 'r':                       // reset / restart
            CmdRestart(); break;
        case 's':                       // show
            CmdShow(); break;
        case 't':                       // thresholds for analog pins (legacy - moved to a)
            CmdThreshold(); break;
        case 'u':                       // pulses per unit for local output
            CmdUnits(); break;
        case 'v':                       // dev verbose
            CmdDevVerbose(); break;
        case 'w':                       // reset wifi settings
            CmdWifiReset(); break;

        default:
            break;
        }
        clearInput();
        //Serial.println(F("D End of command"));
    }
}


void debugDigitalPinChanges() {
    for (uint8_t pinIndex=0; pinIndex < maxPinIndex; pinIndex++) {
        pinData_t *pd = &pinData[pinIndex];        
        if (!pd->analogFlag) {
            uint8_t pinState = digitalRead(pin2GPIO(pd->pinName));
            if (pinState != pd->lastDebugLevel)
                printPinChangeDebug(pd, pinState);
        }   
    }
}


#if defined(WifiSupport)   
void printWifiStatus() {
    Serial.print(F("D Wifi Status is "));
    switch (WiFi.status()) {
        case WL_CONNECT_FAILED: 
        Serial.println(F("Connect Failed")); break;
        case WL_CONNECTION_LOST: 
        Serial.println(F("Connection Lost")); break;
        case WL_DISCONNECTED: 
        Serial.println(F("Disconnected")); break;
        case WL_CONNECTED: 
        Serial.println(F("Connected")); break;
        default:
        Serial.println(WiFi.status());
    }
}


#if !defined(STATIC_WIFI)
void configModeCallback (WiFiManager *myWiFiManager) {
    Serial.println("Entered config mode");
    Serial.println(WiFi.softAPIP());
    //if you used auto generated SSID, print it
    Serial.println(myWiFiManager->getConfigPortalSSID());
#if defined(TFT_DISPLAY)
    tft.fillScreen(TFT_BLUE);
    tft.setTextFont(4);
    tft.setTextSize(1);
    tft.setTextColor(TFT_YELLOW , TFT_BLUE);
    tft.setCursor(0,32);    // x, y,   TFT_HEIGHT=240, TFT_WIDTH=135

    tft.print(F("Entered config mode      "));
    tft.setCursor(0, 100);
    tft.print(myWiFiManager->getConfigPortalSSID());
#endif
}
#endif


void initWifi() {
    TCPconnected = false;
    WiFi.mode(WIFI_STA);
    WiFi.setAutoConnect(true);
    WiFi.setAutoReconnect(true);
    delay (1000);    
    if (WiFi.status() != WL_CONNECTED) {
#if defined(STATIC_WIFI)
#if defined(TFT_DISPLAY) 
        tft.setTextFont(2);
        tft.setTextSize(1);
        tft.setTextColor(TFT_GREEN, TFT_BLACK);
        tft.setCursor(0,0);
        tft.print(F("Conecting WiFi to ")); tft.print(ssid);
#endif
        uint8_t counter = 0;
        Serial.print(F("D Connecting WiFi to ")); Serial.println(ssid);
        WiFi.begin(ssid, password);                 // connect with compiled strings
        while (WiFi.status() != WL_CONNECTED) {
            printWifiStatus();
            delay(1000);
            counter++;
            if (counter > 2) {
#if defined(TFT_DISPLAY)    
                tft.setCursor(0,0);
                tft.print(F("Retry conecting WiFi to")); tft.print(ssid);
#endif
                Serial.println(F("D Retry connecting WiFi"));
                WiFi.begin(ssid, password);         // restart connecting
                delay (1000);
                counter = 0;                        // do forever until connected with retries
            }
        }    
#else                                               // connect using WifiManager if auto reconnect not successful
#if defined(TFT_DISPLAY) 
        tft.setCursor(0,0);
        tft.print(F("try reconecting WiFi"));
#endif
        Serial.println(F("D Try reconnecting WiFi"));
        WiFi.begin();             
        delay(1000);
        printWifiStatus();
        if (WiFi.status() != WL_CONNECTED) {
#if defined(TFT_DISPLAY)    
            tft.setCursor(0,0);
            tft.print(F("Retry reconecting WiFi"));
#endif
            Serial.println(F("D Retry reconnecting WiFi"));
            WiFi.begin();         
            delay (1000);
        }            
        wifiManager.setConfigPortalBlocking(false);
        wifiManager.setAPCallback(configModeCallback);  //set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode
        wifiManager.autoConnect();
#endif
    }
}


void handleConnections() { 
    IPAddress remote;   
    uint32_t now = millis();
    
    if (WiFi.status() == WL_CONNECTED && !serverStarted) {      // first call after connected
        printWifiState (&Serial);
        Server.begin();                                         // Start the TCP server
        Serial.println(F("D TCP Server started"));
        serverStarted = true;                                   // remember we did this already
    } 
    if (WiFi.status() != WL_CONNECTED) {                        // WiFi lost
        if (serverStarted) {                                    // first time we notice ...
            printWifiState (&Serial);                          // show that we lost Wifi
            Server.close();                                     // Stop  the TCP server
            serverStarted = false;                              // so printWifiState will be called after reconnect
            Serial.println(F("D Wifi lost - TCP Server stopped"));
            lastReconnectTry = now;                             // don't try to manually reconnect right now - Framework should do it
        }
        if ((now - lastReconnectTry) > 5000) {
            printWifiState (&Serial);                          // show that we lost Wifi
            Serial.println(F("D Try reconnecting WiFi"));
            WiFi.begin();                                       // try to force reconnect (framework seems to not do it sometimes...)
            lastReconnectTry = now;
            reconnects++;
        }
    }

    if (Client1.connected()) {
        if (Client1.available()) {
            handleInput(Client1.read());                        // input from TCP
            //Serial.println(F("D new Input over TCP"));
        }
        now = millis();                                         // get millis again to avoid keepalive timeout directly after first k command
        if((now - lastKeepAlive) > (keepAliveTimeout*3000)) {   // check keepAlive timout (* 3 secs)
            Serial.println(F("D no keepalive - close"));
            Output->println(F("D no keepalive - close"));   
            //Output->print(F("D timeout was "));
            //Output->print(keepAliveTimeout);
            //Output->print(F(" last at "));
            //Output->print(lastKeepAlive);
            //Output->print(F(" now "));
            //Output->print(now);
            Output->println();
            Client1.stop();                                     // close connection due to keepalive timeout 
        }
        Client2 = Server.available();                           // refuse further connect attempts
        if (Client2) {
            remote = Client2.remoteIP();
            Client2.println(F("conn busy"));
            Client2.stop();
            if (enableSerialEcho) {
                Serial.print(F("D 2nd conn from ")); Serial.print(remote);
                Serial.println(F(" rejected"));
            }
        }
    } else {    // no client connected right now
        if (TCPconnected) {                                 // client used to be connected, now disconnected
            TCPconnected = false;
            Output = &Serial;
            if (enableSerialEcho)
                Serial.println(F("D conn lost"));           // report disconnect via serial
        }
        Client1 = Server.available();
        if (Client1) {                                      // accepting new connection
            remote = Client1.remoteIP();
            if (enableSerialEcho) {
                Serial.print(F("D new conn from ")); Serial.print(remote);
                Serial.println(F(" accepted"));
            }
            TCPconnected = true;                            // remember connection in case we loose it
            Output = &Client1;
            lastKeepAlive = now;
            CmdHello();                                     // say hello to client
        }
    }
} 
#endif


void handleTime() {
    uint32_t now = millis();
    if (now < lastMillis) millisWraps++;
    lastMillis = now;

    if (enableDevTime) 
        if ((long int)now - lastTimeMillis > (int32_t)60 * 60 * 1000) {     // every 60 minutes
            printTime(now);
            lastTimeMillis = now;
        }
}


void detectTrigger(analogData_t *ad, unsigned int val) {
    uint32_t average;
    uint8_t nextState = ad->triggerState;           // initialize next trigger level to be the same as the old one 
    if (val > ad->thresholdMax) {
        nextState = 1;                              // if above upper threshold then change to 1
    } else if (val < ad->thresholdMin) {
        nextState = 0;                              // if below lower threshold then change to 0
    }
    if (ad->avgCnt <= 65000) {
        ad->avgSum += val;
        ad->avgCnt++;
    }
    if (nextState != ad->triggerState) {            // if level has changed
        average = ad->avgSum / ad->avgCnt;
        if (average > 4000) average = 4000;
        doCount (ad->inPinData, nextState, (uint16_t) average);
        ad->avgSum = 0;
        ad->avgCnt = 0;
        if (enablePinDebug)
            printPinChangeDebug(ad->inPinData, nextState);
    }
    ad->triggerState = nextState;                   // save new level
}


void readAnalog() {
    uint32_t now = millis();
    char line[26];
    uint16_t waitForOff = 0; 
    uint16_t waitForOn = 1;

    //if (analogCallLast && (now - analogCallLast) > 5) {
    //    Output->print(F("D readAnalog call delay "));
    //    Output->println(now - analogCallLast);
    //}
    analogCallLast = now;

    if ((now - analogReadLast) > analogReadInterval) {      // time for next analog read?
        switch (analogReadState) {
            case 0:                                                 // initial state
                for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {                    
                    analogData_t *ad = &analogData[aIdx];
                    digitalWrite(pin2GPIO(ad->outPinName) , LOW);   // make sure IR LED is off for first read
                    analogReadCount = 0;                            // initialize sums and counter
                    ad->sumOff = 0; ad->sumOn = 0;
                }
                analogReadState = 1;
                analogReadWait = millis();
                break;
            case 1:                                                 // wait before measuring
                if ((now - analogReadWait) < waitForOff)            // todo: wait in microseconds with micros() function, make witForOff configurable
                    return;
                analogReadState = 2;
                break;
            case 2:
                for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
                    analogData_t *ad = &analogData[aIdx];
                    uint16_t sample = analogRead(pin2GPIO(ad->inPinName));   // read the analog in value (off)
                    ad->sumOff += sample;

                    if (enableAnalogDebug > 2) {
                        Output->print(F("M "));
                        Output->print(millis());
                        Output->print(F(", 0, "));
                        Output->print(sample);
                        Output->print(F(", "));
                        Output->print(analogReadCount);
                        Output->println();
                    }
                }
                if (++analogReadCount < analogReadSamples)
                    break;
                for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
                    analogData_t *ad = &analogData[aIdx];
                    digitalWrite(pin2GPIO(ad->outPinName), HIGH);      // turn IR LED on
                }
                analogReadCount = 0;
                analogReadState = 4;
                analogReadWait = millis();
                break;
            case 4:                                                 // wait again before measuring
                if ((now - analogReadWait) < waitForOn)
                    return;
                analogReadState = 5;
                break;
            case 5:
                int sensorDiff;
                for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
                    analogData_t *ad = &analogData[aIdx];
                    uint16_t sample = analogRead(pin2GPIO(ad->inPinName));   // read the analog in value (on)
                    ad->sumOn += sample;
                    if (enableAnalogDebug > 2) {
                        Output->print(F("M "));
                        Output->print(millis());
                        Output->print(F(", 1, "));
                        Output->print(sample);
                        Output->print(F(", "));
                        Output->print(analogReadCount);
                        Output->println();
                    }

                }
                if (++analogReadCount < analogReadSamples) 
                    break;
                for (uint8_t aIdx = 0; aIdx < maxAnalogIndex; aIdx++) {
                    analogData_t *ad = &analogData[aIdx];
                    digitalWrite(pin2GPIO(ad->outPinName) , LOW);       // turn IR LED off again
                
                    sensorDiff = (ad->sumOn / analogReadSamples) - (ad->sumOff / analogReadSamples);
                    if (sensorDiff < 0) sensorDiff = 0;
                    if (sensorDiff > 4096) sensorDiff = 4096;
                    detectTrigger (ad, sensorDiff);                    // calculate level with triggers
                    if (enableAnalogDebug > 1) {
                        sprintf(line, "L%2d: %4d, %4d -> % 4d", ad->inPinName, 
                            ad->sumOn / analogReadSamples, ad->sumOff / analogReadSamples, sensorDiff);
                        Output->println(line);
                    } else if (enableAnalogDebug) {
                        sprintf(line, "L%2d: % 4d", ad->inPinName, sensorDiff);
                        Output->println(line);
                    }                    
#if defined(TFT_DISPLAY)
                    if (displayMode == 1) {                    
                        int len = sensorDiff * analogReadAmp * TFT_HEIGHT / 4096;
                        tft.fillRect(0,TFT_WIDTH-10-(10*aIdx),len,10, TFT_YELLOW);
                        tft.fillRect(len,TFT_WIDTH-10-(10*aIdx),TFT_HEIGHT-len,10, TFT_BLACK);
                    }
#endif
                }
                analogReadState = 0;
                analogReadLast = now;
                break;
            default:
                analogReadState = 0;
                Output->println(F("Error: wrong analog read state"));
                break;
        }
    }
}

#if defined (TFT_DISPLAY)
/*
void ButtonHandlerChanged(Button2& btn) {
    Serial.println("changed");
}
*/


void ButtonHandlerTap(Button2& btn) {
    displayMode++;
    if (displayMode > displayModeMax) displayMode = 0;
    tft.setTextFont(2);
    tft.setTextSize(1);
    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.fillRect(0, 32, TFT_HEIGHT, TFT_WIDTH-32, TFT_BLACK);       // unteren Teil löschen
    //tft.fillScreen(TFT_BLUE);
    // test             240 (lange Seite) 135 (kurze)
    //tft.fillRect(0, 32, TFT_HEIGHT-1, TFT_WIDTH-32-1, TFT_BLACK);  // lässt einen 1-Pixel blauen Rand
    tft.setCursor(TFT_HEIGHT-20, TFT_WIDTH-25);    // x, y,   TFT_HEIGHT=240, TFT_WIDTH=135
    tft.print(displayMode);
    /*
    Serial.print(F("D displayMode "));
    Serial.println(displayMode);
    */
    if (displayMode == 2) {
        tft.setCursor(0, 32);
        tft.setTextFont(4);
        tft.setTextSize(1);
        tft.setTextColor(TFT_RED, TFT_BLACK);
        tft.print(F("consumption 5s"));
    } else if (displayMode == 3) {
        tft.setCursor(0, 32);
        tft.setTextFont(4);
        tft.setTextSize(1);
        tft.setTextColor(TFT_RED, TFT_BLACK);
        tft.print(F("consumption dyn"));
    }
}
#endif


void setup() {
#if defined (TFT_DISPLAY)
    tft.init();
    tft.fillScreen(TFT_BLACK);
    tft.setRotation(1);             // 1=Querformat (0 wäre Hochformat, TFT_Width / TFT_Height beziehen sich offenbar auf Hochformat)
    tft.setTextFont(2);
    tft.setTextSize(1);
    tft.setTextColor(TFT_GREEN, TFT_BLACK);

    //buttonA.setChangedHandler(ButtonHandlerChanged);
    //buttonA.setPressedHandler(pressed);
    //buttonA.setReleasedHandler(released);

    // captures any type of click, longpress or shortpress
    buttonA.setTapHandler(ButtonHandlerTap);
    //buttonA.setClickHandler(click);
    //buttonA.setLongClickHandler(longClick);
    //buttonA.setDoubleClickHandler(doubleClick);
    //buttonA.setTripleClickHandler(tripleClick);

#endif    

    Serial.begin(SERIAL_SPEED);             // initialize serial
#if defined(ESP8266) || defined (ESP32)
    EEPROM.begin(100);
#endif    
    delay (500);
    interrupts();    
    Serial.println();        
    Output = &Serial;    
    millisWraps = 0;
    lastMillis = millis();
    initialize();     
    CmdHello();                             // started message to serial
#if defined(WifiSupport)
    initWifi();

    ArduinoOTA
    .onStart([]() {
        String type;
        if (ArduinoOTA.getCommand() == U_FLASH)
            type = "sketch";
        else // U_SPIFFS
            type = "filesystem";
        // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
        Serial.println("Start updating " + type);
    });
    ArduinoOTA.onEnd([]() {
        Serial.println("\nEnd");
    });
    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
        Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    });
    ArduinoOTA.onError([](ota_error_t error) {
        Serial.printf("Error[%u]: ", error);
        if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
        else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
        else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
        else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
        else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
    ArduinoOTA.begin();
#endif
}


/*   Main Loop  */
void loop() {
#if defined(WifiSupport)
#if !defined(STATIC_WIFI)
    wifiManager.process();                          // process config portal
#endif
    ArduinoOTA.handle();
    handleConnections();                            // new TCP connection or input over TCP
#endif
#if defined (TFT_DISPLAY)
    buttonA.loop();                                 // check for button events
#endif
    handleTime();                                   // check if millis() wrapped (for reporting)
    if (Serial.available()) handleInput(Serial.read()); // input over serial 
    readAnalog();                                   // analog measurements
    if (enablePinDebug) debugDigitalPinChanges();
    report();                                       // report counts if due
}
