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
\ No newline at end of file
+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!");
}