diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp new file mode 100644 index 0000000000..78c7160d90 --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp @@ -0,0 +1,717 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp +*/ + +/* + 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. + + 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 "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). + + 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 +*/ + + +#include +#include +#include "ets_sys.h" +#include "core_esp8266_waveform.h" +#include "user_interface.h" + +extern "C" { + +// Linker magic +void usePWMFixedNMI() {}; + +// Maximum delay between IRQs +#define MAXIRQUS (10000) + +// 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 +} 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 + + // 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 + + 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; + + uint32_t (*timer1CB)() = NULL; + + // 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; + + +// Ensure everything is read/written to RAM +#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +// Interrupt on/off control +static IRAM_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 IRAM_ATTR void forceTimerInterrupt() { + if (T1L > microsecondsToClockCycles(10)) { + T1L = microsecondsToClockCycles(10); + } +} + +// 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. + +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 _pwmFreq = 1000; +static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; + + +// If there are no more scheduled activities, shut down Timer 1. +// Otherwise, do nothing. +static IRAM_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 IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { + p->pwmUpdate = nullptr; + pwmState.pwmUpdate = p; + MEMBARRIER(); + forceTimerInterrupt(); + while (pwmState.pwmUpdate) { + if (idle) { + esp_yield(); + } + MEMBARRIER(); + } +} + +static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); + + +// Called when analogWriteFreq() changed to update the PWM total period +//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); +void _setPWMFreq_weak(uint32_t freq) { + _pwmFreq = freq; + + // Convert frequency into clock cycles + uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; + + // Simple static adjustment to bring period closer to requested due to overhead + // Empirically determined as a constant PWM delay and a function of the number of PWMs +#if F_CPU == 80000000 + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; +#else + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; +#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.mask = 0; + 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 + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + } +} +/* +static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); +void _setPWMFreq(uint32_t freq) { + _setPWMFreq_bound(freq); +} +*/ + +// 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%)) +//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { + if (!((1<= _pwmPeriod) { + cc = _pwmPeriod - 1; + } + + 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 + } + + // Sanity check for all-on/off + uint32_t cc = (_pwmPeriod * val) / range; + if ((cc == 0) || (cc >= _pwmPeriod)) { + digitalWrite(pin, cc ? HIGH : LOW); + return true; + } + + _addPWMtoList(p, pin, val, range); + + // Set mailbox and wait for ISR to copy it over + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + + // Potentially recalculate the PWM period if we've added another pin + _setPWMFreq(_pwmFreq); + + return true; +} +/* +static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); +bool _setPWM(int pin, uint32_t val, uint32_t range) { + return _setPWM_bound(pin, val, range); +} +*/ + +// 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. +//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); +int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + (void) alignPhase; + (void) phaseOffsetUS; + (void) autoPwm; + + if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 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 + } + + _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) { + esp_yield(); // Wait for waveform to update + MEMBARRIER(); + } + } + + return true; +} +/* +static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); +int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); +} + + +// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, + microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), + microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); +} +*/ + +// Set a callback. Pass in NULL to stop it +//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); +void setTimer1Callback_weak(uint32_t (*fn)()) { + wvfState.timer1CB = fn; + if (fn) { + initTimer(); + forceTimerInterrupt(); + } + disableIdleTimer(); +} +/* +static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); +void setTimer1Callback(uint32_t (*fn)()) { + setTimer1Callback_bound(fn); +} +*/ + +// Stops a waveform on a pin +//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!timerRunning) { + 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<= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} +// ----- @willmmiles end patch ----- + + +// 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(9)/4) + #define adjust(x) ((x) << (turbo ? 1 : 0)) +#else + #define DELTAIRQ (microsecondsToClockCycles(9)/8) + #define adjust(x) ((x) >> 0) +#endif + +// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage +#define MINIRQTIME microsecondsToClockCycles(6) + +static IRAM_ATTR void timer1Interrupt() { + // ----- @willmmiles begin patch ----- + nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- + + // Flag if the core is at 160 MHz, for use by adjust() + bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; + + uint32_t nextEventCycle = GetCycleCountIRQ() + 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 { + nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); + + // PWM state machine implementation + if (pwmState.cnt) { + int32_t cyclesToGo; + do { + 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; + wave->lastEdge = now; + } + nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); + } + + // 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 = nextEventCycle - now; + int32_t cyclesLeftTimeout = timeoutCycle - now; + done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); + } while (!done); + } // if (wvfState.waveformEnabled) + + if (wvfState.timer1CB) { + nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); + } + + int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); + + if (nextEventCycles < MINIRQTIME) { + nextEventCycles = MINIRQTIME; + } + 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); +} + +}; diff --git a/platformio.ini b/platformio.ini index 0e7bf6dad8..af93ecbafd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -294,7 +294,8 @@ lib_deps = ESPAsyncTCP @ 1.2.2 ESPAsyncUDP ;; makuna/NeoPixelBus @ 2.6.9 ;; WLEDMM use if you have problems with 2.7.5 - makuna/NeoPixelBus @ 2.7.5 + makuna/NeoPixelBus @ 2.7.9 ;; 2.7.9 has improved stability of bitbang outputs on 8266 + ESP8266PWM ${env.lib_deps} ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 diff --git a/tools/stress_test.sh b/tools/stress_test.sh new file mode 100644 index 0000000000..d7c344c58b --- /dev/null +++ b/tools/stress_test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Some web server stress tests +# +# Perform a large number of parallel requests, stress testing the web server +# TODO: some kind of performance metrics + +# Accepts three command line arguments: +# - first argument - mandatory - IP or hostname of target server +# - second argument - target type (optional) +# - third argument - xfer count (for replicated targets) (optional) +HOST=$1 +declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" +REPLICATE_COUNT=$(("${3:-10}")) + +PARALLEL_MAX=${PARALLEL_MAX:-50} + +CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}" +CURL_PRINT_RESPONSE_ARGS="-w %{http_code}\n" + +JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') +FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') +# Replicate one target many times +function replicate() { + printf "${1}?%d " $(seq 1 ${REPLICATE_COUNT}) +} +read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") +read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") +read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") +read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") + +# Expand target URLS to full arguments for curl +TARGETS=(${TARGET_STR[@]}) +#echo "${TARGETS[@]}" +FULL_TGT_OPTIONS=$(printf "http://${HOST}/%s -o /dev/null " "${TARGETS[@]}") +#echo ${FULL_TGT_OPTIONS} + +time curl ${CURL_ARGS} ${FULL_TGT_OPTIONS} diff --git a/tools/udp_test.py b/tools/udp_test.py new file mode 100644 index 0000000000..c4c9129cf9 --- /dev/null +++ b/tools/udp_test.py @@ -0,0 +1,46 @@ +import numpy as np +import socket + +class WledRealtimeClient: + def __init__(self, wled_controller_ip, num_pixels, udp_port=21324, max_pixels_per_packet=126): + self.wled_controller_ip = wled_controller_ip + self.num_pixels = num_pixels + self.udp_port = udp_port + self.max_pixels_per_packet = max_pixels_per_packet + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._prev_pixels = np.full((3, self.num_pixels), 253, dtype=np.uint8) + self.pixels = np.full((3, self.num_pixels), 1, dtype=np.uint8) + + def update(self): + # Truncate values and cast to integer + self.pixels = np.clip(self.pixels, 0, 255).astype(np.uint8) + p = np.copy(self.pixels) + + idx = np.where(~np.all(p == self._prev_pixels, axis=0))[0] + num_pixels = len(idx) + n_packets = (num_pixels + self.max_pixels_per_packet - 1) // self.max_pixels_per_packet + idx_split = np.array_split(idx, n_packets) + + header = bytes([1, 2]) # WARLS protocol header + for packet_indices in idx_split: + data = bytearray(header) + for i in packet_indices: + data.extend([i, *p[:, i]]) # Index and RGB values + self._sock.sendto(bytes(data), (self.wled_controller_ip, self.udp_port)) + + self._prev_pixels = np.copy(p) + + + +################################## LED blink test ################################## +if __name__ == "__main__": + WLED_CONTROLLER_IP = "192.168.1.153" + NUM_PIXELS = 255 # Amount of LEDs on your strip + import time + wled = WledRealtimeClient(WLED_CONTROLLER_IP, NUM_PIXELS) + print('Starting LED blink test') + while True: + for i in range(NUM_PIXELS): + wled.pixels[1, i] = 255 if wled.pixels[1, i] == 0 else 0 + wled.update() + time.sleep(.01) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index dd9c9b661d..5c7eb68bfa 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4116,7 +4116,7 @@ uint16_t mode_pacifica() // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. - uint16_t sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; + unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step & 0xFFFF, sCIStart4 = (SEGENV.step >> 16); uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; @@ -4131,7 +4131,7 @@ uint16_t mode_pacifica() sCIStart3 -= (deltams1 * beatsin88(501,5,7)); sCIStart4 -= (deltams2 * beatsin88(257,4,6)); SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2; - SEGENV.step = sCIStart4; SEGENV.step = (SEGENV.step << 16) + sCIStart3; + SEGENV.step = (sCIStart4 << 16) | (sCIStart3 & 0xFFFF); // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); @@ -4162,7 +4162,7 @@ uint16_t mode_pacifica() c.green = scale8(c.green, 200); c |= CRGB( 2, 5, 7); - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } strip.now = nowOld; diff --git a/wled00/const.h b/wled00/const.h index 55f9f780a2..50b837b3f8 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -421,7 +421,7 @@ // string temp buffer (now stored in stack locally) // WLEDMM ...which is actually not the greatest design choice on ESP32 #ifdef ESP8266 -#define SETTINGS_STACK_BUF_SIZE 2048 +#define SETTINGS_STACK_BUF_SIZE 2560 #else #if !defined(USERMOD_AUDIOREACTIVE) #define SETTINGS_STACK_BUF_SIZE 3834 // WLEDMM added 696+32 bytes of margin (was 3096) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index a4cee00716..a91b941a61 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -17,7 +17,8 @@

MoonMod WLED Software Update

##VERSION##
- Download the latest release: + Download the latest binary: 


diff --git a/wled00/json.cpp b/wled00/json.cpp index ac4d1ca598..d5be5ac5cb 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -267,11 +267,11 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // lx parser #ifdef WLED_ENABLE_LOXONE int lx = elem[F("lx")] | -1; - if (lx > 0) { + if (lx >= 0) { parseLxJson(lx, id, false); } int ly = elem[F("ly")] | -1; - if (ly > 0) { + if (ly >= 0) { parseLxJson(ly, id, true); } #endif @@ -573,7 +573,6 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) ps = presetCycCurr; if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) - presetCycCurr = ps; unloadPlaylist(); // applying a preset unloads the playlist applyPreset(ps, callMode); // async load from file system (only preset ID was specified) if (iAmGroot) suspendStripService = false; // WLEDMM release lock diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 3beab1b862..46e8a53d70 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -125,11 +125,21 @@ void initPresetsFile() f.close(); } +#if 0 // not used in WLEDMM +bool applyPresetFromPlaylist(byte index) +{ + DEBUG_PRINTF_P(PSTR("Request to apply preset: %d\n"), index); + presetToApply = presetCycCurr = index; + callModeToApply = CALL_MODE_DIRECT_CHANGE; + return true; +} +#endif + bool applyPreset(byte index, byte callMode) { DEBUG_PRINT(F("Request to apply preset: ")); DEBUG_PRINTLN(index); - presetToApply = index; + presetToApply = presetCycCurr = index; callModeToApply = callMode; return true; } diff --git a/wled00/util.cpp b/wled00/util.cpp index 87e14c2c99..edb1aa925b 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -215,21 +215,33 @@ bool isAsterisksOnly(const char* str, byte maxLen) //threading/network callback details: https://github.com/Aircoookie/WLED/pull/2336#discussion_r762276994 bool requestJSONBufferLock(uint8_t module) { - unsigned long now = millis(); - - while (jsonBufferLock && millis()-now < 1100) delay(1); // wait for fraction for buffer lock +#if defined(ARDUINO_ARCH_ESP32) + // Use a recursive mutex type in case our task is the one holding the JSON buffer. + // This can happen during large JSON web transactions. In this case, we continue immediately + // and then will return out below if the lock is still held. + if (xSemaphoreTakeRecursive(jsonBufferLockMutex, 1100) == pdFALSE) return false; // timed out waiting +#elif defined(ARDUINO_ARCH_ESP8266) + // If we're in system context, delay() won't return control to the user context, so there's + // no point in waiting. + if (can_yield()) { + unsigned long now = millis(); + while (jsonBufferLock && (millis()-now < 1100)) delay(1); // wait for fraction for buffer lock + } +#else + #error Unsupported task framework - fix requestJSONBufferLock +#endif + // If the lock is still held - by us, or by another task if (jsonBufferLock) { - USER_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); - USER_PRINT(jsonBufferLock); - USER_PRINTLN(")"); - return false; // waiting time-outed + DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), module, jsonBufferLock); +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif + return false; } jsonBufferLock = module ? module : 255; - DEBUG_PRINT(F("JSON buffer locked. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("JSON buffer locked. (%d)\n"), jsonBufferLock); fileDoc = &doc; // used for applying presets (presets.cpp) doc.clear(); return true; @@ -238,11 +250,12 @@ bool requestJSONBufferLock(uint8_t module) void releaseJSONBufferLock() { - DEBUG_PRINT(F("JSON buffer released. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); fileDoc = nullptr; + DEBUG_PRINTF_P(PSTR("JSON buffer released. (%d)\n"), jsonBufferLock); jsonBufferLock = 0; +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 3c86c4b045..b27a0e758c 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -82,6 +82,9 @@ static int wledmm_get_tcp_stacksize(void) { } #endif + +extern "C" void usePWMFixedNMI(); // PWM bugfix for 8266 + /* * Main WLED class implementation. Mostly initialization and connection logic */ @@ -634,6 +637,11 @@ void WLED::setup() #else DEBUG_PRINTLN(F("PSRAM not used.")); #endif + +#ifdef ESP8266 + usePWMFixedNMI(); // link the 8266 NMI fix +#endif + #endif #if defined(ARDUINO_ARCH_ESP32) if (strncmp("ESP32-PICO", ESP.getChipModel(), 10) == 0) { // WLEDMM detect pico board at runtime diff --git a/wled00/wled.h b/wled00/wled.h index e3cadc5da9..a89eca1bf4 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -337,8 +337,6 @@ WLED_GLOBAL int8_t irPin _INIT(IRPIN); constexpr uint8_t hardwareTX = 1; #endif -//WLED_GLOBAL byte presetToApply _INIT(0); - WLED_GLOBAL char ntpServerName[33] _INIT("0.wled.pool.ntp.org"); // NTP server to use // WiFi CONFIG (all these can be changed via web UI, no need to set them here) @@ -374,7 +372,11 @@ WLED_GLOBAL byte bootPreset _INIT(0); // save preset to load //if true, a segment per bus will be created on boot and LED settings save //if false, only one segment spanning the total LEDs is created, //but not on LED settings save if there is more than one segment currently +#ifdef WLED_AUTOSEGMENTS +WLED_GLOBAL bool autoSegments _INIT(true); +#else WLED_GLOBAL bool autoSegments _INIT(false); +#endif WLED_GLOBAL bool correctWB _INIT(false); // CCT color correction of RGB color WLED_GLOBAL bool cctFromRgb _INIT(false); // CCT is calculated from RGB instead of using seg.cct WLED_GLOBAL bool gammaCorrectCol _INIT(true ); // use gamma correction on colors // WLEDMM that's what you would think, but the code tells a different story. @@ -822,6 +824,10 @@ WLED_GLOBAL StaticJsonDocument doc; #endif // WLEDMM end WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); +#if defined(ARDUINO_ARCH_ESP32) +WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateRecursiveMutex()); +#endif + // enable additional debug output //WLEDMM: switch between netdebug and serial // cannot do this on -S2, due to buggy USBCDC serial driver: canUseSerial diff --git a/wled00/wled00.ino b/wled00/wled_main.cpp similarity index 94% rename from wled00/wled00.ino rename to wled00/wled_main.cpp index 6f1f22d59a..0c6aa96446 100644 --- a/wled00/wled00.ino +++ b/wled00/wled_main.cpp @@ -1,9 +1,12 @@ +#include /* * WLED Arduino IDE compatibility file. + * (this is the former wled00.ino) * * Where has everything gone? * - * In April 2020, the project's structure underwent a major change. + * In April 2020, the project's structure underwent a major change. + * We now use the platformIO build system, and building WLED in Arduino IDE is not supported any more. * Global variables are now found in file "wled.h" * Global function declarations are found in "fcn_declare.h" *