/* * 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 }