My ESP8266 stacktrace is malformed. What am i doing wrong?

Hi all,

i'm new here. i read the forum guidelines. i was directed here from this github post in which i displayed most of my problem.

i have a "nodeMCU ESP8266 v3 lolin(ESP-12E)" board.
i've used Arduino IDE 1 and 2, but now i'm using 2.

i' m trying to get this "cresta" 7x160 LED marquee working again.
So far, i have

  • an AcccessPoint showing a configuration page, and using HTTP POST to send the settings to server.
  • reading/writing the configuration file from/to LittleFS
  • a DNSServer returning the same IP for all requests
  • Serial comms for allowing adjusting some values (actually a leftover from early stages, so could be removed)
  • The ESP hardware timer at 10msec to call an ISR selecting the next LED-row, and use SPI to send out the bitstring.
  • A boolean flag between ISR and main loop to indicating to create the next image.(double buffer)
  • i'm calling yield() at some places, and try to disable the timer when interacting with LittleFS.

At some point this code seemed stable, but i now realize when recompiling, uploading and uploading new data to LittleFS(using the IDE plugin, of which i can't find the link a.t.m.), the stability seems to differ.

But the config file can always be read, and the ledbar is scrolling the texts. it's only when i'm interacting with the website and reading/writing to littleFS, that the controller may generate a stacktrace/reboot.

What i'm noticing is that sometimes during HTTP POST-ing the configuration, the ESP will throw a stacktrace. But the stacktrace seems malformed, as shown in the github post linked above.
it is

  1. not showing the registers (not sure if this part is optional)
  2. printing a seconds stacktrace over the 1st, when the 1st isn't finished.

i wanted to use the ESP Exception Decoder by dankeboy36 to find out more about what's going wrong, but my stack has a shape which isnt' recognized.

This is most of the code:

Wifi_AP_LichtKrant_v10.ino

/**

   Board = NodeMCU 1.0 (ESP-12E)
   
   NodeMCU V3 ESP8266 (Lolin)

   Code for controlling a

    CRESTA AS-0216 LED MARQUEE

   Provides WIFI AccessPoint "Lichtkrant".
   Configuration via html form at "192.168.4.1"

   == V1 == (previous)
   D1 = Cresta CLK
   D2 = Cresta DATA
   D3 = Shift Reg OE

   D5 = Shift Reg CLK  (SPI CLK )
   D7 = Shift Reg DATA (SPI MOSI)

   == V2 ==
   D1 = Shift Reg CLK
   D2 = Shift Reg DATA
   D3 = Shift Reg OE

   D5 = Cresta CLK (SPI CLK )
   D7 = Cresta DATA(SPI MOSI)

   Different from V1;
     V1 used SPI for vertical line selection on external 8bit shift register for selecting horizontalline, and DO for shifting data into CRESTA
     V2 uses SPI for shifting horizontal-data into Cresta. Using DO for interecting with extrnal 8 bit shift register

  == V3 ==
  Code as V2, but 
  now led refreshing is timed using a 1 ms Timer
  
  == V4 ==
  * Uses ESP_TIMER
  * Scanline timer interval can be changed by keyboard
  * An image is rendered for each frame, and asigned before scanline 0
  * !Removed the option to change render interval, because only 1 image per frame gives a clean scroll

  == V5 ==
  * Use littleFS for Form template file
  * Show setup info at startup
  * let SPI shift 12 bytes instead of 11
  == V6 ==
  * Show setting form based on file template
  * read/write settings to file
  * DNS server returning own IP
   !! the program crashes with a stackdump. i suspect it's the program run during the hardware timer

  == V7 ==
  * move more preparation to the loop() and use 
  == V8 ==
  * make Animation class
  * Use char[] instead of String to ensure fixed memory usage and not have Stack overwriting exceotion(28)
  * Form reads and writes the strings. on/off works. The speed, transitions etc aren't working yet.
  == V9 ==
  * change to binary file access for settings file, to save runtime memory
  * Activated DNSServer to route al names back to my own IP
  * Added a dot to indacte progress
  * moved the image 1 pixel further into the SPI buffer, to use all LEDS.
  * Added 'Word for Word' and ' Wave' transition
  * 'Word for word' only slides if the word is longer than the screen.
  * Fix saving the on/off Form settng for the first row to file
  * Improve Wobble effect per speed.
  == V10 ==
  * Add method to Bold(v), Blink(K), Wave(g), Pause(P) for bot 'shift' and 'word-for-word'
  * Add helptext to HTML

 USE THE LittleFS uploader tool, to upload the contents in /data into the Flash LittleFS
========================================================================================

 In Arduino IDE 1. 
   From Tools menu 'ESP8266 LittleFS Data Upload'
 Description here: https://randomnerdtutorials.com/install-esp8266-nodemcu-littlefs-arduino/

 In Arduino IDE 2. type   
   Ctrl+Shift+P  and select 'Upload LittleFS to Pico/ESP8266'
 Tool here: https://github.com/earlephilhower/arduino-littlefs-upload

*/

#include <avr/pgmspace.h>

#include <ESP8266WiFi.h>  // board: NodeMCU v1.0 (ESP-12E Module)
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <SPI.h>

#include "./DNSServer.h"                  // Patched lib 
#include "./LEDConfig.h"

#include "ESP8266TimerInterrupt.h"  
// To be included only in main(), .ino with setup() to avoid `Multiple Definitions` Linker Error
#include "ESP8266_ISR_Timer.h"                //https://github.com/khoih-prog/ESP8266TimerInterrupt

#include "Animation.h"

#ifndef APSSID
#define APSSID "Lichtkrant"
#define APPSK "looplicht"  // <-- must be longer than 8, otherwise the WIFI SSID becomes " Farylink_..."
#endif

/* Set these to your desired credentials. */
const char        *ssid = APSSID;
const char        *password = APPSK;
const byte        DNS_PORT = 53;          // Capture DNS requests on port 53
DNSServer         dnsServer;              // Create the DNS object 
IPAddress         myIP;
ESP8266WebServer  server(80);
LEDConfig         ledConfig;

/* The ESP8266's only Timer */
ESP8266Timer      ITimer;
void IRAM_ATTR timer_handler_leds();

#define PIN_CLK                     D1
#define PIN_DAT                     D2
#define PIN_OUTPUT_ENABLE_ACTIVELOW D3

#define MAX_SETTINGS             20

#define SCANLINE_SPEED_VERY_FAST 1000
#define SCANLINE_SPEED_FAST      1500
#define SCANLINE_SPEED_MEDIUM    2000
#define SCANLINE_SPEED_SLOW      3000

// Showing setup info
#define SHOW_SETUP_INFO_MSEC      20000
#define SHOW_SETUP_SCANLINE_SPEED SCANLINE_SPEED_SLOW

/** 
   SPI data buffer
   5 LEDS x 19 characters = 95 LEDS   8*12=96 BITs, so 12 Bytes
*/
#define MAX_SCANLINES 7
#define MAX_SPIDATA_BYTES 12 
#define MAX_SPIDATA_LONG 3
#define ONSCREEN_ID (onscreen_id)
#define OFFSCREEN_ID (onscreen_id ^ 1)

// ensure the byte arrays align to 4 byte boundaries! because SPI requires it. otherwise Exception(9) will happen.
uint32_t SPI_buffer[2][MAX_SCANLINES][MAX_SPIDATA_LONG];
int      onscreen_id = 1;
volatile boolean offscreen_is_ready = false;
volatile unsigned int scanline = 0;

/** 
  Render data
*/
long              scanline_timer_interval_ms = 3000;
byte              screen_buffer[MAX_COLUMN]; // Each column is a byte. Every character is 5 wide, and 7 tall.

struct LEDConf    currentConf;
struct LEDConf    tempConf;
Animation         animation((byte*)screen_buffer, MAX_COLUMN, currentConf);
int               activeAnimationNr = -1;


/**
  Show the current configuration in a HTML FORM
*/
void handleRoot() {

  /*
   * Read the template file
   */
  String html_header ="";
  String html_row    ="";
  String html_footer ="";
  char k;

  ITimer.disableTimer();

  File file = LittleFS.open("/config.html", "r");
  while(file.available()){
    k = file.read();
    if (k != '~')
      html_header += k;
    else
      break;
  }

  while(file.available()){
    k = file.read();
    if (k != '~')
      html_row += k;
    else
      break;
  }
  
  while(file.available()){
    k = file.read();
    html_footer += k;
  }

  file.close();

  /*
   * Construct the reply
   */
  String message = "";
  message += html_header;

  for (int t = 0; t < MAX_SETTINGS; t++) {
    ledConfig.readRecord(t, &tempConf);

    String row = html_row;
    char num[10];
    sprintf(num, "%02d", t);
    row.replace("${nr}", num);
    
    row.replace("${actief}", (tempConf.on ? "checked" : "" ) );
   
    row.replace("${tekst}", tempConf.text);

    row.replace("${verloop1}", (tempConf.transition == 1 ? "selected" : "") );
    row.replace("${verloop2}", (tempConf.transition == 2 ? "selected" : "") );
    row.replace("${verloop3}", (tempConf.transition == 3 ? "selected" : "") );

    row.replace("${snelheid1}", (tempConf.speed == 1 ? "selected" : "") );
    row.replace("${snelheid2}", (tempConf.speed == 2 ? "selected" : "") );
    row.replace("${snelheid3}", (tempConf.speed == 3 ? "selected" : "") );
    row.replace("${snelheid4}", (tempConf.speed == 4 ? "selected" : "") );
    message += row;
    yield();
  }
  message += html_footer;
  server.send(200, "text/html", message);

  ITimer.enableTimer();
}

/**
 * Save the HTML Form data to Flash
 */
void handleForm() {
  struct LEDConf myConf;

  if (server.method() != HTTP_POST) {
    String message = "Method Not Allowed: ";
    message += server.method();
    message += ". Post is ";
    message += String(HTTP_POST);
    server.send(405, "text/plain", message);
  } else {

    ITimer.disableTimer();
    
    ledConfig.saveFormToFile(server);
        
    String message = "POST form was:\n";
    for (uint8_t i = 0; i < server.args(); i++) {
      message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
    }
    server.send(200, "text/plain", message);
    
    ITimer.enableTimer();
  }
}

/**
 * Handle invalid HTML page request
 */
void handleNotFound() {
  Serial.print("Unkown path accessed:");Serial.println(server.uri());
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}



void setup() {
  
  delay(1000);
  Serial.begin(9600);

  if(!ledConfig.init()){
    return;
  }

  /* setup WIFI AP */
  WiFi.softAP(ssid, password);
  myIP = WiFi.softAPIP();
  
  Serial.println();
  Serial.print("AP IP address: ");
  Serial.println(myIP);

  // if DNSServer is started with "*" for domain name, it will reply with
  // provided IP to all DNS request
  dnsServer.start(DNS_PORT, "*", myIP); 

  server.on("/", handleRoot);
  server.on("/set_config", handleForm);
  server.onNotFound(handleNotFound);
  server.begin();

  Serial.println("HTTP server started");


  /* Prepare pinout */
  {
    int t;
    t = PIN_CLK; pinMode(t, OUTPUT); digitalWrite(t, LOW);
    t = PIN_DAT; pinMode(t, OUTPUT); digitalWrite(t, LOW);
    t = PIN_OUTPUT_ENABLE_ACTIVELOW; pinMode(t, OUTPUT); digitalWrite(t, LOW);
    /* D5,D7 are used by SPI */
  }

  /* Start HW SPI for pins D5-D8 */
  SPI.begin();
  SPI.beginTransaction(SPISettings(15000000, MSBFIRST, SPI_MODE0)); // ESP8266 V3 Lolin

  /* Setup initial text */
  sprintf(currentConf.text, "## WIFI: Lichtkrant ## Wachtwoord: looplicht ## Instellen: http://%d.%d.%d.%d/ ##", myIP[0], myIP[1], myIP[2], myIP[3]);
  currentConf.on = true;
  currentConf.speed = CF_SP_SLOW;
  currentConf.transition = CF_TX_SHIFTLEFT;

  animation = Animation((byte*)screen_buffer, MAX_COLUMN, currentConf);
  // render at least once before the timer activates
  animation.render();
  
  /* Setup LED scanline timer */
  if (ITimer.attachInterruptInterval(SHOW_SETUP_SCANLINE_SPEED, timer_handler_leds))
  {
    Serial.print(F("Starting  ITimer OK"));
  }
  else
    Serial.println(F("Can't set ITimer correctly. Select another freq. or interval"));


  /* Show Serial menu */
  Serial.println("");
  Serial.println("                         +   - ");
  Serial.println(" ------------------------------");
  Serial.println(" Character spacing       1 / q ");
  Serial.println(" Wobble                  2 / w ");
  Serial.println(" scanline duration(usec) 3 / e ");
  Serial.println(" scanline duration(usec) 4 / r ");
 
  // should start the process
  offscreen_is_ready = false;
}


/**
 
  L O O P

**/
void loop() {
    
  dnsServer.processNextRequest(); 
  server.handleClient();

  
  /* handle user input */
  if (Serial.available())
  {
    bool set = false;
    char k = Serial.read();
    switch (k) {

      case 'q': set = true;
        animation.CHAR_SPACING--;
        if (animation.CHAR_SPACING < 0) {animation.CHAR_SPACING = 0;} break;
      case '1': set = true; 
        animation.CHAR_SPACING++; break;
      
      case 'w': set = true; animation.wobble_size -= 0.5; 
        if (animation.wobble_size < 0) {animation.wobble_size = 0;} break;
      case '2': set = true; animation.wobble_size += 0.5; break;

      case 'e': scanline_timer_interval_ms -= 1000; 
        if (scanline_timer_interval_ms<100) scanline_timer_interval_ms=100;
        Serial.print("Timer interval: ");
        Serial.println(scanline_timer_interval_ms );
        ITimer.setInterval(scanline_timer_interval_ms, timer_handler_leds);
      break;
      case '3': scanline_timer_interval_ms += 1000; 
        Serial.print("Timer interval: ");
        Serial.println(scanline_timer_interval_ms );
        ITimer.setInterval(scanline_timer_interval_ms, timer_handler_leds);
      break;

      case 'r': scanline_timer_interval_ms -= 100; 
        if (scanline_timer_interval_ms<70) scanline_timer_interval_ms=70;
        Serial.print("Timer interval: ");
        Serial.println(scanline_timer_interval_ms );
        ITimer.setInterval(scanline_timer_interval_ms, timer_handler_leds);
      break;      
      case '4': scanline_timer_interval_ms += 100; 
        Serial.print("Timer interval: ");
        Serial.println(scanline_timer_interval_ms );
        ITimer.setInterval(scanline_timer_interval_ms, timer_handler_leds);
      break;

    }
    if (set)
    {
      animation.resetAnimation();

      animation.render();
    }
  }


  /* Render the text using 'font characters' to the text screen_buffer. 
     Then render the screen_buffer to SPI buffer.

     This is separate from I/O with the LED screen.
  */
  if (!offscreen_is_ready) {

    animation.advanceAnimation();

    /* Load new animation settings */
    if (animation.isFinished())
    {
      bool ret;
      int counter = 0; // to exit when none is active
      ITimer.disableTimer();      
      do {
        counter++;
        activeAnimationNr++;
        if (activeAnimationNr >= MAX_SETTINGS) 
        {
          activeAnimationNr = 0;
        }
        ret = ledConfig.readRecord(activeAnimationNr, &currentConf);
        if (counter == 10) 
        {
          yield();
        }
      } while (!currentConf.on && counter <= MAX_SETTINGS);
      
      if (counter > MAX_SETTINGS)
      {
        strcpy(currentConf.text, "! Niets geconfigureerd om te tonen");
        currentConf.speed = CF_SP_SLOW;
        currentConf.transition = CF_TX_SHIFTLEFT;
      } else if (!ret)
      {
        sprintf(currentConf.text, "! Fout bij laden van nummer %d", activeAnimationNr);
        currentConf.speed = CF_SP_SLOW;
        currentConf.transition = CF_TX_SHIFTLEFT;
      }
      ITimer.enableTimer();

      ITimer.setInterval(ledConfig.animationSpeedIdxToMillis(currentConf.speed), timer_handler_leds);
      animation = Animation((byte*)screen_buffer, MAX_COLUMN, currentConf);

    }
    
    animation.render();

    prepareMarqueeSpiDataOffscreen();

    // signal the ISR
    offscreen_is_ready = true;
  }
  
}


/*
   Walks the screen_buffer bitmap 
   Prepare scanline buffer so that it can easily be sent using SPI.
   Prepares all lines at once.
*/
void prepareMarqueeSpiDataOffscreen() {

  for (int line = 0; line < MAX_SCANLINES; line++) {
    // for a single scanline
    char scanline_bit = 0b01000000 >> line;

    uint8_t *p = (uint8_t*)SPI_buffer[OFFSCREEN_ID][line];

    // for each column in screen buffer
    for (int scr_idx = 0; scr_idx < MAX_COLUMN; scr_idx++) {

      bool val = (screen_buffer[scr_idx] & scanline_bit) != 0;
      int bit_nr  = (scr_idx+1) % 8; // 19*5 misses 1 column to be a multiple of 8.
      int byte_nr = (scr_idx+1) / 8; // So, there's one column to shift extra towards the back of the spi-buffer. Hence (scr_idx+1)
      
      if (val)
        *(p + byte_nr) |= (uint8_t)(128 >> bit_nr);  // bit set
      else
        *(p + byte_nr) &= (uint8_t)~(128 >> bit_nr); // bit reset
    }
  }

}

/**
 * Timer interrupt routine
 * Interacting with LED screen.
 * 
 * At scanline 0, the offscreen buffer is swapped.
 * The frame rate(Hz) = 1/(scanline_timer*7)
 */
void IRAM_ATTR timer_handler_leds() {

  if (scanline == 0) {
    /* Switch offscreen to onscreen SPI buffer */   
    if (offscreen_is_ready) {
      onscreen_id = onscreen_id ^ 1;
      offscreen_is_ready = false;
    }
   }
  
  // Send I/O and update scanline 
  sendMarqueeSpiDataOnscreen(scanline);

  // next scanline
  scanline++;
  if (scanline > MAX_SCANLINES)
    scanline = 0;
}


/*
   Send row data via SPI
   Enabling a single row at each call
   Scanline increments each call from BOTTOM to TOP

   Requires ESP8266 library containing 'SPI.writeBytes'

   Transpose the screen buffer which is column oriented into a buffer which is row-oriented.

    The LED shift register shifts in from the LEFT, 
    so the SPI buffer must be a flipped image. 
    The most right LED must be shifted in first.
*/
void sendMarqueeSpiDataOnscreen(unsigned int line) {

  // horizontal line off
  digitalWrite(PIN_OUTPUT_ENABLE_ACTIVELOW, 1 );
  delayMicroseconds(10);

  // initialize the external shift register
  if (line == 0)
  {
    // shift in the first '1' (shifting out the previous 1 into place 8)
    // and give a clock signal, because the shift register has both pins connected, the latch lags 1 clock cycle.
    digitalWrite(PIN_DAT, 1); // set data 1
    digitalWrite(PIN_CLK, 0); // pulse the clk
    digitalWrite(PIN_CLK, 1);
    digitalWrite(PIN_CLK, 0);
    digitalWrite(PIN_DAT, 0); // reset data to 0, so that next triggers clock in a '0'
  }

  // transfer with SPI  
  // Using unit32 buffer to avoid getting exception(9) 'alignment error'.
  uint8_t *addr = (uint8_t *)SPI_buffer[ONSCREEN_ID][line];
  SPI.writeBytes( (const uint8_t*) addr, MAX_SPIDATA_BYTES);

  // shift '1' to next scanline
  digitalWrite(PIN_CLK, 1);
  digitalWrite(PIN_CLK, 0);

  // horizontal line on
  digitalWrite(PIN_OUTPUT_ENABLE_ACTIVELOW, 0 );
} 

Animation.h

#ifndef Animation_h
#define Animation_h

#include <Arduino.h>
#include "LEDConfig.h"
#include "font.h"

#define CHAR_WIDTH               5
#define MAX_COLUMN               (19 * CHAR_WIDTH)
#define TEXT_BUFFER_SZ           CF_MAX_TEXT_SZ
#define BLINK_TIME                100000 // 0.1 second in microseconds

/* escape characters 
  Blinken
  Pauze
  Vet
  Golven
*/

#define DYN_CHAR_ESCAPE '\\'
#define DYN_CHAR_BLINK   'k'
#define DYN_CHAR_BLINK_U 'K'
#define DYN_CHAR_WAIT    'p'
#define DYN_CHAR_WAIT_U  'P'
#define DYN_CHAR_FAT     'v'
#define DYN_CHAR_FAT_U   'V'
#define DYN_CHAR_WAVE    'g'
#define DYN_CHAR_WAVE_U  'G'

/* storage bits */
#define DYN_MARKER_BLINK  0x1
#define DYN_MARKER_WAIT   0x2
#define DYN_MARKER_FAT    0x4
#define DYN_MARKER_WAVE   0x8

class Animation
{
public:
  Animation(byte *screen_buffer, int bufferbytecount, LEDConf &conf);
  
  void resetAnimation();

  void advanceAnimation();

  bool isFinished();

  void render();

  int     CHAR_SPACING = 1;
  double  wobble_angle_rd = 0;          // used for the wobble. in radians
  float   wobble_size = 0;
  float   period = 2;

private:
  int     buffersize;             // each index is a column
  byte*   buffer;

  short  transition;
  short  speed;
  
  float  display_text_x;            // shifts the text to right in column units. i.e. it will become negative. 0 is leftmost column
  int    text_len_px;               // length of text in pixels
  int    word_start_px;
  int    blank_frame_count;         // amount of animation cycles the animation freezes and shows a blank screen
  int    wait_frame_count;          // amount of animation cycles the animation freezes

  void   init_display_buffer(char* config_text);    // parses dynamic text into display_text and display_dyn buffers.
  char   display_text[TEXT_BUFFER_SZ]; // the text to render
  char   display_dyn [TEXT_BUFFER_SZ]; // text animation info. See DYN_MARKER_* bits

  void renderShift();

  void renderWords();

};

#endif 

Animation.cpp

#include "./Animation.h"


Animation::Animation(byte *screen_buffer, int size, LEDConf &config)
{
  buffer = screen_buffer;
  buffersize = size;

  init_display_buffer(config.text);

  text_len_px = strlen(display_text) * (CHAR_WIDTH + CHAR_SPACING);

  transition = config.transition;
  speed = config.speed;
  resetAnimation();
}


void Animation::init_display_buffer(char* config_text)
{
  memset(display_text, 0, TEXT_BUFFER_SZ);
  memset(display_dyn, 0, TEXT_BUFFER_SZ);
  
  bool fat   = false;
  bool blink = false;
  bool wave  = false;

  int dst = 0; // storage pointer for text and dynamic bits
  for (int src = 0; src < TEXT_BUFFER_SZ -1; src++)
  {

    // apply current animation bits
    if (fat)   *(display_dyn + dst) |= DYN_MARKER_FAT;
    if (blink) *(display_dyn + dst) |= DYN_MARKER_BLINK;
    if (wave)  *(display_dyn + dst) |= DYN_MARKER_WAVE;

    // escape character in source?
    if (*(config_text + src) == DYN_CHAR_ESCAPE) 
    {
      src++;
      if (src >= TEXT_BUFFER_SZ)
        break;

      // test escaped character
      switch(*(config_text + src))
      {
        case DYN_CHAR_BLINK:  // toggle activation on 'b' or 'B'
        case DYN_CHAR_BLINK_U:
              blink = !blink;
              break;
        case DYN_CHAR_FAT:  // toggle activation on 'v' or 'P'
        case DYN_CHAR_FAT_U:
              fat = !fat;
              break;
        case DYN_CHAR_WAIT: // active once on 'p' or 'P'
        case DYN_CHAR_WAIT_U:
              *(display_dyn + dst) |= DYN_MARKER_WAIT;
              break;
        case DYN_CHAR_WAVE: // active once on 'g' or 'G'
        case DYN_CHAR_WAVE_U:
              wave = !wave;
              break;
        case DYN_CHAR_ESCAPE:
        default:
              /* copy any unknown character, thus also the escaped \ */
              *(display_text + dst) = *(config_text + src);
              dst++;
      }
    }
    else
    {
      // copy character
      *(display_text + dst) = *(config_text + src);
      dst++;
    }
  }
}


void Animation::resetAnimation()
{
  
  switch (speed)
  {
    case CF_SP_SLOW:    period = 2;
    break;
    case CF_SP_NORMAL:  period = 1;
    break;
    case CF_SP_FAST:    period = 0.75;
    break;
    case CF_SP_VERYFAST:period = 0.5;
    break;
  }

  switch (transition)
  {
    case CF_TX_WAVELEFT: 
        wobble_size = 1.5;
        wait_frame_count = 0;
    case CF_TX_SHIFTLEFT:
        // initial text position
        display_text_x = MAX_COLUMN;
        wobble_size = 1.5;        
        wait_frame_count = 0;
        break;
    case CF_TX_WORD_FOR_WORD:
        display_text_x = -(MAX_COLUMN/2); // lets the first word be hidden for a while
        word_start_px = 0;
        wobble_size = 1.5;        
        blank_frame_count = 0;
        break;
  }
}


void Animation::advanceAnimation()
{
  // even wave while waiting
  switch (speed)
  {
    case CF_SP_SLOW:    wobble_angle_rd += ((10/360.0)*6.28) /1;
    break;
    case CF_SP_NORMAL:  wobble_angle_rd += ((10/360.0)*6.28) /1.5;
    break;
    case CF_SP_FAST:    wobble_angle_rd += ((10/360.0)*6.28) /2;
    break;
    case CF_SP_VERYFAST:wobble_angle_rd += ((10/360.0)*6.28) /3;
    break;
  }

  switch (transition)
  {
    case CF_TX_WAVELEFT:
    case CF_TX_SHIFTLEFT:
        // to move or not to move
        if (wait_frame_count)
          wait_frame_count--;
        if (!wait_frame_count) //must be separate if, otherwise the initial step after wait isn't performed
          display_text_x--;

        break;
    case CF_TX_WORD_FOR_WORD:
        if (blank_frame_count) 
        {
          blank_frame_count--;
        }
        else
        {
          display_text_x++;
          //wobble_angle_rd = wobble_angle_rd + (5/180.0)*3.14;        
        }
        break;
  }
}


bool Animation::isFinished()
{
  switch (transition)
  {
    case CF_TX_WAVELEFT: 
    case CF_TX_SHIFTLEFT:
        return (display_text_x < -text_len_px);
        break;
    case CF_TX_WORD_FOR_WORD:
        return (display_text_x > text_len_px);
        break;
    default:
        return true;
  }
}


void Animation::render()
{
  switch (transition)
  {
    case CF_TX_WAVELEFT: 
    case CF_TX_SHIFTLEFT:
        renderShift();
        break;
    case CF_TX_WORD_FOR_WORD:
        renderWords();    
        break;
  }  
}

/*
     _______. __    __   __   _______ .___________.
    /       ||  |  |  | |  | |   ____||           |
   |   (----`|  |__|  | |  | |  |__   `---|  |----`
    \   \    |   __   | |  | |   __|      |  |     
.----)   |   |  |  |  | |  | |  |         |  |     
|_______/    |__|  |__| |__| |__|         |__|     

https://textkool.com/en/ascii-art-generator?hl=full&vl=full&font=Star%20Wars&text=SHIFT
*/
void Animation::renderShift()
{
  //counter line
  int progress_px = (buffersize/(buffersize + text_len_px + 1.0)) * ((display_text_x - buffersize) * -1.0);

  bool blink_on = (micros() / BLINK_TIME) % 2 == 0;


  for (int kol = 0; kol < buffersize; kol++)
  {
    int kol_in_string   = kol - display_text_x;
    int char_index      = kol_in_string / (CHAR_WIDTH + CHAR_SPACING);
    int char_index_bit  = kol_in_string % (CHAR_WIDTH + CHAR_SPACING);
    double v = 0;
    bool show;

    if (wobble_size > 0)
      v = sin( (-wobble_angle_rd + (((double)kol / buffersize) * period * 2 * 3.14))) * wobble_size;

    // inside the string
    if (kol_in_string >= 0 && char_index >= 0 && char_index < strlen(display_text))
    {
      // found a wait marker?
      if (kol == 0 && !wait_frame_count)
      {
        // only on the first column of the character
        if (char_index_bit == 0 && (display_dyn[char_index] & DYN_MARKER_WAIT))
          wait_frame_count = 100;
      }

      //obtain the character number
      byte ascii_nr = display_text[char_index];

      bool blink_char = display_dyn[char_index] & DYN_MARKER_BLINK;
      bool fat_char   = display_dyn[char_index] & DYN_MARKER_FAT;
      bool wave_char  = display_dyn[char_index] & DYN_MARKER_WAVE;

      show = ( blink_on && blink_char ) || !blink_char;

      //obtain the bitpattern
      const unsigned char* charBitmap = Font8x5[ascii_nr];

      // for a single column, copy the bits
      int char_kol = kol_in_string % (CHAR_WIDTH + CHAR_SPACING);
      if (show && char_kol < CHAR_WIDTH)
      {
        byte cbm = charBitmap[char_kol];

        if (wave_char || transition == CF_TX_WAVELEFT)
        {
          // apply sine wave
          if (v > 0)
            cbm = cbm >> (int)abs(v);
          else if (v < 0)
            cbm = cbm << (int)abs(v);
        }

        // use the previous column to make it bold, for convenience
        if (fat_char && (kol - 1) >= 0)
          *(buffer + kol - 1) |= cbm;

        if (kol == progress_px)
          cbm |= 0b01000000;

        *(buffer + kol) = cbm;
      }
      else
      {
        byte cbm = 0;
        if (kol == progress_px)
          cbm |= 0b01000000;

        // blank column
        *(buffer + kol) = cbm;
      }
    }
    else
    {
      byte cbm = 0;
      if (kol == progress_px)
        cbm |= 0b01000000;

      // outside text
      *(buffer + kol) = cbm;
    }

  }
}

/*
____    __    ____   ______   .______       _______  
\   \  /  \  /   /  /  __  \  |   _  \     |       \ 
 \   \/    \/   /  |  |  |  | |  |_)  |    |  .--.  |
  \            /   |  |  |  | |      /     |  |  |  |
   \    /\    /    |  `--'  | |  |\  \----.|  '--'  |
    \__/  \__/      \______/  | _| `._____||_______/ 
 https://textkool.com/en/ascii-art-generator?hl=full&vl=full&font=Star%20Wars&text=WORD                                                    
*/
void Animation::renderWords()
{
  //counter line
  int progress_px = (buffersize/(text_len_px + 1.0)) * display_text_x;

  bool blink_on = (micros() / BLINK_TIME) % 2 == 0;

  // determine where the word ends
  int index    = display_text_x / (CHAR_WIDTH + CHAR_SPACING);
  while (index < strlen(display_text)-1 && display_text[index] != ' ') {
    index++;
  }

  // if the end of the word is outside the screen, make it start on a new screen
  if ( (index * (CHAR_WIDTH + CHAR_SPACING)) - word_start_px > buffersize)
  {
    // find the beginning of the word
    int index2 = display_text_x / (CHAR_WIDTH + CHAR_SPACING);
    while (index2 > 0 && display_text[index2 - 1] != ' ') {
      index2--;
    } 
    // make it display until the beginning of that word. 
    // unless the word is longer than a screen,    
    if ( (index - index2)*(CHAR_WIDTH + CHAR_SPACING) < buffersize)
    {
      word_start_px = index2 *(CHAR_WIDTH + CHAR_SPACING);
      // introduce a blank page and wait
      blank_frame_count = 20;      
    }
  }

  int word_end_px = ((index+1) * (CHAR_WIDTH + CHAR_SPACING))-1; // the right column of that character (including space)
  int right_align_px;

  // if text is outside the screen and 
  // display_text_x points at last column of the last word,
  // then let the sentence start with next word.
  if ( (display_text_x - word_start_px) > buffersize &&
         word_end_px == display_text_x)
  {
    word_start_px = display_text_x;
    // introduce a blank page and wait
    blank_frame_count = 20;
  }

  for (int kol = 0; kol < buffersize; kol++)
  {
    // if word_end_px ends beyond the last column, 
    // then shift the whole text left so that it is right-aligned.
    if ( (display_text_x - word_start_px) > buffersize)
    {
      right_align_px = ( (display_text_x - word_start_px) - buffersize);
    }
    else
    {
      right_align_px = 0;
    }

    int kol_in_string = kol + word_start_px + right_align_px;
    int char_index    = kol_in_string / (CHAR_WIDTH + CHAR_SPACING);
    double v = 0;
    bool show;

    if (wobble_size > 0)
      v = sin( (-wobble_angle_rd + (((double)kol / buffersize) * period * 2 * 3.14))) * wobble_size;

    if (blank_frame_count)
    {
       byte cbm = 0;
        if (kol == progress_px)
          cbm  ^= 0b01000000;

        // outside text
        *(buffer + kol) = cbm;
    }
    else // animation 
    {
      // inside the string
      if (kol_in_string >= 0 && 
          char_index >= 0 && 
          char_index < strlen(display_text) &&
          (word_start_px + kol) < word_end_px
          )
      {
        //obtain the character number
        byte ascii_nr = display_text[char_index];

        bool blink_char = display_dyn[char_index] & DYN_MARKER_BLINK;
        bool fat_char   = display_dyn[char_index] & DYN_MARKER_FAT;
        bool wave_char  = display_dyn[char_index] & DYN_MARKER_WAVE;

        show = ( blink_on && blink_char ) || !blink_char;

        //obtain the bitpattern
        const unsigned char* charBitmap = Font8x5[ascii_nr];

        // for a single column, copy the bits
        int char_kol = kol_in_string % (CHAR_WIDTH + CHAR_SPACING);
        if (show && char_kol < CHAR_WIDTH)
        {
          byte cbm = charBitmap[char_kol];

          if (wave_char)
          {
            // apply sine wave
            if (v > 0)
              cbm = cbm >> (int)abs(v);
            else if (v < 0)
              cbm = cbm << (int)abs(v);
          }

          // use the previous column to make it bold, for convenience
          if (fat_char && (kol - 1) >= 0)
            *(buffer + kol - 1) |= cbm;

          if (kol == progress_px)
            cbm |= 0b01000000;

          *(buffer + kol) = cbm;
        }
        else
        {
          byte cbm = 0;
          if (kol == progress_px)
            cbm |= 0b01000000;

          // blank column
          *(buffer + kol) = cbm;
        }
      }
      else
      {
        byte cbm = 0;
        if (kol == progress_px)
          cbm |= 0b01000000;

        // outside text
        *(buffer + kol) = cbm;
      }
    } // !blank_frame

  }
} 

LEDConfig.h

#ifndef LEDConfig_h
#define LEDConfig_h

#include <Arduino.h>
#include <ESP8266WebServer.h>
#include "LittleFS.h"

#define CF_MAX_TEXT_SZ 1000
#define CF_READ_BUFFER_SZ (CF_MAX_TEXT_SZ + 100)

#define CF_TX_SHIFTLEFT     1
#define CF_TX_WAVELEFT      2
#define CF_TX_WORD_FOR_WORD 3
#define CF_SP_SLOW     1
#define CF_SP_NORMAL   2
#define CF_SP_FAST     3
#define CF_SP_VERYFAST 4

/* used by program */
struct LEDConf
{
  char   text[CF_MAX_TEXT_SZ];
  bool   on;
  short  transition;
  short  speed;
};

/* structure as in binary file */
struct LEDConfPh
{
  char   text[CF_MAX_TEXT_SZ];
  char   dummy[CF_MAX_TEXT_SZ];  
  char   on;
  char   transition;
  char   speed;
  char   dummy1;
  long   dummy2[10];
};

class LEDConfig
{
public:
    LEDConfig();
    bool init();
    long animationSpeedIdxToMillis(int index);
    void saveFormToFile(ESP8266WebServer &server);
    void dumpFile();
    bool readRecord(int record_nr, struct LEDConf *conf);
    
private:
    
};
#endif

LEDConfig.cpp

#include "./LEDConfig.h"

// made it static so that it doesn't take up stack space
static struct LEDConfPh confPh;


LEDConfig::LEDConfig() 
{
}

bool LEDConfig::init()
{
  if (!LittleFS.begin())
  {
    Serial.println("An Error has occurred while mounting LittleFS");
    return false;
  }

  return true;
}

/**
 * Convert LEDConfig.h CF_SP_*  into millis
 */
long LEDConfig::animationSpeedIdxToMillis(int index) 
{
  /*
  #define CF_SP_SLOW     1
  #define CF_SP_NORMAL   2
  #define CF_SP_FAST     3
  #define CF_SP_VERYFAST 4
  */
  long animationSpeedIdToMillis[] = {3000,2000,1500,1000};
  if (index < 1) index = 1;
  if (index > 4) index = 4;
  return animationSpeedIdToMillis[index - 1];
}

void LEDConfig::saveFormToFile(ESP8266WebServer &server) {
  File file = LittleFS.open("/settings.dat", "w");
  if (file) {
    int lastNr = -1;
    int nr;
    memset((char*)&confPh, 0, sizeof(confPh)); // because 'on' flag isn't always present, clear it(and the rest) upfront.
    for (uint8_t i = 0; i < server.args(); i++) {
      String name = server.argName(i);
      String val = server.arg(i);
      nr = name.toInt();
      if (nr != lastNr && lastNr != -1)
      {
        file.write((char*)&confPh,sizeof(confPh));
        memset((char*)&confPh, 0, sizeof(confPh)); // because 'on' flag isn't always present, clear it(and the rest) upfront.
      }
      if (name.lastIndexOf("aan", 2) != -1)
      {
        // if "aan" is present, it already means it's "on"
        confPh.on = true;
      }
      if (name.lastIndexOf("verloop", 2) != -1)
      {
        // if "aan" is present, it already means it's "on"
        confPh.transition = (char)val.toInt();
      }
      if (name.lastIndexOf("snelheid", 2) != -1)
      {
        // if "aan" is present, it already means it's "on"
        confPh.speed = (char)val.toInt();
      }
      if (name.lastIndexOf("tekst", 2) != -1)
      {
        // if "aan" is present, it already means it's "on"
        memset(confPh.text, 0, sizeof(confPh.text));
        strncpy(confPh.text, val.c_str(), sizeof(confPh.text)-1);
      }
      lastNr = nr;
      yield();
    }
    // save the last record
    file.write((char*)&confPh, sizeof(confPh));

    file.close();
    Serial.println("saved settings to file");
  } else {
    Serial.println("failed to save settings to file");
  }
}

/**
 * print the file content to Serial
 */
void LEDConfig::dumpFile() {
  struct LEDConf conf;
  int i = 0;
  while (readRecord(i, &conf))
  {
    Serial.print("nr:     ");Serial.println(i);
    Serial.print("on:     ");Serial.println(conf.on);
    Serial.print("speed:  ");Serial.println(conf.speed);
    Serial.print("transit:");Serial.println(conf.transition);
    Serial.print("text:   ");Serial.println(conf.text);
    i++;
  }
}

/**
 * Read the particular configuration
 * Param: config_nr
 * param: *LEDconf  (output)
 * On error returns False, and places dummy values
 */
bool LEDConfig::readRecord(int record_nr, struct LEDConf *conf) {
  
  size_t num = -1;
  bool status = true;

  File file = LittleFS.open("/settings.dat", "r");
  
  if (file) {
    unsigned long pos = record_nr * sizeof(confPh);
    if (file.seek(pos))
    {
      num = file.readBytes( (char*) &confPh, sizeof(confPh));
      if (num != sizeof(confPh)) 
      {
        Serial.print("readRecord() Error. Failed to read. bytes read:");Serial.println(num);
        Serial.print("                    Record size:");Serial.println(sizeof(confPh));
        status = false;
      }
    } else {
      Serial.print("readRecord() Error. Failed to seek position:");Serial.println(pos);
      Serial.print("                    File size:");Serial.println(file.size());
      status = false;
    }
    file.close();
  }
  if (status) 
  {
    conf->on = confPh.on;
    conf->speed = confPh.speed;
    conf->transition = confPh.transition;
    memset((char*)conf->text, 0, CF_MAX_TEXT_SZ);
    strncpy(conf->text, confPh.text, CF_MAX_TEXT_SZ-1);
    return true;

  } else {
    Serial.print("                    Record nr:");Serial.println(record_nr);
    
    // store dummy values
    memset((char*)conf, 0, sizeof(struct LEDConf));
    conf->on = true;
    conf->speed = CF_SP_SLOW;
    conf->transition = CF_TX_SHIFTLEFT;
    sprintf(conf->text, "Uw tekst #%02d", record_nr);

    // indicate error and returning dummy values
    return false;
  }
 
}
 

oh, and something which really confuses me.. sometime the config page renders all 20 rows and the footer, but sometimes, it renders about 16 or something rows and the footer. But the rows are handled by a for-loop using a #define for maximum?!

Hoping to get some ideas what to check or change.
Thanks!

it is recommended that interrupt (call callback) routines are kept as short as possble and do not call complex IO functions, e.g. outputing to LCD screens etc. Even SerialIO in interrupt routines can cause problems even though plently of example program on the web show it.
Set a flag in the interrupt routine and do the processing in loop()

A stacktrace issue, makes me look for a possible write beyond the bounds of the array (and so do your other symptoms) And i think i may have found it.

Since there are 'MAX_SCANLINES' elements, element 'MAX_SCANLINES' does not exist, and therefore '>=' should be the roll-over.

On the other note, Calling a function from within an ISR 'adds' to the size of the ISR, in fact that function becomes a part of the ISR. So you ISR is very much bigger than it looks, and even though , that may still be ok, the very least you can do is placing the function that you call in RAM as well.

void IRAM_ATTR sendMarqueeSpiDataOnscreen(unsigned int line) {

So those 2 things first, and then also a suggestion to actually create a method to execute that whole function from outside the ISR, also since it is blocking in nature and fairly time consuming. The idea to keep an ISR short is that while the processor is 'inside' an ISR, no other ISR functions are executed. This may explain the 'Serial output' behavior which is also ISR dependent
Basically the ISR should set a flag which triggers
sendMarqueeSpiDataOnscreen() to be executed from the main loop.
Though looking at it, it is not actually all that long, i think you may get away with it as long as it is in RAM and doesn't write or read beyond the size of the array.

The use of an ISR i would say is not even really requires here, you can get away with polling for a 10ms interval and not using a timer should also improve your reliability.

Oh yes and i would make this variable volatile just in case

volatile int      onscreen_id = 1;

It is being modified inside the ISR, and i can't see where else completely, but if you pull the secondary function out of the ISR then clearly read outside of it as well.

1 Like

Hi. i've tried doing the I/O outside the ISR.
But rendering the offscreen image is also relatively time consuming.
if i recall correctly, that resulted in one of the scanlines have longer delay before deactivating than the rest, which made one scanline light up more.
If nothing else turns out to help, then i might have to split the rending itself up, so that it has to interleave with doing the I/O per scanline.

Hi. Thanks for all the info.
Can't wait to get at it again :slight_smile:

i'll record some timing to find out how long rendering and I/O both actually take and see if i could move the I/O outside of the ISR without being left with a jittery-led screen.

Great!
i did the volatile, the >= and the IRAM_ATTR.
My stacktraces are happening less often.

I don't see it when normally doing HTTP form POST's.
i got an exception after many quick re-POSTs storing the configuration.
it then showed a proper exception format. (looks like i'm out of memory, i guess, bumping my head against the stack?) e.g.

Stack smashing detected.

Exception (5):
epc1=0x4010022f epc2=0x00000000 epc3=0x00000000 excvaddr=0x00000000 depc=0x00000000

>>>stack>>>

ctx: sys
sp: 3fffffa0 end: 3fffffb0 offset: 0000
3fffffa0:  3fffdad0 00000000 3fff121c 4010022f  
<<<stack<<<

Only during HTTP POST file uploads, it still shows this weird overlapping stacktrace when it fails.

The "config page sometimes printing less than 20 rows" problem, does sometimes still happen.(second last paragraph in my initial post)

ps. i forgot to mention, i disabled the timer altogether a while ago, to see if it would improve stability(so nothing showing on leds). But e.g., it didn't change the problem of the overlapping-exception during receiving a file via HTTP POST.

i think i found out what's going on with the issue during receiving a file upload..
When i created a lookup table to replace the sin() function, i declared it as a double[360] private field of the Animation class. Then when the program started it would immediately say "stack smashing". When i declared the array as static, the program did run.
i think because i don't use the new operator but declare everything on the stack, that i have stack shortage.

Ah Ok, well how many objects of the animation class do you create ? i mean it is 2800 bytes, It anyway should probably be in PROGMEM since it is a constant anyway.

Hi. i'll make an overview of what goes where, so that i know what sizes i'm looking at.
But regarding the sinus-table, i better have it in ram because it needs to be fast.
I'm thinking the String+= operation could be another issue, because it is said to cause fragmentation of the heap

Well it should be constant static for sure or it will be loaded into ram for every instance of the object.

That depends how you use it. A 'String' object takes up space on thee heap, and when it grows it will need to expand. If there is no space on the heap for it to grow from the original location it will be moved to an area that can accommodate it's new size. Once the object goes out of scope, the memory is freed again.
So as long as your Strings are local variables, they should not cause heap fragments, since all is freed once the function is terminated.
When increasing the size or creation of more than 1 String object, it can happen that fragments are created, but as long as all of those objects go out of scope at some point this is not a real problem on something with significant memory like an ESP.
If a String object is reduced in size

String s = "a word";
s = "";

The reserved memory is not freed until the object goes out of scope.
Global variables & Objects never go out of scope. If you want to use global String objects, the best is to allocate the maximum size you intend to make them using the .reserve() String class member function. In effect it increases the String to that size and then returns it to it's previous state.
Best practice is to do this right at the beginning of setup(), to prevent any of the local Strings that you may use within setup() to leave you a gap (Things like Serial.print() may use temporary local String objects that go out of scope when setup() is terminated)
If you use multiple local Strings it may happen that you do have gaps in the heap, but again, once those objects go out of scope you will be able to go on with a clean slate again.
You can even create a separated scope by using curly braces if needed (also practical to clear temporary variables from the stack within the same function.)

String s;
s.reserve(100);   // omitting this line will cause 's' to be moved when it's expanded
s = "a word";
{ 
  String x = " or Two";     // x is created in the first part of memory big enough to contain it
  x += " or even more";   // and it is expanded in place if there is space
  s += x;          // the expansion of s occurs here.
}  // by this time x does no longer exist and if s has not grown beyond it's size it will not have moved
1 Like

Hi. small update on the progress;

  • i used the reserve()
  • added a define to [dis/en]able Serial and serial.print code.
  • i removed the extra space from the config struct, halving the size. Thinking less work via LittleFS would be beneficial. (but i didn't notice much difference)

There was still one bug which wasn't fixed:

  • When uploading the (now)20KB config data file to the webserver, it would consistently crash and print a double stacktrace.

i tested moving the I/O to the main loop, but it didn't improve stability, rather, it very much worsen the LED graphics, making horizontal lines brightness unstable. so i undid that change.

Then I did a systematic debug to find out what makes the uploading of a file to the webserver fail.
(before, i already had tested if disabling the hardware timer, which calls the ISR to do the I/O, would improve stability. but it didn't get better)

  • Then i disabled the rendering code completely, while the I/O ISR was still operational.
  • i first made it show the very first rendered image. Now it wasn't crashing on file upload.
  • then i made it animate the intro-text. No crashing on ..
  • then i added a call to read from LittleFS, No crashing on .. etc.
  • until i added code to assign a new Animation value to the animation variable.. then crashing on file uploaded occurred again..:thinking::man_shrugging: i checked the class code, and it uses char[] and native types only. I have no clue how this can create the problem..

So now i changed it to re-use the same animation variable, and instead of having the constructor parse the config, i moved that to an init function.
It hasn't crashed since🥂

where was the class code again ? if a char-array changes in size that will cause an issue. If you treat it as a char* and there is no null-terminator, that will cause an issue.

c-strings can be just as tricky as Strings, but in a different way.

  memset((char*)conf->text, 0, CF_MAX_TEXT_SZ);
  strncpy(conf->text, confPh.text, CF_MAX_TEXT_SZ-1);

memset & strncpy should be used with extra care, mid you these 2 lines should be fine, you even leave a null pointer at the end of the array,

I think the whole class is a bit to complex for me to check just by looking at it, can you identify which call is causing the crash ?

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