This is something of an experiment with sentimental value. At the end of the last century I worked in a business partnership and my first one-man-developed project started as a text terminal more for interest than anything else at first. At the time all I had a job with the Z80 processor and assembly language feeding a large pub monitor. I spent weeks of coding to develop the terminal, which eventually turned into something much bigger – a complete advertising system with block graphics.
After spending the last few weeks coding with ChatGPT to program the ESP32 and an ST7789v display board for something completely different, yesterday for no good reason I decided it would be fun to make a small serial/WiFi terminal out of the two and so pulled out one of my larger ESP32 WROOM-32 boards (lots of RAM to scroll the SSP interface etc.) the ST7789v board. Pin usage and code provided.
This is an ESPHOME project – if you are familiar with ESPHOME, WIFI secrets should be familiar to you… I have 7 potential SSIDs when you move around our home, so the 7 you see in the code further down the page – they all have the same password. Maybe you only have 1?
The result? Well, it looks like I now have a simple 256-color ANSI terminal that can take ANSI text input via WIFI or USB-serial (standard 9600 baud) and with its own ESPHOME WebUI. At this point, claiming that it is fully tested would be an exaggeration because the implementation as you can see below took much more time than I had planned – but now I can burn in the text via WiFi or serial (USB connector on the ESP32) from, for example, MOBAXTERM complete with ANSI clear screen and color control if necessary along with various other commands – scrolling with the default color 9pt lines and 30 font at the bottom. Backspace works.

Note: MISO on the display board is not necessary.
Feel free to take the header and main file, put it in your ESPHOME folder and build the board (the ST7789v is NOT the touch version and only costs a few euros)… no guarantees but it seems to be good so far.
ST7789V LIVE MINI KIT ESP32 VCC ──────────── 3.3V GND ──────────── GND SCK ──────────── GPIO22 SDI/MOSI ──────────── GPIO21 CS ──────────── GPIO25 DC ──────────── GPIO16 RESET ──────────── GPIO17 LED ──────────── GPIO4 External serial: Terminal TX ────────── GPIO26 (ESP RX) Terminal RX ────────── GPIO27 (ESP TX) Terminal GND ───────── GND
Here is the YAML project file – ansi_terminal.yaml – You can change font and change WiFi information easily.
substitutions:
terminal_version: "0.94"
terminal_welcome: "ESP32 ANSI terminal"
name: "ansi-terminal"
friendly_name: "ANSI Terminal"
esphome:
name: ${name}
friendly_name: ${friendly_name}
includes:
- ansi_terminal.h
libraries:
- WiFi
- Network
esp32:
board: esp32dev
framework:
type: arduino
logger:
baud_rate: 0
api:
ota:
- platform: esphome
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
- ssid: !secret wifi_ssid2
password: !secret wifi_password
- ssid: !secret wifi_ssid3
password: !secret wifi_password
- ssid: !secret wifi_ssid4
password: !secret wifi_password
- ssid: !secret wifi_ssid5
password: !secret wifi_password
- ssid: !secret wifi_ssid6
password: !secret wifi_password
- ssid: !secret wifi_ssid7
password: !secret wifi_password
web_server:
port: 80
version: 3
spi:
clk_pin: GPIO22
mosi_pin: GPIO21
interface: hardware
font:
- file: "gfonts://Roboto Mono"
id: terminal_font
size: 8
bpp: 1
glyphsets:
- GF_Latin_Core
output:
- platform: ledc
pin: GPIO4
id: display_backlight_pwm
frequency: 1000 Hz
light:
- platform: monochromatic
output: display_backlight_pwm
id: display_backlight
name: "Display Backlight"
restore_mode: ALWAYS_ON
display:
- platform: mipi_spi
id: terminal_display
model: ST7789V
cs_pin: GPIO25
dc_pin: GPIO16
reset_pin: GPIO17
rotation: 0°
color_order: bgr
data_rate: 20MHz
color_depth: 16
buffer_size: 12%
update_interval: 250ms
lambda: |-
if (terminal_instance != nullptr) {
terminal_instance->render(it);
}
select:
- platform: template
name: "Terminal Baud Rate"
id: terminal_baud_select
optimistic: true
restore_value: true
initial_option: "9600"
options:
- "1200"
- "2400"
- "4800"
- "9600"
- "19200"
- "38400"
- "57600"
- "115200"
- "230400"
set_action:
- lambda: |-
if (terminal_instance != nullptr) {
terminal_instance->set_baud((uint32_t) atoi(x.c_str()));
}
text_sensor:
- platform: wifi_info
ip_address:
name: "Terminal IP Address"
ssid:
name: "Terminal WiFi SSID"
- platform: template
name: "Terminal Firmware Version"
lambda: |-
return {"${terminal_version}"};
update_interval: 1h
interval:
- interval: 5ms
then:
- lambda: |-
if (terminal_instance == nullptr) {
terminal_instance = new AnsiTerminal(id(terminal_display), id(terminal_font), "${terminal_version}");
terminal_instance->setup();
}
terminal_instance->loop();
Here is the header file – ansi_terminal.h – The C file is not so easy to manipulate.
#pragma once #include "esphome.h" #include#include #include #include class AnsiTerminal : public Component { public: static constexpr uint8_t COLS = 48; static constexpr uint8_t ROWS = 39; static constexpr uint8_t CELL_W = 5; static constexpr uint8_t CELL_H = 8; static constexpr uint8_t ATTR_BOLD = 1; static constexpr uint8_t ATTR_DIM = 2; static constexpr uint8_t ATTR_UNDERLINE = 4; static constexpr uint8_t ATTR_REVERSE = 8; struct Cell { uint8_t ch; uint8_t fg; uint8_t bg; uint8_t attr; }; AnsiTerminal(esphome::display::Display *display, esphome::font::Font *font, const char *version) : display_(display), font_(font), version_(version), server_(2323) { clear_buffer_(); cursor_blink_start_ = millis(); } void setup() override { Serial.begin(9600); server_.begin(); ESP_LOGI("ansi_terminal", "TCP SERVER: listening on port 2323"); server_.setNoDelay(true); put_string("\033[2J\033[H"); put_string("\033[38;5;51mESP32 ANSI terminal\033[0m\r\n"); put_string("Version: "); put_string(version_); put_string("\r\n"); put_string("Serial: 9600 TCP: 2323\r\n"); put_string("ANSI 256 colour mode\r\n"); } void loop() override { // Accept a new TCP client whenever one arrives. NetworkClient incoming = server_.accept(); if (incoming) { if (client_.connected()) client_.stop(); client_ = incoming; client_.setNoDelay(true); telnet_iac_ = false; telnet_cmd_ = 0; telnet_subneg_ = false; suppress_next_lf_ = false; telnet_option_pending_ = false; telnet_option_cmd_ = 0; // Tell the Telnet client that the ESP32 will handle echoing. // This prevents MobaXterm from doing a second local echo. const uint8_t telnet_echo[] = {0xFF, 0xFB, 0x01, 0xFF, 0xFB, 0x03}; client_.write(telnet_echo, sizeof(telnet_echo)); put_string("\033[2J\033[H"); put_string("\033[38;5;51mESP32 ANSI terminal\033[0m"); put_string("\r\n"); put_string("Version: "); put_string(version_); put_string("\r\n"); put_string("Baud: "); char b[16]; snprintf(b, sizeof(b), "%lu", (unsigned long) baud_); put_string(b); put_string("\r\n"); } // TCP -> terminal. The Telnet negotiation above asks MobaXterm to // disable local echo, so echo characters here from the ESP32. if (client_.connected()) { while (client_.available()) { uint8_t c = (uint8_t) client_.read(); process_tcp_byte(c); if (c == '\r') { const uint8_t crlf[] = {'\r', '\n'}; client_.write(crlf, sizeof(crlf)); } else if (c == '\b' || c == 0x7F) { const uint8_t bs[] = {'\b', ' ', '\b'}; client_.write(bs, sizeof(bs)); } else if (c >= 32 && c <= 126) { client_.write(&c, 1); } } } // Serial -> terminal. Also echo to TCP when connected, so the TCP // terminal can be used as a monitor of the physical serial port. while (Serial.available()) { uint8_t c = (uint8_t) Serial.read(); process_tcp_byte(c); if (client_.connected()) client_.write(&c, 1); } } void process_tcp_byte(uint8_t c) { // Filter Telnet negotiation bytes first. if (telnet_subneg_) { if (telnet_iac_) { telnet_iac_ = false; if (c == 0xF0) // SE telnet_subneg_ = false; return; } if (c == 0xFF) { telnet_iac_ = true; } return; } if (telnet_option_pending_) { // Consume the option byte belonging to DO/DONT/WILL/WONT. telnet_option_pending_ = false; telnet_option_cmd_ = 0; return; } if (telnet_iac_) { telnet_iac_ = false; if (c == 0xFF) { process_byte(c); return; } if (c == 0xFA) { // SB telnet_subneg_ = true; return; } if (c == 0xFB || c == 0xFC || c == 0xFD || c == 0xFE) { // WILL/WONT/DO/DONT: consume the following option byte. telnet_option_pending_ = true; telnet_option_cmd_ = c; return; } // Other one-byte Telnet commands (NOP, GA, etc.). return; } if (c == 0xFF) { telnet_iac_ = true; return; } // MobaXterm normally sends CR when Enter is pressed. A terminal // newline needs both horizontal return and vertical movement. // Generate CR+LF ourselves and suppress the LF if the client sends CRLF. if (c == '\r') { process_byte('\r'); process_byte('\n'); suppress_next_lf_ = true; return; } if (c == '\n') { if (suppress_next_lf_) { suppress_next_lf_ = false; return; } process_byte('\n'); return; } suppress_next_lf_ = false; process_byte(c); } void set_baud(uint32_t baud) { if (baud < 300 || baud > 2000000) return; Serial.flush(); Serial.updateBaudRate(baud); baud_ = baud; put_string("\r\n"); char b[64]; snprintf(b, sizeof(b), "[Serial baud now %lu]\r\n", (unsigned long) baud_); put_string(b); } uint32_t baud() const { return baud_; } void render(esphome::display::Display &it) { for (uint8_t r = 0; r < ROWS; r++) { uint8_t start = 0; while (start < COLS) { const Cell &c0 = cells_[r * COLS + start]; uint8_t fg = c0.fg; uint8_t bg = c0.bg; uint8_t attr = c0.attr; uint8_t end = start + 1; while (end < COLS) { const Cell &c = cells_[r * COLS + end]; if (c.fg != fg || c.bg != bg || c.attr != attr) break; end++; } char text[COLS + 1]; uint8_t n = 0; for (uint8_t x = start; x < end; x++) { uint8_t ch = cells_[r * COLS + x].ch; text[n++] = (ch >= 32 && ch <= 126) ? (char) ch : ' '; } text[n] = 0; Color fgc = ansi_color_(fg); Color bgc = ansi_color_(bg); if (attr & ATTR_REVERSE) { Color t = fgc; fgc = bgc; bgc = t; } if (attr & ATTR_DIM) fgc = dim_(fgc); if (attr & ATTR_BOLD) fgc = brighten_(fgc); it.print(start * CELL_W, r * CELL_H, font_, fgc, text, bgc); if (attr & ATTR_UNDERLINE) { it.horizontal_line(start * CELL_W, r * CELL_H + 7, (end - start) * CELL_W, fgc); } start = end; } } // Draw the terminal cursor. if (cursor_visible_) { bool cursor_on = true; if (cursor_blink_) { cursor_on = ((millis() - cursor_blink_start_) % 1000) < 600; } if (cursor_on && row_ < ROWS && col_ < COLS) { const Cell &c = cells_[row_ * COLS + col_]; // Draw a clearly visible white cursor block. Color cursor_bg(255, 255, 255); Color cursor_fg(0, 0, 0); it.filled_rectangle( col_ * CELL_W, row_ * CELL_H, CELL_W, CELL_H, cursor_bg); // Draw the character underneath in black. char cursor_char[2]; cursor_char[0] = (c.ch >= 32 && c.ch <= 126) ? c.ch : ' '; cursor_char[1] = 0; it.print( col_ * CELL_W, row_ * CELL_H, font_, cursor_fg, cursor_char); } } } void put_string(const char *s) { while (*s) process_byte((uint8_t) *s++); } private: esphome::display::Display *display_; esphome::font::Font *font_; const char *version_; NetworkServer server_; NetworkClient client_; Cell cells_[COLS * ROWS]; uint8_t row_ = 0; uint8_t col_ = 0; bool cursor_visible_ = true; bool cursor_blink_ = true; uint32_t cursor_blink_start_ = 0; uint8_t fg_ = 7; uint8_t bg_ = 0; uint8_t attr_ = 0; uint8_t saved_row_ = 0; uint8_t saved_col_ = 0; uint8_t saved_fg_ = 7; uint8_t saved_bg_ = 0; uint8_t saved_attr_ = 0; uint8_t state_ = 0; int csi_[8]; uint8_t csi_count_ = 0; int csi_value_ = 0; bool csi_have_value_ = false; uint32_t baud_ = 9600; bool csi_private_ = false; // Minimal Telnet negotiation filter. MobaXterm's Telnet client sends // negotiation bytes immediately after connecting; these must not reach // the ANSI terminal renderer. bool telnet_iac_ = false; uint8_t telnet_cmd_ = 0; bool telnet_subneg_ = false; bool suppress_next_lf_ = false; bool telnet_option_pending_ = false; uint8_t telnet_option_cmd_ = 0; bool command_mode_ = false; char command_buf_[32]; uint8_t command_len_ = 0; void clear_buffer_() { for (auto &c : cells_) { c.ch=" "; c.fg = 0; c.bg = 0; c.attr = 0; } } Cell blank_cell_() const { Cell c; c.ch=" "; c.fg = fg_; c.bg = bg_; c.attr = 0; return c; } void clear_screen_(uint8_t mode) { if (mode == 2 || mode == 3) { clear_buffer_(); row_ = 0; col_ = 0; } else if (mode == 0) { for (uint8_t r = row_; r < ROWS; r++) { uint8_t first = (r == row_) ? col_ : 0; for (uint8_t c = first; c < COLS; c++) cells_[r * COLS + c] = blank_cell_(); } } else if (mode == 1) { for (uint8_t r = 0; r <= row_; r++) { uint8_t last = (r == row_) ? col_ : COLS - 1; for (uint8_t c = 0; c <= last; c++) cells_[r * COLS + c] = blank_cell_(); } } } void clear_line_(uint8_t mode) { if (mode == 2) { for (uint8_t c = 0; c < COLS; c++) cells_[row_ * COLS + c] = blank_cell_(); } else if (mode == 0) { for (uint8_t c = col_; c < COLS; c++) cells_[row_ * COLS + c] = blank_cell_(); } else if (mode == 1) { for (uint8_t c = 0; c <= col_; c++) cells_[row_ * COLS + c] = blank_cell_(); } } void scroll_() { for (uint8_t r = 1; r < ROWS; r++) { for (uint8_t c = 0; c < COLS; c++) cells_[(r - 1) * COLS + c] = cells_[r * COLS + c]; } for (uint8_t c = 0; c < COLS; c++) cells_[(ROWS - 1) * COLS + c] = blank_cell_(); row_ = ROWS - 1; } void newline_() { row_++; if (row_ >= ROWS) scroll_(); } void put_char_(uint8_t ch) { if (col_ >= COLS) { col_ = 0; newline_(); } Cell &c = cells_[row_ * COLS + col_]; c.ch = ch; c.fg = fg_; c.bg = bg_; c.attr = attr_; col_++; if (col_ >= COLS) { col_ = 0; newline_(); } } void handle_command_() { if (!strncmp(command_buf_, "CLEAR", 5)) { clear_buffer_(); row_ = 0; col_ = 0; reset_attributes_(); return; } if (!strncmp(command_buf_, "BAUD?", 5)) { char b[48]; snprintf(b, sizeof(b), "\r\n[Serial baud: %lu]\r\n", (unsigned long) baud_); put_string(b); return; } if (!strncmp(command_buf_, "BAUD ", 5)) { uint32_t new_baud = strtoul(command_buf_ + 5, nullptr, 10); if (new_baud >= 300 && new_baud <= 2000000) { set_baud(new_baud); } else { put_string("\r\n[Invalid baud]\r\n"); } return; } put_string("\r\n[Unknown terminal command]\r\n"); } void process_byte(uint8_t ch) { // Out-of-band commands, accepted over serial OR TCP: // // ~BAUD 19200 // ~BAUD? // ~CLEAR if (state_ == 0 && command_mode_) { if (ch == '\r' || ch == '\n') { command_buf_[command_len_] = 0; handle_command_(); command_mode_ = false; command_len_ = 0; return; } if (command_len_ < sizeof(command_buf_) - 1 && ch >= 32 && ch <= 126) { command_buf_[command_len_++] = (char) ch; } else { command_mode_ = false; command_len_ = 0; } return; } if (state_ == 0 && ch == '~' && col_ == 0) { command_mode_ = true; command_len_ = 0; return; } if (state_ == 0) { if (ch == 0x1B) { state_ = 1; return; } if (ch == '\r') { col_ = 0; return; } if (ch == '\n') { newline_(); return; } if (ch == '\b' || ch == 0x7F) { if (col_ > 0) { col_--; cells_[row_ * COLS + col_] = blank_cell_(); } return; } if (ch == '\t') { uint8_t next = (col_ + 8) & 0xF8; while (col_ < next) put_char_(' '); return; } if (ch < 32 || ch == 0x7F) return; put_char_(ch); return; } if (state_ == 1) { if (ch == '[') { state_ = 2; csi_count_ = 0; csi_value_ = 0; csi_have_value_ = false; return; } if (ch == '7') { save_cursor_(); state_ = 0; return; } if (ch == '8') { restore_cursor_(); state_ = 0; return; } if (ch == 'c') { reset_terminal_(); state_ = 0; return; } state_ = 0; return; } // CSI parser. if (ch >= '0' && ch <= '9') { csi_value_ = csi_value_ * 10 + (ch - '0'); csi_have_value_ = true; return; } if (ch == ';') { add_csi_param_(); return; } if (ch == '?') { csi_private_ = true; return; } if (ch == '>' || ch == '!') return; add_csi_param_(); handle_csi_(ch); csi_private_ = false; state_ = 0; } void add_csi_param_() { if (csi_count_ < 8) csi_[csi_count_++] = csi_have_value_ ? csi_value_ : 0; csi_value_ = 0; csi_have_value_ = false; } int p_(uint8_t n, int def = 1) const { if (n >= csi_count_ || csi_[n] == 0) return def; return csi_[n]; } void handle_csi_(char cmd) { switch (cmd) { case 'A': row_ = (row_ > p_(0)) ? row_ - p_(0) : 0; break; case 'B': row_ = min (ROWS - 1, row_ + p_(0)); break; case 'C': col_ = min (COLS - 1, col_ + p_(0)); break; case 'D': col_ = (col_ > p_(0)) ? col_ - p_(0) : 0; break; case 'G': case '`': col_ = min (COLS - 1, max (0, p_(0) - 1)); break; case 'd': row_ = min (ROWS - 1, max (0, p_(0) - 1)); break; case 'H': case 'f': row_ = min (ROWS - 1, max (0, p_(0) - 1)); col_ = min (COLS - 1, max (0, p_(1) - 1)); break; case 'J': clear_screen_(p_(0, 0)); break; case 'K': clear_line_(p_(0, 0)); break; case 's': save_cursor_(); break; case 'u': restore_cursor_(); break; case 'm': sgr_(); break; case 'h': if (csi_private_) { for (uint8_t i = 0; i < csi_count_; i++) { if (csi_[i] == 12) { cursor_blink_ = true; cursor_blink_start_ = millis(); } if (csi_[i] == 25) { cursor_visible_ = true; cursor_blink_start_ = millis(); } } } break; case 'l': if (csi_private_) { for (uint8_t i = 0; i < csi_count_; i++) { if (csi_[i] == 12) cursor_blink_ = false; if (csi_[i] == 25) cursor_visible_ = false; } } break; case 'n': // Not needed for this terminal. break; default: break; } } void sgr_() { if (csi_count_ == 0) { reset_attributes_(); return; } for (uint8_t i = 0; i < csi_count_; i++) { int n = csi_[i]; if (n == 0) { reset_attributes_(); continue; } if (n == 1) { attr_ |= ATTR_BOLD; continue; } if (n == 2) { attr_ |= ATTR_DIM; continue; } if (n == 22) { attr_ &= ~(ATTR_BOLD | ATTR_DIM); continue; } if (n == 4) { attr_ |= ATTR_UNDERLINE; continue; } if (n == 24) { attr_ &= ~ATTR_UNDERLINE; continue; } if (n == 7) { attr_ |= ATTR_REVERSE; continue; } if (n == 27) { attr_ &= ~ATTR_REVERSE; continue; } if (n >= 30 && n <= 37) { fg_ = n - 30; continue; } if (n >= 40 && n <= 47) { bg_ = n - 40; continue; } if (n >= 90 && n <= 97) { fg_ = n - 90 + 8; continue; } if (n >= 100 && n <= 107) { bg_ = n - 100 + 8; continue; } if (n == 39) { fg_ = 7; continue; } if (n == 49) { bg_ = 0; continue; } // 256-colour ANSI: // 38;5;n = foreground // 48;5;n = background if (n == 38 || n == 48) { if (i + 2 < csi_count_ && csi_[i + 1] == 5) { uint8_t colour = constrain(csi_[i + 2], 0, 255); if (n == 38) fg_ = colour; else bg_ = colour; i += 2; } } } } void save_cursor_() { saved_row_ = row_; saved_col_ = col_; saved_fg_ = fg_; saved_bg_ = bg_; saved_attr_ = attr_; } void restore_cursor_() { row_ = saved_row_; col_ = saved_col_; fg_ = saved_fg_; bg_ = saved_bg_; attr_ = saved_attr_; } void reset_attributes_() { fg_ = 7; bg_ = 0; attr_ = 0; } void reset_terminal_() { clear_buffer_(); row_ = 0; col_ = 0; reset_attributes_(); saved_row_ = 0; saved_col_ = 0; state_ = 0; } Color ansi_color_(uint8_t n) const { static const uint8_t base[16][3] = { {0,0,0}, {128,0,0}, {0,128,0}, {128,128,0}, {0,0,128}, {128,0,128}, {0,128,128}, {192,192,192}, {128,128,128}, {255,0,0}, {0,255,0}, {255,255,0}, {0,0,255}, {255,0,255}, {0,255,255}, {255,255,255} }; if (n < 16) return Color(base[n][0], base[n][1], base[n][2]); if (n < 232) { uint8_t v = n - 16; uint8_t r = v / 36; uint8_t g = (v / 6) % 6; uint8_t b = v % 6; static const uint8_t cube[6] = {0, 95, 135, 175, 215, 255}; return Color(cube[r], cube[g], cube[b]); } uint8_t grey = 8 + (n - 232) * 10; return Color(grey, grey, grey); } Color dim_(Color c) const { return Color(c.r / 2, c.g / 2, c.b / 2); } Color brighten_(Color c) const { return Color( min(255, (int)c.r + (255 - c.r) / 4), min(255, (int)c.g + (255 - c.g) / 4), min(255, (int)c.b + (255 - c.b) / 4)); } }; AnsiTerminal *terminal_instance = nullptr;
Code function
- ESC[0mResetAttributes[0mResetattributes[0mResetAttributer[0mResetattributes
- ESC[1mBold/brighter[1mFett/méihell[1mBold/brighter
- ESC[2mDim[2mDim[2mDim[2mDim
- ESC[22mCancelBold/Dim[22mCancelbold/dim[22mOfbriechenFett/Dim[22mCancelbold/dim
- ESC[4mUnderline[4mUnderline[4mUnderline[4mUnderline
- ESC[24mCancelUnderline[24mCancelunderline[24mOfbriechenënnersträichen[24mCancelunderline
- ESC[7mReverseVideo[7mReverseVideo[7mReverseVideo[7mReversevideo
- ESC[27mCancelreverse[27mCancelreverse[27mOfbriechenëmgedréint[27mCancelreverse
- ESC[30–37mStandardforegroundcolours[30–37mStandardforegroundcolours[30–37mStandardVirdergrondFaarwen[30–37mStandardforegroundcolours
- ESC[40–47mStandardbackgroundcolours[40–47mStandardbackgroundcolours[40–47mStandardHannergrondfaarwen[40–47mStandardbackgroundcolours
- ESC[90–97mBrightforegroundcolours[90–97mBrightforegroundcolours[90–97mHelleVirdergrondFaarwen[90–97mBrightforegroundcolours
- ESC[100–107mBrightbackgroundcolours[100–107mBrightbackgroundcolours[100–107mHellHannergrondfaarwen[100–107mBrightbackgroundcolours
- ESC[39mDefaultforeground[39mDefaultforeground[39mStandardVirdergrond[39mDefaultforeground
- ESC[49mDefaultBackground[49mDefaultbackground[49mDefaultHannergrond[49mDefaultbackground
- ESC[38;5;[38;5;[38;5;[38;5;
m 256-color foreground - ESC[48;5;[48;5;[48;5;[48;5;
m 256-color background - ESC[2JClearscreen[2JClearscreen[2JKloerÉcran[2JClearscreen
- ESC[0JClearfromcursor[0JClearfromcursor[0JKloervumCursor[0JClearfromcursor
- ESC[1JCleartoCursor[1JCleartocursor[1JCleartoCursor[1JCleartocursor
- ESC[2KClearwholeline[2KKloerganzLinn[2KClearwholeline
- ESC[0KClearfromcursor[0KClearfromcursor[0KKloervumCursor[0KClearfromcursor
- ESC[1KCleartoCursor[1KCleartocursor[1KCleartoCursor[1KCleartocursor
- ESC[nACursorup[nACursorup[nACursorerop[nACursorup
- ESC[nBCursordown[nBCursordown[nBCursorerof[nBCursordown
- ESC[nCCursorriets[nCCursorright[nCCursorriets[nCCursorright
- ESC[nDCursorleft[nDCursorleft[nDCursorlénks[nDCursorleft
- ESC[nGCursoropColumn[nGCursortocolumn[nGCursoropKolonn[nGCursortocolumn
- ESC[andCursortorow[ndCursortorow[andCursortorow[ndCursortorow
- ESC[r;cHCursorposition[r;cHCursorposition[r;cHCursorposition[r;cHCursorposition
- ESC[r;cfCursorposition[r;cfCursorposition[r;cfCursorposition[r;cfCursorposition
- ESC[sSavecursor[sCursorspäicheren[sSavecursor
- ESC[uCursorRestore[uRestorecursor[uCursorRestauréieren[uRestorecursor
- ESC7 Save cursor
- ESC8 Restore cursor
- ESC[cResetterminal[cResetterminal[cResetterminal[cResetterminal
- TAB Go to the next 8-column tab stop
- Backspace Move left and delete character
- CR Return to column 0
- LF Move down one line
- ESC[?25hShowcursor[?25hCursorweisen[?25hShowcursor
- ESC[?25lHidecursor[?25lHidecursor[?25lCursorverstoppen[?25lHidecursor
- ESC[?12hTurnCursorblinkon[?12hTurncursorlinkingon[?12hTurnCursorblénktop[?12hTurncursorblinkingon
- ESC[?12lTurncursorlinkingoff[?12lCursorausschalten[?12lTurncursorblinkingoff
- And the 256-color range is the full 0-255 palette, including the 216-color cube and 24 gray.
And here is the WebUI and ESPHome…

