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
- not showing the registers (not sure if this part is optional)
- 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, ¤tConf);
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!