/*
Use the Parola library to scroll text on the display
use wifimanager to get ip address / OTA updates
use serial or connect via telnet and accept commands there (only one client supported)
type HELP to get available commands
provides basic web page for testing http://<ipaddress>/test
Simone
--- Libraries used (kudos to all of the authors) ---
- MD_Parola
LED matrix text display special effects
Author majicDesigns
Website https://github.com/MajicDesigns/MD_Parola
- CommandParser
An Arduino library for parsing commands of the form COMMAND_NAME ARG1 ARG2 ARG3.
Author Anthony Zhang (Uberi)
Website https://github.com/Uberi/Arduino-CommandParser
- ESP Telnet
ESP8266/ESP32 library that allows you to setup a telnet server.
Author Lennart Hennigs
Website https://github.com/LennartHennigs/ESPTelnet
- WiFiManager
WiFi Configuration manager with web configuration portal for Espressif ESPx boards, by tzapu
Author tzapu
Website https://github.com/tzapu/WiFiManager
*/
// commenting this will disable all network related stuff. It will be possible to interact via serial as usual
#define USE_NETWORK
// if running inside WOKWI must define correct hardware config for matrix display & disable network
// remember to comment this when running on real hardware
#define WOKWI
// if running inside wokwi we disable network anyway
#ifdef WOKWI
#undef USE_NETWORK
#endif
#ifdef USE_NETWORK
#include <WebServer.h>
// Web server running on port 80
WebServer server(80);
// setup AP to connect to when there is no stored wifi credentials
#include <WiFiManager.h>
WiFiManager wm;
#endif
// Parse all commands with arguments checking
#include <CommandParser.h>
/*
size_t COMMANDS = 16 - up to 16 commands can be registered. Past this limit, registerCommand will return false.
size_t COMMAND_ARGS = 4 - up to 4 arguments are supported for any given command. Past this limit, registerCommand will return false.
size_t COMMAND_NAME_LENGTH = 10 - command names can be up to 10 bytes. Past this limit, registerCommand will return false.
size_t COMMAND_ARG_SIZE = 32 - arguments passed to commands can be up to 32 bytes in size. Note that there may be more than 32 characters used to represent the argument; for example, a string argument "\x41\x42\x43" is 14 characters but the argument would only be 3 bytes, 0x41, 0x42, and 0x43. Past this limit, processCommand will return false.
size_t RESPONSE_SIZE = 64 - responses from command callback can be up to 64 characters in length. Past this limit, there is a risk of buffer overflow - always use bounded string handling functions such as strlcpy and snprintf when writing responses.
*/
typedef CommandParser<16, 8, 10, 512, 64> MyCommandParser;
MyCommandParser parser;
// Parola library for scolling on matrix display
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
// some sprites definitions..
#include "sprites.h"
// and complete iso8859-1 font
#include "utf8fontdata.h"
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
#ifdef WOKWI
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
#else
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#endif
#define MAX_DEVICES 8
#define CLK_PIN 18
#define DATA_PIN 37
#define CS_PIN 16
// SOFTWARE SPI for ESP32
MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
// Global message buffers to parse commandline
#define BUF_SIZE 1024
char newMessage[BUF_SIZE] = { "" };
bool newMessageAvailable = false;
struct msgdetails {
bool enabled = false;
char text[BUF_SIZE] ={""};
textPosition_t align=PA_LEFT;
int speedin= 10;
int speedout= 10;
int pause=0;
textEffect_t effectin=PA_SCROLL_LEFT;
textEffect_t effectout=PA_SCROLL_LEFT;
int intensity=1;
bool invert = false;
int spritein=0;
int spriteout=0;
int zone=0;
};
#define NUM_MESSAGES 10 // max number of messages
struct msgdetails displaylines[NUM_MESSAGES];
// state machine status
#define STATE_STOP 0
#define STATE_START 1
#define STATE_PLAY 2
#define STATE_IDLE 3
#define STATE_WIFI 4
int currentstate = STATE_IDLE; // current state for state machine
int autostop = -1;
int maximpressions = -1;
unsigned long startedtimestamp = 0;
#define MAX_ZONES 3
int zonecount = 1;
int currmessageidxs[MAX_ZONES];
void resetCurrMsgIdxs(){
for(int i=0;i<MAX_ZONES;i++){
currmessageidxs[i]=-1;
}
}
// also can get commands from serial
void readSerial(void)
{
static char *cp = newMessage;
while (Serial.available())
{
*cp = (char)Serial.read();
if ((*cp == '\n') || (cp - newMessage >= BUF_SIZE-2)) // end of message character or full buffer
{
*cp = '\0'; // end the string
// restart the index for next filling spree and flag we have a message waiting
cp = newMessage;
newMessageAvailable = true;
Serial.println(newMessage);
}
else // move char pointer to next position
cp++;
}
}
// called when there is a new line from telnet
void newlineFromTelnet(String line){
line.toCharArray(newMessage, BUF_SIZE);
newMessageAvailable = true;
}
// helpers to translate from strings to objects/constants required by parola
textPosition_t getTextPositionFromName(char* textpositionname){
char * ids [] ={"LEFT","CENTER","RIGHT"};
textPosition_t position[]={PA_LEFT , PA_CENTER , PA_RIGHT};
int len = 3;
for(int i = 0; i < len; ++i){
if(strcmp(ids[i], textpositionname)==0){
return position[i];
}
}
return PA_LEFT;
}
textEffect_t getEffectFromName(char* effectname){
char * ids [] ={"NO_EFFECT" , "PRINT" , "SCROLL_UP" , "SCROLL_DOWN" ,
"SCROLL_LEFT" , "SCROLL_RIGHT" , "SPRITE" , "SLICE" ,
"MESH" , "FADE" , "DISSOLVE" , "BLINDS" ,
"RANDOM" , "WIPE" , "WIPE_CURSOR" , "SCAN_HORIZ" ,
"SCAN_HORIZX" , "SCAN_VERT" , "SCAN_VERTX" , "OPENING" ,
"OPENING_CURSOR" , "CLOSING" , "CLOSING_CURSOR" , "SCROLL_UP_LEFT" ,
"SCROLL_UP_RIGHT" , "SCROLL_DOWN_LEFT" , "SCROLL_DOWN_RIGHT" , "GROW_UP" ,
"GROW_DOWN"};
textEffect_t effects[]={PA_NO_EFFECT , PA_PRINT , PA_SCROLL_UP , PA_SCROLL_DOWN ,
PA_SCROLL_LEFT , PA_SCROLL_RIGHT , PA_SPRITE , PA_SLICE ,
PA_MESH , PA_FADE , PA_DISSOLVE , PA_BLINDS ,
PA_RANDOM , PA_WIPE , PA_WIPE_CURSOR , PA_SCAN_HORIZ ,
PA_SCAN_HORIZX , PA_SCAN_VERT , PA_SCAN_VERTX , PA_OPENING ,
PA_OPENING_CURSOR , PA_CLOSING , PA_CLOSING_CURSOR , PA_SCROLL_UP_LEFT ,
PA_SCROLL_UP_RIGHT , PA_SCROLL_DOWN_LEFT , PA_SCROLL_DOWN_RIGHT , PA_GROW_UP ,
PA_GROW_DOWN};
int len = 29;
for(int i = 0; i < len; ++i){
if(strcmp(ids[i], effectname)==0){
return effects[i];
}
}
return PA_SCROLL_LEFT;
}
int getSpriteFromName(char* spritename){
char * ids [] ={ "WALKER", "INVADER", "CHEVRON", "HEART", "ARROW1", "STEAMBOAT", "FBALL", "ROCKET",
"ROLL2", "PMAN2", "LINES", "ROLL1", "SAILBOAT", "ARROW2", "WAVE", "PMAN1" };
int len = 16;
for(int i = 0; i < len; ++i){
if(strcmp(ids[i], spritename)==0){
return i;
}
}
return 0;
}
zoneEffect_t getZoneEffectFromName(char* effectname){
char * ids [] ={"FLIP_UD" , "FLIP_LR"};
zoneEffect_t effects[]={PA_FLIP_UD , PA_FLIP_LR };
int len = 2;
for(int i = 0; i < len; ++i){
if(strcmp(ids[i], effectname)==0){
return effects[i];
}
}
return PA_FLIP_UD;
}
MD_MAX72XX::fontType_t* getFontFromName(char* fontname){
char * ids [] ={"DEFAULT" , "GEOS", "ALTERNATE"};
MD_MAX72XX::fontType_t* fonts[]={DEFAULTfont , GEOSfont, ALTERNATEfont };
int len = 3;
for(int i = 0; i < len; ++i){
if(strcmp(ids[i], fontname)==0){
return fonts[i];
}
}
return DEFAULTfont;
}
unsigned long cmd_count_SET = 0;
unsigned long cmd_count_START = 0;
unsigned long cmd_count_STOP = 0;
unsigned long cmd_count_CLEAR = 0;
unsigned long cmd_count_AUTOSTOP = 0;
unsigned long cmd_count_MAXIMPR = 0;
unsigned long cmd_count_ATTRIBUTES = 0;
unsigned long cmd_count_SPRITES = 0;
unsigned long cmd_count_ZONE = 0;
unsigned long cmd_count_WIFIAP = 0;
unsigned long cmd_count_FONT = 0;
unsigned long cmd_count_STATUS = 0;
unsigned long cmd_count_HELP = 0;
unsigned long count_impressions = 0;
unsigned long started_count_impressions = 0;
// methods called by command parser with pre checked arguments
void cmd_set(MyCommandParser::Argument *args, char *response) {
int idx = args[0].asInt64;
displaylines[idx].align = getTextPositionFromName(args[1].asString);
// copy text
strcpy(displaylines[idx].text, args[2].asString);
displaylines[idx].enabled = true;
displaylines[idx].speedin = args[3].asInt64;
displaylines[idx].speedout = args[4].asInt64;
displaylines[idx].pause = args[5].asInt64;
displaylines[idx].effectin = getEffectFromName(args[6].asString);
displaylines[idx].effectout = getEffectFromName(args[7].asString);
strlcpy(response, "SET command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_SET++;
}
void cmd_start(MyCommandParser::Argument *args, char *response) {
currentstate=STATE_START;
strlcpy(response, "START command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_START++;
}
void cmd_stop(MyCommandParser::Argument *args, char *response) {
currentstate=STATE_STOP;
strlcpy(response, "STOP command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_STOP++;
}
void cmd_clear(MyCommandParser::Argument *args, char *response) {
for(int i=0; i<NUM_MESSAGES; i++){
displaylines[i].enabled=false;
displaylines[i].zone=0;
}
autostop=-1;
maximpressions=-1;
P.displayClear();
P.setZone(0,0,MAX_DEVICES-1);
P.setZoneEffect(0,false, PA_FLIP_UD);
zonecount=1;
strlcpy(response, "CLEAR command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_CLEAR++;
}
void cmd_autostop(MyCommandParser::Argument *args, char *response) {
int seconds = args[0].asInt64;
autostop=seconds;
strlcpy(response, "AUTOSTOP command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_AUTOSTOP++;
}
void cmd_maximpr(MyCommandParser::Argument *args, char *response) {
int count = args[0].asInt64;
maximpressions=count;
strlcpy(response, "MAXIMPR command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_MAXIMPR++;
}
void cmd_attributes(MyCommandParser::Argument *args, char *response) {
int idx = args[0].asInt64;
displaylines[idx].zone = args[1].asInt64;
displaylines[idx].intensity = args[2].asInt64;
displaylines[idx].invert = args[3].asInt64 > 0 ? true : false;
strlcpy(response, "ATTRIBUTES command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_ATTRIBUTES++;
}
void cmd_sprites(MyCommandParser::Argument *args, char *response) {
int idx = args[0].asInt64;
displaylines[idx].spritein = getSpriteFromName(args[1].asString);
displaylines[idx].spriteout = getSpriteFromName(args[2].asString);
strlcpy(response, "SPRITES command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_SPRITES++;
}
void cmd_zone(MyCommandParser::Argument *args, char *response) {
int zoneid = args[0].asInt64;
// autoupdate number of zones if a one with index > number of zones is added
if(zoneid+1 > zonecount){
zonecount=zoneid+1;
}
P.displayClear();
P.setZone(args[0].asInt64,args[1].asInt64,args[2].asInt64);
P.setZoneEffect(args[0].asInt64,false, PA_FLIP_UD);
if(strcmp(args[3].asString, "NO_EFFECT")!=0){
P.setZoneEffect(args[0].asInt64,true, getZoneEffectFromName(args[3].asString));
} else {
}
strlcpy(response, "ZONE command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_ZONE++;
}
void cmd_wifi(MyCommandParser::Argument *args, char *response) {
currentstate = STATE_WIFI; // summon wifi manager
strlcpy(response, "WIFIAP command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_WIFIAP++;
}
void cmd_font(MyCommandParser::Argument *args, char *response) {
P.setFont(getFontFromName(args[0].asString));
strlcpy(response, "FONT command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_FONT++;
}
void cmd_reset(MyCommandParser::Argument *args, char *response) {
strlcpy(response, "RESET command success", MyCommandParser::MAX_RESPONSE_SIZE);
ESP.restart();
}
void cmd_status(MyCommandParser::Argument *args, char *response) {
//currentstate = STATE_WIFI; // summon wifi manager
// print some informations like ->
// current mode -> PLAY STOP etc..
// current wifi stats
// current temp
// current uptime
String out[3];
out[0]=String("\nSTATE: ");
switch (currentstate){
case STATE_STOP:
out[0]+="STOP";
break;
case STATE_START:
out[0]+="START";
break;
case STATE_PLAY:
out[0]+="PLAY";
break;
case STATE_WIFI:
out[0]+="WIFI";
break;
case STATE_IDLE:
out[0]+="IDLE";
break;
}
out[1]=String("IMPRESSIONS: ")+String(count_impressions);
out[2]=String("COUNT: ");
out[2]+= "SET="+String(cmd_count_SET);
out[2]+= ";START="+String(cmd_count_START );
out[2]+= ";STOP="+String(cmd_count_STOP);
out[2]+= ";CLEAR="+String(cmd_count_CLEAR);
out[2]+= ";AUTOSTOP="+String(cmd_count_AUTOSTOP);
out[2]+= ";MAXIMPR="+String(cmd_count_MAXIMPR);
out[2]+= ";ATTRIBUTES="+String(cmd_count_ATTRIBUTES);
out[2]+= ";SPRITES="+String(cmd_count_SPRITES);
out[2]+= ";ZONE="+String(cmd_count_ZONE);
out[2]+= ";WIFIAP="+String(cmd_count_WIFIAP);
out[2]+= ";FONT="+String(cmd_count_FONT);
out[2]+= ";STATUS="+String(cmd_count_STATUS);
out[2]+= ";HELP="+String(cmd_count_HELP);
int i=0;
for(i=0;i<3;i++){
Serial.println(out[i]);
#ifdef USE_NETWORK
telnet_println(out[i]);
#endif
}
strlcpy(response, "STATUS command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_STATUS++;
}
void cmd_help(MyCommandParser::Argument *args, char *response) {
char * help [] ={
"\nSET <msg number> <text align> <text> <speed in> <speed out> <pause delay> <effect in> <effect out>"
," with set is possible to create up to ten messages (msg number 0-9) to display"
," each message can have different parameters"
," <msg number>: id of message from 0 to 9"
," <text align>: must be one of LEFT RIGHT CENTER"
," <text>: message to display, use quotes if contains spaces"
," <speed in> <speed out>: delay when updating display higher means slower (range 10-500)"
," <pause delay>: after effect in how much time does display stay on hold before effect OUTPUT"
," <effect in> <effect out>: which effect to use when showing/hiding text"
," can be one of"
," NO_EFFECT PRINT SCROLL_UP SCROLL_DOWN SCROLL_LEFT SCROLL_RIGHT SPRITE SLICE"
," MESH FADE DISSOLVE BLINDS RANDOM WIPE WIPE_CURSOR SCAN_HORIZ SCAN_HORIZX"
," SCAN_VERT SCAN_VERTX OPENING OPENING_CURSOR CLOSING CLOSING_CURSOR SCROLL_UP_LEFT"
," SCROLL_UP_RIGHT SCROLL_DOWN_LEFT SCROLL_DOWN_RIGHT GROW_UP GROW_DOWN"
,"\nSTART"
," start displaying set messages. Once animation for message ends, will get next one and display it."
," messages will be displayed in sequence from 0 to 9."
,"\nSTOP"
," stop displaying messages"
,"\nCLEAR"
," disable all messages & reset autostop"
,"\nAUTOSTOP <seconds>"
," go to stop mode after <seconds> after start"
,"\nMAXIMPR <count>"
," go to stop mode after <count> impressions after start"
,"\nATTRIBUTES <msg number> <zone><intensity> <invert>"
," set extra attributes for message"
," <msg number>: id of message from 0 to 9"
," <zone> zone for this message"
," <intensity>: brightness in range 0-15"
," <invert>: should line be inverted? 0=false 1=true"
,"\nSPRITES <msg number> <sprite in> <sprite out>"
," set sprites to use when in text effect is SPRITE"
," <msg number>: id of message from 0 to 9"
," <sprite in> <sprite out>: name of dprite to use.. one of"
," WALKER INVADER CHEVRON HEART ARROW1 STEAMBOAT FBALL ROCKET"
," ROLL2 PMAN2 LINES ROLL1 SAILBOAT ARROW2 WAVE PMAN1"
,"\nZONE <zonenumber> <firstmodule><lastmodule> <zoneffect>"
," define text zone number <zonenumber> spanning from <firstmodule> to <lastmodule>"
," zone will apply <zoneeffect> where it can be one of"
," NO_EFFECT FLIP_UD FLIP_LR"
,"\nWIFIAP"
," put esp in AP mode to set wifi credentials/upload new firmware"
,"\nFONT <fontname>"
," set font to use: <fontname> can be one of"
," DEFAULT GEOS ALTERNATE"
,"\nSTATUS"
," print status informations"
,"\nexample"
,"CLEAR"
,"SET 0 CENTER \"The quick\" 20 20 1000 SCROLL_UP SCROLL_UP"
,"SET 1 CENTER \"brown fox\" 20 20 1000 SCROLL_UP SCROLL_UP"
,"SET 2 CENTER \"jumps over\" 20 20 1000 SPRITE SPRITE"
,"SET 3 CENTER \"the lazy dog !\" 20 20 1000 SCROLL_UP SCROLL_UP"
,"SPRITES 2 INVADER INVADER"
,"START"
,"\nexample 2"
,"CLEAR"
,"ZONE 0 0 3 FLIP_UD"
,"ZONE 1 4 7 NO_EFFECT"
,"SET 0 CENTER \"zone 0\" 20 20 1000 SCROLL_UP SCROLL_UP"
,"SET 1 CENTER \"zone 1\" 20 20 1000 SCROLL_LEFT SCROLL_RIGHT"
,"SET 2 CENTER \"zone 1bis\" 20 20 1000 SPRITE SPRITE"
,"ATTRIBUTES 0 0 1 0"
,"ATTRIBUTES 1 1 1 0"
,"START"
};
for(int i = 0; i < 66; ++i){
Serial.println(help[i]);
#ifdef USE_NETWORK
telnet_println(help[i]);
#endif
}
strlcpy(response, "HELP command success", MyCommandParser::MAX_RESPONSE_SIZE);
cmd_count_HELP++;
}
// retrieve next message to show if available
int getNextMessageIdx(int z){
int i =0;
for(int idx=currmessageidxs[z]+1; idx<currmessageidxs[z]+NUM_MESSAGES+1; idx++){
i=idx%NUM_MESSAGES;
if(displaylines[i].enabled==true && displaylines[i].zone==z){
return i;
}
}
return -1;
}
void playNextMessage(){
// for each zone
for(int z=0;z<zonecount; z++){
// start first possible line & change state to PLAY
int i = getNextMessageIdx(z);
if(i!=-1){
P.displayZoneText(z,displaylines[i].text,
displaylines[i].align,
displaylines[i].speedin,
displaylines[i].pause,
displaylines[i].effectin,
displaylines[i].effectout);
P.setSpeedInOut(z,displaylines[i].speedin,displaylines[i].speedout);
P.setIntensity(z,displaylines[i].intensity);
P.setInvert(z,displaylines[i].invert);
int sprin = displaylines[i].spritein;
int sprout = displaylines[i].spriteout;
P.setSpriteData(z,sprite[sprin].data, sprite[sprin].width, sprite[sprin].frames, // entry sprite
sprite[sprout].data, sprite[sprout].width, sprite[sprout].frames); // exit sprite
P.displayReset(z);
currmessageidxs[z] = i;
Serial.println(String("select: ")+String(millis())+String(" ")+String(i)+String(" ")+String(count_impressions));
#ifdef USE_NETWORK
telnet_println(String("select: ")+String(millis())+String(" ")+String(i)+String(" ")+String(count_impressions));
#endif
}
}
P.synchZoneStart();
}
// busywait for animation end
void print_msg_and_wait(char* msg){
P.displayZoneText(0,msg, PA_CENTER, 0, 500, PA_PRINT, PA_PRINT);
while (!P.displayAnimate()) { delay(5);}
}
// called when wifi manager goes in AP mode
#ifdef USE_NETWORK
void configModeCallback (WiFiManager *myWiFiManager) {
Serial.println("Entered config mode");
Serial.println(WiFi.softAPIP());
Serial.println(myWiFiManager->getConfigPortalSSID());
print_msg_and_wait("- AP MODE -");
}
#endif
#ifdef USE_NETWORK
void setup_web_server() {
server.on("/test", [](){
String message = "";
message+="<html><body><h1>TEST page</h1><script>\n";
message+="function tr(phrase){\n";
message+=" const iso8859escaped= escape(phrase).replace(/%20/g,' ').replace(/%22/g,'\"');\n";
message+=" const iso8859escapedtoconsole = iso8859escaped.replace(/[%]/g,\"\\\\x\")\n";
message+=" return iso8859escapedtoconsole\n";
message+="}\n";
message+="function send(){\n";
message+=" var v = document.getElementById('txt').value.trim().split('\\n');\n";
message+=" var r=v.map((x) => tr(x) );\n";
message+=" const map1 = r.map((x) => ['c',x ]);\n";
message+=" const q = new URLSearchParams(map1);\n";
message+=" document.getElementById('ptxt').value=r.join('\\n')\n";
message+=" document.getElementById('qtxt').value=q;\n";
message+=" fetch('/msg?' + q)\n";
message+="}\n";
message+="</script><button onclick='send()'>Send</button><br>\n";
message+="<textarea id='txt' rows='20' cols='80'>\n";
message+="CLEAR\n";
message+="SET 0 CENTER \"The quick\" 20 20 1000 SCROLL_UP SCROLL_UP\n";
message+="SET 1 CENTER \"brown fox\" 20 20 1000 SCROLL_UP SCROLL_UP\n";
message+="SET 2 CENTER \"jumps over\" 20 20 1000 SPRITE SPRITE\n";
message+="SET 3 CENTER \"the lazy dog !\" 20 20 1000 SCROLL_UP SCROLL_UP\n";
message+="SPRITES 2 INVADER INVADER\n";
message+="START\n";
message+="</textarea><br>\n";
message+="<textarea id='ptxt' rows='10' cols='80' readonly></textarea><br>\n";
message+="<textarea id='qtxt' rows='5' cols='80' readonly wrap='soft'></textarea>\n";
message+="</body></html>\n";
server.send(200, "text/html", message);
});
server.on("/msg", []() {
if (!server.hasArg("c")){
server.send(200, "text/plain", "no c parameter found!");
return;
}
for (uint8_t i = 0; i < server.args(); i++) {
if(server.argName(i) != "c") continue;
char response[MyCommandParser::MAX_RESPONSE_SIZE];
server.arg(i).toCharArray(newMessage, BUF_SIZE);
Serial.print("Web: ");Serial.println(newMessage);
parser.processCommand(newMessage, response);
#ifdef USE_NETWORK
telnet_println(response);
#endif
Serial.println(response);
}
server.send(200, "text/plain", "CMD sent");
});
server.onNotFound([]() {
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);
});
// start server
server.begin();
}
#endif
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH );
Serial.begin(57600);
// start parola
resetCurrMsgIdxs();
P.begin(MAX_ZONES);
P.setZone(0,0,MAX_DEVICES-1);
P.setFont(DEFAULTfont);
#ifdef USE_NETWORK
print_msg_and_wait("- WiFi -");
// connection manager
// reset settings - wipe stored credentials for testing
// these are stored by the esp library
//wm.resetSettings();
// Automatically connect using saved credentials,
// if connection fails, it starts an access point with the specified name
// if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
// then goes into a blocking loop awaiting configuration and will return success result
//wm.setWiFiAutoReconnect(true);
wm.setConfigPortalTimeout(180);
wm.setAPCallback(configModeCallback);
if(!wm.autoConnect("ESP_DisplayScroller")) {
Serial.println("Failed to connect");
print_msg_and_wait("Conn. failed..");
ESP.restart();
}
char localip[16] = { "xxx.xxx.xxx.xxx" };
WiFi.localIP().toString().toCharArray(localip, 16);
print_msg_and_wait(localip);
#else
print_msg_and_wait("Serial only");
#endif
delay(1500);
P.displayClear();
// setup command parser
parser.registerCommand("SET", "issiiiss", &cmd_set);
parser.registerCommand("START", "", &cmd_start);
parser.registerCommand("STOP", "", &cmd_stop);
parser.registerCommand("CLEAR", "", &cmd_clear);
parser.registerCommand("AUTOSTOP", "i", &cmd_autostop);
parser.registerCommand("MAXIMPR", "i", &cmd_maximpr);
parser.registerCommand("ATTRIBUTES", "iiii", &cmd_attributes);
parser.registerCommand("SPRITES", "iss", &cmd_sprites);
parser.registerCommand("ZONE", "iiis", &cmd_zone);
parser.registerCommand("WIFIAP", "", &cmd_wifi);
parser.registerCommand("HELP", "", &cmd_help);
parser.registerCommand("FONT", "s", &cmd_font);
parser.registerCommand("STATUS", "", &cmd_status);
parser.registerCommand("RESET", "", &cmd_reset);
#ifdef USE_NETWORK
// now setup telnet server
setup_telnet_server();
// setup web api
setup_web_server();
#endif
// now we can start
print_msg_and_wait("Ready!");
delay(500);
P.displayClear();
#ifdef WOKWI
Serial.println("Scroller ready: typing HELP you'll get some commands and examples");
#endif
}
void loop()
{
static unsigned long lastminutecheck = 0;
static unsigned long numlastminutecheck = 0;
static unsigned long lastcheck = 0;
static bool ledstatus = true;
unsigned long now = millis();
if( (now-lastminutecheck)>60000 ){
lastminutecheck = now;
numlastminutecheck++;
Serial.println(String("keepalive: ")+String(now)+String(" ")+String(numlastminutecheck)+String(" ")+String(currentstate)
#ifdef USE_NETWORK
+String(" ")+String(WiFi.status())
#endif
+String(" ")+String(ESP.getFreeHeap()));
#ifdef USE_NETWORK
telnet_println(String("keepalive: ")+String(now)+String(" ")+String(numlastminutecheck)+String(" ")+String(currentstate)+String(" ")+String(WiFi.status())+String(" ")+String(ESP.getFreeHeap()));
#endif
}
if( (now-lastcheck)>1000 ){
ledstatus = !ledstatus;
lastcheck=now;
digitalWrite(LED_BUILTIN, ledstatus==true ? HIGH : LOW );
#ifdef USE_NETWORK
if ( WiFi.status() != WL_CONNECTED ){
Serial.println('Wifi disconnected -> resetting');
ESP.restart();
}
#endif
}
#ifdef USE_NETWORK
// handle telnet connection
loop_telnet_server();
// handle web connection
server.handleClient();
#endif
// summon wifimanager
if (currentstate==STATE_WIFI) {
currentstate=STATE_STOP;
Serial.println(String("wifimode: ")+String(now));
#ifdef USE_NETWORK
telnet_println(String("wifimode: ")+String(now));
server.stop();
wm.startConfigPortal("ESP_DisplayScroller_ondemand");
#endif
}
// stop immediately
if (currentstate==STATE_STOP) {
P.displayClear();
P.displayShutdown(true);
currentstate=STATE_IDLE;
Serial.println(String("stop: ")+String(now)+String(" ")+String(count_impressions));
#ifdef USE_NETWORK
telnet_println(String("stop: ")+String(now)+String(" ")+String(count_impressions));
#endif
}
// start with first possible
if (currentstate==STATE_START) {
P.displayClear();
P.displayShutdown(false);
resetCurrMsgIdxs();
startedtimestamp = now;
started_count_impressions = count_impressions;
Serial.println(String("start: ")+String(now)+String(" ")+String(started_count_impressions));
#ifdef USE_NETWORK
telnet_println(String("start: ")+String(now)+String(" ")+String(started_count_impressions));
#endif
playNextMessage();
currentstate=STATE_PLAY;
}
// start with first possible
if (currentstate==STATE_PLAY && autostop>0) {
// check if has to stop
if (now - startedtimestamp >autostop*1000 ){
currentstate=STATE_STOP;
Serial.println(String("autostop: ")+String(now)+String(" ")+String(autostop));
#ifdef USE_NETWORK
telnet_println(String("autostop: ")+String(now)+String(" ")+String(autostop));
#endif
}
}
if (currentstate==STATE_PLAY && maximpressions>0) {
// check if has to stop
if (count_impressions - started_count_impressions >= maximpressions ){
currentstate=STATE_STOP;
Serial.println(String("maximpr: ")+String(now)+String(" ")+String(maximpressions));
#ifdef USE_NETWORK
telnet_println(String("maximpr: ")+String(now)+String(" ")+String(maximpressions));
#endif
}
}
// while playing & animation has finished find and play next
if (currentstate==STATE_PLAY && P.displayAnimate()) {
// check if all one have finished
boolean bAllDone = true;
for (uint8_t i=0; i<zonecount && bAllDone; i++){
bAllDone = bAllDone && P.getZoneStatus(i);
}
// all zones completed -> play new text
if (bAllDone){
#ifndef WOKWI
Serial.println(String("played: ")+String(now)+String(" ")+String(count_impressions));
#endif
#ifdef USE_NETWORK
telnet_println(String("played: ")+String(now)+String(" ")+String(count_impressions));
#endif
count_impressions++;
playNextMessage();
}
}
// we have a new message coming from serial
if (newMessageAvailable) {
//char line[128];
char response[MyCommandParser::MAX_RESPONSE_SIZE];
//strcpy(line, newMessage);
parser.processCommand(newMessage, response);
#ifdef USE_NETWORK
telnet_println(response);
#endif
Serial.println(response);
newMessageAvailable = false;
}
readSerial();
}
Loading
wemos-s2-mini
wemos-s2-mini