diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 410ecc1d1..121e9e428 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -62,6 +62,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT - Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset`` - Fix escape of non-JSON received serial data (#8329) +- Fix exception or watchdog on rule re-entry (#8757) - Add command ``Rule0`` to change global rule parameters - Add command ``Time 4`` to display timestamp using milliseconds (#8537) - Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616) @@ -88,3 +89,4 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add Library to be used for decoding Teleinfo (French Metering Smart Meter) - Add basic support for ESP32 ethernet adding commands ``Wifi 0/1`` and ``Ethernet 0/1`` both default ON - Add support for single wire LMT01 temperature Sensor by justifiably (#8713) +- Add compile time interlock parameters (#8759) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 0311c6280..902675fe4 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -36,10 +36,10 @@ default_envs = [common] -platform = ${core_active.platform} -platform_packages = ${core_active.platform_packages} -build_unflags = ${core_active.build_unflags} -build_flags = ${core_active.build_flags} +platform = ${core.platform} +platform_packages = ${core.platform_packages} +build_unflags = ${core.build_unflags} +build_flags = ${core.build_flags} ; *** Optional Debug messages ; -DDEBUG_TASMOTA_CORE ; -DDEBUG_TASMOTA_DRIVER @@ -71,7 +71,7 @@ extra_scripts = ${scripts_defaults.extra_scripts} ; pio/strip-floats.py ; pio/http-uploader.py -[core_active] +[core] ; Activate only (one set) if you want to override the standard core defined in platformio.ini !!! ;platform = ${tasmota_stage.platform} @@ -87,7 +87,7 @@ extra_scripts = ${scripts_defaults.extra_scripts} [tasmota_stage] ; *** Esp8266 core for Arduino version Tasmota stage -extends = core +platform = espressif8266@2.5.3 platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#52b3e5b7b3ccedcede665682f7896b637b64dbf5 build_unflags = ${esp_defaults.build_unflags} build_flags = ${esp82xx_defaults.build_flags} @@ -123,7 +123,7 @@ build_flags = ${esp82xx_defaults.build_flags} [core_stage] ; *** Esp8266 core for Arduino version latest development version -extends = core +platform = espressif8266@2.5.3 platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git build_unflags = ${esp_defaults.build_unflags} build_flags = ${esp82xx_defaults.build_flags} @@ -160,7 +160,6 @@ build_flags = ${esp82xx_defaults.build_flags} ; *** Debug version used for PlatformIO Home Project Inspection [env:tasmota-debug] -extends = core build_type = debug build_unflags = ${esp_defaults.build_unflags} build_flags = ${esp82xx_defaults.build_flags} diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 3ba3a1d38..2a2a822f7 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -6,6 +6,8 @@ - Add support for Energy sensor (Denky) for French Smart Metering meter provided by global Energy Providers, need a adaptater. See dedicated full [blog](http://hallard.me/category/tinfo/) about French teleinformation stuff - Add library to be used for decoding Teleinfo (French Metering Smart Meter) - Add support for single wire LMT01 temperature Sensor by justifiably (#8713) +- Add compile time interlock parameters (#8759) +- Fix exception or watchdog on rule re-entry (#8757) - Change ESP32 USER GPIO template representation decreasing template message size - Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT - Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset`` diff --git a/tasmota/core_esp8266_waveform.cpp b/tasmota/core_esp8266_waveform.cpp index 1ff0a3687..371e9e554 100644 --- a/tasmota/core_esp8266_waveform.cpp +++ b/tasmota/core_esp8266_waveform.cpp @@ -3,25 +3,26 @@ supporting outputs on all pins in parallel. Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + Copyright (c) 2020 Dirk O. Kaar. The core idea is to have a programmable waveform generator with a unique - high and low period (defined in microseconds or CPU clock cycles). TIMER1 - is set to 1-shot mode and is always loaded with the time until the next - edge of any live waveforms. + high and low period (defined in microseconds or CPU clock cycles). TIMER1 is + set to 1-shot mode and is always loaded with the time until the next edge + of any live waveforms. Up to one waveform generator per pin supported. - Each waveform generator is synchronized to the ESP clock cycle counter, not - the timer. This allows for removing interrupt jitter and delay as the - counter always increments once per 80MHz clock. Changes to a waveform are + Each waveform generator is synchronized to the ESP clock cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are contiguous and only take effect on the next waveform transition, allowing for smooth transitions. This replaces older tone(), analogWrite(), and the Servo classes. - Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() - clock cycle count, or an interval measured in CPU clock cycles, but not - TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). + Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() + clock cycle time, or an interval measured in clock cycles, but not TIMER1 + cycles (which may be 2 CPU clock cycles @ 160MHz). This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -40,576 +41,400 @@ #ifdef ESP8266 +#include "core_esp8266_waveform.h" #include #include "ets_sys.h" -#include "core_esp8266_waveform.h" -#include "user_interface.h" -extern "C" { +#include -// Internal-only calls, not for applications -extern void _setPWMFreq(uint32_t freq); -extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t val, uint32_t range); -extern int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles); +// Timer is 80MHz fixed. 160MHz CPU frequency need scaling. +constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160; +// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz +constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000); +// Maximum servicing time for any single IRQ +constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18); +// The latency between in-ISR rearming of the timer and the earliest firing +constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2); +// The SDK and hardware take some time to actually get to our NMI code +constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? + microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2); -// Maximum delay between IRQs -#define MAXIRQUS (10000) +// for INFINITE, the NMI proceeds on the waveform without expiry deadline. +// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. +// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. +// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. +enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, INIT = 3}; // Waveform generator can create tones, PWM, and servos typedef struct { - uint32_t nextServiceCycle; // ESP cycle timer when a transition required - uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop - uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) - uint32_t timeLowCycles; // - uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal - uint32_t desiredLowCycles; // - uint32_t lastEdge; // Cycle when this generator last changed + uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. If WaveformMode::INIT, temporarily holds positive phase offset ccy count + uint32_t endDutyCcy; // ESP clock cycle when going from duty to off + int32_t dutyCcys; // Set next off cycle at low->high to maintain phase + int32_t adjDutyCcys; // Temporary correction for next period + int32_t periodCcys; // Set next phase cycle at low->high to maintain phase + uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count + WaveformMode mode; + int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin + bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings } Waveform; -class WVFState { -public: - Waveform waveform[17]; // State of all possible pins - uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code - uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code +namespace { - // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine - uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin - uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation + static struct { + Waveform pins[17]; // State of all possible pins + uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code - uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI - uint32_t waveformNewHigh = 0; - uint32_t waveformNewLow = 0; + // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine + int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform + int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation - uint32_t (*timer1CB)() = NULL; + uint32_t(*timer1CB)() = nullptr; - // Optimize the NMI inner loop by keeping track of the min and max GPIO that we - // are generating. In the common case (1 PWM) these may be the same pin and - // we can avoid looking at the other pins. - uint16_t startPin = 0; - uint16_t endPin = 0; -}; -static WVFState wvfState; + bool timer1Running = false; + uint32_t nextEventCcy; + } waveform; -// Ensure everything is read/written to RAM -#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } +} + +// Interrupt on/off control +static ICACHE_RAM_ATTR void timer1Interrupt(); // Non-speed critical bits #pragma GCC optimize ("Os") -// Interrupt on/off control -static ICACHE_RAM_ATTR void timer1Interrupt(); -static bool timerRunning = false; - -static __attribute__((noinline)) void initTimer() { - if (!timerRunning) { - timer1_disable(); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); - timerRunning = true; - timer1_write(microsecondsToClockCycles(10)); - } +static void initTimer() { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + waveform.timer1Running = true; + timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste } -static ICACHE_RAM_ATTR void forceTimerInterrupt() { - if (T1L > microsecondsToClockCycles(10)) { - T1L = microsecondsToClockCycles(10); - } +static void ICACHE_RAM_ATTR deinitTimer() { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + waveform.timer1Running = false; } -// PWM implementation using special purpose state machine -// -// Keep an ordered list of pins with the delta in cycles between each -// element, with a terminal entry making up the remainder of the PWM -// period. With this method sum(all deltas) == PWM period clock cycles. -// -// At t=0 set all pins high and set the timeout for the 1st edge. -// On interrupt, if we're at the last element reset to t=0 state -// Otherwise, clear that pin down and set delay for next element -// and so forth. +extern "C" { -constexpr int maxPWMs = 8; - -// PWM machine state -typedef struct PWMState { - uint32_t mask; // Bitmask of active pins - uint32_t cnt; // How many entries - uint32_t idx; // Where the state machine is along the list - uint8_t pin[maxPWMs + 1]; - uint32_t delta[maxPWMs + 1]; - uint32_t nextServiceCycle; // Clock cycle for next step - struct PWMState *pwmUpdate; // Set by main code, cleared by ISR -} PWMState; - -static PWMState pwmState; -static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / 1000; - - -// If there are no more scheduled activities, shut down Timer 1. -// Otherwise, do nothing. -static ICACHE_RAM_ATTR void disableIdleTimer() { - if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - timer1_disable(); - timer1_isr_init(); - timerRunning = false; - } -} - -// Notify the NMI that a new PWM state is available through the mailbox. -// Wait for mailbox to be emptied (either busy or delay() as needed) -static ICACHE_RAM_ATTR void _notifyPWM(PWMState *p, bool idle) { - p->pwmUpdate = nullptr; - pwmState.pwmUpdate = p; - MEMBARRIER(); - forceTimerInterrupt(); - while (pwmState.pwmUpdate) { - if (idle) { - delay(0); - } - MEMBARRIER(); - } -} - -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); - -// Called when analogWriteFreq() changed to update the PWM total period -void _setPWMFreq(uint32_t freq) { - // Convert frequency into clock cycles - uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; - - // Simple static adjustment to bring period closer to requested due to overhead -#if F_CPU == 80000000 - cc -= microsecondsToClockCycles(2); -#else - cc -= microsecondsToClockCycles(1); -#endif - - if (cc == _pwmPeriod) { - return; // No change - } - - _pwmPeriod = cc; - - if (pwmState.cnt) { - PWMState p; // The working copy since we can't edit the one in use - p.cnt = 0; - for (uint32_t i = 0; i < pwmState.cnt; i++) { - auto pin = pwmState.pin[i]; - _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); - } - // Update and wait for mailbox to be emptied +// Set a callback. Pass in NULL to stop it +void setTimer1Callback(uint32_t (*fn)()) { + waveform.timer1CB = fn; + std::atomic_thread_fence(std::memory_order_acq_rel); + if (!waveform.timer1Running && fn) { initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); + } else if (waveform.timer1Running && !fn && !waveform.enabled) { + deinitTimer(); } } -// Helper routine to remove an entry from the state machine -// and clean up any marked-off entries -static void _cleanAndRemovePWM(PWMState *p, int pin) { - uint32_t leftover = 0; - uint32_t in, out; - for (in = 0, out = 0; in < p->cnt; in++) { - if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { - p->pin[out] = p->pin[in]; - p->delta[out] = p->delta[in] + leftover; - leftover = 0; - out++; - } else { - leftover += p->delta[in]; - p->mask &= ~(1<pin[in]); - } - } - p->cnt = out; - // Final pin is never used: p->pin[out] = 0xff; - p->delta[out] = p->delta[in] + leftover; -} - - -// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) -ICACHE_RAM_ATTR bool _stopPWM(int pin) { - if (!((1<= _pwmPeriod) { - _stopPWM(pin); - digitalWrite(pin, HIGH); - return; - } - - if (p.cnt == 0) { - // Starting up from scratch, special case 1st element and PWM period - p.pin[0] = pin; - p.delta[0] = cc; - // Final pin is never used: p.pin[1] = 0xff; - p.delta[1] = _pwmPeriod - cc; - } else { - uint32_t ttl = 0; - uint32_t i; - // Skip along until we're at the spot to insert - for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { - ttl += p.delta[i]; - } - // Shift everything out by one to make space for new edge - for (int32_t j = p.cnt; j >= (int)i; j--) { - p.pin[j + 1] = p.pin[j]; - p.delta[j + 1] = p.delta[j]; - } - int off = cc - ttl; // The delta from the last edge to the one we're inserting - p.pin[i] = pin; - p.delta[i] = off; // Add the delta to this new pin - p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant - } - p.cnt++; - p.mask |= 1<= maxPWMs) { - return false; // No space left - } - - _addPWMtoList(p, pin, val, range); - - // Set mailbox and wait for ISR to copy it over - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - return true; +int startWaveform(uint8_t pin, uint32_t highUS, uint32_t lowUS, + uint32_t runTimeUS, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles(pin, + microsecondsToClockCycles(highUS), microsecondsToClockCycles(lowUS), + microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); } // Start up a waveform on a pin, or change the current one. Will change to the new // waveform smoothly on next low->high transition. For immediate change, stopWaveform() // first, then it will immediately begin. -int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS) { - return startWaveformClockCycles(pin, microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), microsecondsToClockCycles(runTimeUS)); -} - -int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles) { - if ((pin > 16) || isFlashInterfacePin(pin)) { +int startWaveformClockCycles(uint8_t pin, uint32_t highCcys, uint32_t lowCcys, + uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) { + uint32_t periodCcys = highCcys + lowCcys; + if (periodCcys < MAXIRQTICKSCCYS) { + if (!highCcys) { + periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + else if (!lowCcys) { + highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + } + // sanity checks, including mixed signed/unsigned arithmetic safety + if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) || + static_cast(periodCcys) <= 0 || + static_cast(highCcys) < 0 || static_cast(lowCcys) < 0) { return false; } - Waveform *wave = &wvfState.waveform[pin]; - wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; - if (runTimeCycles && !wave->expiryCycle) { - wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it - } + Waveform& wave = waveform.pins[pin]; + wave.dutyCcys = highCcys; + wave.adjDutyCcys = 0; + wave.periodCcys = periodCcys; + wave.autoPwm = autoPwm; - _stopPWM(pin); // Make sure there's no PWM live here - - uint32_t mask = 1<timeHighCycles = timeHighCycles; - wave->desiredHighCycles = timeHighCycles; - wave->timeLowCycles = timeLowCycles; - wave->desiredLowCycles = timeLowCycles; - wave->lastEdge = 0; - wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); - wvfState.waveformToEnable |= mask; - MEMBARRIER(); - initTimer(); - forceTimerInterrupt(); - while (wvfState.waveformToEnable) { - delay(0); // Wait for waveform to update - // No mem barrier here, the call to a global function implies global state updated + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + if (!waveform.timer1Running) { + initTimer(); + } + else if (T1V > IRQLATENCYCCYS) { + // Must not interfere if Timer is due shortly + timer1_write(IRQLATENCYCCYS); } } - + else { + wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI + std::atomic_thread_fence(std::memory_order_release); + wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count + if (runTimeCcys) { + wave.mode = WaveformMode::UPDATEEXPIRY; + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } + } + std::atomic_thread_fence(std::memory_order_acq_rel); + while (waveform.toSetBits) { + delay(0); // Wait for waveform to update + std::atomic_thread_fence(std::memory_order_acquire); + } return true; } - -// Set a callback. Pass in NULL to stop it -void setTimer1Callback(uint32_t (*fn)()) { - wvfState.timer1CB = fn; - if (fn) { - initTimer(); - forceTimerInterrupt(); - } - disableIdleTimer(); -} - - -// Speed critical bits -#pragma GCC optimize ("O2") - -// Normally would not want two copies like this, but due to different -// optimization levels the inline attribute gets lost if we try the -// other version. -static inline ICACHE_RAM_ATTR uint32_t GetCycleCountIRQ() { - uint32_t ccount; - __asm__ __volatile__("rsr %0,ccount":"=a"(ccount)); - return ccount; -} - -static inline ICACHE_RAM_ATTR uint32_t min_u32(uint32_t a, uint32_t b) { - if (a < b) { - return a; - } - return b; -} - // Stops a waveform on a pin int ICACHE_RAM_ATTR stopWaveform(uint8_t pin) { // Can't possibly need to stop anything if there is no timer active - if (!timerRunning) { + if (!waveform.timer1Running) { return false; } // If user sends in a pin >16 but <32, this will always point to a 0 bit // If they send >=32, then the shift will result in 0 and it will also return false - uint32_t mask = 1< IRQLATENCYCCYS) { + timer1_write(IRQLATENCYCCYS); } - forceTimerInterrupt(); - while (wvfState.waveformToDisable) { - MEMBARRIER(); // If it wasn't written yet, it has to be by now + while (waveform.toDisableBits) { /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ + std::atomic_thread_fence(std::memory_order_acquire); } } - disableIdleTimer(); + if (!waveform.enabled && !waveform.timer1CB) { + deinitTimer(); + } return true; } -// The SDK and hardware take some time to actually get to our NMI code, so -// decrement the next IRQ's timer value by a bit so we can actually catch the -// real CPU cycle counter we want for the waveforms. - -// The SDK also sometimes is running at a different speed the the Arduino core -// so the ESP cycle counter is actually running at a variable speed. -// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. -#if F_CPU == 80000000 - #define DELTAIRQ (microsecondsToClockCycles(3)) - #define adjust(x) ((x) << (turbo ? 1 : 0)) -#else - #define DELTAIRQ (microsecondsToClockCycles(2)) - #define adjust(x) ((x) >> (turbo ? 0 : 1)) -#endif - - -static ICACHE_RAM_ATTR void timer1Interrupt() { - // Flag if the core is at 160 MHz, for use by adjust() - bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; - - uint32_t nextEventCycles = microsecondsToClockCycles(MAXIRQUS); - uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); - - if (wvfState.waveformToEnable || wvfState.waveformToDisable) { - // Handle enable/disable requests from main app - wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off - wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started - wvfState.waveformToEnable = 0; - wvfState.waveformToDisable = 0; - // No mem barrier. Globals must be written to RAM on ISR exit. - // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) - wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; - // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) - wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); - } else if (!pwmState.cnt && pwmState.pwmUpdate) { - // Start up the PWM generator by copying from the mailbox - pwmState.cnt = 1; - pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 - pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! - // No need for mem barrier here. Global must be written by IRQ exit - } - - bool done = false; - if (wvfState.waveformEnabled || pwmState.cnt) { - do { - nextEventCycles = microsecondsToClockCycles(MAXIRQUS); - - // PWM state machine implementation - if (pwmState.cnt) { - int32_t cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); - if (cyclesToGo < 0) { - if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new - if (pwmState.pwmUpdate) { - // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmState.pwmUpdate; - } - GPOS = pwmState.mask; // Set all active pins high - if (pwmState.mask & (1<<16)) { - GP16O = 1; - } - pwmState.idx = 0; - } else { - do { - // Drop the pin at this edge - if (pwmState.mask & (1<expiryCycle) { - int32_t expiryToGo = wave->expiryCycle - now; - if (expiryToGo < 0) { - // Done, remove! - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - wvfState.waveformEnabled &= ~mask; - continue; - } - } - - // Check for toggles - int32_t cyclesToGo = wave->nextServiceCycle - now; - if (cyclesToGo < 0) { - uint32_t nextEdgeCycles; - uint32_t desired = 0; - uint32_t *timeToUpdate; - wvfState.waveformState ^= mask; - if (wvfState.waveformState & mask) { - if (i == 16) { - GP16O = 1; - } - GPOS = mask; - - if (wvfState.waveformToChange & mask) { - // Copy over next full-cycle timings - wave->timeHighCycles = wvfState.waveformNewHigh; - wave->desiredHighCycles = wvfState.waveformNewHigh; - wave->timeLowCycles = wvfState.waveformNewLow; - wave->desiredLowCycles = wvfState.waveformNewLow; - wave->lastEdge = 0; - wvfState.waveformToChange = 0; - } - if (wave->lastEdge) { - desired = wave->desiredLowCycles; - timeToUpdate = &wave->timeLowCycles; - } - nextEdgeCycles = wave->timeHighCycles; - } else { - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - desired = wave->desiredHighCycles; - timeToUpdate = &wave->timeHighCycles; - nextEdgeCycles = wave->timeLowCycles; - } - if (desired) { - desired = adjust(desired); - int32_t err = desired - (now - wave->lastEdge); - if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal - err /= 2; - *timeToUpdate += err; - } - } - nextEdgeCycles = adjust(nextEdgeCycles); - wave->nextServiceCycle = now + nextEdgeCycles; - nextEventCycles = min_u32(nextEventCycles, nextEdgeCycles); - wave->lastEdge = now; - } else { - uint32_t deltaCycles = wave->nextServiceCycle - now; - nextEventCycles = min_u32(nextEventCycles, deltaCycles); - } - } - - // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur - uint32_t now = GetCycleCountIRQ(); - int32_t cycleDeltaNextEvent = timeoutCycle - (now + nextEventCycles); - int32_t cyclesLeftTimeout = timeoutCycle - now; - done = (cycleDeltaNextEvent < 0) || (cyclesLeftTimeout < 0); - } while (!done); - } // if (wvfState.waveformEnabled) - - if (wvfState.timer1CB) { - nextEventCycles = min_u32(nextEventCycles, wvfState.timer1CB()); - } - - if (nextEventCycles < microsecondsToClockCycles(5)) { - nextEventCycles = microsecondsToClockCycles(5); - } - nextEventCycles -= DELTAIRQ; - - // Do it here instead of global function to save time and because we know it's edge-IRQ - T1L = nextEventCycles >> (turbo ? 1 : 0); -} - }; -#endif // ESP8266 \ No newline at end of file +// Speed critical bits +#pragma GCC optimize ("O2") + +// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted. +// Using constexpr makes sure that the CPU clock frequency is compile-time fixed. +static inline ICACHE_RAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) { + if (ISCPUFREQ160MHZ) { + return isCPU2X ? ccys : (ccys >> 1); + } + else { + return isCPU2X ? (ccys << 1) : ccys; + } +} + +static ICACHE_RAM_ATTR void timer1Interrupt() { + const uint32_t isrStartCcy = ESP.getCycleCount(); + int32_t clockDrift = isrStartCcy - waveform.nextEventCcy; + const bool isCPU2X = CPU2X & 1; + if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { + // Handle enable/disable requests from main app. + waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + waveform.toDisableBits = 0; + } + + if (waveform.toSetBits) { + const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1; + Waveform& wave = waveform.pins[toSetPin]; + switch (wave.mode) { + case WaveformMode::INIT: + waveform.states &= ~waveform.toSetBits; // Clear the state of any just started + if (wave.alignPhase >= 0 && waveform.enabled & (1UL << wave.alignPhase)) { + wave.nextPeriodCcy = waveform.pins[wave.alignPhase].nextPeriodCcy + wave.nextPeriodCcy; + } + else { + wave.nextPeriodCcy = waveform.nextEventCcy; + } + if (!wave.expiryCcy) { + wave.mode = WaveformMode::INFINITE; + break; + } + // fall through + case WaveformMode::UPDATEEXPIRY: + // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count + wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); + wave.mode = WaveformMode::EXPIRES; + break; + default: + break; + } + waveform.toSetBits = 0; + } + + // Exit the loop if the next event, if any, is sufficiently distant. + const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS; + uint32_t busyPins = waveform.enabled; + waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS; + + uint32_t now = ESP.getCycleCount(); + uint32_t isrNextEventCcy = now; + while (busyPins) { + if (static_cast(isrNextEventCcy - now) > IRQLATENCYCCYS) { + waveform.nextEventCcy = isrNextEventCcy; + break; + } + isrNextEventCcy = waveform.nextEventCcy; + uint32_t loopPins = busyPins; + while (loopPins) { + const int pin = __builtin_ffsl(loopPins) - 1; + const uint32_t pinBit = 1UL << pin; + loopPins ^= pinBit; + + Waveform& wave = waveform.pins[pin]; + + if (clockDrift) { + wave.endDutyCcy += clockDrift; + wave.nextPeriodCcy += clockDrift; + wave.expiryCcy += clockDrift; + } + + uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; + if (WaveformMode::EXPIRES == wave.mode && + static_cast(waveNextEventCcy - wave.expiryCcy) >= 0 && + static_cast(now - wave.expiryCcy) >= 0) { + // Disable any waveforms that are done + waveform.enabled ^= pinBit; + busyPins ^= pinBit; + } + else { + const int32_t overshootCcys = now - waveNextEventCcy; + if (overshootCcys >= 0) { + const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X); + if (waveform.states & pinBit) { + // active configuration and forward are 100% duty + if (wave.periodCcys == wave.dutyCcys) { + wave.nextPeriodCcy += periodCcys; + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + if (wave.autoPwm) { + wave.adjDutyCcys += overshootCcys; + } + waveform.states ^= pinBit; + if (16 == pin) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + waveNextEventCcy = wave.nextPeriodCcy; + } + else { + wave.nextPeriodCcy += periodCcys; + if (!wave.dutyCcys) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X); + if (dutyCcys <= wave.adjDutyCcys) { + dutyCcys >>= 1; + wave.adjDutyCcys -= dutyCcys; + } + else if (wave.adjDutyCcys) { + dutyCcys -= wave.adjDutyCcys; + wave.adjDutyCcys = 0; + } + wave.endDutyCcy = now + dutyCcys; + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + waveform.states |= pinBit; + if (16 == pin) { + GP16O = 1; + } + else { + GPOS = pinBit; + } + } + waveNextEventCcy = wave.endDutyCcy; + } + + if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) > 0) { + waveNextEventCcy = wave.expiryCcy; + } + } + + if (static_cast(waveNextEventCcy - isrTimeoutCcy) >= 0) { + busyPins ^= pinBit; + if (static_cast(waveform.nextEventCcy - waveNextEventCcy) > 0) { + waveform.nextEventCcy = waveNextEventCcy; + } + } + else if (static_cast(isrNextEventCcy - waveNextEventCcy) > 0) { + isrNextEventCcy = waveNextEventCcy; + } + } + now = ESP.getCycleCount(); + } + clockDrift = 0; + } + + int32_t callbackCcys = 0; + if (waveform.timer1CB) { + callbackCcys = scaleCcys(microsecondsToClockCycles(waveform.timer1CB()), isCPU2X); + } + now = ESP.getCycleCount(); + int32_t nextEventCcys = waveform.nextEventCcy - now; + // Account for unknown duration of timer1CB(). + if (waveform.timer1CB && nextEventCcys > callbackCcys) { + waveform.nextEventCcy = now + callbackCcys; + nextEventCcys = callbackCcys; + } + + // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. + int32_t deltaIrqCcys = DELTAIRQCCYS; + int32_t irqLatencyCcys = IRQLATENCYCCYS; + if (isCPU2X) { + nextEventCcys >>= 1; + deltaIrqCcys >>= 1; + irqLatencyCcys >>= 1; + } + + // Firing timer too soon, the NMI occurs before ISR has returned. + if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) { + waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS; + nextEventCcys = irqLatencyCcys; + } + else { + nextEventCcys -= deltaIrqCcys; + } + + // Register access is fast and edge IRQ was configured before. + T1L = nextEventCcys; +} + +#endif // ESP8266 diff --git a/tasmota/core_esp8266_waveform.h b/tasmota/core_esp8266_waveform.h new file mode 100644 index 000000000..ff5a0f56f --- /dev/null +++ b/tasmota/core_esp8266_waveform.h @@ -0,0 +1,93 @@ +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + Copyright (c) 2020 Dirk O. Kaar. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 is + set to 1-shot mode and is always loaded with the time until the next edge + of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() + clock cycle count, or an interval measured in CPU clock cycles, but not TIMER1 + cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifdef ESP8266 + +#include + +#ifndef __ESP8266_WAVEFORM_H +#define __ESP8266_WAVEFORM_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Start or change a waveform of the specified high and low times on specific pin. +// If runtimeUS > 0 then automatically stop it after that many usecs, relative to the next +// full period. +// If waveform is not yet started on pin, and on pin == alignPhase a waveform is running, +// the new waveform is started at phaseOffsetUS phase offset, in microseconds, to that. +// Setting autoPwm to true allows the wave generator to maintain PWM duty to idle cycle ratio +// under load, for applications where frequency or duty cycle must not change, leave false. +// Returns true or false on success or failure. +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, + uint32_t runTimeUS = 0, int8_t alignPhase = -1, uint32_t phaseOffsetUS = 0, bool autoPwm = false); +// Start or change a waveform of the specified high and low CPU clock cycles on specific pin. +// If runtimeCycles > 0 then automatically stop it after that many CPU clock cycles, relative to the next +// full period. +// If waveform is not yet started on pin, and on pin == alignPhase a waveform is running, +// the new waveform is started at phaseOffsetCcys phase offset, in CPU clock cycles, to that. +// Setting autoPwm to true allows the wave generator to maintain PWM duty to idle cycle ratio +// under load, for applications where frequency or duty cycle must not change, leave false. +// Returns true or false on success or failure. +int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCcys, uint32_t timeLowCcys, + uint32_t runTimeCcys = 0, int8_t alignPhase = -1, uint32_t phaseOffsetCcys = 0, bool autoPwm = false); +// Stop a waveform, if any, on the specified pin. +// Returns true or false on success or failure. +int stopWaveform(uint8_t pin); + +// Add a callback function to be called on *EVERY* timer1 trigger. The +// callback returns the number of microseconds until the next desired call. +// However, since it is called every timer1 interrupt, it may be called +// again before this period. It should therefore use the ESP Cycle Counter +// to determine whether or not to perform an operation. +// Pass in NULL to disable the callback and, if no other waveforms being +// generated, stop the timer as well. +// Make sure the CB function has the ICACHE_RAM_ATTR decorator. +void setTimer1Callback(uint32_t (*fn)()); + +#ifdef __cplusplus +} +#endif + +#endif + +#endif // ESP8266 diff --git a/tasmota/core_esp8266_wiring_digital.cpp b/tasmota/core_esp8266_wiring_digital.cpp index 199fbabd8..982e1c6bf 100644 --- a/tasmota/core_esp8266_wiring_digital.cpp +++ b/tasmota/core_esp8266_wiring_digital.cpp @@ -33,12 +33,6 @@ extern "C" { -// Internal-only calls, not for applications -extern void _setPWMFreq(uint32_t freq); -extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t val, uint32_t range); -extern void resetPins(); - volatile uint32_t* const esp8266_gpioToFn[16] PROGMEM = { &GPF0, &GPF1, &GPF2, &GPF3, &GPF4, &GPF5, &GPF6, &GPF7, &GPF8, &GPF9, &GPF10, &GPF11, &GPF12, &GPF13, &GPF14, &GPF15 }; extern void __pinMode(uint8_t pin, uint8_t mode) { @@ -91,8 +85,7 @@ extern void __pinMode(uint8_t pin, uint8_t mode) { } extern void ICACHE_RAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) { - stopWaveform(pin); // Disable any tone - _stopPWM(pin); // ...and any analogWrite + stopWaveform(pin); if(pin < 16){ if(val) GPOS = (1 << pin); else GPOC = (1 << pin); @@ -140,8 +133,10 @@ typedef struct { static interrupt_handler_t interrupt_handlers[16] = { {0, 0, 0, 0}, }; static uint32_t interrupt_reg = 0; -void ICACHE_RAM_ATTR interrupt_handler(void*) +void ICACHE_RAM_ATTR interrupt_handler(void *arg, void *frame) { + (void) arg; + (void) frame; uint32_t status = GPIE; GPIEC = status;//clear them interrupts uint32_t levels = GPI; @@ -271,4 +266,4 @@ extern void detachInterrupt(uint8_t pin) __attribute__ ((weak, alias("__detachIn }; -#endif // ESP8266 \ No newline at end of file +#endif // ESP8266 diff --git a/tasmota/core_esp8266_wiring_pwm.cpp b/tasmota/core_esp8266_wiring_pwm.cpp index 5189fa8f5..f74334e43 100644 --- a/tasmota/core_esp8266_wiring_pwm.cpp +++ b/tasmota/core_esp8266_wiring_pwm.cpp @@ -28,12 +28,9 @@ extern "C" { -// Internal-only calls, not for applications -extern void _setPWMFreq(uint32_t freq); -extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t val, uint32_t range); - +static uint32_t analogMap = 0; static int32_t analogScale = PWMRANGE; +static uint16_t analogFreq = 1000; extern void __analogWriteRange(uint32_t range) { if (range > 0) { @@ -43,28 +40,38 @@ extern void __analogWriteRange(uint32_t range) { extern void __analogWriteFreq(uint32_t freq) { if (freq < 40) { - freq = 40; + analogFreq = 40; } else if (freq > 60000) { - freq = 60000; + analogFreq = 60000; } else { - freq = freq; + analogFreq = freq; } - _setPWMFreq(freq); } extern void __analogWrite(uint8_t pin, int val) { if (pin > 16) { return; } - + uint32_t analogPeriod = microsecondsToClockCycles(1000000UL) / analogFreq; if (val < 0) { val = 0; } else if (val > analogScale) { val = analogScale; } - pinMode(pin, OUTPUT); - _setPWM(pin, val, analogScale); + if (analogMap & 1UL << pin) { + analogMap &= ~(1 << pin); + } + else { + pinMode(pin, OUTPUT); + } + uint32_t high = (analogPeriod * val) / analogScale; + uint32_t low = analogPeriod - high; + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + int phaseReference = __builtin_ffs(analogMap) - 1; + if (startWaveformClockCycles(pin, high, low, 0, phaseReference, 0, true)) { + analogMap |= (1 << pin); + } } extern void analogWrite(uint8_t pin, int val) __attribute__((weak, alias("__analogWrite"))); @@ -73,4 +80,4 @@ extern void analogWriteRange(uint32_t range) __attribute__((weak, alias("__analo }; -#endif // ESP8266 \ No newline at end of file +#endif // ESP8266 diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 4f5623de1..e0de6bbf3 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -280,6 +280,12 @@ #define APP_DISABLE_POWERCYCLE false // [SetOption65] Disable fast power cycle detection for device reset #define DEEPSLEEP_BOOTCOUNT false // [SetOption76] Enable incrementing bootcount when deepsleep is enabled +#define APP_INTERLOCK_MODE false // [Interlock] Relay interlock mode +#define APP_INTERLOCK_GROUP_1 0xFF // [Interlock] Relay bitmask for interlock group 1 (0xFF if undef) +//#define APP_INTERLOCK_GROUP_2 0x00 // [Interlock] Relay bitmask for interlock group 2 (0x00 if undef) +//#define APP_INTERLOCK_GROUP_3 0x00 // [Interlock] Relay bitmask for interlock group 3 (0x00 if undef) +//#define APP_INTERLOCK_GROUP_4 0x00 // [Interlock] Relay bitmask for interlock group 4 (0x00 if undef) + // -- Lights -------------------------------------- #define WS2812_LEDS 30 // [Pixels] Number of WS2812 LEDs to start with (max is 512) #define LIGHT_MODE true // [SetOption15] Switch between commands PWM or COLOR/DIMMER/CT/CHANNEL diff --git a/tasmota/settings.ino b/tasmota/settings.ino index d7ede5f25..e7a43b70a 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -765,8 +765,11 @@ void SettingsDefaultSet2(void) } // Module -// flag.interlock |= 0; - Settings.interlock[0] = 0xFF; // Legacy support using all relays in one interlock group + flag.interlock |= APP_INTERLOCK_MODE; + Settings.interlock[0] = APP_INTERLOCK_GROUP_1; + Settings.interlock[1] = APP_INTERLOCK_GROUP_2; + Settings.interlock[2] = APP_INTERLOCK_GROUP_3; + Settings.interlock[3] = APP_INTERLOCK_GROUP_4; Settings.module = MODULE; Settings.fallback_module = FALLBACK_MODULE; ModuleDefault(WEMOS); @@ -1188,8 +1191,11 @@ void SettingsDelta(void) Settings.param[P_MDNS_DELAYED_START] = 0; } if (Settings.version < 0x0604010B) { - Settings.interlock[0] = 0xFF; // Legacy support using all relays in one interlock group - for (uint32_t i = 1; i < MAX_INTERLOCKS; i++) { Settings.interlock[i] = 0; } + Settings.flag.interlock = APP_INTERLOCK_MODE; + Settings.interlock[0] = APP_INTERLOCK_GROUP_1; + Settings.interlock[1] = APP_INTERLOCK_GROUP_2; + Settings.interlock[2] = APP_INTERLOCK_GROUP_3; + Settings.interlock[3] = APP_INTERLOCK_GROUP_4; } if (Settings.version < 0x0604010D) { Settings.param[P_BOOT_LOOP_OFFSET] = BOOT_LOOP_OFFSET; // SetOption36 diff --git a/tasmota/support.ino b/tasmota/support.ino index cdd1b3cd6..427a47dcf 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -1265,11 +1265,7 @@ uint32_t ValidPin(uint32_t pin, uint32_t gpio) bool ValidGPIO(uint32_t pin, uint32_t gpio) { -#ifdef ESP8266 - return (GPIO_USER == ValidPin(pin, gpio)); // Only allow GPIO_USER pins -#else // ESP32 - return (GPIO_USER == ValidPin(pin, gpio >> 5)); // Only allow GPIO_USER pins -#endif // ESP8266 - ESP32 + return (GPIO_USER == ValidPin(pin, BGPIO(gpio))); // Only allow GPIO_USER pins } #ifdef ESP8266 diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index e23fda355..9c4c858cc 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -1113,10 +1113,8 @@ void CmndGpio(void) } } char sindex[4] = { 0 }; -#ifdef ESP8266 - uint32_t sensor_name_idx = sensor_type; -#else // ESP32 - uint32_t sensor_name_idx = sensor_type >> 5; + uint32_t sensor_name_idx = BGPIO(sensor_type); +#ifdef ESP32 uint32_t nice_list_search = sensor_type & 0xFFE0; for (uint32_t j = 0; j < ARRAY_SIZE(kGpioNiceList); j++) { uint32_t nls_idx = pgm_read_word(kGpioNiceList + j); @@ -1125,7 +1123,7 @@ void CmndGpio(void) break; } } -#endif // ESP8266 - ESP32 +#endif // ESP32 const char *sensor_names = kSensorNames; if (sensor_name_idx > GPIO_FIX_START) { sensor_name_idx = sensor_name_idx - GPIO_FIX_START -1; @@ -1156,7 +1154,7 @@ void CmndGpios(void) uint32_t ridx = midx; #else // ESP32 uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0; - uint32_t midx = ridx >> 5; + uint32_t midx = BGPIO(ridx); #endif // ESP8266 - ESP32 if ((XdrvMailbox.payload != 255) && GetUsedInModule(midx, cmodule.io)) { continue; } if (!jsflg) { @@ -1627,7 +1625,8 @@ void CmndInterlock(void) } ResponseAppend_P(PSTR("\"}")); } else { - Settings.flag.interlock = 0; // CMND_INTERLOCK - Enable/disable interlock + // never ever reset interlock mode inadvertently if we forced it upon compilation + Settings.flag.interlock = APP_INTERLOCK_MODE; // CMND_INTERLOCK - Enable/disable interlock ResponseCmndStateText(Settings.flag.interlock); } } diff --git a/tasmota/tasmota_globals.h b/tasmota/tasmota_globals.h index 620c6ff68..791896ac3 100644 --- a/tasmota/tasmota_globals.h +++ b/tasmota/tasmota_globals.h @@ -88,6 +88,22 @@ String EthernetMacAddress(void); #undef USE_RF_FLASH // Disable RF firmware flash when Sonoff Rf is disabled #endif +#ifndef APP_INTERLOCK_MODE +#define APP_INTERLOCK_MODE false // [Interlock] Relay interlock mode +#endif +#ifndef APP_INTERLOCK_GROUP_1 +#define APP_INTERLOCK_GROUP_1 0xFF // [Interlock] Relay bitmask for interlock group 1 - Legacy support using all relays in one interlock group +#endif +#ifndef APP_INTERLOCK_GROUP_2 +#define APP_INTERLOCK_GROUP_2 0x00 // [Interlock] Relay bitmask for interlock group 2 +#endif +#ifndef APP_INTERLOCK_GROUP_3 +#define APP_INTERLOCK_GROUP_3 0x00 // [Interlock] Relay bitmask for interlock group 3 +#endif +#ifndef APP_INTERLOCK_GROUP_4 +#define APP_INTERLOCK_GROUP_4 0x00 // [Interlock] Relay bitmask for interlock group 4 +#endif + #ifndef SWITCH_MODE #define SWITCH_MODE TOGGLE // TOGGLE, FOLLOW or FOLLOW_INV (the wall switch state) #endif @@ -356,8 +372,10 @@ const char kWebColors[] PROGMEM = #ifdef ESP8266 #define AGPIO(x) (x) +#define BGPIO(x) (x) #else // ESP32 #define AGPIO(x) (x<<5) +#define BGPIO(x) (x>>5) #endif // ESP8266 - ESP32 #ifdef USE_DEVICE_GROUPS diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index d18368431..b5e5c9571 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -1728,7 +1728,7 @@ void HandleTemplateConfiguration(void) WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), D_SENSOR_USER); // }2'255'>User}3 } uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0; - uint32_t midx = ridx >> 5; + uint32_t midx = BGPIO(ridx); WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames)); #endif // ESP8266 - ESP32 } @@ -1905,7 +1905,7 @@ void HandleModuleConfiguration(void) } #else // ESP32 uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0; - midx = ridx >> 5; + midx = BGPIO(ridx); if (!GetUsedInModule(midx, cmodule.io)) { WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames)); } diff --git a/tasmota/xdrv_05_irremote_full.ino b/tasmota/xdrv_05_irremote_full.ino index 60e16b30d..457ff63f4 100644 --- a/tasmota/xdrv_05_irremote_full.ino +++ b/tasmota/xdrv_05_irremote_full.ino @@ -470,6 +470,21 @@ uint32_t IrRemoteCmndIrSendRaw(void) return IE_INVALID_RAWDATA; } // Parameters must be at least 3 + if (strcasecmp(str, "gc") == 0) { //if first parameter is gc then we process global cache data else it is raw + uint16_t GC[count+1]; + for (uint32_t i = 0; i <= count; i++) { + GC[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0); + if (!GC[i]) { + return IE_INVALID_RAWDATA; + } + } + irsend_active = true; + for (uint32_t r = 0; r <= repeat; r++) { + irsend->sendGC(GC, count+1); + } + return IE_NO_ERROR; + } + uint16_t parm[count]; for (uint32_t i = 0; i < count; i++) { parm[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0); diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 49762ad85..ddeb82403 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -168,6 +168,7 @@ struct RULES { uint16_t vars_event = 0; uint8_t mems_event = 0; bool teleperiod = false; + bool busy = false; char event_data[100]; } Rules; @@ -564,7 +565,6 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Match 1 %d"), match); - if (bitRead(Settings.rule_once, rule_set)) { if (match) { // Only allow match state changes if (!bitRead(Rules.triggers[rule_set], Rules.trigger_count[rule_set])) { @@ -751,28 +751,34 @@ bool RulesProcessEvent(char *json_event) { bool serviced = false; + if (!Rules.busy) { + Rules.busy = true; + #ifdef USE_DEBUG_DRIVER - ShowFreeMem(PSTR("RulesProcessEvent")); + ShowFreeMem(PSTR("RulesProcessEvent")); #endif - String event_saved = json_event; - // json_event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} - // json_event = {"System":{"Boot":1}} - // json_event = {"SerialReceived":"on"} - invalid but will be expanded to {"SerialReceived":{"Data":"on"}} - char *p = strchr(json_event, ':'); - if ((p != NULL) && !(strchr(++p, ':'))) { // Find second colon - event_saved.replace(F(":"), F(":{\"Data\":")); - event_saved += F("}"); - // event_saved = {"SerialReceived":{"Data":"on"}} - } - event_saved.toUpperCase(); + String event_saved = json_event; + // json_event = {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} + // json_event = {"System":{"Boot":1}} + // json_event = {"SerialReceived":"on"} - invalid but will be expanded to {"SerialReceived":{"Data":"on"}} + char *p = strchr(json_event, ':'); + if ((p != NULL) && !(strchr(++p, ':'))) { // Find second colon + event_saved.replace(F(":"), F(":{\"Data\":")); + event_saved += F("}"); + // event_saved = {"SerialReceived":{"Data":"on"}} + } + event_saved.toUpperCase(); //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Event %s"), event_saved.c_str()); - for (uint32_t i = 0; i < MAX_RULE_SETS; i++) { - if (GetRuleLen(i) && bitRead(Settings.rule_enabled, i)) { - if (RuleSetProcess(i, event_saved)) { serviced = true; } + for (uint32_t i = 0; i < MAX_RULE_SETS; i++) { + if (GetRuleLen(i) && bitRead(Settings.rule_enabled, i)) { + if (RuleSetProcess(i, event_saved)) { serviced = true; } + } } + + Rules.busy = false; } return serviced; } @@ -796,7 +802,7 @@ void RulesInit(void) void RulesEvery50ms(void) { - if (Settings.rule_enabled) { // Any rule enabled + if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled char json_event[120]; if (-1 == Rules.new_power) { Rules.new_power = power; } @@ -916,7 +922,7 @@ uint8_t rules_xsns_index = 0; void RulesEvery100ms(void) { - if (Settings.rule_enabled && (uptime > 4)) { // Any rule enabled and allow 4 seconds start-up time for sensors (#3811) + if (Settings.rule_enabled && !Rules.busy && (uptime > 4)) { // Any rule enabled and allow 4 seconds start-up time for sensors (#3811) mqtt_data[0] = '\0'; int tele_period_save = tele_period; tele_period = 2; // Do not allow HA updates during next function call @@ -925,14 +931,14 @@ void RulesEvery100ms(void) if (strlen(mqtt_data)) { mqtt_data[0] = '{'; // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089} ResponseJsonEnd(); - RulesProcess(); + RulesProcessEvent(mqtt_data); } } } void RulesEverySecond(void) { - if (Settings.rule_enabled) { // Any rule enabled + if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled char json_event[120]; if (RtcTime.valid) { @@ -956,7 +962,7 @@ void RulesEverySecond(void) void RulesSaveBeforeRestart(void) { - if (Settings.rule_enabled) { // Any rule enabled + if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled char json_event[32]; strncpy_P(json_event, PSTR("{\"System\":{\"Save\":1}}"), sizeof(json_event)); diff --git a/tasmota/xdrv_24_buzzer.ino b/tasmota/xdrv_24_buzzer.ino index f158f3cd9..10232cc52 100644 --- a/tasmota/xdrv_24_buzzer.ino +++ b/tasmota/xdrv_24_buzzer.ino @@ -101,9 +101,9 @@ void BuzzerEnabledBeep(uint32_t count, uint32_t duration) bool BuzzerPinState(void) { - if (XdrvMailbox.index == GPIO_BUZZER_INV) { + if (XdrvMailbox.index == AGPIO(GPIO_BUZZER_INV)) { Buzzer.inverted = 1; - XdrvMailbox.index -= (GPIO_BUZZER_INV - GPIO_BUZZER); + XdrvMailbox.index -= (AGPIO(GPIO_BUZZER_INV) - AGPIO(GPIO_BUZZER)); return true; } return false; diff --git a/tasmota/xdrv_27_shutter.ino b/tasmota/xdrv_27_shutter.ino index 71b3374f2..d4a384672 100644 --- a/tasmota/xdrv_27_shutter.ino +++ b/tasmota/xdrv_27_shutter.ino @@ -206,7 +206,8 @@ void ShutterInit(void) Shutter.pwm_frequency[i] = 0; Shutter.accelerator[i] = 0; analogWriteFreq(Shutter.pwm_frequency[i]); - ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); + analogWrite(Pin(GPIO_PWM1, i), 0); +// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); } } @@ -364,8 +365,8 @@ void ShutterUpdatePosition(void) while (RtcSettings.pulse_counter[i] < (uint32_t)(Shutter.target_position[i]-Shutter.start_position[i])*Shutter.direction[i]*Shutter.max_pwm_frequency/2000) { delay(1); } - //analogWrite(Pin(GPIO_PWM1, i), 0); // removed with 8.3 because of reset caused by watchog - ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); + analogWrite(Pin(GPIO_PWM1, i), 0); // removed with 8.3 because of reset caused by watchog +// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); Shutter.real_position[i] = ShutterCounterBasedPosition(i); AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Real %d, pulsecount %d, start %d"), Shutter.real_position[i],RtcSettings.pulse_counter[i], Shutter.start_position[i]); @@ -461,7 +462,7 @@ void ShutterWaitForMotorStop(uint32_t i) delay(50); } analogWrite(Pin(GPIO_PWM1, i), 0); - ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); +// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER); Shutter.real_position[i] = ShutterCounterBasedPosition(i); } else { ExecuteCommandPower(Settings.shutter_startrelay[i], 0, SRC_SHUTTER); diff --git a/tasmota/xdrv_31_tasmota_client.ino b/tasmota/xdrv_31_tasmota_client.ino index 1708df44f..3fdbad076 100644 --- a/tasmota/xdrv_31_tasmota_client.ino +++ b/tasmota/xdrv_31_tasmota_client.ino @@ -503,15 +503,17 @@ void CmndClientReset(void) { } void CmndClientSend(void) { - if (0 < XdrvMailbox.data_len) { - TasmotaClient_sendCmnd(CMND_CLIENT_SEND, XdrvMailbox.data_len); - TasmotaClient_Serial->write(char(PARAM_DATA_START)); - for (uint8_t idx = 0; idx < XdrvMailbox.data_len; idx++) { - TasmotaClient_Serial->write(XdrvMailbox.data[idx]); + if (TClient.SerialEnabled) { + if (0 < XdrvMailbox.data_len) { + TasmotaClient_sendCmnd(CMND_CLIENT_SEND, XdrvMailbox.data_len); + TasmotaClient_Serial->write(char(PARAM_DATA_START)); + for (uint8_t idx = 0; idx < XdrvMailbox.data_len; idx++) { + TasmotaClient_Serial->write(XdrvMailbox.data[idx]); + } + TasmotaClient_Serial->write(char(PARAM_DATA_END)); } - TasmotaClient_Serial->write(char(PARAM_DATA_END)); + ResponseCmndDone(); } - ResponseCmndDone(); } void TasmotaClient_ProcessIn(void) { diff --git a/tasmota/xdsp_04_ili9341.ino b/tasmota/xdsp_04_ili9341.ino index 813fe22ff..191c074bb 100644 --- a/tasmota/xdsp_04_ili9341.ino +++ b/tasmota/xdsp_04_ili9341.ino @@ -34,12 +34,29 @@ Adafruit_ILI9341 *tft; -uint16_t tft_scroll = TFT_TOP; uint16_t tft_top = TFT_TOP; uint16_t tft_bottom = TFT_BOTTOM; +uint16_t tft_scroll = TFT_TOP; +uint16_t tft_cols = 0; /*********************************************************************************************/ +bool Ili9341Header(void) { + if (Settings.display_cols[0] != tft_cols) { + tft_cols = Settings.display_cols[0]; + if (tft_cols > 17) { + tft_top = TFT_TOP; + tft_bottom = TFT_BOTTOM; + } else { + tft_top = 0; + tft_bottom = 0; + } + tft_scroll = tft_top; + tft->setScrollMargins(tft_top, tft_bottom); + } + return (tft_cols > 17); +} + void Ili9341InitMode(void) { tft->setRotation(Settings.display_rotate); // 0 @@ -52,15 +69,12 @@ void Ili9341InitMode(void) tft->setTextColor(ILI9341_WHITE, ILI9341_BLACK); tft->setTextSize(1); } else { - tft_top = TFT_TOP; - tft_bottom = TFT_BOTTOM; - tft->setScrollMargins(tft_top, tft_bottom); + Ili9341Header(); tft->setCursor(0, 0); tft->setTextColor(ILI9341_YELLOW, ILI9341_BLACK); tft->setTextSize(2); // tft->println("HEADER"); - tft_scroll = tft_top; } } @@ -105,6 +119,8 @@ void Ili9341InitDriver(void) #endif // USE_DISPLAY_MODES1TO5 Ili9341InitMode(); + + AddLog_P2(LOG_LEVEL_INFO, PSTR("DSP: ILI9341")); } } @@ -168,7 +184,7 @@ void Ili9341PrintLog(void) tft->setCursor(0, tft_scroll); tft->fillRect(0, tft_scroll, tft->width(), theight, ILI9341_BLACK); // Erase line tft->print(txt); - if (tft_top) { tft_scroll += theight; } + tft_scroll += theight; if (tft_scroll >= (tft->height() - tft_bottom)) { tft_scroll = tft_top; } @@ -201,17 +217,13 @@ void Ili9341Refresh(void) // Every second // 24-04-2017 13:45:43 = 19 + 1 ('\0') = 20 // 24-04-2017 13:45 = 16 + 1 ('\0') = 17 - if (Settings.display_cols[0] > 17) { + if (Ili9341Header()) { char tftdt[Settings.display_cols[0] +1]; char date4[11]; // 24-04-2017 uint8_t time_size = (Settings.display_cols[0] >= 20) ? 9 : 6; // 13:45:43 or 13:45 char spaces[Settings.display_cols[0] - (8 + time_size)]; char time[time_size]; // 13:45:43 - tft_top = TFT_TOP; - tft_bottom = TFT_BOTTOM; - tft_scroll = tft_top; - tft->setScrollMargins(tft_top, tft_bottom); tft->setTextSize(Settings.display_size); tft->setTextColor(ILI9341_YELLOW, ILI9341_RED); // Add background color to solve flicker tft->setCursor(0, 0); @@ -224,10 +236,6 @@ void Ili9341Refresh(void) // Every second tft->print(tftdt); } else { - tft_top = 0; - tft_bottom = 0; - tft_scroll = tft_top; - tft->setScrollMargins(tft_top, tft_bottom); tft->setCursor(0, 0); } diff --git a/tasmota/xsns_01_counter.ino b/tasmota/xsns_01_counter.ino index fec5791f7..3dfbdfaaf 100644 --- a/tasmota/xsns_01_counter.ino +++ b/tasmota/xsns_01_counter.ino @@ -113,9 +113,9 @@ void CounterUpdate4(void) bool CounterPinState(void) { - if ((XdrvMailbox.index >= GPIO_CNTR1_NP) && (XdrvMailbox.index < (GPIO_CNTR1_NP + MAX_COUNTERS))) { - bitSet(Counter.no_pullup, XdrvMailbox.index - GPIO_CNTR1_NP); - XdrvMailbox.index -= (GPIO_CNTR1_NP - GPIO_CNTR1); + if ((XdrvMailbox.index >= AGPIO(GPIO_CNTR1_NP)) && (XdrvMailbox.index < (AGPIO(GPIO_CNTR1_NP) + MAX_COUNTERS))) { + bitSet(Counter.no_pullup, XdrvMailbox.index - AGPIO(GPIO_CNTR1_NP)); + XdrvMailbox.index -= (AGPIO(GPIO_CNTR1_NP) - AGPIO(GPIO_CNTR1)); return true; } return false; diff --git a/tasmota/xsns_06_dht.ino b/tasmota/xsns_06_dht.ino index 7bb1a3513..d2e47fe17 100644 --- a/tasmota/xsns_06_dht.ino +++ b/tasmota/xsns_06_dht.ino @@ -188,12 +188,12 @@ bool DhtRead(uint32_t sensor) bool DhtPinState() { - if ((XdrvMailbox.index >= GPIO_DHT11) && (XdrvMailbox.index <= GPIO_SI7021)) { + if ((XdrvMailbox.index >= AGPIO(GPIO_DHT11)) && (XdrvMailbox.index <= AGPIO(GPIO_SI7021))) { if (dht_sensors < DHT_MAX_SENSORS) { Dht[dht_sensors].pin = XdrvMailbox.payload; - Dht[dht_sensors].type = XdrvMailbox.index; + Dht[dht_sensors].type = BGPIO(XdrvMailbox.index); dht_sensors++; - XdrvMailbox.index = GPIO_DHT11; + XdrvMailbox.index = AGPIO(GPIO_DHT11); } else { XdrvMailbox.index = 0; } diff --git a/tasmota/xsns_52_ibeacon.ino b/tasmota/xsns_52_ibeacon.ino index 8efcc1b0a..5c8b0afd4 100755 --- a/tasmota/xsns_52_ibeacon.ino +++ b/tasmota/xsns_52_ibeacon.ino @@ -25,7 +25,7 @@ #include -#define TMSBSIZ 512 +#define TMSBSIZ52 512 #define HM17_BAUDRATE 9600 @@ -101,7 +101,7 @@ void IBEACON_Init() { // actually doesnt work reliably with software serial if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) { - IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ); + IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ52); if (IBEACON_Serial->begin(HM17_BAUDRATE)) { if (IBEACON_Serial->hardwareSerial()) { ClaimSerial(); diff --git a/tasmota/xsns_61_MI_NRF24.ino b/tasmota/xsns_61_MI_NRF24.ino index c1aa1f4b4..f68c29054 100644 --- a/tasmota/xsns_61_MI_NRF24.ino +++ b/tasmota/xsns_61_MI_NRF24.ino @@ -606,13 +606,17 @@ bool MINRFhandleBeacon(scan_entry_t * entry, uint32_t offset){ * @brief increase beacon timer every second and process the result * */ -void MINRFbeaconCounter(void){ - if(MINRF.beacon.active) { +void MINRFbeaconCounter(void) { + if (MINRF.beacon.active) { MINRF.beacon.time++; +/* char stemp[20]; snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"Beacon\": %u}}"),D_CMND_NRF, MINRF.beacon.time); AddLog_P2(LOG_LEVEL_DEBUG, stemp); RulesProcessEvent(stemp); +*/ + Response_P(PSTR("{%s:{\"Beacon\":%u}}"), D_CMND_NRF, MINRF.beacon.time); + XdrvRulesProcess(); } } @@ -721,10 +725,10 @@ void MINRFAddKey(char* payload){ } /** - * @brief Convert combined key-MAC-string to + * @brief Convert combined key-MAC-string to * * @param _string input string in format: AABBCCDDEEFF... (upper case!), must be 44 chars!! - * @param _mac target byte array with fixed size of 16 + 6 + * @param _mac target byte array with fixed size of 16 + 6 */ void MINRFKeyMACStringToBytes(char* _string,uint8_t _keyMac[]) { //uppercase uint32_t index = 0; diff --git a/tasmota/xsns_62_MI_HM10.ino b/tasmota/xsns_62_MI_HM10.ino index 6655a0c87..6b5a797eb 100644 --- a/tasmota/xsns_62_MI_HM10.ino +++ b/tasmota/xsns_62_MI_HM10.ino @@ -977,11 +977,15 @@ void HM10_TaskEvery100ms(){ } } -void HM10StatusInfo(){ +void HM10StatusInfo() { +/* char stemp[20]; snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"found\": %u}}"),D_CMND_HM10, MIBLEsensors.size()); AddLog_P2(LOG_LEVEL_INFO, stemp); RulesProcessEvent(stemp); +*/ + Response_P(PSTR("{%s:{\"found\":%u}}"), D_CMND_HM10, MIBLEsensors.size()); + XdrvRulesProcess(); } /**