From fd6587d2bb99d2bf14d91b661725b984ea08463d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 23 Jan 2017 16:34:43 +0100 Subject: [PATCH] Added terminal stuff (experimental) --- build_parser.sh | 3 + html/index.html | 2 +- html/script.js | 2 +- html_orig/index.html | 2 +- html_orig/script.js | 73 ++++-- user/ansi_parser.c | 447 ++++++++++++++++++++++++++++++++++ user/ansi_parser.h | 24 ++ user/ansi_parser.rl | 312 ++++++++++++++++++++++++ user/screen.c | 559 +++++++++++++++++++++++++++++++++++++++++++ user/screen.h | 135 +++++++++++ user/serial.c | 5 +- user/user_main.c | 44 +++- 12 files changed, 1581 insertions(+), 27 deletions(-) create mode 100755 build_parser.sh create mode 100644 user/ansi_parser.c create mode 100644 user/ansi_parser.h create mode 100644 user/ansi_parser.rl create mode 100644 user/screen.c create mode 100644 user/screen.h diff --git a/build_parser.sh b/build_parser.sh new file mode 100755 index 0000000..fd0a7be --- /dev/null +++ b/build_parser.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +ragel -L -G0 user/ansi_parser.rl -o user/ansi_parser.c diff --git a/html/index.html b/html/index.html index 96a53c1..201330f 100644 --- a/html/index.html +++ b/html/index.html @@ -1 +1 @@ -ESP8266 Remote Terminal
ESP8266 Remote Terminal
\ No newline at end of file +ESP8266 Remote Terminal
ESP8266 Remote Terminal
\ No newline at end of file diff --git a/html/script.js b/html/script.js index 406dd46..c01760d 100644 --- a/html/script.js +++ b/html/script.js @@ -1 +1 @@ -var $=function(d,c){d=d.match(/^(\W)?(.*)/);return(c||document)["getElement"+(d[1]?d[1]=="#"?"ById":"sByClassName":"sByTagName")](d[2])};var m=function(e,d,f){d=document;f=d.createElement("p");f.innerHTML=e;e=d.createDocumentFragment();while(d=f.firstChild){e.appendChild(d)}return e};(function(){function a(r){return document.createElement(r)}var b=26,k=10;var l={a:false,x:0,y:0,suppress:false,hidden:false};var o=[];var g=["#111213","#CC0000","#4E9A06","#C4A000","#3465A4","#75507B","#06989A","#D3D7CF","#555753","#EF2929","#8AE234","#FCE94F","#729FCF","#AD7FA8","#34E2E2","#EEEEEC"];function p(){o.forEach(function(r,s){r.t=" ";r.fg=7;r.bg=0;d(r)})}function c(s,r){return o[s*b+r]}function i(){return c(l.y,l.x)}function q(r){l.hidden=!r;l.a&=r;d(i(),l.a)}function f(s,r){l.suppress=true;d(i(),false);l.x=r;l.y=s;l.suppress=false;d(i(),l.a)}function d(s,r){var v=s.e,t,u;t=r?s.bg:s.fg;u=r?s.fg:s.bg;v.innerText=(s.t+" ")[0];v.style.color=e(t);v.style.backgroundColor=e(u);v.style.fontWeight=t>7?"bold":"normal"}function h(){o.forEach(function(s,t){var r=l.a&&(t==l.y*b+l.x);d(s,r)})}function j(s){if(s.length!=b*k*3){throw"Bad data format."}for(var t=0;t15){r=0}return g[r]}function n(){var u,t=$("#screen");for(var s=0;s0)&&(s%b==0)){t.appendChild(a("br"))}t.appendChild(u);var r={t:" ",fg:7,bg:0,e:u};o.push(r);d(r)}setInterval(function(){l.a=!l.a;if(l.hidden){l.a=false}if(!l.suppress){d(i(),l.a)}},500)}window.Term={init:n,load:j,setCursor:f,enableCursor:q,clear:p}})();(function(){var g="ws://"+window.location.host+"/ws/update.cgi";var d;function c(i){console.log("CONNECTED")}function b(i){console.error("SOCKET CLOSED")}function f(i){console.log("Message received!",i.data)}function e(i){console.error(i.data)}function a(i){d.send(i)}function h(){d=new WebSocket(g);d.onopen=c;d.onclose=b;d.onmessage=f;d.onerror=e;console.log("Opening socket.")}window.Conn={ws:null,init:h}})();function init(){Term.init();Conn.init()}; \ No newline at end of file +var $=function(d,c){d=d.match(/^(\W)?(.*)/);return(c||document)["getElement"+(d[1]?d[1]=="#"?"ById":"sByClassName":"sByTagName")](d[2])};var m=function(e,d,f){d=document;f=d.createElement("p");f.innerHTML=e;e=d.createDocumentFragment();while(d=f.firstChild){e.appendChild(d)}return e};(function(){function a(r){return document.createElement(r)}var b=26,k=10;var l={a:false,x:0,y:0,suppress:false,hidden:false};var o=[];var g=["#111213","#CC0000","#4E9A06","#C4A000","#3465A4","#75507B","#06989A","#D3D7CF","#555753","#EF2929","#8AE234","#FCE94F","#729FCF","#AD7FA8","#34E2E2","#EEEEEC"];function p(){o.forEach(function(r,s){r.t=" ";r.fg=7;r.bg=0;d(r)})}function c(s,r){return o[s*b+r]}function i(){return c(l.y,l.x)}function q(r){l.hidden=!r;l.a&=r;d(i(),l.a)}function f(s,r){l.suppress=true;d(i(),false);l.x=r;l.y=s;l.suppress=false;d(i(),l.a)}function d(s,r){var v=s.e,t,u;t=r?s.bg:s.fg;u=r?s.fg:s.bg;v.innerText=(s.t+" ")[0];v.style.color=e(t);v.style.backgroundColor=e(u);v.style.fontWeight=t>7?"bold":"normal"}function h(){o.forEach(function(s,t){var r=l.a&&(t==l.y*b+l.x);d(s,r)})}function j(u){l.x=u.x;l.y=u.y;var v=0,B=0,x=u.screen;var r,w,A,z,s,y;while(v0){y=parseInt(x.substr(v+1,s));v=v+s+1;for(;y>0&&B15){r=0}return g[r]}function n(v){var u,r,t=$("#screen");for(var s=0;s0)&&(s%b==0)){t.appendChild(a("br"))}t.appendChild(u);r={t:" ",fg:7,bg:0,e:u};o.push(r);d(r)}setInterval(function(){l.a=!l.a;if(l.hidden){l.a=false}if(!l.suppress){d(i(),l.a)}},500);j(v)}window.Term={init:n,load:j,setCursor:f,enableCursor:q,clear:p}})();(function(){var g="ws://"+window.location.host+"/ws/update.cgi";var d;function c(i){console.log("CONNECTED")}function b(i){console.error("SOCKET CLOSED")}function f(i){try{Term.load(JSON.parse(i.data))}catch(j){console.error(j)}}function e(i){console.error(i.data)}function a(i){if(typeof i!="string"){i=JSON.stringify(i)}d.send(i)}function h(){d=new WebSocket(g);d.onopen=c;d.onclose=b;d.onmessage=f;d.onerror=e;console.log("Opening socket.")}window.Conn={ws:null,init:h,send:a}})();function init(a){Term.init(a);Conn.init()}; \ No newline at end of file diff --git a/html_orig/index.html b/html_orig/index.html index be4367b..e5b2a34 100644 --- a/html_orig/index.html +++ b/html_orig/index.html @@ -17,4 +17,4 @@ - + diff --git a/html_orig/script.js b/html_orig/script.js index 2dcda4c..ce44c25 100644 --- a/html_orig/script.js +++ b/html_orig/script.js @@ -158,31 +158,54 @@ var m = function( } /** Load screen content from a 'binary' sequence */ - function load(seq) { - if (seq.length != W*H*3) throw "Bad data format."; - - // primitive format with 3 chars per cell: letter, fg [hex], bg [hex] - for (var i = 0; i < W * H; i++) { - var cell = screen[i]; - cell.t = seq[i*3]; - cell.fg = parseInt(seq[i*3+1], 16); - cell.bg = parseInt(seq[i*3+2], 16); - } + function load(obj) { + cursor.x = obj.x; + cursor.y = obj.y; + + // Simple compression - hexFG hexBG 'ASCII' (r/s/t/u NUM{1,2,3,4})? + + var i = 0, ci = 0, str = obj.screen; + var fg, bg, t, cell, repchars, rep; + while(i < str.length && ci 0) { + rep = parseInt(str.substr(i+1,repchars)); + i = i + repchars + 1; + for (; rep>0 && ci 15) c = 0; return CLR[c]; } /** Init the terminal */ - function init() { + function init(obj) { /* Build screen & show */ - var e, scr = $('#screen'); + var e, cell, scr = $('#screen'); for(var i = 0; i < W*H; i++) { e = make('span'); @@ -193,7 +216,7 @@ var m = function( /* The cell */ scr.appendChild(e); - var cell = {t: ' ', fg: 7, bg: 0, e: e}; + cell = {t: ' ', fg: 7, bg: 0, e: e}; screen.push(cell); blit(cell); } @@ -209,6 +232,8 @@ var m = function( blit(cursorCell(), cursor.a); } }, 500); + + load(obj); } // publish @@ -217,7 +242,7 @@ var m = function( load: load, setCursor: cursorSet, enableCursor: cursorEnable, - clear: cls, + clear: cls }; })(); @@ -237,8 +262,12 @@ var m = function( } function onMessage(evt) { - console.log("Message received!", evt.data); - // TODO process + try { + // Assume all our messages are screen updates + Term.load(JSON.parse(evt.data)); + } catch(e) { + console.error(e); + } } function onError(evt) { @@ -246,13 +275,16 @@ var m = function( } function doSend(message) { + if (typeof message != "string") { + message = JSON.stringify(message); + } ws.send(message); } function init() { ws = new WebSocket(wsUri); ws.onopen = onOpen; - ws.onclose = onClose + ws.onclose = onClose; ws.onmessage = onMessage; ws.onerror = onError; @@ -262,10 +294,11 @@ var m = function( window.Conn = { ws: null, init: init, + send: doSend }; })(); -function init() { - Term.init(); +function init(obj) { + Term.init(obj); Conn.init(); } diff --git a/user/ansi_parser.c b/user/ansi_parser.c new file mode 100644 index 0000000..7dbddd1 --- /dev/null +++ b/user/ansi_parser.c @@ -0,0 +1,447 @@ + +/* #line 1 "user/ansi_parser.rl" */ +#include +#include "screen.h" +#include "ansi_parser.h" + +// Max nr of CSI parameters +#define CSI_N_MAX 3 + +/** + * \brief Handle fully received CSI ANSI sequence + * \param leadchar - private range leading character, 0 if none + * \param params - array of CSI_N_MAX ints holding the numeric arguments + * \param keychar - the char terminating the sequence + */ +void ICACHE_FLASH_ATTR +handle_CSI(char leadchar, int *params, char keychar) +{ + /* + Implemented codes (from Wikipedia) + + CSI n A CUU – Cursor Up + CSI n B CUD – Cursor Down + CSI n C CUF – Cursor Forward + CSI n D CUB – Cursor Back + CSI n E CNL – Cursor Next Line + CSI n F CPL – Cursor Previous Line + CSI n G CHA – Cursor Horizontal Absolute + CSI n ; m H CUP – Cursor Position + CSI n J ED – Erase Display + CSI n K EL – Erase in Line + CSI n S SU – Scroll Up + CSI n T SD – Scroll Down + CSI n ; m f HVP – Horizontal and Vertical Position + CSI n m SGR – Select Graphic Rendition (Implemented only some) + CSI 6n DSR – Device Status Report NOT IMPL + CSI s SCP – Save Cursor Position + CSI u RCP – Restore Cursor Position + CSI ?25l DECTCEM Hides the cursor + CSI ?25h DECTCEM Shows the cursor + */ + + int n1 = params[0]; + int n2 = params[1]; +// int n3 = params[2]; + + // defaults + switch (keychar) { + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'S': + case 'T': + if (n1 == 0) n1 = 1; + break; + + case 'H': + case 'f': + if (n1 == 0) n1 = 1; + if (n2 == 0) n2 = 1; + break; + + case 'J': + case 'K': + if (n1 > 2) n1 = 0; + break; + } + + switch (keychar) { + // CUU CUD CUF CUB + case 'A': screen_cursor_move(0, -n1); break; + case 'B': screen_cursor_move(0, n1); break; + case 'C': screen_cursor_move(n1, 0); break; + case 'D': screen_cursor_move(-n1, 0); break; + + case 'E': // CNL + screen_cursor_move(0, n1); + screen_cursor_set_x(0); + break; + + case 'F': // CPL + screen_cursor_move(0, -n1); + screen_cursor_set_x(0); + break; + + // CHA + case 'G': + screen_cursor_set_x(n1 - 1); break; // 1-based + + // SU, SD + case 'S': screen_scroll_up(n1); break; + case 'T': screen_scroll_down(n1); break; + + // CUP,HVP + case 'H': + case 'f': + screen_cursor_set(n2-1, n1-1); break; // 1-based + + case 'J': // ED + if (n1 == 0) { + screen_clear(CLEAR_TO_CURSOR); + } else if (n1 == 1) { + screen_clear(CLEAR_FROM_CURSOR); + } else { + screen_clear(CLEAR_ALL); + screen_cursor_set(0, 0); + } + break; + + case 'K': // EL + if (n1 == 0) { + screen_clear_line(CLEAR_TO_CURSOR); + } else if (n1 == 1) { + screen_clear_line(CLEAR_FROM_CURSOR); + } else { + screen_clear_line(CLEAR_ALL); + screen_cursor_set_x(0); + } + break; + + // SCP, RCP + case 's': screen_cursor_save(); break; + case 'u': screen_cursor_restore(); break; + + // DECTCEM cursor show hide + case 'l': + if (leadchar == '?' && n1 == 25) { + screen_cursor_enable(1); + } + break; + + case 'h': + if (leadchar == '?' && n1 == 25) { + screen_cursor_enable(0); + } + break; + + case 'm': // SGR + // iterate arguments + for (int i = 0; i < CSI_N_MAX; i++) { + int n = params[i]; + + if (i == 0 && n == 0) { // reset SGR + screen_set_fg(7); + screen_set_bg(0); + break; // cannot combine reset with others + } + else if (n >= 30 && n <= 37) screen_set_fg(n-30); // ANSI normal fg + else if (n >= 40 && n <= 47) screen_set_bg(n-40); // ANSI normal bg + else if (n == 39) screen_set_fg(7); // default fg + else if (n == 49) screen_set_bg(0); // default bg + else if (n == 7) screen_inverse(1); // inverse + else if (n == 27) screen_inverse(0); // positive + else if (n == 1) screen_set_bright_fg(); // ANSI bold = bright fg + else if (n >= 90 && n <= 97) screen_set_fg(n-90+8); // AIX bright fg + else if (n >= 100 && n <= 107) screen_set_bg(n-100+8); // AIX bright bg + } + break; + } +} + +/** + * \brief Handle a request to reset the display device + */ +void ICACHE_FLASH_ATTR +handle_RESET_cmd(void) +{ + screen_reset(); +} + +/** + * \brief Handle a received plain character + * \param c - the character + */ +void ICACHE_FLASH_ATTR +handle_plainchar(char c) +{ + screen_putchar(c); +} + +/* Ragel constants block */ + +/* #line 188 "user/ansi_parser.c" */ +static const char _ansi_actions[] = { + 0, 1, 0, 1, 1, 1, 2, 1, + 3, 1, 4, 1, 5, 1, 6, 1, + 7, 1, 8 +}; + +static const char _ansi_eof_actions[] = { + 0, 0, 0, 13, 13, 0, 0 +}; + +static const int ansi_start = 1; +static const int ansi_first_final = 5; +static const int ansi_error = 0; + +static const int ansi_en_CSI_body = 3; +static const int ansi_en_main = 1; + + +/* #line 187 "user/ansi_parser.rl" */ + + +/** + * \brief Linear ANSI chars stream parser + * + * Parses a stream of bytes using a Ragel parser. The defined + * grammar does not use 'unget', so the entire buffer is + * always processed in a linear manner. + * + * \attention -> but always check the Ragel output for 'p--' + * or 'p -=', that means trouble. + * + * \param newdata - array of new chars to process + * \param len - length of the newdata buffer + */ +void ICACHE_FLASH_ATTR +ansi_parser(const char *newdata, size_t len) +{ + static int cs = -1; + + // The CSI code is built here + static char csi_leading; //!< Leading char, 0 if none + static int csi_ni; //!< Number of the active digit + static int csi_n[CSI_N_MAX]; //!< Param digits + static char csi_char; //!< CSI action char (end) + + if (len == 0) len = strlen(newdata); + + // Load new data to Ragel vars + const char *p = newdata; + const char *eof = NULL; + const char *pe = newdata + len; + + // Init Ragel on the first run + if (cs == -1) { + +/* #line 244 "user/ansi_parser.c" */ + { + cs = ansi_start; + } + +/* #line 223 "user/ansi_parser.rl" */ + } + + // The parser + +/* #line 254 "user/ansi_parser.c" */ + { + const char *_acts; + unsigned int _nacts; + + if ( p == pe ) + goto _test_eof; + if ( cs == 0 ) + goto _out; +_resume: + switch ( cs ) { +case 1: + if ( (*p) == 27 ) + goto tr1; + goto tr0; +case 2: + switch( (*p) ) { + case 91: goto tr2; + case 93: goto tr4; + case 99: goto tr5; + } + goto tr3; +case 0: + goto _out; +case 5: + if ( (*p) == 27 ) + goto tr1; + goto tr0; +case 3: + if ( (*p) == 59 ) + goto tr9; + if ( (*p) < 60 ) { + if ( (*p) > 47 ) { + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr8; + } else if ( (*p) >= 32 ) + goto tr7; + } else if ( (*p) > 64 ) { + if ( (*p) > 90 ) { + if ( 97 <= (*p) && (*p) <= 122 ) + goto tr10; + } else if ( (*p) >= 65 ) + goto tr10; + } else + goto tr7; + goto tr6; +case 4: + if ( (*p) == 59 ) + goto tr9; + if ( (*p) < 65 ) { + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr8; + } else if ( (*p) > 90 ) { + if ( 97 <= (*p) && (*p) <= 122 ) + goto tr10; + } else + goto tr10; + goto tr6; +case 6: + goto tr6; + } + + tr3: cs = 0; goto _again; + tr6: cs = 0; goto f4; + tr0: cs = 1; goto f0; + tr1: cs = 2; goto _again; + tr7: cs = 4; goto f5; + tr8: cs = 4; goto f6; + tr9: cs = 4; goto f7; + tr2: cs = 5; goto f1; + tr4: cs = 5; goto f2; + tr5: cs = 5; goto f3; + tr10: cs = 6; goto f8; + + f0: _acts = _ansi_actions + 1; goto execFuncs; + f1: _acts = _ansi_actions + 3; goto execFuncs; + f5: _acts = _ansi_actions + 5; goto execFuncs; + f6: _acts = _ansi_actions + 7; goto execFuncs; + f7: _acts = _ansi_actions + 9; goto execFuncs; + f8: _acts = _ansi_actions + 11; goto execFuncs; + f4: _acts = _ansi_actions + 13; goto execFuncs; + f2: _acts = _ansi_actions + 15; goto execFuncs; + f3: _acts = _ansi_actions + 17; goto execFuncs; + +execFuncs: + _nacts = *_acts++; + while ( _nacts-- > 0 ) { + switch ( *_acts++ ) { + case 0: +/* #line 233 "user/ansi_parser.rl" */ + { + handle_plainchar((*p)); + } + break; + case 1: +/* #line 240 "user/ansi_parser.rl" */ + { + /* Reset the CSI builder */ + csi_leading = csi_char = 0; + csi_ni = 0; + + /* Zero out digits */ + for(int i = 0; i < CSI_N_MAX; i++) { + csi_n[i] = 0; + } + + {cs = 3; goto _again;} + } + break; + case 2: +/* #line 253 "user/ansi_parser.rl" */ + { + csi_leading = (*p); + } + break; + case 3: +/* #line 257 "user/ansi_parser.rl" */ + { + /* x10 + digit */ + if (csi_ni < CSI_N_MAX) { + csi_n[csi_ni] = csi_n[csi_ni]*10 + ((*p) - '0'); + } + } + break; + case 4: +/* #line 264 "user/ansi_parser.rl" */ + { + csi_ni++; + } + break; + case 5: +/* #line 268 "user/ansi_parser.rl" */ + { + csi_char = (*p); + + handle_CSI(csi_leading, csi_n, csi_char); + + {cs = 1; goto _again;} + } + break; + case 6: +/* #line 276 "user/ansi_parser.rl" */ + { + {cs = 1; goto _again;} + } + break; + case 7: +/* #line 288 "user/ansi_parser.rl" */ + { + // TODO implement OS control code parsing + {cs = 1; goto _again;} + } + break; + case 8: +/* #line 293 "user/ansi_parser.rl" */ + { + // Reset screen + handle_RESET_cmd(); + {cs = 1; goto _again;} + } + break; +/* #line 415 "user/ansi_parser.c" */ + } + } + goto _again; + +_again: + if ( cs == 0 ) + goto _out; + if ( ++p != pe ) + goto _resume; + _test_eof: {} + if ( p == eof ) + { + const char *__acts = _ansi_actions + _ansi_eof_actions[cs]; + unsigned int __nacts = (unsigned int) *__acts++; + while ( __nacts-- > 0 ) { + switch ( *__acts++ ) { + case 6: +/* #line 276 "user/ansi_parser.rl" */ + { + {cs = 1; goto _again;} + } + break; +/* #line 438 "user/ansi_parser.c" */ + } + } + } + + _out: {} + } + +/* #line 311 "user/ansi_parser.rl" */ + +} diff --git a/user/ansi_parser.h b/user/ansi_parser.h new file mode 100644 index 0000000..2770b9d --- /dev/null +++ b/user/ansi_parser.h @@ -0,0 +1,24 @@ +#ifndef ANSI_PARSER_H +#define ANSI_PARSER_H + +#include + +// Max nr of CSI parameters +#define CSI_N_MAX 3 + +/** + * \brief Linear ANSI chars stream parser + * + * Parses a stream of bytes using a Ragel parser. The defined + * grammar does not use 'unget', so the entire buffer is + * always processed in a linear manner. + * + * \attention -> but always check the Ragel output for 'p--' + * or 'p -=', that means trouble. + * + * \param newdata - array of new chars to process + * \param len - length of the newdata buffer + */ +void ansi_parser(const char *newdata, size_t len); + +#endif // ANSI_PARSER_H diff --git a/user/ansi_parser.rl b/user/ansi_parser.rl new file mode 100644 index 0000000..b4e3c27 --- /dev/null +++ b/user/ansi_parser.rl @@ -0,0 +1,312 @@ +#include +#include "screen.h" +#include "ansi_parser.h" + +// Max nr of CSI parameters +#define CSI_N_MAX 3 + +/** + * \brief Handle fully received CSI ANSI sequence + * \param leadchar - private range leading character, 0 if none + * \param params - array of CSI_N_MAX ints holding the numeric arguments + * \param keychar - the char terminating the sequence + */ +void ICACHE_FLASH_ATTR +handle_CSI(char leadchar, int *params, char keychar) +{ + /* + Implemented codes (from Wikipedia) + + CSI n A CUU – Cursor Up + CSI n B CUD – Cursor Down + CSI n C CUF – Cursor Forward + CSI n D CUB – Cursor Back + CSI n E CNL – Cursor Next Line + CSI n F CPL – Cursor Previous Line + CSI n G CHA – Cursor Horizontal Absolute + CSI n ; m H CUP – Cursor Position + CSI n J ED – Erase Display + CSI n K EL – Erase in Line + CSI n S SU – Scroll Up + CSI n T SD – Scroll Down + CSI n ; m f HVP – Horizontal and Vertical Position + CSI n m SGR – Select Graphic Rendition (Implemented only some) + CSI 6n DSR – Device Status Report NOT IMPL + CSI s SCP – Save Cursor Position + CSI u RCP – Restore Cursor Position + CSI ?25l DECTCEM Hides the cursor + CSI ?25h DECTCEM Shows the cursor + */ + + int n1 = params[0]; + int n2 = params[1]; +// int n3 = params[2]; + + // defaults + switch (keychar) { + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'S': + case 'T': + if (n1 == 0) n1 = 1; + break; + + case 'H': + case 'f': + if (n1 == 0) n1 = 1; + if (n2 == 0) n2 = 1; + break; + + case 'J': + case 'K': + if (n1 > 2) n1 = 0; + break; + } + + switch (keychar) { + // CUU CUD CUF CUB + case 'A': screen_cursor_move(0, -n1); break; + case 'B': screen_cursor_move(0, n1); break; + case 'C': screen_cursor_move(n1, 0); break; + case 'D': screen_cursor_move(-n1, 0); break; + + case 'E': // CNL + screen_cursor_move(0, n1); + screen_cursor_set_x(0); + break; + + case 'F': // CPL + screen_cursor_move(0, -n1); + screen_cursor_set_x(0); + break; + + // CHA + case 'G': + screen_cursor_set_x(n1 - 1); break; // 1-based + + // SU, SD + case 'S': screen_scroll_up(n1); break; + case 'T': screen_scroll_down(n1); break; + + // CUP,HVP + case 'H': + case 'f': + screen_cursor_set(n2-1, n1-1); break; // 1-based + + case 'J': // ED + if (n1 == 0) { + screen_clear(CLEAR_TO_CURSOR); + } else if (n1 == 1) { + screen_clear(CLEAR_FROM_CURSOR); + } else { + screen_clear(CLEAR_ALL); + screen_cursor_set(0, 0); + } + break; + + case 'K': // EL + if (n1 == 0) { + screen_clear_line(CLEAR_TO_CURSOR); + } else if (n1 == 1) { + screen_clear_line(CLEAR_FROM_CURSOR); + } else { + screen_clear_line(CLEAR_ALL); + screen_cursor_set_x(0); + } + break; + + // SCP, RCP + case 's': screen_cursor_save(); break; + case 'u': screen_cursor_restore(); break; + + // DECTCEM cursor show hide + case 'l': + if (leadchar == '?' && n1 == 25) { + screen_cursor_enable(1); + } + break; + + case 'h': + if (leadchar == '?' && n1 == 25) { + screen_cursor_enable(0); + } + break; + + case 'm': // SGR + // iterate arguments + for (int i = 0; i < CSI_N_MAX; i++) { + int n = params[i]; + + if (i == 0 && n == 0) { // reset SGR + screen_set_fg(7); + screen_set_bg(0); + break; // cannot combine reset with others + } + else if (n >= 30 && n <= 37) screen_set_fg(n-30); // ANSI normal fg + else if (n >= 40 && n <= 47) screen_set_bg(n-40); // ANSI normal bg + else if (n == 39) screen_set_fg(7); // default fg + else if (n == 49) screen_set_bg(0); // default bg + else if (n == 7) screen_inverse(1); // inverse + else if (n == 27) screen_inverse(0); // positive + else if (n == 1) screen_set_bright_fg(); // ANSI bold = bright fg + else if (n >= 90 && n <= 97) screen_set_fg(n-90+8); // AIX bright fg + else if (n >= 100 && n <= 107) screen_set_bg(n-100+8); // AIX bright bg + } + break; + } +} + +/** + * \brief Handle a request to reset the display device + */ +void ICACHE_FLASH_ATTR +handle_RESET_cmd(void) +{ + screen_reset(); +} + +/** + * \brief Handle a received plain character + * \param c - the character + */ +void ICACHE_FLASH_ATTR +handle_plainchar(char c) +{ + screen_putchar(c); +} + +/* Ragel constants block */ +%%{ + machine ansi; + write data; +}%% + +/** + * \brief Linear ANSI chars stream parser + * + * Parses a stream of bytes using a Ragel parser. The defined + * grammar does not use 'unget', so the entire buffer is + * always processed in a linear manner. + * + * \attention -> but always check the Ragel output for 'p--' + * or 'p -=', that means trouble. + * + * \param newdata - array of new chars to process + * \param len - length of the newdata buffer + */ +void ICACHE_FLASH_ATTR +ansi_parser(const char *newdata, size_t len) +{ + static int cs = -1; + + // The CSI code is built here + static char csi_leading; //!< Leading char, 0 if none + static int csi_ni; //!< Number of the active digit + static int csi_n[CSI_N_MAX]; //!< Param digits + static char csi_char; //!< CSI action char (end) + + if (len == 0) len = strlen(newdata); + + // Load new data to Ragel vars + const char *p = newdata; + const char *eof = NULL; + const char *pe = newdata + len; + + // Init Ragel on the first run + if (cs == -1) { + %% write init; + } + + // The parser + %%{ + ESC = 27; + NOESC = (any - ESC); + TOK_ST = ESC '\\'; # String terminator - used for OSC commands + + # --- Regular characters to be printed --- + + action plain_char { + handle_plainchar(fc); + } + + # --- CSI CSI commands (Select Graphic Rendition) --- + # Text color & style + + action CSI_start { + /* Reset the CSI builder */ + csi_leading = csi_char = 0; + csi_ni = 0; + + /* Zero out digits */ + for(int i = 0; i < CSI_N_MAX; i++) { + csi_n[i] = 0; + } + + fgoto CSI_body; + } + + action CSI_leading { + csi_leading = fc; + } + + action CSI_digit { + /* x10 + digit */ + if (csi_ni < CSI_N_MAX) { + csi_n[csi_ni] = csi_n[csi_ni]*10 + (fc - '0'); + } + } + + action CSI_semi { + csi_ni++; + } + + action CSI_end { + csi_char = fc; + + handle_CSI(csi_leading, csi_n, csi_char); + + fgoto main; + } + + action CSI_fail { + fgoto main; + } + + CSI_body := ((32..47|60..64) @CSI_leading)? + ((digit @CSI_digit)* ';' @CSI_semi)* + (digit @CSI_digit)* alpha @CSI_end $!CSI_fail; + + + # --- OSC commands (Operating System Commands) --- + # Module parametrisation + + action OSC_start { + // TODO implement OS control code parsing + fgoto main; + } + + action RESET_cmd { + // Reset screen + handle_RESET_cmd(); + fgoto main; + } + + # --- Main parser loop --- + + main := + ( + (NOESC @plain_char)* ESC ( + '[' @CSI_start | + ']' @OSC_start | + 'c' @RESET_cmd + ) + )+; + + write exec; + }%% +} diff --git a/user/screen.c b/user/screen.c new file mode 100644 index 0000000..e04ebc0 --- /dev/null +++ b/user/screen.c @@ -0,0 +1,559 @@ +#include +#include +#include "screen.h" + +//region Data structures + +/** + * Highest permissible value of the color attribute + */ +#define COLOR_MAX 15 + +/** + * Screen cell data type + */ +typedef struct { + char c; + Color fg; + Color bg; +} Cell; + +/** + * The screen data array + */ +static Cell screen[MAX_SCREEN_SIZE]; + +/** + * Cursor position and attributes + */ +static struct { + Coordinate x; //!< X coordinate + Coordinate y; //!< Y coordinate + bool visible; //!< Visible + bool inverse; //!< Inverse colors + Color fg; //!< Foreground color for writing + Color bg; //!< Background color for writing +} cursor; + +/** + * Saved cursor position, used with the SCP RCP commands + */ +static struct { + Coordinate x; + Coordinate y; +} cursor_sav; + +/** + * Active screen width + */ +static Coordinate W = SCREEN_DEF_W; + +/** + * Active screen height + */ +static Coordinate H = SCREEN_DEF_H; + +//endregion + +//region Helpers + +/** + * Reset a cell + */ +static inline void +cell_init(Cell *cell) +{ + cell->c = ' '; + cell->fg = SCREEN_DEF_FG; + cell->bg = SCREEN_DEF_BG; +} + +/** + * Clear range, inclusive + */ +static inline void +clear_range(unsigned int from, unsigned int to) +{ + for (unsigned int i = from; i <= to; i++) { + cell_init(&screen[i]); + } +} + +/** + * Reset the cursor position & colors + */ +static void ICACHE_FLASH_ATTR +cursor_reset(void) +{ + cursor.x = 0; + cursor.y = 0; + cursor.fg = SCREEN_DEF_FG; + cursor.bg = SCREEN_DEF_BG; + cursor.visible = 1; + cursor.inverse = 0; +} + +//endregion + +//region Screen clearing + +/** + * Init the screen (entire mappable area - for consistency) + */ +void ICACHE_FLASH_ATTR +screen_init(void) +{ + for (unsigned int i = 0; i < MAX_SCREEN_SIZE; i++) { + cell_init(&screen[i]); + } + + cursor_reset(); + screen_notifyChange(); +} + +/** + * Reset the screen (only the visible area) + */ +void ICACHE_FLASH_ATTR +screen_reset(void) +{ + screen_clear(CLEAR_ALL); + cursor_reset(); + screen_notifyChange(); +} + +/** + * Clear screen area + */ +void ICACHE_FLASH_ATTR +screen_clear(ClearMode mode) +{ + switch (mode) { + case CLEAR_ALL: + clear_range(0, W * H - 1); + break; + + case CLEAR_FROM_CURSOR: + clear_range((cursor.y * W) + cursor.x, W * H - 1); + break; + + case CLEAR_TO_CURSOR: + clear_range(0, (cursor.y * W) + cursor.x); + break; + } + screen_notifyChange(); +} + +/** + * Line reset to gray-on-white, empty + */ +void ICACHE_FLASH_ATTR +screen_clear_line(ClearMode mode) +{ + switch (mode) { + case CLEAR_ALL: + clear_range(cursor.y * W, (cursor.y + 1) * W - 1); + break; + + case CLEAR_FROM_CURSOR: + clear_range(cursor.y * W + cursor.x, (cursor.y + 1) * W - 1); + break; + + case CLEAR_TO_CURSOR: + clear_range(cursor.y * W, cursor.y * W + cursor.x); + break; + } + screen_notifyChange(); +} + +//endregion + +//region Screen manipulation + +/** + * Change the screen size + * + * @param w - new width + * @param h - new height + */ +void ICACHE_FLASH_ATTR +screen_resize(Coordinate w, Coordinate h) +{ + // sanitize + if (w < 1) w = 1; + if (h < 1) h = 1; + + W = w; + H = h; + screen_reset(); + screen_notifyChange(); +} + +/** + * Shift screen upwards + */ +void ICACHE_FLASH_ATTR +screen_scroll_up(unsigned int lines) +{ + if (lines >= H - 1) { + screen_clear(CLEAR_ALL); + return; + } + + if (lines == 0) { + return; + } + + int y; + for (y = 0; y < H - lines; y++) { + memcpy(screen + y * W, screen + (y + lines) * W, W * sizeof(Cell)); + } + + clear_range(y * W, W * H - 1); + screen_notifyChange(); +} + +/** + * Shift screen downwards + */ +void ICACHE_FLASH_ATTR +screen_scroll_down(unsigned int lines) +{ + if (lines >= H - 1) { + screen_clear(CLEAR_ALL); + return; + } + + if (lines == 0) { + return; + } + + int y; + for (y = H-1; y >= lines; y--) { + memcpy(screen + y * W, screen + (y - lines) * W, W * sizeof(Cell)); + } + + clear_range(0, lines * W-1); + screen_notifyChange(); +} + +//endregion + +//region Cursor manipulation + +/** + * Set cursor position + */ +void ICACHE_FLASH_ATTR +screen_cursor_set(Coordinate x, Coordinate y) +{ + if (x >= W) x = W - 1; + if (y >= H) y = H - 1; + cursor.x = x; + cursor.y = y; + screen_notifyChange(); +} + +/** + * Set cursor X position + */ +void ICACHE_FLASH_ATTR +screen_cursor_set_x(Coordinate x) +{ + if (x >= W) x = W - 1; + cursor.x = x; + screen_notifyChange(); +} + +/** + * Set cursor Y position + */ +void ICACHE_FLASH_ATTR +screen_cursor_set_y(Coordinate y) +{ + if (y >= H) y = H - 1; + cursor.y = y; + screen_notifyChange(); +} + +/** + * Relative cursor move + */ +void ICACHE_FLASH_ATTR +screen_cursor_move(int dx, int dy) +{ + if (dx < 0 && -dx > cursor.x) dx = -cursor.x; + if (dy < 0 && -dy > cursor.y) dy = -cursor.y; + screen_cursor_set(cursor.x + dx, cursor.y + dy); + screen_notifyChange(); +} + +/** + * Save the cursor pos + */ +void ICACHE_FLASH_ATTR +screen_cursor_save(void) +{ + cursor_sav.x = cursor.x; + cursor_sav.y = cursor.y; +} + +/** + * Restore the cursor pos + */ +void ICACHE_FLASH_ATTR +screen_cursor_restore(void) +{ + cursor.x = cursor_sav.x; + cursor.y = cursor_sav.y; + screen_notifyChange(); +} + +/** + * Enable cursor display + */ +void ICACHE_FLASH_ATTR +screen_cursor_enable(bool enable) +{ + cursor.visible = enable; + screen_notifyChange(); +} + +//endregion + +//region Colors + +/** + * Set cursor foreground color + */ +void ICACHE_FLASH_ATTR +screen_set_fg(Color color) +{ + if (color > COLOR_MAX) color = COLOR_MAX; + cursor.fg = color; +} + +/** + * Set cursor background coloor + */ +void ICACHE_FLASH_ATTR +screen_set_bg(Color color) +{ + if (color > COLOR_MAX) color = COLOR_MAX; + cursor.bg = color; +} + +/** + * Set cursor foreground and background color + */ +void ICACHE_FLASH_ATTR +screen_set_colors(Color fg, Color bg) +{ + screen_set_fg(fg); + screen_set_bg(bg); +} + +/** + * Invert colors + */ +void ICACHE_FLASH_ATTR +screen_inverse(bool inverse) +{ + cursor.inverse = inverse; +} + +/** + * Make foreground bright. + * + * This relates to the '1' SGR command which originally means + * "bold font". We interpret that as "Bright", similar to other + * terminal emulators. + */ +void ICACHE_FLASH_ATTR +screen_set_bright_fg(void) +{ + cursor.fg = (cursor.fg % 8) + 8; +} + +//endregion + +/** + * Set a character in the cursor color, move to right with wrap. + */ +void ICACHE_FLASH_ATTR +screen_putchar(char ch) +{ + Cell *c = &screen[cursor.x + cursor.y * W]; + c->c = ch; + + if (cursor.inverse) { + c->fg = cursor.bg; + c->bg = cursor.fg; + } else { + c->fg = cursor.fg; + c->bg = cursor.bg; + } + + cursor.x++; + // X wrap + if (cursor.x >= W) { + cursor.x = 0; + cursor.y++; + // Y wrap + if (cursor.y > H-1) { + // Scroll up, so we have space for writing + screen_scroll_up(1); + cursor.y = H-1; + } + } + + screen_notifyChange(); +} + + +//region Serialization + +#if 0 +/** + * Debug dump + */ +void screen_dd(void) +{ + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + Cell *cell = &screen[y * W + x]; + + // FG + printf("\033["); + if (cell->fg > 7) { + printf("%d", 90 + cell->fg - 8); + } else { + printf("%d", 30 + cell->fg); + } + printf("m"); + + // BG + printf("\033["); + if (cell->bg > 7) { + printf("%d", 100 + cell->bg - 8); + } else { + printf("%d", 40 + cell->bg); + } + printf("m"); + + printf("%c", cell->c); + } + printf("\033[0m\n"); + } +} +#endif + +struct ScreenSerializeState { + Color lastFg; + Color lastBg; + char lastChar; + int index; +}; + +/** + * Serialize the screen to a data buffer. May need multiple calls if the buffer is insufficient in size. + * + * @warning MAKE SURE *DATA IS NULL BEFORE FIRST CALL! + * Call with NULL 'buffer' at the end to free the data struct. + * + * @param buffer - buffer array of limited size. If NULL, indicates this is the last call. + * @param buf_len - buffer array size + * @param data - opaque pointer to internal data structure for storing state between repeated calls + * if NULL, indicates this is the first call. + * @return HTTPD_CGI_DONE or HTTPD_CGI_MORE. If more, repeat with the same DATA. + */ +httpd_cgi_state ICACHE_FLASH_ATTR +screenSerializeToBuffer(char *buffer, size_t buf_len, void **data) +{ + struct ScreenSerializeState *ss = *data; + + if (buffer == NULL) { + if (ss != NULL) free(ss); + return HTTPD_CGI_DONE; + } + + Cell *cell, *cell0; + + size_t remain = buf_len; int used = 0; + char *bb = buffer; + + // Ideally we'd use snprintf here! + #define bufprint(fmt, ...) do { \ + used = sprintf(bb, fmt, ##__VA_ARGS__); \ + if(used>0) { bb += used; remain -= used; } \ + } while(0) + + if (ss == NULL) { + *data = ss = malloc(sizeof(struct ScreenSerializeState)); + ss->index = 0; + ss->lastBg = 0; + ss->lastFg = 0; + ss->lastChar = '\0'; + + bufprint("{x:%d,y:%d,screen:\"", cursor.x, cursor.y); + } + + int i = ss->index; + while(i < W*H && remain > 6) { + cell = cell0 = &screen[i]; + + // Count how many times same as previous + int repCnt = 0; + while (i < W*H + && cell->fg == ss->lastFg + && cell->bg == ss->lastBg + && cell->c == ss->lastChar) { + // Repeat + repCnt++; + cell = &screen[++i]; + } + + if (repCnt == 0) { + if (cell0->fg == ss->lastFg && cell0->bg == ss->lastBg) { + // same colors as previous + bufprint(",%c", cell0->c); + } else { + bufprint("%X%X%c", cell0->fg, cell0->bg, cell0->c); + } + + ss->lastFg = cell0->fg; + ss->lastBg = cell0->bg; + ss->lastChar = cell0->c; + + i++; + } else { + char code; + if(repCnt<10) { + code = 'r'; + } else if(repCnt<100) { + code = 's'; + } else if(repCnt<1000) { + code = 't'; + } else { + code = 'u'; + } + + bufprint("%c%d", code, repCnt); + } + } + + ss->index = i; + + if (i < W*H-1) { + return HTTPD_CGI_MORE; + } + + if (remain >= 3) { + bufprint("\"}"); + return HTTPD_CGI_DONE; + } else { + return HTTPD_CGI_MORE; + } +} + +//endregion diff --git a/user/screen.h b/user/screen.h new file mode 100644 index 0000000..4f0da26 --- /dev/null +++ b/user/screen.h @@ -0,0 +1,135 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#include +#include +#include +#include + +/** + * This module handles the virtual screen and operations on it. + * + * It is interfaced by calls from the ANSI parser, and the screen + * data can be rendered for the front-end. + * + * --- + * + * Colors are 0-15, 0-7 dim, 8-15 bright. + * + * NORMAL + * 0 black, 1 red, 2 green, 3 yellow + * 4 blue, 5 mag, 6 cyan, 7 white + * + * BRIGHT + * 8 black, 9 red, 10 green, 11 yellow + * 12 blue, 13 mag, 14 cyan, 15 white + * + * Coordinates are 0-based, left-top is the origin. + * X grows to the right, Y to the bottom. + * + * +----> + * | X + * | + * V Y + * + */ + +/** + * Maximum screen size (determines size of the static data array) + * + * TODO May need adjusting if there are size problems when flashing the ESP. + * We could also try to pack the Cell struct to a single 32bit word. + */ +#define MAX_SCREEN_SIZE (80*25) + +#define SCREEN_DEF_W 26 //!< Default screen width +#define SCREEN_DEF_H 10 //!< Default screen height + +#define SCREEN_DEF_BG 0 //!< Default screen background +#define SCREEN_DEF_FG 7 //!< Default screen foreground + +typedef enum { + CLEAR_TO_CURSOR=0, CLEAR_FROM_CURSOR=1, CLEAR_ALL=2 +} ClearMode; + +typedef uint8_t Color; +typedef unsigned int Coordinate; + +httpd_cgi_state ICACHE_FLASH_ATTR +screenSerializeToBuffer(char *buffer, size_t buf_len, void **data); + +/** Init the screen */ +void screen_init(void); + +/** Change the screen size */ +void screen_resize(Coordinate w, Coordinate h); + +// --- Clearing --- + +/** Screen reset to default state */ +void screen_reset(void); + +/** Clear entire screen, set all to 7 on 0 */ +void screen_clear(ClearMode mode); + +/** Line reset to gray-on-white, empty */ +void screen_clear_line(ClearMode mode); + +/** Shift screen upwards */ +void screen_scroll_up(unsigned int lines); + +/** Shift screen downwards */ +void screen_scroll_down(unsigned int lines); + +// --- Cursor control --- + +/** Set cursor position */ +void screen_cursor_set(Coordinate x, Coordinate y); + +/** Set cursor X position */ +void screen_cursor_set_x(Coordinate x); + +/** Set cursor Y position */ +void screen_cursor_set_y(Coordinate y); + +/** Relative cursor move */ +void screen_cursor_move(int dx, int dy); + +/** Save the cursor pos */ +void screen_cursor_save(void); + +/** Restore the cursor pos */ +void screen_cursor_restore(void); + +/** Enable cursor display */ +void screen_cursor_enable(bool enable); + +// --- Colors --- + +/** Set cursor foreground color */ +void screen_set_fg(Color color); + +/** Set cursor background coloor */ +void screen_set_bg(Color color); + +/** make foreground bright */ +void screen_set_bright_fg(void); + +/** Set cursor foreground and background color */ +void screen_set_colors(Color fg, Color bg); + +/** Invert colors */ +void screen_inverse(bool inverse); + + +/** Set a character in the cursor color, move to right with wrap. */ +void screen_putchar(char c); + +#if 0 +/** Debug dump */ +void screen_dd(void); +#endif + +extern void screen_notifyChange(void); + +#endif // SCREEN_H diff --git a/user/serial.c b/user/serial.c index 46dbf60..99c9f10 100644 --- a/user/serial.c +++ b/user/serial.c @@ -1,6 +1,7 @@ #include #include "uart_driver.h" #include "uart_handler.h" +#include "ansi_parser.h" // Here the bitrates are defined #define UART0_BAUD BIT_RATE_115200 @@ -24,5 +25,7 @@ void ICACHE_FLASH_ATTR serialInit(void) */ void ICACHE_FLASH_ATTR UART_HandleRxByte(char c) { - printf("'%c',", c); + // TODO buffering, do not run parser after just 1 char + printf("(%c)", c); + ansi_parser(&c, 1); } \ No newline at end of file diff --git a/user/user_main.c b/user/user_main.c index 56c6022..1e8a45d 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -19,14 +19,49 @@ #include #include "serial.h" #include "io.h" +#include "screen.h" #define FIRMWARE_VERSION "0.1" #define SHOW_HEAP_USE 1 +void screen_notifyChange() { + // TODO cooldown / buffering to reduce nr of such events + dbg("Screen notifyChange"); -void myWebsocketConnect(Websock *ws) { - // NOOP + void *data = NULL; + + const int bufsiz = 1024; + char buff[bufsiz]; + for (int i = 0; i < 20; i++) { + httpd_cgi_state cont = screenSerializeToBuffer(buff, bufsiz, &data); + cgiWebsockBroadcast("/ws/update.cgi", buff, (int)strlen(buff), (cont == HTTPD_CGI_MORE) ? WEBSOCK_FLAG_CONT : WEBSOCK_FLAG_NONE); + if (cont == HTTPD_CGI_DONE) break; + } +} + +void ICACHE_FLASH_ATTR myWebsocketConnect(Websock *ws) { + dbg("Socket connected."); +} + +httpd_cgi_state ICACHE_FLASH_ATTR tplScreen(HttpdConnData *connData, char *token, void **arg) { + // cleanup + if (!connData) { + // Release data object + screenSerializeToBuffer(NULL, 0, arg); + return HTTPD_CGI_DONE; + } + + const int bufsiz = 1024; + char buff[bufsiz]; + + if (streq(token, "screenData")) { + httpd_cgi_state cont = screenSerializeToBuffer(buff, bufsiz, arg); + httpdSend(connData, buff, -1); + return cont; + } + + return HTTPD_CGI_DONE; } @@ -51,7 +86,7 @@ CgiUploadFlashDef uploadParams={ #endif /** Routes */ -HttpdBuiltInUrl builtInUrls[]={ +HttpdBuiltInUrl builtInUrls[]={ //ICACHE_RODATA_ATTR // redirect func for the captive portal ROUTE_CGI_ARG("*", cgiRedirectApClientToHostname, "esp8266.nonet"), @@ -59,6 +94,7 @@ HttpdBuiltInUrl builtInUrls[]={ // TODO add funcs for WiFi management (when web UI is added) + ROUTE_TPL_FILE("/", tplScreen, "index.html"), ROUTE_FILESYSTEM(), ROUTE_END(), }; @@ -107,6 +143,8 @@ void user_init(void) { os_timer_arm(&prHeapTimer, 3000, 1); #endif + screen_init(); + info("System ready!"); }