From a197c4f2390ab98fc3abcb3247922e16c6f8bba0 Mon Sep 17 00:00:00 2001 From: StefanStrobel Date: Wed, 10 Jun 2020 16:48:23 +0000 Subject: [PATCH] 98_Arducounter.pm: added firmware source and platformio.ini sample git-svn-id: https://svn.fhem.de/fhem/trunk@22152 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- .../ArduCounter-platformio-sample.ini | 108 + fhem/FHEM/firmware/ArduCounter.cpp | 2219 +++++++++++++++++ 2 files changed, 2327 insertions(+) create mode 100755 fhem/FHEM/firmware/ArduCounter-platformio-sample.ini create mode 100755 fhem/FHEM/firmware/ArduCounter.cpp diff --git a/fhem/FHEM/firmware/ArduCounter-platformio-sample.ini b/fhem/FHEM/firmware/ArduCounter-platformio-sample.ini new file mode 100755 index 000000000..2beaf6cc7 --- /dev/null +++ b/fhem/FHEM/firmware/ArduCounter-platformio-sample.ini @@ -0,0 +1,108 @@ +;PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +;default_envs = esp32dev +;default_envs = esp32dev_static_wifi +;default_envs = esp32_TFT_eSPI +;default_envs = Wasserzaehler +;default_envs = d1_mini_pro +;default_envs = nanoatmega328new +default_envs = esp32dev, esp32_TFT_eSPI, d1_mini_pro, nanoatmega328new + +[env] +;monitor_speed = 38400 +monitor_speed = 115200 +framework = arduino + +[env:nanoatmega328new] +platform = atmelavr +board = nanoatmega328new + +[env:d1_mini_pro] +platform = espressif8266 +board = d1_mini_pro +lib_deps = https://github.com/tzapu/WiFiManager.git#development + +[env:esp32dev] +platform = espressif32 +board = esp32dev +upload_protocol = esptool +lib_deps = https://github.com/tzapu/WiFiManager.git#development + +[env:esp32_TFT_eSPI] +platform = espressif32 +board = esp32dev +upload_protocol = esptool +build_flags = + -DTFT_DISPLAY + -DUSER_SETUP_LOADED + -DST7789_DRIVER + -DTFT_WIDTH=135 + -DTFT_HEIGHT=240 + -DCGRAM_OFFSET + -DTFT_MOSI=19 + -DTFT_SCLK=18 + -DTFT_CS=5 + -DTFT_DC=16 + -DTFT_RST=23 + -DTFT_BL=4 + -DTFT_BACKLIGHT_ON=HIGH + -DLOAD_GLCD + -DLOAD_FONT2 + -DLOAD_FONT4 + -DLOAD_FONT6 + -DLOAD_FONT7 + -DLOAD_FONT8 + -DLOAD_GFXFF + -DSMOOTH_FONT + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=6000000 +lib_ldf_mode = chain+ +lib_deps = + TFT_eSPI + Button2 + https://github.com/tzapu/WiFiManager.git#development + + +[env:MeinWasserzaehlerOTA] +platform = espressif32 +board = esp32dev +upload_protocol = espota +upload_port = 192.168.1.111 +build_flags = + -DTFT_DISPLAY + -DUSER_SETUP_LOADED + -DST7789_DRIVER + -DTFT_WIDTH=135 + -DTFT_HEIGHT=240 + -DCGRAM_OFFSET + -DTFT_MOSI=19 + -DTFT_SCLK=18 + -DTFT_CS=5 + -DTFT_DC=16 + -DTFT_RST=23 + -DTFT_BL=4 + -DTFT_BACKLIGHT_ON=HIGH + -DLOAD_GLCD + -DLOAD_FONT2 + -DLOAD_FONT4 + -DLOAD_FONT6 + -DLOAD_FONT7 + -DLOAD_FONT8 + -DLOAD_GFXFF + -DSMOOTH_FONT + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=6000000 +lib_ldf_mode = chain+ +lib_deps = + TFT_eSPI + Button2 + https://github.com/tzapu/WiFiManager.git#development diff --git a/fhem/FHEM/firmware/ArduCounter.cpp b/fhem/FHEM/firmware/ArduCounter.cpp new file mode 100755 index 000000000..f9b5c394d --- /dev/null +++ b/fhem/FHEM/firmware/ArduCounter.cpp @@ -0,0 +1,2219 @@ +/* + * 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 + +#if defined(TFT_DISPLAY) +#include +#include +#include +#include +#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 + +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 +#elif defined(ESP32) +#include +#endif +#include +#include + +#if defined(STATIC_WIFI) +#include "ArduCounterTestConfig.h" +#else +#include +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 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 +}