#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 { int x; //!< X coordinate int 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 { int x; int y; } cursor_sav; /** * Active screen width */ static int W = SCREEN_DEF_W; /** * Active screen height */ static int H = SCREEN_DEF_H; // XXX volatile is probably not needed static volatile int notifyLock = 0; //endregion //region Helpers #define NOTIFY_LOCK() do { \ notifyLock++; \ } while(0) #define NOTIFY_DONE() do { \ if (notifyLock > 0) notifyLock--; \ if (notifyLock == 0) screen_notifyChange(); \ } while(0) /** * 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) { NOTIFY_LOCK(); for (unsigned int i = 0; i < MAX_SCREEN_SIZE; i++) { cell_init(&screen[i]); } cursor_reset(); NOTIFY_DONE(); } /** * Reset the screen (only the visible area) */ void ICACHE_FLASH_ATTR screen_reset(void) { NOTIFY_LOCK(); screen_clear(CLEAR_ALL); cursor_reset(); NOTIFY_DONE(); } /** * Clear screen area */ void ICACHE_FLASH_ATTR screen_clear(ClearMode mode) { NOTIFY_LOCK(); 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; } NOTIFY_DONE(); } /** * Line reset to gray-on-white, empty */ void ICACHE_FLASH_ATTR screen_clear_line(ClearMode mode) { NOTIFY_LOCK(); 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; } NOTIFY_DONE(); } //endregion //region Screen manipulation /** * Change the screen size * * @param cols - new width * @param rows - new height */ void ICACHE_FLASH_ATTR screen_resize(int rows, int cols) { NOTIFY_LOCK(); // sanitize if (cols < 1 || rows < 1) { error("Screen size must be positive"); goto done; } if (cols * rows > MAX_SCREEN_SIZE) { error("Max screen size exceeded"); goto done; } W = cols; H = rows; screen_reset(); done: NOTIFY_DONE(); } /** * Shift screen upwards */ void ICACHE_FLASH_ATTR screen_scroll_up(unsigned int lines) { NOTIFY_LOCK(); if (lines >= H - 1) { screen_clear(CLEAR_ALL); goto done; } // bad cmd if (lines == 0) { goto done; } 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); done: NOTIFY_DONE(); } /** * Shift screen downwards */ void ICACHE_FLASH_ATTR screen_scroll_down(unsigned int lines) { NOTIFY_LOCK(); if (lines >= H - 1) { screen_clear(CLEAR_ALL); goto done; } // bad cmd if (lines == 0) { goto done; } 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); done: NOTIFY_DONE(); } //endregion //region Cursor manipulation /** * Set cursor position */ void ICACHE_FLASH_ATTR screen_cursor_set(int y, int x) { NOTIFY_LOCK(); if (x >= W) x = W - 1; if (y >= H) y = H - 1; cursor.x = x; cursor.y = y; NOTIFY_DONE(); } /** * Set cursor position */ void ICACHE_FLASH_ATTR screen_cursor_get(int *y, int *x) { *x = cursor.x; *y = cursor.y; } /** * Set cursor X position */ void ICACHE_FLASH_ATTR screen_cursor_set_x(int x) { NOTIFY_LOCK(); if (x >= W) x = W - 1; cursor.x = x; NOTIFY_DONE(); } /** * Set cursor Y position */ void ICACHE_FLASH_ATTR screen_cursor_set_y(int y) { NOTIFY_LOCK(); if (y >= H) y = H - 1; cursor.y = y; NOTIFY_DONE(); } /** * Relative cursor move */ void ICACHE_FLASH_ATTR screen_cursor_move(int dy, int dx) { NOTIFY_LOCK(); int move; cursor.x += dx; cursor.y += dy; if (cursor.x >= W) cursor.x = W - 1; if (cursor.x < 0) cursor.x = 0; if (cursor.y < 0) { move = -cursor.y; cursor.y = 0; screen_scroll_down((unsigned int)move); } if (cursor.y >= H) { move = cursor.y - (H - 1); cursor.y = H - 1; screen_scroll_up((unsigned int)move); } NOTIFY_DONE(); } /** * 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) { NOTIFY_LOCK(); cursor.x = cursor_sav.x; cursor.y = cursor_sav.y; NOTIFY_DONE(); } /** * Enable cursor display */ void ICACHE_FLASH_ATTR screen_cursor_enable(bool enable) { NOTIFY_LOCK(); cursor.visible = enable; NOTIFY_DONE(); } //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 /** * Check if coords are in range * * @param y * @param x * @return OK */ bool ICACHE_FLASH_ATTR screen_isCoordValid(int y, int x) { return x >= 0 && y >= 0 && x < W && y < H; } /** * Set a character in the cursor color, move to right with wrap. */ void ICACHE_FLASH_ATTR screen_putchar(char ch) { NOTIFY_LOCK(); Cell *c = &screen[cursor.x + cursor.y * W]; // Special treatment for CRLF switch (ch) { case '\r': screen_cursor_set_x(0); goto done; case '\n': screen_cursor_move(1, 0); goto done; case 8: // BS if (cursor.x > 0) { cursor.x--; } else { // wrap around start of line if (cursor.y>0) { cursor.x=W-1; cursor.y--; } } // erase target cell c = &screen[cursor.x + cursor.y * W]; c->c = ' '; goto done; case 9: // TAB if (cursor.x<((W-1)-(W-1)%4)) { c->c = ' '; do { screen_putchar(' '); } while(cursor.x%4!=0); } goto done; default: if (ch < ' ') { // Discard warn("Ignoring control char %d", (int)ch); goto done; } } 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; } } done: NOTIFY_DONE(); } //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("{\n \"w\": %d, \"h\": %d,\n \"x\": %d, \"y\": %d,\n \"cv\": %d,\n \"screen\": \"", W, H, cursor.x, cursor.y, cursor.visible); } int i = ss->index; while(i < W*H && remain > 12) { 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) { // All this crap is needed because it's JSON and also // embedded in HTML (hence the angle brackets) if (cell0->fg == ss->lastFg && cell0->bg == ss->lastBg) { // same colors as previous bufprint(","); } else { bufprint("%X%X", cell0->fg, cell0->bg); } char c = cell0->c; if (c == '"' || c == '\\') { bufprint("\\%c", c); } else if (c == '<' || c == '>' || c == '\'' || c == '/' || c == '&') { bufprint("\\u00%02X", (int)c); } else { bufprint("%c", 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("\"\n}"); return HTTPD_CGI_DONE; } else { return HTTPD_CGI_MORE; } } //endregion