Out of Memory perhaps? How do I solve this one

Hi all.
I have a large project which is currently running on an ESP32 using VSCode and PlatformIO.
I have recently added some functionality, and now get the following error when navigating to a previously working web page:

Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x40089e4f  PS      : 0x00060730  A0      : 0x800ed5db  A1      : 0x3ffe8830  
A2      : 0x3fff1d88  A3      : 0x00000000  A4      : 0x00000001  A5      : 0x3fffd2b8
A6      : 0x00000000  A7      : 0x0000006e  A8      : 0x00000000  A9      : 0x00000000  
A10     : 0x00000001  A11     : 0x0000727e  A12     : 0x00000000  A13     : 0x0000ff00
A14     : 0x00ff0000  A15     : 0xff000000  SAR     : 0x00000017  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000000  LBEG    : 0x40089dad  LEND    : 0x40089dcf  LCOUNT  : 0x00000000


Backtrace:0x40089e4c:0x3ffe88300x400ed5d8:0x3ffe8850 0x400ed6f6:0x3ffe8870 0x400e7159:0x3ffe8890 0x400e55d6:0x3ffe8900 0x400e5529:0x3ffe8950 0x400d4025:0x3ffe8970 0x400e88a5:0x3ffe89c0 0x400e63d9:0x3ffe8a10 0x400e64e9:0x3ffe8a60 0x400e66d1:0x3ffe8ab0 0x40177fb5:0x3ffe8ad0 0x40178031:0x3ffe8b00 0x40178836:0x3ffe8b20 

  #0  0x40089e4c:0x3ffe8830 in memmove at /builds/idf/crosstool-NG/.build/HOST-x86_64-w64-mingw32/xtensa-esp32-elf/src/newlib/newlib/libc/string/memmove.c:66
  #1  0x400ed5d8:0x3ffe8850 in String::move(String&) at C:/Users/Jim/.platformio/packages/framework-arduinoespressif32/cores/esp32/WString.cpp:266
  #2  0x400ed6f6:0x3ffe8870 in String::operator=(String&&) at C:/Users/Jim/.platformio/packages/framework-arduinoespressif32/cores/esp32/WString.cpp:308
  #3  0x400e7159:0x3ffe8890 in AsyncBasicResponse::_respond(AsyncWebServerRequest*) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebResponses.cpp:205 (discriminator 1)
  #4  0x400e55d6:0x3ffe8900 in AsyncWebServerRequest::send(AsyncWebServerResponse*) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebRequest.cpp:725
  #5  0x400e5529:0x3ffe8950 in AsyncWebServerRequest::send(int, String const&, String const&) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebRequest.cpp:772
  #6  0x400d4025:0x3ffe8970 in std::_Function_handler<void (AsyncWebServerRequest*), setup()::{lambda(AsyncWebServerRequest*)#28}>::_M_invoke(std::_Any_data const&, AsyncWebServerRequest*&&) at src/main.cpp:1373
      (inlined by) _M_invoke at c:\users\jim\.platformio\packages\toolchain-xtensa-esp32\xtensa-esp32-elf\include\c++\8.4.0\bits/std_function.h:297
  #7  0x400e88a5:0x3ffe89c0 in std::function<void (AsyncWebServerRequest*)>::operator()(AsyncWebServerRequest*) const at c:\users\jim\.platformio\packages\toolchain-xtensa-esp32\xtensa-esp32-elf\include\c++\8.4.0\bits/std_function.h:687
      (inlined by) AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest*) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebHandlerImpl.h:132
  #8  0x400e63d9:0x3ffe8a10 in AsyncWebServerRequest::_parseLine() at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebRequest.cpp:581 (discriminator 1)
  #9  0x400e64e9:0x3ffe8a60 in AsyncWebServerRequest::_onData(void*, unsigned int) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebRequest.cpp:123
  #10 0x400e66d1:0x3ffe8ab0 in std::_Function_handler<void (void*, AsyncClient*, void*, unsigned int), AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer*, AsyncClient*)::{lambda(void*, AsyncClient*, void*, unsigned int)#8}>::_M_invoke(std::_Any_data const&, void*&&, AsyncClient*&&, std::_Any_data const&, unsigned int&&) at .pio/libdeps/esp32doit-devkit-v1/ESPAsyncWebServer-esphome/src/WebRequest.cpp:76
      (inlined by) _M_invoke at c:\users\jim\.platformio\packages\toolchain-xtensa-esp32\xtensa-esp32-elf\include\c++\8.4.0\bits/std_function.h:297
  #11 0x40177fb5:0x3ffe8ad0 in std::function<void (void*, AsyncClient*, void*, unsigned int)>::operator()(void*, AsyncClient*, void*, unsigned int) const at c:\users\jim\.platformio\packages\toolchain-xtensa-esp32\xtensa-esp32-elf\include\c++\8.4.0\bits/std_function.h:687    
      (inlined by) AsyncClient::_recv(tcp_pcb*, pbuf*, signed char) at .pio/libdeps/esp32doit-devkit-v1/AsyncTCP-esphome/src/AsyncTCP.cpp:961
  #12 0x40178031:0x3ffe8b00 in AsyncClient::_s_recv(void*, tcp_pcb*, pbuf*, signed char) at .pio/libdeps/esp32doit-devkit-v1/AsyncTCP-esphome/src/AsyncTCP.cpp:1253
  #13 0x40178836:0x3ffe8b20 in _async_service_task(void*) at .pio/libdeps/esp32doit-devkit-v1/AsyncTCP-esphome/src/AsyncTCP.cpp:164       
      (inlined by) _async_service_task at .pio/libdeps/esp32doit-devkit-v1/AsyncTCP-esphome/src/AsyncTCP.cpp:199

ELF file SHA256: 0000000000000000

Rebooting...

Now to follow the forum guidelines I should now post all 2343 lines here, not including the several hundred web pages, but I think that would be somewhat excessive. I can do it if required though so please request,

The error occurs when the request->send line (which is Line 1373 as called out in the dump) is called in the following snippet. If I replace with the commented out /request-> send then no crash occurs.

  server.on("/edit/actions", HTTP_GET, [](AsyncWebServerRequest *request){
  printHeap();
#ifdef DEBUG_JIM
    Serial.println("/edit/actions");
#endif
    request->send(200, "text/html", action_edit_html);
    //request->send(200, "text/plain", "OK");
  });

I have printed out the Heap and it reports a healthy 98908 prior to the crash, but uploading reports that I am at at 95.8% of flash memory.

I'm a bit at a loss as to how to proceed now - I have attempted to remove as many of the usages of String as possible, and any unused libraries are gone, but I am using some large arrays.
The hardware is an ESP32 devkit V4

So, pointers to how to go about fault finding this now would be gratefully received.
Many thanks, Jim

Probably not a memory problem. Looking at this:

I'd say an invalid pointer or accessing beyond the end of an array.
I think this:

may indicate that you're attempting to dereference the null pointer. Fire up the Exception Decoder to find out for sure.

what does this do?

how is action_edit_html defined?

Hi gfvalvo.
Thanks for the reply.
The first code snippet is the output from the exception decoder. It points to the code line I mentioned. The code is parseing a web page stored as a const char [] to the web server for responding to the Get request - it was all working fine until I added more code elsewhere.

Hi J-M-L Jackson.
PrintHeap prints the remaining heap of the ESP32.

void printHeap()
{
  Serial.printf("Heap=%d\n",xPortGetFreeHeapSize());
}

action_edit_htlm is stored as a const char [] as follows:

const char action_edit_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>

<head>
<meta name="viewport" content="width=device-width, initial-scale=1">  
<style>
  body {
    font-family: "Montserrat", sans-serif;
    font-weight: 600;
    background-color: #09484e;
    color: #81d0fe;
  }
  .button-container {
    display: flex;
    margin: 0px 0.5rem;
    flex-direction: row;
  }
  .multi-button {
    display: flex;
    align-items: center;
    //justify-content: center;
    flex-wrap: wrap;
    padding: 10px 16px;
    border-radius: 25px;
    background: #f8f8f8;
    border: 2px solid #ddd;
    box-shadow: 5px 10px #063236;
    margin: 0.5rem;
  }
  .multi-button legend {        
    text-align: center;
    font-weight: 600;
    background-color: #116870;
    border-radius: 14px;
    padding: 4px 9px;
  }
  .multi-button action {
    align-items: left;
  }
  
  label {
    display:flex;
    flex-direction:column;
    vertical-align: top;
    float:left;
    margin-left:5px;
    margin-right:5px;    
  }

  input {
    display:flex;
    flex-direction:column;
  }

  h1 {
    text-align: center;
    font-weight: 600;
  }
  .button {
    background-color: #0078d0;        
    height: 106px;
    width: 106px;
    border: 1px solid black;
    margin: 5px;
    border-radius: 50%;
    color: #fff;
    cursor: pointer;
    display: inline-block;
    font-size: 18px;
    font-weight: 500;
    font-family: "Montserrat", sans-serif;
    outline: 0;
    position: relative;
    text-align: center;
    text-decoration: none;
    transition: all 0.3s;
    user-select: none;
    -webkit-user-select: none;
    touch-action: manipulation;
    box-shadow: 5px 9px #999;   
  }
  .btnMomentary {border-radius: 25%;}
  .btnRedOff {background-color: #900000;} /* Red */
  .btnRedOn {background-color: #FF0000;} /* Red */  
  .btnGreenOff {background-color: #266e39;} /* Green */
  .btnGreenOn {background-color: #4CAF50;} /* Green */  
  .btnBlueOff {background-color: #035773;} /* Blue */
  .btnBlueOn {background-color: #0055ff;} /* Blue */  
  .btnWhiteOff {background-color: rgb(203, 169, 1);} /* White */
  .btnWhiteOn {background-color: #ffa600;color:#010405} /* White */  
  .btnYellowOff {background-color: #924900;} /* Yellow */
  .btnYellowOn {background-color: #FF8000;} /* Yellow */
  .btnNav {border-radius: 10%; background-color: #e100ff; height:60px; box-shadow: 0 0;}
  .btnGroup {border-radius: 35%; background-color: #007eb9;}
  .btnScreen {background-color: #0055ff;} /* Blue */
  .btnPart {background-color: #00A055;}
  .btnBreak {background-color: #ff8000;}
  .btnTry {line-height:2; align-content:center; background-color: #00A055; height: 50px; width: 50px;}
  .btnSave {line-height:2; align-content:center; background-color: #00A055; height: 50px; width: 50px;}
  .btnDelete {line-height:2; align-content:center; background-color: #ff2000; height: 50px; width: 50px;}
  .btnInsert {line-height:2; align-content:center; background-color: #0040FF; height: 50px; width: 50px;}
  .btnAdd {line-height:2; align-content:center; background-color: #0040FF; height: 50px; width: 50px;}  

  button:before {
    background-color: initial;
    background-image: linear-gradient(#fff 0, rgba(255, 255, 255, 0) 100%);
    border-radius: 125px;
    content: "";
    height: 50%;
    left: 4%;
    opacity: 0.5;
    position: absolute;
    top: 0;
    transition: all 0.3s;
    width: 92%;
  }

  /*
    button:hover:not(.btnEmpty) {
    box-shadow: rgba(255, 255, 255, 0.2) 0 3px 15px inset,
      rgba(0, 0, 0, 0.1) 0 3px 5px, rgba(0, 0, 0, 0.1) 0 10px 13px;
    transform: scale(1.05);
  }
*/
  .button:active {
    box-shadow: 0 5px #666;
    transform: translateY(4px);
  }
  .color {
        width: 100px;
        height: 60px;
    }
    
  @media (max-width: 768px) {
    .button-container {
      flex-direction: column;
      margin: 0rem;
    }
    .multi-button {
        margin: 0.5rem 0rem;
    }
    button {      
        height: 79px;
        width: 79px;
        margin: 5px;
        font-size: 14px;
    }
  }
</style>
 
  <TITLE>Control</TITLE>
  </head>
 
  <body>
    <h2 style='text-align: center; color: #5e9ca0;'>Action Edit</h2>
    <div style="display: flex;align-items: center;justify-content: center;">
      <button onclick="window.location.href='/';" class='button btnNav'>Home</button>
      <button onclick="window.location.href='/edit/partsandbreaks';" class='button btnNav'>Back</button>      
      <button onclick="songsSave()" class='button btnNav'>Save</button>
      <label id='songInfoLabel' class='label'>Song Info</label>
    </div>
    <div id='fieldDiv' style="display: flex;align-items: center; justify-content: center;">
    </div>

  <script>

  var elem = document.documentElement;

  //                 index         group       tempo      play      Colour    cGrad     Delay       Loops
  const isVisible=[ ["visible",  "visible",  "hidden",  "hidden",  "hidden", "hidden", "visible", "visible"],   // Preset Solid
                    ["visible",  "visible",  "hidden",  "visible", "hidden", "hidden", "visible", "visible"],   // Preset Grad
                    ["visible",  "visible",  "hidden",  "hidden",  "hidden", "hidden", "visible", "visible"],   // Preset Fx
                    ["hidden",   "visible",  "visible", "hidden",  "hidden", "hidden", "visible", "visible"],   // BPM
                    ["hidden",   "visible",  "hidden",  "hidden",  "visible","hidden", "visible", "visible"],   // Custom Colour
                    ["hidden",   "visible",  "hidden",  "visible", "hidden", "visible","visible", "visible"],   // Custom Grad
                    ["hidden",   "visible",  "hidden",  "visible", "hidden", "hidden", "visible", "visible"]    // Play/Pause
                ];

    var selectedSong = 0;
    var partsOrBreaks = "unknown";
    var selectedPorB = 0;
    let presetArray;
    let selectedType;  

    function buttonDown(buttonID)
    {
      //console.log("button " + buttonID + " down");gfg
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/edit/buttondown?button="+buttonID, true);
      xhr.send();   
    }

    function buttonUp(buttonID)
    {
      //console.log("button " + buttonID + " up");
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/edit/buttonup?button="+buttonID, true);
      xhr.send();
    }  

    function requestPresetNames()
    {
      var xmlhttp = new XMLHttpRequest();
      var url = "/getPresetNames";
      console.log("Requesting Preset Names");

      xmlhttp.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {
          presetArray = JSON.parse(this.responseText);

          console.log("There are "+presetArray.solid.length+" Solid Presets");
          console.log("There are "+presetArray.multi.length+" Grad Presets");
          console.log("There are "+presetArray.fx.length+" FX Presets");
          requestSongInfo();          
        }
      };
      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }

    function requestSongInfo()
    {
      var xmlhttp = new XMLHttpRequest();
      var url = "/getSongInfo";
      console.log("Requesting Song Info");

      xmlhttp.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {
          var infoArray = JSON.parse(this.responseText);
          var labelID = document.getElementById('songInfoLabel');

          var songInfoText = "Song " + (infoArray[0]+1) + " (" + infoArray[1] +")";
          selectedSong = infoArray[0];

          if (infoArray[1] == 0) // unknown
          {       
            songInfoText += ", Unknown Part/Break ";
            partsOrBreaks = "unknown";
          }
          else if (infoArray[2] == 1) // parts
          {
            songInfoText += ", Part " + (infoArray[3]+1) + " (" + infoArray[4] + ")";
            partsOrBreaks = "parts";
            requestPartActions()
          }
          else if (infoArray[2] == 2) // breaks
          {
            songInfoText += ", Break " + (infoArray[3]+1) + " (" + infoArray[4] + ")";
            partsOrBreaks = "breaks";
            requestBreakActions();
          }
          else console.log("Unknown Parts or Breaks ID: " + infoArray[2]);

          selectedPorB = infoArray[3];
          labelID.innerHTML = songInfoText;
        }
      };
      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }

    function requestPartActions()
    {
      var xmlhttp = new XMLHttpRequest();
      var url = "/edit/getpartactions";
      console.log("Requesting Part Actions");

      xmlhttp.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {
          var myVar = JSON.parse(this.responseText);
          console.log("Length = " + Object.keys(myVar).length);

          // remove all exisiting from the actions field
          document.querySelectorAll('.multi-button').forEach(e => e.remove());

          var isLast = "false";
          for (i=0; i < Object.keys(myVar).length;i++)
          {
            var actionKey = "action"+i;
            console.log("ActionKey = "+actionKey);
            if (i == Object.keys(myVar).length-1)
              isLast = "true";            
            addAction(i,myVar[actionKey],isLast);
          }
        }
      };
      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }

    function requestBreakActions()
    {    
      var xmlhttp = new XMLHttpRequest();
      var url = "/edit/getbreakactions";
      console.log("Requesting Break Actions");

      xmlhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200)
        {
          var myVar = JSON.parse(this.responseText);
          console.log("Length = " + Object.keys(myVar).length);

          // remove all exisiting from the actions field
          document.querySelectorAll('.multi-button').forEach(e => e.remove());

          var isLast = "false";
          for (i=0; i < Object.keys(myVar).length;i++)
          {
            var actionKey = "action"+i;
            console.log("ActionKey = "+actionKey);
            if (i == Object.keys(myVar).length-1)
              isLast = "true";
            addAction(i,myVar[actionKey], isLast);
          }   
        }
      };
      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }

    function songsSave()
    {
      var xmlhttp = new XMLHttpRequest();
      var url = "/edit/songsSave";

      xmlhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200)
        {
          console.log("Songs Saved OK");
          alert("Songs Saved to Internal Flash Memory");
        }
      };
      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }

    /* View in fullscreen */
    function openFullScreen()
    {
      if (elem.requestFullscreen) {
        elem.requestFullscreen();
      } else if (elem.webkitRequestFullscreen) { /* Safari */
        elem.webkitRequestFullscreen();
      } else if (elem.msRequestFullscreen) { /* IE11 */
        elem.msRequestFullscreen();
      }
    }

    /* Close fullscreen */
    function closeFullScreen()
    {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.webkitExitFullscreen) { /* Safari */
        document.webkitExitFullscreen();
      } else if (document.msExitFullscreen) { /* IE11 */
        document.msExitFullscreen();
      }
    }

    function addAction(actionNum, actionData, isLast)
    {
      console.log("Adding Action " + actionNum + " : " + actionData.name);
    
      let divID = document.getElementById('fieldDiv');

      let fieldSet = document.createElement("actionsField");
      fieldSet.className = "multi-button";
      fieldSet.id = "actionFieldSet"+actionNum;
      var fieldParent = divID.parentNode;
      fieldParent.appendChild(fieldSet);

      var form = document.createElement("form");
      form.setAttribute("method", "post");
      form.id = "actionForm"+actionNum;
      form.setAttribute("action", "/edit/actionForm"+actionNum);
      document.getElementById("actionFieldSet"+actionNum).appendChild(form);

      {
        let newElement = document.createElement('legend');
        newElement.innerHTML =  "Action " + (i+1);
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.value = actionData.name;
        newElement.type = "text";
        newElement.style.backgroundColor = "rgb(127,159,212)";
        newElement.style.width = 
        newElement.id = "actionName"+actionNum;
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('select');
        let newOption = new Option("Select Type");
        newOption.disabled = true;
        newOption = new Option("Preset Single Colour");
        newElement.add(newOption,0);
        newOption = new Option("Preset Multi-Colour");
        newElement.add(newOption,1); 
        newOption = new Option("Preset Effect");
        newElement.add(newOption,2);
        newOption = new Option("BPM Change");
        newElement.add(newOption,3);   
        newOption = new Option("Custom Single Colour");
        newElement.add(newOption,4);
        newOption = new Option("Custom Multi-Colour");
        newElement.add(newOption,5);
        newOption = new Option("Play/Stop Control");
        newElement.add(newOption,6);         
        newElement.id = "actionTypeSelect"+actionNum;
        newElement.selectedIndex = actionData.type;
        selectedType = actionData.type;
        newElement.addEventListener ("change",function() {changeType(newElement.id);});
        console.log("Added event listener for element id "+newElement.id);
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('select');
        newElement.id = "actionIndex"+actionNum;        
        newElement.style.visibility = isVisible[actionData.type][0];
        fieldSet.appendChild(newElement);
        fillPresetNames(selectedType,actionNum);
        newElement.selectedIndex = actionData.presetIdx;
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
      }
      {
        let newElement = document.createElement('select');
        let newOption = new Option("All");
        newElement.add(newOption,0);
        newOption = new Option("Tams");
        newElement.add(newOption,1); 
        newOption = new Option("Snares");
        newElement.add(newOption,2);
        newOption = new Option("Reps");
        newElement.add(newOption,3);   
        newOption = new Option("Surdus");
        newElement.add(newOption,4);
        newOption = new Option("Other");
        newElement.add(newOption,5);
        newOption = new Option("Lows");
        newElement.add(newOption,6);
        newOption = new Option("Mids");
        newElement.add(newOption,7);
        newOption = new Option("Highs");
        newElement.add(newOption,8);
        newOption = new Option("Others");
        newElement.add(newOption,9);
        newElement.id = "actionGroup"+actionNum;
        newElement.selectedIndex = actionData.group;
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        newElement.style.visibility = isVisible[actionData.type][1];   
        fieldSet.appendChild(newElement);
      }
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "BPM";
        newLabel.htmlFor = "actionBPM"+actionNum;
        newLabel.id = "actionLabBPM"+actionNum;
        newLabel.style.visibility = isVisible[actionData.type][2]; 
        fieldSet.appendChild(newLabel);
        
        let newElement = document.createElement('input');
        newElement.id = "actionBPM"+actionNum;
        newElement.type = "number";
        newElement.min="30";
        newElement.max="240";
        newElement.value = actionData.BPM;
        newElement.style.visibility = isVisible[actionData.type][2];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});        
        fieldSet.appendChild(newElement);
      }
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "Playing";
        newLabel.htmlFor = "actionIsPlaying"+actionNum;
        newLabel.id = "actionLabIsPlaying"+actionNum;
        newLabel.style.visibility      = isVisible[actionData.type][3];     
        fieldSet.appendChild(newLabel);

        let newElement = document.createElement('input');
        newElement.type = "checkbox";
        newElement.id = "actionIsPlaying"+actionNum;
        newElement.checked = actionData.isPlaying;
        newElement.style.visibility = isVisible[actionData.type][3];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});        
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "color";
        newElement.id = "actionColour"+actionNum;
        newElement.value = makeHexColour(actionData.cRGBred,actionData.cRGBgreen,actionData.cRGBblue);
        newElement.style.visibility = isVisible[actionData.type][4];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);      
      }
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "#Cols";
        newLabel.htmlFor = "actionGradHues"+actionNum;
        newLabel.id = "actionLabGradHues"+actionNum;
        newLabel.style.visibility     = isVisible[actionData.type][5];
        fieldSet.appendChild(newLabel);
        
        let newElement = document.createElement('input');
        newElement.type = "number";
        newElement.id = "actionGradHues"+actionNum;
        newElement.min="2";
        newElement.max="4";
        newElement.value = actionData.gradHueCount;
        newElement.style.visibility     = isVisible[actionData.type][5];
        newElement.addEventListener ("change",function() {changeGradCols(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "%";
        newLabel.htmlFor = "actionGrad%"+actionNum;
        newLabel.id = "actionLabGradPercent"+actionNum;
        newLabel.style.visibility     = isVisible[actionData.type][5];
        fieldSet.appendChild(newLabel);
        
        let newElement = document.createElement('input');
        newElement.type = "number";
        newElement.id = "actionGradPercent"+actionNum;      
        newElement.min="0";
        newElement.max="100";
        newElement.value = actionData.gradPercent;
        newElement.style.visibility     = isVisible[actionData.type][5];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {     
        let newElement = document.createElement('select');
        let newOption = new Option("Forward");
        newElement.add(newOption,0);
        newOption = new Option("Reverse");
        newElement.add(newOption,1);
        newOption = new Option("Bounce");
        newElement.add(newOption,2);
        newElement.id = "actionGradDir"+actionNum;
        newElement.selectedIndex = actionData.gradDir;      
        newElement.style.visibility     = isVisible[actionData.type][5];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "color";
        newElement.id = "actionGdC1"+actionNum;
        newElement.value = makeHexColour(actionData.gradRGB1r,actionData.gradRGB1g,actionData.gradRGB1b);
        newElement.style.visibility     = isVisible[actionData.type][5];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "color";
        newElement.id = "actionGdC2"+actionNum;
        newElement.value = makeHexColour(actionData.gradRGB2r,actionData.gradRGB2g,actionData.gradRGB2b);
        newElement.style.visibility     = isVisible[actionData.type][5];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "color";
        newElement.id = "actionGdC3"+actionNum;
        newElement.value = makeHexColour(actionData.gradRGB3r,actionData.gradRGB3g,actionData.gradRGB3b);
        if(actionData.gradHueCount > 2)
          newElement.style.visibility     = isVisible[actionData.type][5];
        else
          newElement.style.visibility="hidden";
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "color";
        newElement.id = "actionGdC4"+actionNum;
        newElement.value = makeHexColour(actionData.gradRGB4r,actionData.gradRGB4g,actionData.gradRGB4b);
        if(actionData.gradHueCount > 3)
          newElement.style.visibility     = isVisible[actionData.type][5];
        else
          newElement.style.visibility="hidden";
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});  
        fieldSet.appendChild(newElement);
      }      
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "Delay mS";
        newLabel.htmlFor = "actionDelay"+actionNum;
        newLabel.id = "actionLabDelay"+actionNum;
        newLabel.style.visibility      = isVisible[actionData.type][6];
        fieldSet.appendChild(newLabel);

        let newElement = document.createElement('input');
        newElement.id = "actionDelay"+actionNum;
        newElement.type = "number";
        newElement.min="50";
        newElement.max="5000";
        newElement.value = actionData.delay;
        newElement.style.visibility = isVisible[actionData.type][6];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newLabel = document.createElement('label');
        newLabel.className = "label";
        newLabel.innerText = "Loop";
        newLabel.htmlFor = "actionloop"+actionNum;
        newLabel.id =  "actionLabLoop"+actionNum;
        newLabel.style.visibility      = isVisible[actionData.type][7];           
        fieldSet.appendChild(newLabel);

        let newElement = document.createElement('input');
        newElement.type = "checkbox";
        newElement.id = "actionLoop"+actionNum;
        newElement.checked = actionData.loop;
        newElement.style.visibility        = isVisible[actionData.type][7];
        newElement.addEventListener ("change",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "button";
        newElement.className = "button btnTry";
        newElement.id = "actionTry"+actionNum;
        newElement.value = "Try";
        newElement.addEventListener ("click",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "button";
        newElement.className = "button btnInsert";
        newElement.id = "actionInsert"+actionNum;
        newElement.value = "Ins.";
        newElement.addEventListener ("click",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      {
        let newElement = document.createElement('input');
        newElement.type = "button"
        newElement.className = "button btnDelete";
        newElement.id = "actionDelete"+actionNum;
        newElement.value = "Del.";
        newElement.addEventListener ("click",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }
      if(isLast == "true")
      {
        let newElement = document.createElement('input');
        newElement.type = "button"
        newElement.className = "button btnAdd";
        newElement.id = "actionAdd"+actionNum;
        newElement.value = "Add";
        newElement.addEventListener ("click",function() {handleChange(newElement.id);});
        fieldSet.appendChild(newElement);
      }      
/*
      console.log("Action: Name="+actionData.name);
      console.log("Action: Type="+actionData.type);
      console.log("Action: PresetIndex="+actionData.presetIdx);
      console.log("Action: Group="+actionData.group);
      console.log("Action: BPM="+actionData.BPM);
      console.log("Action: isPlaying="+actionData.isplaying);
      console.log("Action: delay="+actionData.delay);
      console.log("Action: loop="+actionData.loop);
      console.log("Action: cRGB="+actionData.cRGBred+"/"+actionData.cRGBgreen+"/"+actionData.cRGBblue);
      console.log("Action: cGrad Count="+actionData.gradHueCount);
      console.log("Action: cGrad RGB1="+actionData.gradRGB1r+"/"+actionData.gradRGB1g+"/"+actionData.gradRGB1b);
      console.log("Action: cGrad RGB2="+actionData.gradRGB2r+"/"+actionData.gradRGB2g+"/"+actionData.gradRGB2b);
      console.log("Action: cGrad RGB3="+actionData.gradRGB3r+"/"+actionData.gradRGB3g+"/"+actionData.gradRGB3b);
      console.log("Action: cGrad RGB4="+actionData.gradRGB4r+"/"+actionData.gradRGB4g+"/"+actionData.gradRGB4b);
      console.log("Action: cGrad %="+actionData.gradPercent);      
      console.log("Action: cGrad Dir="+actionData.gradDir);
*/
    }

    function makeHexColour(Red, Green, Blue)
    {
        var hexString = "#";
        var hexPair = Red.toString(16);
        if (hexPair == "0") hexPair = "00";
        hexString += hexPair;

        hexPair = Green.toString(16);
        if (hexPair == "0") hexPair = "00";
        hexString += hexPair;

        hexPair = Blue.toString(16);
        if (hexPair == "0") hexPair = "00";
        hexString += hexPair;

        //console.log("Converted Colour = "+hexString);
        return hexString;
    }

    function changeType(id)
    {
      console.log("Element "+id+" has changed");
     
      var element = document.getElementById(id);
      var actionNum = id.substring(id.length-1,id.length);
      var index = element.selectedIndex;
      console.log("Action: "+actionNum+" New Type: " + index + ". Value = "+element.value);
      updateActionItems(index, actionNum);
      selectedType = index;
      //fillPresetNames(selectedType,actionNum);
      handleChange(id);
    }

    function fillPresetNames(type, actionNum)
    {
      if( (type == '0') || (type=='1') || (type=='2'))
      {
        console.log("Filling Preset Names for Action " + actionNum + ", Preset type "+type);

        let presetList = document.getElementById("actionIndex"+actionNum);

        // remove all exisiting from the actions field
        while (presetList.length > 0)
          presetList.remove(0);

        var i;
        var newOption;

        if (type=='0')
        {
          console.log("Preset=Solid");
          for (i=0; i<presetArray.solid.length;i++)
          {
            newOption = new Option(presetArray.solid[i]);
            presetList.add(newOption,i);         
          }
        } else if (type=='1')
        {
          console.log("Preset=Multi");
          for (i=0; i<presetArray.multi.length;i++)
          {
            newOption = new Option(presetArray.multi[i]);
            presetList.add(newOption,i);          
          }
        } else if (type=='2')
        {
          console.log("Preset=FX");        
          for (i=0; i<presetArray.fx.length;i++)
          {
            newOption = new Option(presetArray.fx[i]);
            presetList.add(newOption,i);            
          }
        }
        presetList.selectedIndex = 0;
      }
    }

    function changeGradCols(id)
    {
      var hues = document.getElementById(id).value;
      console.log("Change Grad Cols: Element "+id+" has changed to "+hues);

      var actionNum = id.substring(id.length-1,id.length);
      console.log("Action Num="+actionNum);

      var eColour = document.getElementById("actionGdC3"+actionNum);
      if(hues > 2)    
        eColour.style.visibility = "visible";
      else
        eColour.style.visibility = "hidden";
    
      eColour = document.getElementById("actionGdC4"+actionNum);
      if(hues > 3)
        eColour.style.visibility = "visible";
      else
        eColour.style.visibility = "hidden";

      handleChange(id);
    }

    function handleChange(id)
    {

      var element = document.getElementById(id);
      var type = element.type;
      console.log("Handle Change ID: "+id+", Type: "+type);

      var xmlhttp = new XMLHttpRequest();
      xmlhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200)
        {
          console.log("Response to Action Change Received");
          if (partsOrBreaks == "parts")
          {
            console.log("Requesting Part Actions");
            requestPartActions();
          }
          else if (partsOrBreaks == 'breaks')
          {
            console.log("Requesting Part Actions");
            requestBreakActions();
          }
          else (console.log("Handle Change: Error in partsOrBreaks"))
        }
      };

      var url = "/edit/actionChange?id="+id;

      if (type == "text")
      {
        url += "&value="+element.value;
        console.log("Number Value = "+element.value);        
      }
      else if (type == "number")
      {
        url += "&value="+element.value;
        console.log("Number Value = "+element.value);        
      }
      else if (type == "select-one")
      {
        url += "&index="+element.selectedIndex;
        console.log("Select Value = "+element.value);           
      }
      else if (type == "checkbox")
      {
        url += "&check="+element.checked;
        console.log("Checkbox = "+element.checked);  
      }
      else if (type == "color")
      {
        var hsvValue = element.value;
        hsvValue = hsvValue.substr(1,6);        
        url += "&value="+hsvValue;
        console.log("Colour Value = "+element.value);
      }
      else if (type == "button")
      {
        url += "&value="+element.value;
        console.log("Button Value = "+element.value);
      }
      console.log("Handle Change- GET->: "+url);

      xmlhttp.open("GET", url, true);
      xmlhttp.send();
    }
    
    function updateActionItems(actionType, actionNum)
    {
      console.log("UpdateActionItems. Type="+actionType+"  Num="+actionNum);

      let element = document.getElementById("actionIndex"+actionNum);
      element.style.visibility = isVisible[actionType][0];

      element = document.getElementById("actionBPM"+actionNum);
      element.style.visibility = isVisible[actionType][2];
      element = document.getElementById("actionLabBPM"+actionNum);
      element.style.visibility = isVisible[actionType][2];   

      element = document.getElementById("actionIsPlaying"+actionNum);
      element.style.visibility = isVisible[actionType][3];
      element = document.getElementById("actionLabIsPlaying"+actionNum);
      element.style.visibility = isVisible[actionType][3];      

      element = document.getElementById("actionColour"+actionNum);
      element.style.visibility = isVisible[actionType][4];

      let elementHues = document.getElementById("actionGradHues"+actionNum);
      elementHues.style.visibility = isVisible[actionType][5];
      element = document.getElementById("actionLabGradHues"+actionNum);
      element.style.visibility = isVisible[actionType][5];      

      element = document.getElementById("actionGradPercent"+actionNum);
      element.style.visibility = isVisible[actionType][5];
      element = document.getElementById("actionLabGradPercent"+actionNum);
      element.style.visibility = isVisible[actionType][5];      

      element = document.getElementById("actionGradDir"+actionNum);
      element.style.visibility = isVisible[actionType][5];

      element = document.getElementById("actionGdC1"+actionNum);
      element.style.visibility = isVisible[actionType][5];
      
      element = document.getElementById("actionGdC2"+actionNum);
      element.style.visibility = isVisible[actionType][5];

      element = document.getElementById("actionGdC3"+actionNum);
      if(elementHues.value > 2)    
        element.style.visibility = isVisible[actionType][5];
      else
        element.style.visibility = "hidden";

      element = document.getElementById("actionGdC4"+actionNum);
      if(elementHues.value > 3)
        element.style.visibility = isVisible[actionType][5];
      else
        element.style.visibility = "hidden";

      element = document.getElementById("actionDelay"+actionNum);
      element.style.visibility = isVisible[actionType][6];
      element = document.getElementById("actionLabDelay"+actionNum);
      element.style.visibility = isVisible[actionType][6];      

      element = document.getElementById("actionLoop"+actionNum);
      element.style.visibility = isVisible[actionType][7];
      element = document.getElementById("actionLabLoop"+actionNum);
      element.style.visibility = isVisible[actionType][7];
    }

    function hsl2rgb(h, s, l)
    {
      var m1, m2, hue;
      var r, g, b
      s /=100;
      l /= 100;
      if (s == 0)
          r = g = b = (l * 255);
      else
      {
        if (l <= 0.5)
            m2 = l * (s + 1);
        else
            m2 = l + s - l * s;
        m1 = l * 2 - m2;
        hue = h / 360;
        r = Math.round(HueToRgb(m1, m2, hue + 1/3));
        g = Math.round(HueToRgb(m1, m2, hue));
        b = Math.round(HueToRgb(m1, m2, hue - 1/3));
      }
      return {r: r, g: g, b: b};
    }

    function HueToRgb(m1, m2, hue)
    {
      var v;
      if (hue < 0)
          hue += 1;
      else if (hue > 1)
          hue -= 1;

      if (6 * hue < 1)
          v = m1 + (m2 - m1) * hue * 6;
      else if (2 * hue < 1)
          v = m2;
      else if (3 * hue < 2)
          v = m1 + (m2 - m1) * (2/3 - hue) * 6;
      else
          v = m1;

      return 255 * v;
    }
    </script>    
  </body>
  </html>
)rawliteral";

I have discovered that reducing the size of the const char array that is the web page, the page gets served OK (albeit without functionality now).
Maybe theres a limit to the array size?

Is it on the stack or a global? What stack size have you set for the task that's running this code?

My money is still on undefined behavior caused by pointer / array mismanagement.

It does appear that you can send a large array from PROGMEM directly using send_P() so it may not appear on the stack if this is relevant to your code:

const char index_html[] PROGMEM = "..."; // large char array, tested with 14k
request->send_P(200, "text/html", index_html);

However, PROGMEM is an AVR relic and maybe creating a global array would be better on an ESP32.

1 Like

PROGMEM on the ESP32 does nothing. that's its definition

Constant data is stored in flash by default and internally the data is mapped into the CPU address space via a transparent cache. for this to work the const keyword needs to be used

May be when the data is really large and ESPAsyncWebServer gets confused somewhere ?

with large content you should use Chunked Response

1 Like

Hi 6v6gt and thanks for your reply.

My array is probably more like 36K long - perhaps that's the issue.
Also, I'm using request->send and not request->send_P but I don't suppose thats going to make a difference.

Is it on the stack or a global? What stack size have you set for the task that's running this code?
Its a global const char[ ].
I havent set a stack size at all - in fact I never have. Is this a common thing to have to do?

Jim

Maybe you could also split the constant array yourself into N parts and just send them sequentially. But
this looks good.

actually it might.

although _P does not do anything, the library does not have a ::send() method taking a const char * and so possibly your rawliteral gets moved into memory as a String and then flushed out.

using _P would mean using a different piece of code to get to the data and read from flash properly...

worth trying

Update: So I stripped out everything I could from the rawliteral string - no CSS, no console logs etc, and slowly added functionality so the string was getting bigger and bigger.
I now have the system working again with full functionality so I am sure its to do with the size of the array being parsed to {send}.

Should I look at modifying the heap size first do we think, or looking into chunked response or maybe send_P? Whats the general opinion?

I was also considering storing the array as a file and sending it from SPIFFS or similar...

So many choices! lol
Cheers,
Jim

Amazing

I changed

    request->send(200, "text/html", action_edit_html);

to

    request->send_P(200, "text/html", action_edit_html);

and now no more crashing. Well done J-M-L Jackson!

cool :slight_smile:

if the files are static, that's an option. At the end of the day it's also reading from flash memory. The nice thing is that you can manage those files without modifying the code, so it might ne more flexible

I've actually got an external SD card on the ESP32 as well, so it wont take up the flash.
As you say, be nicer to being able to manage them without uploading.
Most of the pages are dynamically built, but its all the CSS and JS that is taking up the room now.

Thanks very much for your help.
Jim

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.