So this article will go over the basics of the software, although all the real heavy stuff is done inside the various libraries.
I could do with a little help here actually, as the WiFi code is a little bit unreliable, possibly to do with a relatively large server.send() buffer - please feel free to look over the code and contribute.
Firstly, Set up your build environment (Arduino 1.6.5) and get together all the various libs. The build and upload parameters are so -
I have no idea how much Flash memory my particular ESP-07 has, so I just set it to 512K, the lowest. It's currently running at 71% when compiled.
Notice how the chip is running at 160Mhz - this is double what is recommended, however it seems to work really well.
Basically just use the boards manager in the Arduino program to grab the ESP8266 build environment - For the record I'm using ESP8266 Community version 1.6.5-947-g39819f0. You may need to point the boards manager to a URL it can fetch the configuration from.
Also fetch the WS2812 Library from - https://github.com/JoDaNl/esp8266_ws2812_i2s. Add it into the Arduino libraries bit.
You should then be able to load in the Arduino Sketch and compile it fully.
Some points of note about the code -
- If you send a string via a web page form, it'll come out the other end with all spaces changed to + (interestingly not %20) and convert any non alphanumeric chars to a hex code, prefixed with a % char. The plus to space conversion is dead simple. This code below I knocked up quickly to do a simple conversion to a single char from the hex code representation. Because we've still got three chars to convert to a single char, to save mashing the string up I just set a special character of 127 to be a skipping char - the text scroller sees 127 and just moves straight away to the next char.
if(*p == '%') { *p = 127; //127 = skip char, see text_scroller() //This is an HTML represented HEX character condition - convert it to the actual character. int rslt = 0; p++; //Convert from Hex String to Int cos there's no avbl library routines I could find that don't hard reset the ESP8266! if(*p > '/' && *p < ':') rslt = 16 * (*p - 48); // between 0 and 9 if(*p > '@' && *p < 'G') rslt = 16 * (*p - 55); // between A and F *p = 127; p++; if(*p > '/' && *p < ':') rslt += (*p - 48); // between 0 and 9 if(*p > '@' && *p < 'G') rslt += (*p - 55); // between A and F Serial.print("output = "); Serial.println(rslt); *p = (char)rslt; } NOTE - I tried to use strtol(), but the Arduino implementation kept killing the ESP for some reason. This code is of course strictly reliant on all chars represented with %hexhex, a single byte, which it will be from a web page, I think!
- To represent the character set, I wrote a small Windows program which allows you to draw a character in a 7x6 grid, and give you the C code to binary represent it -
char characters[128][6] = { { 0b01000000, 0b01000000, 0b01000000, 0b00000000, 0b00000000, 0b00000000 }, { 0b01011111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 }, { 0b00000011, 0b00000000, 0b00000011, 0b00000000, 0b00000000, 0b00000000 }, { 0b00101000, 0b01111100, 0b00101000, 0b01111100, 0b00101000, 0b00000000 }, { 0b01001000, 0b01010100, 0b11111110, 0b01010100, 0b00100100, 0b00000000 }, { 0b01000110, 0b00110000, 0b00001100, 0b01100010, 0b00000000, 0b00000000 }, { 0b00110100, 0b01001010, 0b01011010, 0b00100100, 0b01000000, 0b00000000 }, { 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 }, . . .
I then put together a whole section of the ASCII table in order, for easy referencing when constructing the graphics, currently excludes lower case chars. As can be seen, each line represents a single character, and they're just in a 2d char array. Very simple. Each byte is a vertical stripe of the character, and there are 6 pixels wide, so 6 bytes. - There is a function called 'SetupHTMLString()'. This is called in setup() only, it fills a C++ String with the full web page (only 1 page) as shown in the project gallery. It's surprisingly big considering it's only a table of colours and a very simple text input form. The WiFi libs don't like it this big at all, as it makes things a little unreliable building this large string every web request, so I decided to make the string a global var, and get the string populated early on, so the program doesn't have to keep reconstructing the string every time you server.send(...) the page. The string never changes. This does make the page much more reliable (Early on at least!), however I think I'm now falling foul of some garbage collection, or something, because after a while, the webserver decides to send back nothing if asked for it - I just get a 'no data received' error in the browser, in which I have to reset the chip. Any hints as to how to make this more reliable will be appreciated!
- During development it just joined my home WiFi network, which did seem to be a little more reliable than SoftAP mode, however it makes no sense making it a requirement to connect to your home network, as this is a WiFi hat, able to go anywhere, so it now creates an access point called 'LEDHat'. You then connect to it via the password set in code -
const char *ssid = "LEDHat"; const char *password = "YourPasswordHere";
You then point your browser towards 192.168.4.1, and you'll see the control page (See gallery image). You can simply click on any colour to instantly change the text colour as it scrolls, or enter a new string (so far up to 100 chars. Yes it's buffer overflowable very easily!!) and click 'Submit'. Obviously in future I intend on making animatable colours and other special effects, but if I've already hit the upper limit on server.send(), so I'll have to think about splitting into multiple pages. - I thought about using the cool mDNS feature they built into it, but it does require Bonjour on Windows to work - it basically allows you to just point your browser to a known name instead of an IP address, eg. esp8266.local. I have no idea whether it'd work on an Android smartphone, and it's no biggy just typing an IP Address in! I guess it would work on an iPhone though, as Bonjour is kind of an Apple thing. I think I freed up a whole percentage point of memory removing the mDNS components from the code, so it's a win!
- I like to keep code reasonably easy to understand wherever I can, so I represented the display code in simplistic terms, using a 2d matrix[x][y] array of WS2812 pixels type.
Pixel_t matrix[WIDTH][HEIGHT];
Simply filling in the relevant pixel colour at the required coordinates will put that colour right there. Why make it tougher to understand? I'm sure there are much much more efficient ways of representing it, but because it's running at 160Mhz, I've managed to 'clock' the text scroller at well over 100+ frames per second. The display is only 18x7 after all. Can this really be called a 'frame buffer'!? - The code to render the display is pretty simple too -
void matrix_render(void) { //Render all channels composited together. int ct = 0; for(int y = 0; y < HEIGHT; y++) { for(int x = 0; x < WIDTH; x++) { pixels[ct++] = matrix[x][y]; } } ledstrip.show(pixels); }
- So now all is required is to find a way of generating pretty visuals. I have a text colour variable, currently a global var. I've always been of the opinion that if you pass things around via function params, you're only adding to the workload of the CPU and stack memory, whereas modern 32 bit processors can easily reference globals without a single wasted CPU cycle or byte of RAM. I also consider these globals mostly read-only, up until a single 'authoritative' routine makes a change to it, perhaps only once an animation frame. This means thinking about code construction and being careful not to 'cross your streams' so to speak. This var is what the text scroller uses to update the pixels in the 'frame buffer'.
- So the text_scroller() routine just carefully manages which character within the text string it is currently rendering (charpos), plus which column of pixels it's working with (subcharpos) in that character. Between animation frames the whole routine drops out back to loop() so it needs to persistently keep track of where it is via these two vars. It simply scrolls the whole display one pixel to the left - notice the careful ordering of X and Y loops, and copying right side data to the left, so as not to overwrite needed data as we copy it around -
//Scroll the whole layer to the left for (x = 0; x < WIDTH - 1; x++) { for (y = 0; y < HEIGHT; y++) { matrix[x][y] = matrix[x + 1][y]; } }
blanks the pixels on the right edge,//Reset the rightmost column to blank for (y = 0; y < HEIGHT; y++) { matrix[WIDTH - 1][y].R = 0; matrix[WIDTH - 1][y].G = 0; matrix[WIDTH - 1][y].B = 0; }
then constructs the rightmost column of pixels from the character set -if(spacechr == false) { tmp2 = coldata & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 7] = textcolour; tmp2 = (coldata >> 1) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 6] = textcolour; tmp2 = (coldata >> 2) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 5] = textcolour; tmp2 = (coldata >> 3) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 4] = textcolour; tmp2 = (coldata >> 4) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 3] = textcolour; tmp2 = (coldata >> 5) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 2] = textcolour; tmp2 = (coldata >> 6) & 0x01; if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 1] = textcolour; //tmp2 = (coldata >> 7) & 0x01; //if(tmp2 == 0x01) matrix[WIDTH - 1][HEIGHT - 1] = textcolour; }
Yes it's pretty ugly, pretty inefficient I'm sure, but I've not yet ran into any performance issues. It picks out coldata and ANDs it with a binary 1 to pick out that particular pixel in the column, tests it against 1 (it's only 1 bit/pixel) then if so, sets it to the current text colour - that way your 1 bit colour can be any colour you choose. It does this for each pixel in the column one pixel at a time with an unrolled loop. Notice how it bit shifts ( coldata >> #) to pick out the relevant bit from the character set at that point. - The main loop is very simple -
void loop() { static int txtscrl = 0; server.handleClient(); HandleColourChange(); text_scroller(); matrix_render(); HandleTextChange(); delay(1000 / FPS); }
I find it is always key to keep the software ticking over pretty permanently without ridiculous delay() calls all over the place. If you have time critical code that shouldn't be called twice in one animation frame update, then just exit until your CPU time count (millis()) has gone over a certain value. Luckily the WiFi libraries do all of their work during delay() calls, so it's actively encouraged to use them just to keep the WiFi responsive. Regardless, the delay() call at the end of the loop keeps it fixed to the pre-determined FPS define, currently only 17fps due to the text scrolling speed. This gives the WiFi code plenty of spare CPU cycles to do it' The server.handleClient() is another obvious way for the WiFi libraries to 'keep up'. HandleColourChange() is a way I previously had to animate the text colour, and will be utilised in future for a rainbow effect, I'm sure. - HandleTextChange() is admittedly a bit of an odd one. I was concerned about how updating the text box with some new text might disrupt my text_scroller() function - if the WiFi code disrupted the contents of the txt variable while the scroller was accessing it, all hell could break loose, so I did a kind of a 'double buffering' of the text field.
void HandleTextChange() { if(textchanged == true) { strcpy(txt, newtxt); charpos = 0; subcharpos = 0; texthold = 0; textchanged = false; } }
I wanted the TextScroller() to definitely not be acting on the data while the text buffer was being changed over, so this happens outside of that routine every frame. When you submit a new line of text, it goes into a completely independent text buffer, newtxt. See line 7 below -void handleSubmit() { if (server.args() > 0 ) { for ( uint8_t i = 0; i < server.args(); i++ ) { if (server.argName(i) == "displaytext") { // do something here with value from server.arg(i); server.arg(i).toCharArray(newtxt,99); for (char *p = newtxt; *p != '\0'; ++p) { *p = toupper(*p); //No lower case chars represented (yet) so convert to upper across whole string. if(*p == '+') *p = ' '; //Convert HTML + chars to spaces. if(*p == '%') { *p = 127; //127 = skip char, see text_scroller() //This is an HTML represented HEX character condition - convert it to the actual character. int rslt = 0; p++; //Convert from Hex String to Int cos there's no avbl library routines I could find that don't hard reset the ESP8266! if(*p > '/' && *p < ':') rslt = 16 * (*p - 48); // between 0 and 9 if(*p > '@' && *p < 'G') rslt = 16 * (*p - 55); // between A and F *p = 127; p++; if(*p > '/' && *p < ':') rslt += (*p - 48); // between 0 and 9 if(*p > '@' && *p < 'G') rslt += (*p - 55); // between A and F Serial.print("strtol output = "); Serial.println(rslt); *p = (char)rslt; } } textchanged = true; //Handle the change somewhere else where we guarantee it's not drawing text as we change the string. //Serial.print("Got some new text - "); //Serial.print(server.arg(i)); //Serial.print(" converted to char array looks like - "); //Serial.print(txt); } } } }
During the critical point when HandleTextChange() is called, it is string copied into the real scroll buffer, then all the relevant text scroll state vars are reset, so as to restart the whole process. It turned out to be a little too efficient, as it doesn't do anything with the scroll position, or add it after a few spaces, it just starts scrolling the next text the instant it is changed over - this has the effect of making the text change-over point difficult to read! Oh well, I could add in something to space it all out I suppose with a little more effort.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.