A “Simple” Terminal with ESP32 and ST7789v Display

This is something of an experiment with sentimental value. Late last century I worked in a business partnership and my first one-man-developed project started off as a text terminal more for interest than anything else initially. At the time all I had to work with was the Z80 processor and assembly language feeding a large pub monitor. I spent weeks of coding developing the terminal which eventually grew into something much larger – a full advertising system with blocky graphics.

Having spent the last couple of weeks working on coding with ChatGPT to program up ESP32 and an ST7789v display board for something completely different, yesterday for no good reason I decided it might be fun to make a little serial/WiFi terminal from the two and so dragged out one of my larger ESP32 WROOM-32 boards (plenty of RAM for scrolling buffer etc) and fastened the ESP32 up to the SPI interface on the ST7789v board. Pin usage and code provided.

This is an ESPHOME project – if you’re familiar with ESPHOME, WIFI secrets should be familiar to you… I have 7 potential SSIDs when moving around our home hence the 7 you’ll see in the code further down the page – they all have the same password. You may only have 1?

The result? Well, it looks like I now have a simple 256-colour ANSI terminal able to take ANSI text input over WIFI or the USB serial (default 9600 baud) and with it’s own ESPHOME WebUI. At this point, claiming it is fully tested would be an overstretch because implementation as you’ll see below took a lot more time than I’d planned – but now I can fire in text via WiFi or serial (USB connector on the ESP32) from, for example, MOBAXTERM complete with ANSI clear screen and colour control if needed along with various other commands – with the default 8pt font, I get 40 characters a line and 39 lines with scrolling at the bottom. Backspace works.

ST7789v 320*240 TFT display and WROOM32 ESP32 board

Note: MISO on the display board is not needed.

Feel free to take the header and main file, put them into your ESPHOME folder and build up the board (the ST7789v is NOT the touch version and costs only a few euros)… no guarantees but it seems to be fine up to now.

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’s 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’s the header file – ansi_terminal.h – the C file is not so easy to tinker with.

#pragma once

#include "esphome.h"
#include <WiFi.h>
#include <Network.h>
#include <HardwareSerial.h>
#include <string.h>

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<CR>
    //   ~BAUD?<CR>
    //   ~CLEAR<CR>

    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<int>(ROWS - 1, row_ + p_(0));
        break;

      case 'C':
        col_ = min<int>(COLS - 1, col_ + p_(0));
        break;

      case 'D':
        col_ = (col_ > p_(0)) ? col_ - p_(0) : 0;
        break;

      case 'G':
      case '`':
        col_ = min<int>(COLS - 1,
                        max<int>(0, p_(0) - 1));
        break;

      case 'd':
        row_ = min<int>(ROWS - 1,
                        max<int>(0, p_(0) - 1));
        break;

      case 'H':
      case 'f':
        row_ = min<int>(ROWS - 1,
                        max<int>(0, p_(0) - 1));

        col_ = min<int>(COLS - 1,
                        max<int>(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[0m Reset attributes
  • ESC[1m Bold / brighter
  • ESC[2m Dim
  • ESC[22m Cancel bold/dim
  • ESC[4m Underline
  • ESC[24m Cancel underline
  • ESC[7m Reverse video
  • ESC[27m Cancel reverse
  • ESC[30–37m Standard foreground colours
  • ESC[40–47m Standard background colours
  • ESC[90–97m Bright foreground colours
  • ESC[100–107m Bright background colours
  • ESC[39m Default foreground
  • ESC[49m Default background
  • ESC[38;5;<colour number>m 256-colour foreground
  • ESC[48;5;<colour number>m 256-colour background
  • ESC[2J Clear screen
  • ESC[0J Clear from cursor
  • ESC[1J Clear to cursor
  • ESC[2K Clear whole line
  • ESC[0K Clear from cursor
  • ESC[1K Clear to cursor
  • ESC[nA Cursor up
  • ESC[nB Cursor down
  • ESC[nC Cursor right
  • ESC[nD Cursor left
  • ESC[nG Cursor to column
  • ESC[nd Cursor to row
  • ESC[r;cH Cursor position
  • ESC[r;cf Cursor position
  • ESC[s Save cursor
  • ESC[u Restore cursor
  • ESC7 Save cursor
  • ESC8 Restore cursor
  • ESC[c Reset terminal
  • TAB Move to next 8-column tab stop
  • Backspace Move left and erase character
  • CR Return to column 0
  • LF Move down one line
  • ESC[?25h Show cursor
  • ESC[?25l Hide cursor
  • ESC[?12h Turn cursor blinking on
  • ESC[?12l Turn cursor blinking off
  • And the 256-colour range is the full 0–255 palette, including the 216-colour cube and 24 greys.

And here is the WebUI in ESPHome…

ANSI Terminal

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave the field below empty!

Are you human? Please solve:Captcha


The maximum upload file size: 512 MB. You can upload: image, audio, video, document, spreadsheet, interactive, text, archive, code, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here