diff --git a/user/apars_csi.c b/user/apars_csi.c index ed18ac9..072d673 100644 --- a/user/apars_csi.c +++ b/user/apars_csi.c @@ -757,11 +757,11 @@ do_csi_set_private_option(CSI_Data *opts) } else if (n == 800) { // ESPTerm: Toggle display of buttons termconf_live.show_buttons = yn; - screen_notifyChange(CHANGE_CONTENT); // this info is included in the screen preamble + screen_notifyChange(TOPIC_CHANGE_SCREEN_OPTS); // this info is included in the screen preamble } else if (n == 801) { // ESPTerm: Toggle display of config links termconf_live.show_config_links = yn; - screen_notifyChange(CHANGE_CONTENT); // this info is included in the screen preamble + screen_notifyChange(TOPIC_CHANGE_SCREEN_OPTS); // this info is included in the screen preamble } else { ansi_noimpl("?OPTION %d", n); diff --git a/user/cgi_main.c b/user/cgi_main.c index da13a41..fe33ab6 100644 --- a/user/cgi_main.c +++ b/user/cgi_main.c @@ -25,11 +25,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplScreen(HttpdConnData *connData, char *token char buff[150]; - if (streq(token, "labels_seq")) { - screenSerializeLabelsToBuffer(buff, 150); - tplSend(connData, buff, -1); - } - else if (streq(token, "theme")) { + if (streq(token, "theme")) { sprintf(buff, "%d", termconf->theme); tplSend(connData, buff, -1); } diff --git a/user/cgi_sockets.c b/user/cgi_sockets.c index 7f4fcfc..baddd07 100644 --- a/user/cgi_sockets.c +++ b/user/cgi_sockets.c @@ -16,6 +16,8 @@ // Must be less than httpd sendbuf #define SOCK_BUF_LEN 2000 +volatile ScreenNotifyTopics pendingBroadcastTopics = 0; + // flags for screen update timeouts volatile bool notify_available = true; volatile bool notify_cooldown = false; @@ -24,12 +26,11 @@ volatile bool notify_cooldown = false; * and we have to tell it we're ready again */ volatile bool browser_wants_xon = false; -static ETSTimer notifyContentTim; -static ETSTimer notifyLabelsTim; +static ETSTimer updateNotifyTim; static ETSTimer notifyCooldownTim; static ETSTimer heartbeatTim; -volatile int active_clients = 0; +volatile int term_active_clients = 0; // we're trying to do a kind of mutex here, without the actual primitives // this might glitch, very rarely. @@ -52,25 +53,21 @@ notifyCooldownTimCb(void *arg) * @param arg */ static void ICACHE_FLASH_ATTR -notifyContentTimCb(void *arg) +updateNotify_do(Websock *ws, ScreenNotifyTopics topics) { - Websock *ws = arg; void *data = NULL; - int max_bl, total_bl; char sock_buff[SOCK_BUF_LEN]; - cgiWebsockMeasureBacklog(URL_WS_UPDATE, &max_bl, &total_bl); - - if (!notify_available || notify_cooldown || (max_bl > 2048)) { // do not send if we have anything significant backlogged - // postpone a little - TIMER_START(¬ifyContentTim, notifyContentTimCb, 4, 0); - inp_dbg("postpone notify content"); - return; - } notify_available = false; for (int i = 0; i < 20; i++) { - httpd_cgi_state cont = screenSerializeToBuffer(sock_buff, SOCK_BUF_LEN, &data); + if (! ws) { + // broadcast + topics = pendingBroadcastTopics; + pendingBroadcastTopics = 0; + } + httpd_cgi_state cont = screenSerializeToBuffer(sock_buff, SOCK_BUF_LEN, topics, &data); + int flg = 0; if (cont == HTTPD_CGI_MORE) flg |= WEBSOCK_FLAG_MORE; if (i > 0) flg |= WEBSOCK_FLAG_CONT; @@ -86,56 +83,42 @@ notifyContentTimCb(void *arg) } // cleanup - screenSerializeToBuffer(NULL, SOCK_BUF_LEN, &data); + screenSerializeToBuffer(NULL, 0, 0, &data); - notify_cooldown = true; notify_available = true; - - TIMER_START(¬ifyCooldownTim, notifyCooldownTimCb, termconf->display_cooldown_ms, 0); } /** - * Tell browsers about the new text labels + * Tell browser we have new content * @param arg */ static void ICACHE_FLASH_ATTR -notifyLabelsTimCb(void *arg) +updateNotifyCb(void *arg) { - Websock *ws = arg; - char sock_buff[SOCK_BUF_LEN]; + int max_bl, total_bl; + cgiWebsockMeasureBacklog(URL_WS_UPDATE, &max_bl, &total_bl); - if (!notify_available || notify_cooldown) { + if (!notify_available || notify_cooldown || (max_bl > 2048)) { // do not send if we have anything significant backlogged // postpone a little - TIMER_START(¬ifyLabelsTim, notifyLabelsTimCb, 7, 0); - inp_dbg("postpone notify labels"); + TIMER_START(&updateNotifyTim, updateNotifyCb, 4, 0); + inp_dbg("postpone notify content"); return; } - notify_available = false; - - screenSerializeLabelsToBuffer(sock_buff, SOCK_BUF_LEN); - if (ws) { - cgiWebsocketSend(ws, sock_buff, (int) strlen(sock_buff), 0); - } else { - cgiWebsockBroadcast(URL_WS_UPDATE, sock_buff, (int) strlen(sock_buff), 0); - resetHeartbeatTimer(); - } + updateNotify_do(arg, 0); notify_cooldown = true; - notify_available = true; TIMER_START(¬ifyCooldownTim, notifyCooldownTimCb, termconf->display_cooldown_ms, 0); } + /** Beep */ void ICACHE_FLASH_ATTR send_beep(void) { - if (active_clients == 0) return; - - // here's some potential for a race error with the other broadcast functions :C - cgiWebsockBroadcast(URL_WS_UPDATE, "B", 1, 0); - resetHeartbeatTimer(); + if (term_active_clients == 0) return; + screen_notifyChange(TOPIC_BELL); // XXX has latency, maybe better to send directly } @@ -143,10 +126,10 @@ send_beep(void) void ICACHE_FLASH_ATTR notify_growl(char *msg) { - if (active_clients == 0) return; + if (term_active_clients == 0) return; - // TODO via timer... - // here's some potential for a race error with the other broadcast functions :C + // here's some potential for a race error with the other broadcast functions + // - we assume app won't send notifications in the middle of updating content cgiWebsockBroadcast(URL_WS_UPDATE, msg, (int) strlen(msg), 0); resetHeartbeatTimer(); } @@ -157,24 +140,17 @@ notify_growl(char *msg) * This is a callback for the Screen module, * called after each visible screen modification. */ -void ICACHE_FLASH_ATTR screen_notifyChange(ScreenNotifyChangeTopic topic) +void ICACHE_FLASH_ATTR screen_notifyChange(ScreenNotifyTopics topics) { - if (active_clients == 0) return; + if (term_active_clients == 0) return; // this is probably not needed here - ensure timeout is not 0 - if (termconf->display_tout_ms == 0) + if (termconf->display_tout_ms == 0) { termconf->display_tout_ms = SCR_DEF_DISPLAY_TOUT_MS; - - // NOTE: the timers are restarted if already running - - if (topic == CHANGE_LABELS) { - // separate timer from content change timer, to avoid losing that update - TIMER_START(¬ifyLabelsTim, notifyLabelsTimCb, termconf->display_tout_ms+2, 0); // this delay is useful when both are fired at once on screen reset - } - else if (topic == CHANGE_CONTENT) { - // throttle delay - TIMER_START(¬ifyContentTim, notifyContentTimCb, termconf->display_tout_ms, 0); } + + // NOTE: the timer is restarted if already running + TIMER_START(&updateNotifyTim, updateNotifyCb, termconf->display_tout_ms, 0); // note - this adds latency to beep } /** @@ -296,8 +272,7 @@ static void ICACHE_FLASH_ATTR updateSockRx(Websock *ws, char *data, int len, int case 'i': // requests initial load inp_dbg("Client requests initial load"); - notifyContentTimCb(ws); // delay?? - notifyLabelsTimCb(ws); + updateNotify_do(ws, TOPIC_INITIAL); break; case 'm': @@ -322,7 +297,7 @@ static void ICACHE_FLASH_ATTR updateSockRx(Websock *ws, char *data, int len, int /** Send a heartbeat msg */ static void ICACHE_FLASH_ATTR heartbeatTimCb(void *unused) { - if (active_clients > 0) { + if (term_active_clients > 0) { if (notify_available) { inp_dbg("."); @@ -348,9 +323,9 @@ static void ICACHE_FLASH_ATTR resetHeartbeatTimer(void) static void ICACHE_FLASH_ATTR closeSockCb(Websock *ws) { - active_clients--; - if (active_clients <= 0) { - active_clients = 0; + term_active_clients--; + if (term_active_clients <= 0) { + term_active_clients = 0; if (mouse_tracking.focus_tracking) { UART_SendAsync("\x1b[O", 3); @@ -368,7 +343,7 @@ void ICACHE_FLASH_ATTR updateSockConnect(Websock *ws) ws->recvCb = updateSockRx; ws->closeCb = closeSockCb; - if (active_clients == 0) { + if (term_active_clients == 0) { if (mouse_tracking.focus_tracking) { UART_SendAsync("\x1b[I", 3); } @@ -376,7 +351,7 @@ void ICACHE_FLASH_ATTR updateSockConnect(Websock *ws) resetHeartbeatTimer(); } - active_clients++; + term_active_clients++; } ETSTimer xonTim; diff --git a/user/cgi_sockets.h b/user/cgi_sockets.h index 2010cfb..f608b3d 100644 --- a/user/cgi_sockets.h +++ b/user/cgi_sockets.h @@ -15,6 +15,8 @@ void send_beep(void); /** open pop-up notification */ void notify_growl(char *msg); +extern volatile int term_active_clients; + // defined in the makefile #if DEBUG_INPUT #define inp_warn warn diff --git a/user/cgi_term_cfg.c b/user/cgi_term_cfg.c index 775db58..85f810e 100644 --- a/user/cgi_term_cfg.c +++ b/user/cgi_term_cfg.c @@ -40,7 +40,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) char buff[50]; char redir_url_buf[100]; int32 n, w, h; - bool notify_screen_content = 0, notify_screen_labels = 0; + ScreenNotifyTopics topics = 0; bool shall_clear_screen = false; bool shall_init_uart = false; @@ -94,7 +94,8 @@ cgiTermCfgSetParams(HttpdConnData *connData) if (termconf->width != w || termconf->height != h) { termconf->width = w; termconf->height = h; - shall_clear_screen = true; // this causes a notify + shall_clear_screen = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS | TOPIC_CHANGE_CONTENT_ALL; } } while (0); } @@ -112,6 +113,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) if (termconf->default_bg != n) { termconf->default_bg = n; // this is current not sent through socket, no use to notify + topics |= TOPIC_CHANGE_SCREEN_OPTS; } } @@ -128,6 +130,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) if (termconf->default_fg != n) { termconf->default_fg = n; // this is current not sent through socket, no use to notify + topics |= TOPIC_CHANGE_SCREEN_OPTS; } } @@ -168,40 +171,49 @@ cgiTermCfgSetParams(HttpdConnData *connData) cgi_dbg("FN alt mode: %s", buff); n = atoi(buff); termconf->fn_alt_mode = (bool)n; - notify_screen_content = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("want_all_fn")) { cgi_dbg("AllFN mode: %s", buff); n = atoi(buff); termconf->want_all_fn = (bool)n; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("crlf_mode")) { cgi_dbg("CRLF mode: %s", buff); n = atoi(buff); termconf->crlf_mode = (bool)n; - notify_screen_content = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("show_buttons")) { cgi_dbg("Show buttons: %s", buff); n = atoi(buff); termconf->show_buttons = (bool)n; - notify_screen_content = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("show_config_links")) { cgi_dbg("Show config links: %s", buff); n = atoi(buff); termconf->show_config_links = (bool)n; - notify_screen_content = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("loopback")) { cgi_dbg("Loopback: %s", buff); n = atoi(buff); termconf->loopback = (bool)n; + topics |= TOPIC_CHANGE_SCREEN_OPTS; + } + + if (GET_ARG("debugbar")) { + cgi_dbg("Debugbar: %s", buff); + n = atoi(buff); + termconf->debugbar = (bool)n; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } if (GET_ARG("theme")) { @@ -213,6 +225,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) } else { cgi_warn("Bad theme num: %s", buff); redir_url += sprintf(redir_url, "theme,"); + topics |= TOPIC_CHANGE_SCREEN_OPTS; } } @@ -221,7 +234,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) n = atoi(buff); if (n >= 0 && n <= 6 && n != 1) { termconf->cursor_shape = (enum CursorShape) n; - notify_screen_content = true; + topics |= TOPIC_CHANGE_SCREEN_OPTS; } else { cgi_warn("Bad cursor_shape num: %s", buff); redir_url += sprintf(redir_url, "cursor_shape,"); @@ -231,7 +244,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) if (GET_ARG("term_title")) { cgi_dbg("Terminal title default text: \"%s\"", buff); strncpy_safe(termconf->title, buff, 64); // ATTN those must match the values in - notify_screen_labels = true; + topics |= TOPIC_CHANGE_TITLE; } for (int btn_i = 1; btn_i <= TERM_BTN_COUNT; btn_i++) { @@ -239,7 +252,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) if (GET_ARG(buff)) { cgi_dbg("Button%d default text: \"%s\"", btn_i, buff); strncpy_safe(termconf->btn[btn_i-1], buff, TERM_BTN_LEN); - notify_screen_labels = true; + topics |= TOPIC_CHANGE_BUTTONS; } sprintf(buff, "bm%d", btn_i); @@ -368,13 +381,7 @@ cgiTermCfgSetParams(HttpdConnData *connData) serialInit(); } - if (notify_screen_content) { - screen_notifyChange(CHANGE_CONTENT); - } - - if (notify_screen_labels) { - screen_notifyChange(CHANGE_LABELS); - } + if (topics) screen_notifyChange(topics); httpdRedirect(connData, SET_REDIR_SUC "?msg=Settings%20saved%20and%20applied."); } else { @@ -440,6 +447,9 @@ tplTermCfg(HttpdConnData *connData, char *token, void **arg) else if (streq(token, "loopback")) { sprintf(buff, "%d", (int)termconf->loopback); } + else if (streq(token, "debugbar")) { + sprintf(buff, "%d", (int)termconf->debugbar); + } else if (streq(token, "theme")) { sprintf(buff, "%d", termconf->theme); } diff --git a/user/routes.h b/user/routes.h index 2ba843f..5296b48 100644 --- a/user/routes.h +++ b/user/routes.h @@ -6,7 +6,4 @@ extern const HttpdBuiltInUrl routes[]; -/** Broadcast screen state to sockets */ -void screen_notifyChange(); - #endif //ROUTES_H diff --git a/user/screen.c b/user/screen.c index 3a7af28..e24aee5 100644 --- a/user/screen.c +++ b/user/screen.c @@ -5,10 +5,9 @@ #include "sgr.h" #include "ascii.h" #include "apars_logging.h" -#include "jstring.h" #include "character_sets.h" #include "utf8.h" -#include "uart_buffer.h" +#include "cgi_sockets.h" TerminalConfigBundle * const termconf = &persist.current.termconf; TerminalConfigBundle termconf_live; @@ -74,7 +73,6 @@ typedef struct { bool hanging; //!< xenl state - cursor half-wrapped /* SGR */ - bool inverse; //!< not in attrs bc it's applied immediately when writing the cell bool conceal; //!< similar to inverse, causes all to be replaced by SP u16 attrs; Color fg; //!< Foreground color for writing @@ -139,14 +137,19 @@ static struct { * (from nested calls) */ static volatile int notifyLock = 0; +static volatile ScreenNotifyTopics lockTopics = 0; -#define NOTIFY_LOCK() do { \ +#define NOTIFY_LOCK() do { \ notifyLock++; \ } while(0) - -#define NOTIFY_DONE() do { \ + +#define NOTIFY_DONE(updateTopics) do { \ + lockTopics |= (updateTopics); \ if (notifyLock > 0) notifyLock--; \ - if (notifyLock == 0) screen_notifyChange(CHANGE_CONTENT); \ + if (notifyLock == 0) { \ + screen_notifyChange(lockTopics); \ + lockTopics = 0;\ + } \ } while(0) /** Clear the hanging attribute if the cursor is no longer >= W */ @@ -185,6 +188,7 @@ terminal_restore_defaults(void) termconf->cursor_shape = SCR_DEF_CURSOR_SHAPE; termconf->crlf_mode = SCR_DEF_CRLF; termconf->want_all_fn = SCR_DEF_ALLFN; + termconf->debugbar = SCR_DEF_DEBUGBAR; } /** @@ -203,12 +207,12 @@ terminal_apply_settings_noclear(void) { bool changed = false; -// // Migrate to v1 -// if (termconf->config_version < 1) { -// persist_dbg("termconf: Updating to version %d", 1); -// termconf->display_cooldown_ms = SCR_DEF_DISPLAY_COOLDOWN_MS; -// changed = 1; -// } + // Migrate to v1 + if (termconf->config_version < 1) { + persist_dbg("termconf: Updating to version %d", 1); + termconf->debugbar = SCR_DEF_DEBUGBAR; + changed = 1; + } termconf->config_version = TERMCONF_VERSION; @@ -257,6 +261,8 @@ screen_init(void) static void ICACHE_FLASH_ATTR cursor_reset(void) { + NOTIFY_LOCK(); + cursor.x = 0; cursor.y = 0; cursor.hanging = false; @@ -266,6 +272,8 @@ cursor_reset(void) cursor.charset1 = CS_0_DEC_SUPPLEMENTAL; screen_reset_sgr(); + + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } /** @@ -285,7 +293,7 @@ screen_reset_on_resize(void) // size is left unchanged screen_clear(CLEAR_ALL); // also clears utf cache - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -294,15 +302,20 @@ screen_reset_on_resize(void) void ICACHE_FLASH_ATTR screen_reset_sgr(void) { + NOTIFY_LOCK(); + cursor.fg = 0; cursor.bg = 0; cursor.attrs = 0; cursor.conceal = false; + + NOTIFY_DONE(TOPIC_INTERNAL); } static void ICACHE_FLASH_ATTR screen_reset_do(bool size, bool labels) { + ScreenNotifyTopics topics = TOPIC_CHANGE_SCREEN_OPTS | TOPIC_CHANGE_CURSOR | TOPIC_CHANGE_CONTENT_ALL; NOTIFY_LOCK(); // DECopts @@ -350,7 +363,7 @@ screen_reset_do(bool size, bool labels) termconf_live.show_buttons = termconf->show_buttons; termconf_live.show_config_links = termconf->show_config_links; - screen_notifyChange(CHANGE_LABELS); + topics |= TOPIC_CHANGE_TITLE | TOPIC_CHANGE_BUTTONS; } // initial values in the save buffer in case of receiving restore without storing first @@ -368,7 +381,7 @@ screen_reset_do(bool size, bool labels) opt_backup.show_buttons = termconf_live.show_buttons; opt_backup.show_config_links = termconf_live.show_config_links; - NOTIFY_DONE(); + NOTIFY_DONE(topics); } /** @@ -395,8 +408,11 @@ screen_swap_state(bool alternate) return; // nothing to do } + NOTIFY_LOCK(); + if (alternate) { ansi_dbg("Swap to alternate"); + NOTIFY_LOCK(); // store old state memcpy(state_backup.title, termconf_live.title, TERM_TITLE_LEN); memcpy(state_backup.btn, termconf_live.btn, sizeof(termconf_live.btn)); @@ -411,7 +427,6 @@ screen_swap_state(bool alternate) } else { ansi_dbg("Unswap from alternate"); - NOTIFY_LOCK(); memcpy(termconf_live.title, state_backup.title, TERM_TITLE_LEN); memcpy(termconf_live.btn, state_backup.btn, sizeof(termconf_live.btn)); memcpy(termconf_live.btn_msg, state_backup.btn_msg, sizeof(termconf_live.btn_msg)); @@ -421,11 +436,10 @@ screen_swap_state(bool alternate) // this may clear the screen as a side effect if size changed screen_resize(state_backup.height, state_backup.width); // TODO restore screen content (if this is ever possible) - NOTIFY_DONE(); - screen_notifyChange(CHANGE_LABELS); } state_backup.alternate_active = alternate; + NOTIFY_DONE(TOPIC_INITIAL); } //endregion @@ -435,19 +449,25 @@ screen_swap_state(bool alternate) void ICACHE_FLASH_ATTR screen_clear_all_tabs(void) { + NOTIFY_LOCK(); memset(scr.tab_stops, 0, sizeof(scr.tab_stops)); + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_tab(void) { + NOTIFY_LOCK(); scr.tab_stops[cursor.x/32] |= (1<<(cursor.x%32)); + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_clear_tab(void) { + NOTIFY_LOCK(); scr.tab_stops[cursor.x/32] &= ~(1<<(cursor.x%32)); + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -529,7 +549,7 @@ screen_tab_forward(int count) cursor.x = W - 1; } } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } void ICACHE_FLASH_ATTR @@ -544,7 +564,7 @@ screen_tab_reverse(int count) cursor.x = 0; } } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } //endregion @@ -733,7 +753,7 @@ screen_clear(ClearMode mode) clear_range_utf(0, (cursor.y * W) + cursor.x); break; } - NOTIFY_DONE(); + NOTIFY_DONE(mode == CLEAR_ALL ? TOPIC_CHANGE_CONTENT_ALL : TOPIC_CHANGE_CONTENT_PART); } /** @@ -756,7 +776,7 @@ screen_clear_line(ClearMode mode) clear_range_utf(cursor.y * W, cursor.y * W + cursor.x); break; } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } void ICACHE_FLASH_ATTR @@ -768,7 +788,7 @@ screen_clear_in_line(unsigned int count) } else { clear_range_utf(cursor.y * W + cursor.x, cursor.y * W + cursor.x + count - 1); } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } void ICACHE_FLASH_ATTR @@ -796,7 +816,7 @@ screen_insert_lines(unsigned int lines) copy_row(i, cursor.y); } } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } void ICACHE_FLASH_ATTR @@ -821,7 +841,7 @@ screen_delete_lines(unsigned int lines) clear_range_noutf((movedBlockEnd+1)*W, (BTM+1)*W-1); } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } void ICACHE_FLASH_ATTR @@ -844,7 +864,7 @@ screen_insert_characters(unsigned int count) } clear_range_utf(cursor.y * W + cursor.x, cursor.y * W + targetStart - 1); } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } void ICACHE_FLASH_ATTR @@ -870,7 +890,7 @@ screen_delete_characters(unsigned int count) screen_clear_line(CLEAR_FROM_CURSOR); } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } //endregion @@ -891,7 +911,7 @@ screen_fill_with_E(void) for (unsigned int i = 0; i <= W*H-1; i++) { memcpy(&screen[i], &sample, sizeof(Cell)); } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_ALL); } /** @@ -920,14 +940,15 @@ screen_resize(int rows, int cols) W = cols; H = rows; screen_reset_on_resize(); - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS|TOPIC_CHANGE_CONTENT_ALL|TOPIC_CHANGE_CURSOR); } void ICACHE_FLASH_ATTR screen_set_title(const char *title) { + NOTIFY_LOCK(); strncpy(termconf_live.title, title, TERM_TITLE_LEN); - screen_notifyChange(CHANGE_LABELS); + NOTIFY_DONE(TOPIC_CHANGE_TITLE); } /** @@ -938,8 +959,9 @@ screen_set_title(const char *title) void ICACHE_FLASH_ATTR screen_set_button_text(int num, const char *text) { + NOTIFY_LOCK(); strncpy(termconf_live.btn[num-1], text, TERM_BTN_LEN); - screen_notifyChange(CHANGE_LABELS); + NOTIFY_DONE(TOPIC_CHANGE_BUTTONS); } /** @@ -970,7 +992,7 @@ screen_scroll_up(unsigned int lines) clear_range_noutf(y * W, (BTM + 1) * W - 1); done: - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } /** @@ -1000,13 +1022,14 @@ screen_scroll_down(unsigned int lines) clear_range_noutf(TOP * W, TOP * W + lines * W - 1); done: - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CONTENT_PART); } /** Set scrolling region */ void ICACHE_FLASH_ATTR screen_set_scrolling_region(int from, int to) { + NOTIFY_LOCK(); if (from <= 0 && to <= 0) { scr.vm0 = 0; scr.vm1 = H-1; @@ -1020,6 +1043,7 @@ screen_set_scrolling_region(int from, int to) // Always move cursor home (may be translated due to DECOM) screen_cursor_set(0, 0); + NOTIFY_DONE(TOPIC_INTERNAL); } //endregion @@ -1033,7 +1057,7 @@ screen_cursor_shape(enum CursorShape shape) NOTIFY_LOCK(); if (shape == CURSOR_DEFAULT) shape = termconf->cursor_shape; termconf_live.cursor_shape = shape; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } /** set cursor blink option */ @@ -1050,7 +1074,7 @@ screen_cursor_blink(bool blink) if (termconf_live.cursor_shape == CURSOR_BAR_BL) termconf_live.cursor_shape = CURSOR_BAR; if (termconf_live.cursor_shape == CURSOR_UNDERLINE_BL) termconf_live.cursor_shape = CURSOR_UNDERLINE; } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } /** @@ -1062,7 +1086,7 @@ screen_cursor_set(int y, int x) NOTIFY_LOCK(); screen_cursor_set_x(x); screen_cursor_set_y(y); - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } /** @@ -1093,7 +1117,7 @@ screen_cursor_set_x(int x) // hanging happens when the cursor is virtually at col=81, which // cannot be set using the cursor-set commands. cursor.hanging = false; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } /** @@ -1112,7 +1136,7 @@ screen_cursor_set_y(int y) if (y < 0) y = 0; } cursor.y = y; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } /** @@ -1195,7 +1219,7 @@ screen_cursor_move(int dy, int dx, bool scroll) } } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR | (scroll*TOPIC_CHANGE_CONTENT_PART)); } /** @@ -1232,21 +1256,23 @@ screen_cursor_restore(bool withAttrs) } } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_CURSOR); } void ICACHE_FLASH_ATTR screen_back_index(int count) { NOTIFY_LOCK(); + ScreenNotifyTopics topics = TOPIC_CHANGE_CURSOR; int new_x = cursor.x - count; if (new_x >= 0) { cursor.x = new_x; } else { cursor.x = 0; screen_insert_characters(-new_x); + topics |= TOPIC_CHANGE_CONTENT_PART; } - NOTIFY_DONE(); + NOTIFY_DONE(topics); } //endregion @@ -1261,7 +1287,7 @@ screen_set_cursor_visible(bool visible) { NOTIFY_LOCK(); scr.cursor_visible = visible; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } /** @@ -1270,7 +1296,9 @@ screen_set_cursor_visible(bool visible) void ICACHE_FLASH_ATTR screen_wrap_enable(bool enable) { + NOTIFY_LOCK(); cursor.auto_wrap = enable; + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -1279,7 +1307,9 @@ screen_wrap_enable(bool enable) void ICACHE_FLASH_ATTR screen_reverse_wrap_enable(bool enable) { + NOTIFY_LOCK(); cursor.reverse_wrap = enable; + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -1288,8 +1318,10 @@ screen_reverse_wrap_enable(bool enable) void ICACHE_FLASH_ATTR screen_set_fg(Color color) { + NOTIFY_LOCK(); cursor.fg = color; cursor.attrs |= ATTR_FG; + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -1298,8 +1330,10 @@ screen_set_fg(Color color) void ICACHE_FLASH_ATTR screen_set_bg(Color color) { + NOTIFY_LOCK(); cursor.bg = color; cursor.attrs |= ATTR_BG; + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -1308,8 +1342,10 @@ screen_set_bg(Color color) void ICACHE_FLASH_ATTR screen_set_default_fg(void) { + NOTIFY_LOCK(); cursor.fg = 0; cursor.attrs &= ~ATTR_FG; + NOTIFY_DONE(TOPIC_INTERNAL); } /** @@ -1318,45 +1354,57 @@ screen_set_default_fg(void) void ICACHE_FLASH_ATTR screen_set_default_bg(void) { + NOTIFY_LOCK(); cursor.bg = 0; cursor.attrs &= ~ATTR_BG; + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_sgr(CellAttrs attrs, bool ena) { + NOTIFY_LOCK(); if (ena) { cursor.attrs |= attrs; } else { cursor.attrs &= ~attrs; } + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_sgr_conceal(bool ena) { + NOTIFY_LOCK(); cursor.conceal = ena; + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_charset_n(int Gx) { + NOTIFY_LOCK(); if (Gx < 0 || Gx > 1) return; // bad n cursor.charsetN = Gx; + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_charset(int Gx, char charset) { + NOTIFY_LOCK(); if (Gx == 0) cursor.charset0 = charset; else if (Gx == 1) cursor.charset1 = charset; + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR screen_set_insert_mode(bool insert) { + NOTIFY_LOCK(); scr.insert_mode = insert; + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR @@ -1364,7 +1412,7 @@ screen_set_numpad_alt_mode(bool alt_mode) { NOTIFY_LOCK(); scr.numpad_alt_mode = alt_mode; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } void ICACHE_FLASH_ATTR @@ -1372,7 +1420,7 @@ screen_set_cursors_alt_mode(bool alt_mode) { NOTIFY_LOCK(); scr.cursors_alt_mode = alt_mode; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } void ICACHE_FLASH_ATTR @@ -1380,7 +1428,7 @@ screen_set_reverse_video(bool reverse) { NOTIFY_LOCK(); scr.reverse_video = reverse; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } void ICACHE_FLASH_ATTR @@ -1388,7 +1436,7 @@ screen_set_bracketed_paste(bool ena) { NOTIFY_LOCK(); scr.bracketed_paste = ena; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } void ICACHE_FLASH_ATTR @@ -1396,29 +1444,34 @@ screen_set_newline_mode(bool nlm) { NOTIFY_LOCK(); termconf_live.crlf_mode = nlm; - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_CHANGE_SCREEN_OPTS); } void ICACHE_FLASH_ATTR screen_set_origin_mode(bool region_origin) { + NOTIFY_LOCK(); cursor.origin_mode = region_origin; screen_cursor_set(0, 0); + NOTIFY_DONE(TOPIC_INTERNAL); } static void ICACHE_FLASH_ATTR do_save_private_opt(int n, bool save) { + ScreenNotifyTopics topics = TOPIC_INTERNAL; + if (!save) NOTIFY_LOCK(); #define SAVE_RESTORE(sf, of) do { if (save) sf=(of); else of=(sf); } while(0) switch (n) { case 1: SAVE_RESTORE(opt_backup.cursors_alt_mode, scr.cursors_alt_mode); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 5: SAVE_RESTORE(opt_backup.reverse_video, scr.reverse_video); break; case 6: - SAVE_RESTORE(opt_backup.origin_mode, cursor.origin_mode); + SAVE_RESTORE(opt_backup.origin_mode, cursor.origin_mode); // XXX maybe we should move cursor to 1,1 if it's restored to True break; case 7: SAVE_RESTORE(opt_backup.auto_wrap, cursor.auto_wrap); @@ -1429,14 +1482,17 @@ do_save_private_opt(int n, bool save) case 1002: case 1003: SAVE_RESTORE(opt_backup.mouse_tracking, mouse_tracking.mode); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 1004: SAVE_RESTORE(opt_backup.focus_tracking, mouse_tracking.focus_tracking); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 1005: case 1006: case 1015: SAVE_RESTORE(opt_backup.mouse_encoding, mouse_tracking.encoding); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 12: // cursor blink if (save) { @@ -1444,9 +1500,11 @@ do_save_private_opt(int n, bool save) } else { screen_cursor_blink(opt_backup.cursor_blink); } + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 25: SAVE_RESTORE(opt_backup.cursor_visible, scr.cursor_visible); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 45: SAVE_RESTORE(opt_backup.reverse_wrap, cursor.reverse_wrap); @@ -1456,19 +1514,24 @@ do_save_private_opt(int n, bool save) break; case 800: SAVE_RESTORE(opt_backup.show_buttons, termconf_live.show_buttons); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; case 801: SAVE_RESTORE(opt_backup.show_config_links, termconf_live.show_config_links); + topics |= TOPIC_CHANGE_SCREEN_OPTS; break; default: ansi_warn("Cannot store ?%d", n); } + if (!save) NOTIFY_DONE(topics); } void ICACHE_FLASH_ATTR screen_save_private_opt(int n) { + NOTIFY_LOCK(); do_save_private_opt(n, true); + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR @@ -1476,7 +1539,7 @@ screen_restore_private_opt(int n) { NOTIFY_LOCK(); do_save_private_opt(n, false); - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_INTERNAL); } void ICACHE_FLASH_ATTR @@ -1599,7 +1662,7 @@ screen_putchar(const char *ch) strncpy(scr.last_char, result, 4); done: - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_WRITE); } /** @@ -1621,7 +1684,7 @@ screen_repeat_last_character(int count) putchar_graphic(scr.last_char); count--; } - NOTIFY_DONE(); + NOTIFY_DONE(TOPIC_WRITE); } /** @@ -1674,29 +1737,12 @@ struct ScreenSerializeState { UnicodeCacheRef lastSymbol; char lastChar[4]; u8 lastCharLen; - int index; + int index; // index in the screen buffer + ScreenNotifyTopics topics; + ScreenNotifyTopics last_topic; + ScreenNotifyTopics current_topic; }; -/** - * buffer should be at least 64+5*10+6 long (title + buttons + 6), ie. 120 - * @param buffer - * @param buf_len - */ -void ICACHE_FLASH_ATTR -screenSerializeLabelsToBuffer(char *buffer, size_t buf_len) -{ - (void)buf_len; - // let's just assume it's long enough - called with the huge websocket buffer - sprintf(buffer, "T%s\x01%s\x01%s\x01%s\x01%s\x01%s", // use 0x01 as separator - termconf_live.title, - termconf_live.btn[0], - termconf_live.btn[1], - termconf_live.btn[2], - termconf_live.btn[3], - termconf_live.btn[4] - ); -} - /** * Serialize the screen to a data buffer. May need multiple calls if the buffer is insufficient in size. * @@ -1705,12 +1751,14 @@ screenSerializeLabelsToBuffer(char *buffer, size_t buf_len) * * @param buffer - buffer array of limited size. If NULL, indicates this is the last call. * @param buf_len - buffer array size + * @param topics - what should be included in the message (ignored after the first call) * @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. + * if NULL, indicates this is the first call; the structure will be allocated. + * + * @return HTTPD_CGI_DONE or HTTPD_CGI_MORE. If more, repeat with the same `data` pointer. */ httpd_cgi_state ICACHE_FLASH_ATTR -screenSerializeToBuffer(char *buffer, size_t buf_len, void **data) +screenSerializeToBuffer(char *buffer, size_t buf_len, ScreenNotifyTopics topics, void **data) { struct ScreenSerializeState *ss = *data; @@ -1719,11 +1767,11 @@ screenSerializeToBuffer(char *buffer, size_t buf_len, void **data) return HTTPD_CGI_DONE; } - Cell *cell, *cell0; + Cell *cell, *cell0; // temporary cell pointers for finding repetitions - u8 nbytes; - size_t remain = buf_len; - char *bb = buffer; + u8 nbytes; // temporary variable for utf writing utilities + size_t remain = buf_len; // remaining space in the output buffer + char *bb = buffer; // write pointer #define bufput_c(c) do { \ *bb = (char)(c); \ @@ -1742,47 +1790,180 @@ screenSerializeToBuffer(char *buffer, size_t buf_len, void **data) bufput_utf8((num)); \ } while(0) + // tags for screen serialization +#define SEQ_TAG_REPEAT '\x02' +#define SEQ_TAG_COLORS '\x03' +#define SEQ_TAG_ATTRS '\x04' +#define SEQ_TAG_FG '\x05' +#define SEQ_TAG_BG '\x06' + +#define TOPICMARK_SCREEN_OPTS 'O' +#define TOPICMARK_TITLE 'T' +#define TOPICMARK_BUTTONS 'B' +#define TOPICMARK_DEBUG 'D' +#define TOPICMARK_BELL '!' +#define TOPICMARK_CURSOR 'C' +#define TOPICMARK_SCREEN_ALL 'S' +#define TOPICMARK_SCREEN_PART 's' + if (ss == NULL) { + // START! + *data = ss = malloc(sizeof(struct ScreenSerializeState)); + + if (topics == 0 || termconf_live.debugbar) { + topics |= TOPIC_INTERNAL; + } + ss->index = 0; - ss->lastBg = 0xFF; - ss->lastFg = 0xFF; - ss->lastAttrs = 0xFFFF; - ss->lastCharLen = 0; - ss->lastSymbol = 0; + + ss->topics = topics; + ss->last_topic = 0; // to be filled + ss->current_topic = 0; // to be filled strncpy(ss->lastChar, " ", 4); - bufput_c('S'); - // H W X Y Attribs - bufput_utf8(H); - bufput_utf8(W); - bufput_utf8(cursor.y); - bufput_utf8(cursor.x); - // 3B has 18 free bits - bufput_utf8( - (scr.cursor_visible << 0) | - (cursor.hanging << 1) | - (scr.cursors_alt_mode << 2) | - (scr.numpad_alt_mode << 3) | - (termconf_live.fn_alt_mode << 4) | - ((mouse_tracking.mode>MTM_NONE) << 5) | // disables context menu - ((mouse_tracking.mode>=MTM_NORMAL) << 6) | // disables selecting - (termconf_live.show_buttons << 7) | - (termconf_live.show_config_links << 8) | - ((termconf_live.cursor_shape&0x07) << 9) | // 9,10,11 - cursor shape based on DECSCUSR - (termconf_live.crlf_mode << 12) | - (scr.bracketed_paste << 13) | - (scr.reverse_video << 14) - ); + dbg("NOTIFY -> %02X", topics); + + bufput_c('U'); // - stands for "update" + + bufput_utf8(topics & ~TOPIC_INTERNAL); } -#define SEQ_TAG_REPEAT '\x02' -#define SEQ_TAG_COLORS '\x03' -#define SEQ_TAG_ATTRS '\x04' -#define SEQ_TAG_FG '\x05' -#define SEQ_TAG_BG '\x06' + int begun_topic = 0; + int prev_topic = 0; + +#define BEGIN_TOPIC(topic, size) \ + if (ss->last_topic == prev_topic) { \ + begun_topic = (topic); \ + if (ss->topics & (topic)) { \ + if (remain < (size)) return HTTPD_CGI_MORE; + +#define END_TOPIC \ + } \ + ss->last_topic = begun_topic; \ + ss->current_topic = 0; \ + } \ + prev_topic = begun_topic; + + if (ss->current_topic == 0) { + BEGIN_TOPIC(TOPIC_CHANGE_SCREEN_OPTS, 32+1) + bufput_c(TOPICMARK_SCREEN_OPTS); + + bufput_utf8(H); + bufput_utf8(W); + bufput_utf8(termconf_live.theme); + + bufput_utf8(termconf_live.default_fg & 0x000FFF); + bufput_utf8((termconf_live.default_fg & 0xFFF000) >> 12); + + bufput_utf8(termconf_live.default_bg & 0x000FFF); + bufput_utf8((termconf_live.default_bg & 0xFFF000) >> 12); + + bufput_utf8( + (scr.cursor_visible << 0) | + (termconf_live.debugbar << 1) | // debugbar - this was previously "hanging" + (scr.cursors_alt_mode << 2) | + (scr.numpad_alt_mode << 3) | + (termconf_live.fn_alt_mode << 4) | + ((mouse_tracking.mode > MTM_NONE) << 5) | // disables context menu + ((mouse_tracking.mode >= MTM_NORMAL) << 6) | // disables selecting + (termconf_live.show_buttons << 7) | + (termconf_live.show_config_links << 8) | + ((termconf_live.cursor_shape & 0x07) << 9) | // 9,10,11 - cursor shape based on DECSCUSR + (termconf_live.crlf_mode << 12) | + (scr.bracketed_paste << 13) | + (scr.reverse_video << 14) + ); + END_TOPIC + + BEGIN_TOPIC(TOPIC_CHANGE_TITLE, TERM_TITLE_LEN+4+1) + bufput_c(TOPICMARK_TITLE); + + int len = (int) strlen(termconf_live.title); + bufput_utf8(len); + memcpy(bb, termconf_live.title, len); + bb += len; + remain -= len; + END_TOPIC + + BEGIN_TOPIC(TOPIC_CHANGE_BUTTONS, (TERM_BTN_LEN+4)*TERM_BTN_COUNT+1+4) + bufput_c(TOPICMARK_BUTTONS); + + bufput_utf8(TERM_BTN_COUNT); + + for (int i = 0; i < TERM_BTN_COUNT; i++) { + int len = (int) strlen(termconf_live.btn[i]); + bufput_utf8(len); + memcpy(bb, termconf_live.btn[i], len); + bb += len; + remain -= len; + } + END_TOPIC + + BEGIN_TOPIC(TOPIC_INTERNAL, 45) + bufput_c(TOPICMARK_DEBUG); + // General flags + bufput_utf8( + (scr.insert_mode << 0) | + (cursor.conceal << 1) | + (cursor.auto_wrap << 2) | + (cursor.reverse_wrap << 3) | + (cursor.origin_mode << 4) | + (cursor_saved << 5) | + (state_backup.alternate_active << 6) + ); + bufput_utf8(cursor.attrs); + bufput_utf8(scr.vm0); + bufput_utf8(scr.vm1); + bufput_utf8(cursor.charsetN); + bufput_c(cursor.charset0); + bufput_c(cursor.charset1); + bufput_utf8(system_get_free_heap_size()); + bufput_utf8(term_active_clients); + END_TOPIC + + BEGIN_TOPIC(TOPIC_BELL, 1) + bufput_c(TOPICMARK_BELL); + END_TOPIC + + BEGIN_TOPIC(TOPIC_CHANGE_CURSOR, 13) + bufput_c(TOPICMARK_CURSOR); + bufput_utf8(cursor.y); + bufput_utf8(cursor.x); + bufput_utf8( + (cursor.hanging << 0) + ); + END_TOPIC + + if (ss->last_topic == TOPIC_CHANGE_CURSOR) { + // now we can begin any of the two screen sequences + + if (ss->topics & TOPIC_CHANGE_CONTENT_ALL) { + ss->current_topic = TOPIC_CHANGE_CONTENT_ALL; + } + if (ss->topics & TOPIC_CHANGE_CONTENT_PART) { + ss->current_topic = TOPIC_CHANGE_CONTENT_PART; + } + if (ss->current_topic == 0) { + // no screen mode - wrap it up + goto ser_done; + } + } + } + + // screen contents - TODO implement partial update int i = ss->index; + if (i == 0) { + bufput_c(TOPICMARK_SCREEN_ALL); // desired update mode is in `ss->current_topic` + + ss->index = 0; + ss->lastBg = 0xFF; + ss->lastFg = 0xFF; + ss->lastAttrs = 0xFFFF; + ss->lastCharLen = 0; + ss->lastSymbol = 0; + } while(i < W*H && remain > 12) { cell = cell0 = &screen[i]; @@ -1848,20 +2029,22 @@ screenSerializeToBuffer(char *buffer, size_t buf_len, void **data) } ss->index = i; + if (i >= W*H-1) goto ser_done; + + // MORE TO WRITE... bufput_c('\0'); // terminate the string + return HTTPD_CGI_MORE; + +ser_done: + bufput_c('\0'); // terminate the string + return HTTPD_CGI_DONE; +} +//endregion #if 0 - printf("MSG: "); +printf("MSG: "); for (int j=0;j