From 5b40a9b0832e4d3cb246371d706f09c7fad57144 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sat, 13 Mar 2021 18:25:12 +0100 Subject: [PATCH] Initial commit --- README.md | 5 + video-terminal/Display.h | 119 +++++ video-terminal/Keyboard.h | 216 +++++++++ video-terminal/font454.h | 274 +++++++++++ video-terminal/tintty.cpp | 758 ++++++++++++++++++++++++++++++ video-terminal/tintty.h | 37 ++ video-terminal/video-terminal.ino | 153 ++++++ 7 files changed, 1562 insertions(+) create mode 100644 video-terminal/Display.h create mode 100644 video-terminal/Keyboard.h create mode 100644 video-terminal/font454.h create mode 100644 video-terminal/tintty.cpp create mode 100644 video-terminal/tintty.h create mode 100644 video-terminal/video-terminal.ino diff --git a/README.md b/README.md index 37d1e65..4de52f5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Physical recreation of a video terminal device for connecting to a serial consol - [TinTTY](https://github.com/unframework/tintty) for VT100 emulation - [ESP32Lib](https://github.com/bitluni/ESP32Lib/tree/development) for composite video output +## Building +1. Connect up the hardware (TODO). +2. Get the Arduino IDE, Install ESP32 board package, Install ESP32Lib library. +3. Compile and upload the sketch in `video-terminal/video-terminal.ino` + ## Resources - [LK201 Interface](http://www.netbsd.org/docs/Hardware/Machines/DEC/lk201.html) for keyboard specification diff --git a/video-terminal/Display.h b/video-terminal/Display.h new file mode 100644 index 0000000..beb498e --- /dev/null +++ b/video-terminal/Display.h @@ -0,0 +1,119 @@ +/* + CONNECTION from ESP32 to a composite video TV + + A) voltageDivider = false; B) voltageDivider = true + + 55 shades 179 shades + + ESP32 TV ESP32 TV + -----+ -----+ ____ 100 ohm + G|- G|---|____|+ + pin25|--------- Comp pin25|---|____|+--------- Comp + pin26|- pin26|- 220 ohm + | | + | | + -----+ -----+ + + Connect pin 25 or 26 + + C) R–2R resistor ladder; D) unequal rungs ladder + + 55 shades up to 254 shades? + + ESP32 TV ESP32 TV + -----+ -----+ ____ + G|-+_____ G|---|____| + pinA0|-| R2R |- Comp pinA0|---|____|+--------- Comp + pinA1|-| | pinA1|---|____| + pinA2|-| | ...| + ...|-|_____| | + -----+ -----+ + + Connect pins of your choice (A0...A8=any pins). + Custom ladders can be used by tweaking colorMinValue and colorMaxValue +*/ +#include +#include +//#include +//#include +//#include +//#include +#include + +//pin configuration for DAC +const int outputPin = 25; +// A) B) +CompositeGrayDAC videodisplay; +// C) D) +//CompositeGrayLadder videodisplay; + +class Display { + +public: + // view(able) size + int vw = 350; + int vh = 235; + + // offsets (to fix overscan, depends on your display) + int ow = 10; + int oh = 26; + + // display size + int w = 365; // vw + 2*ow + int h = 275; // vh + 2*oh + + #define FONT CodePage437_9x16 // for options, see the Resources includes ^^^ + int font_w = 9; + int font_h = 16; + + int cols = vw / font_w; + int rows = vh / font_h; + + void setup() + { + // Composite video init, see options in the header ^^^ + // A) + //videodisplay.init(CompMode::MODEPAL288P, 25, false); + // B) + videodisplay.init(CompMode::MODEPAL288P, 25, true); + videodisplay.clear(); + + videodisplay.xres = w; + videodisplay.yres = h; + + videodisplay.setFont(FONT); + + /*/ view area test + videodisplay.rect(ow, oh, vw, vh, 50); + /**/ + } + + void scroll(int d) { + videodisplay.scroll(d, 0); + } + + void fill_rect(int x, int y, int w, int h, int color) { + videodisplay.fillRect(x, y, w, h, color); + } + + void pixel(int x, int y, int color) { + videodisplay.dotFast(x, y, color); + } + + void print_character(int col, int row, int fg_color, int bg_color, char character) { + int x = ow + col*font_w; + int y = oh + row*font_h; + videodisplay.setCursor(x, y); + videodisplay.frontColor = fg_color; + videodisplay.backColor = bg_color; + videodisplay.print(character); + } + + int get_display_width() { return w; } + int get_display_height() { return h; } + int get_view_width() { return vw; } + int get_view_height() { return vh; } + int get_view_width_offset() { return ow; } + int get_view_height_offset() { return oh; } + +}; diff --git a/video-terminal/Keyboard.h b/video-terminal/Keyboard.h new file mode 100644 index 0000000..37c8b3b --- /dev/null +++ b/video-terminal/Keyboard.h @@ -0,0 +1,216 @@ +/* SOURCE: http://www.netbsd.org/docs/Hardware/Machines/DEC/lk201.html */ + +/**********************************************************************/ + +/* requires LED number data */ +#define LK_LED_ENABLE 0x13 /* light LED */ +#define LK_LED_DISABLE 0x11 /* turn off LED */ + +#define LED_WAIT 0x81 /* Wait LED */ +#define LED_COMP 0x82 /* Compose LED */ +#define LED_LOCK 0x84 /* Lock LED */ +#define LED_HOLD 0x88 /* Hold Screen LED */ +#define LED_ALL 0x8F /* All LED's */ + +/**********************************************************************/ + +/* Requires volume data byte */ +#define LK_CL_ENABLE 0x1B /* keyclick enable. Requires volume */ + /* byte. This does not affect the */ + /* SHIFT key. The CTRL key requires */ + /* LK_CL_ENABLE and LK_CCL_ENABLE to */ + /* have been sent before it clicks. */ + /* All other keys are only controlled */ + /* by LK_CL_ENABLE. */ + +#define LK_CCL_ENABLE 0xBB /* Enable keyclicks for the CTRL key. */ + /* The CTRL keyclick volume is set to */ + /* be the same as the rest of the keys */ + /* LK_CCL_ENABLE sets a flag in the */ + /* keyboard with is logically AND'ed */ + /* with the LK_CL_ENABLE flag to enable*/ + /* CTRL key keyclicks. */ + +#define LK_CL_DISABLE 0x99 /* keyclick disable */ +#define LK_CCL_DISABLE 0xB9 /* CTRL key keyclick disable */ + +#define LK_SOUND_CLICK 0x9F /* causes the LK201 to sound a keyclick*/ + +/* max volume is 0, lowest is 0x7 */ +#define LK_PARAM_VOLUME(v) (0x80|((v)&0x7)) + +/**********************************************************************/ + +/* requires bell volume data */ +#define LK_BELL_ENABLE 0x23 /* enable the keyboard bell. Requires */ + /* volume data byte. */ + +#define LK_BELL_DISABLE 0xA1 /* disable the keyboard bell. */ + +#define LK_RING_BELL 0xA7 /* ring the keyboard bell */ + +/* max volume is 0, lowest is 0x7 */ +#define LK_PARAM_VOLUME(v) (0x80|((v)&0x7)) + +/**********************************************************************/ + +#define LK_UPDOWN 0x86 +#define LK_AUTODOWN 0x82 +#define LK_DOWN 0x80 + +#define LK_CMD_MODE(m,div) ((m)|((div)<<3)) + +#define LK_MODECHG_ACK 0xBA /* sent by the keyboard to acknowledge a */ + /* successful mode change. */ + + +#define LK_PFX_KEYDOWN 0xB9 /* indicates that the next byte is a key- */ + /* code for a key already down in a */ + /* division that has been changed to */ + /* LK_UPDOWN. I think this means that if */ + /* for example, the 'a' key is in LK_DOWN */ + /* mode and the key is being held down and*/ + /* division 1 is switched to LK_UPDOWN */ + /* mode, the keyboard will produce the */ + /* byte LK_PFX_KEYDOWN followed by 0xC2 */ + /* (KEY_A). */ + +#define LK_CMD_RPT_TO_DOWN 0xD9 /* This command causes all divisions which */ + /* are programmed for LK_AUTODOWN mode to */ + /* be switched to LK_DOWN mode. */ + + +#define LK_CMD_ENB_RPT 0xE3 /* enables auto repeat on the keys */ + /* which are in LK_AUTODOWN mode */ + +#define LK_CMD_DIS_RPT 0xE1 /* disables auto repeat on all keys, but */ + /* does not change the mode that the */ + /* divisions are programmed to. */ + +#define LK_CMD_TMP_NORPT 0xD1 /* temporary auto repeat disable. This */ + /* command disables auto repeat for the key*/ + /* which is currently pressed down. Auto */ + /* repeat is re-enabled when another key is*/ + /* pressed. */ + + +#define LK_INPUT_ERROR 0xB6 /* sent by the keyboard if it receives an */ + /* invalid command. */ + +#define LK_NO_ERROR 0x00 /* No Error */ +#define LK_KDOWN_ERROR 0x3D /* Key down on powerup error */ +#define LK_POWER_ERROR 0x3E /* Keyboard failure on pwrup tst */ + +#define LK_ALLUP 0xB3 + +/**********************************************************************/ + +bool mod_shift = false; +bool mod_ctrl = false; +#define KB_CHAR(key_base, key_shift) c = ((mod_shift) ? (key_shift) : (key_base)); + +void keyboard_handle_key(int key) +{ + int c = -1; + switch (key) { + /* Key Division 191-255 */ + case 191: KB_CHAR('`', '~'); break; // (xbf): KEY_TILDE + case 192: KB_CHAR('1', '!'); break; // (xc0): KEY_TR_1 + case 193: KB_CHAR('q', 'Q'); break; // (xc1): KEY_Q + case 194: KB_CHAR('a', 'A'); break; // (xc2): KEY_A + case 195: KB_CHAR('z', 'Z'); break; // (xc3): KEY_Z + case 197: KB_CHAR('2', '@'); break; // (xc5): KEY_TR_2 + case 198: KB_CHAR('w', 'W'); break; // (xc6): KEY_W + case 199: KB_CHAR('s', 'S'); break; // (xc7): KEY_S + case 200: KB_CHAR('x', 'X'); break; // (xc8): KEY_X + case 201: KB_CHAR('<', '>'); break; // (xc9): KEY_LANGLE_RANGLE + case 203: KB_CHAR('3', '#'); break; // (xcb): KEY_TR_3 + case 204: KB_CHAR('e', 'E'); break; // (xcc): KEY_E + case 205: KB_CHAR('d', 'D'); break; // (xcd): KEY_D + case 206: KB_CHAR('c', 'C'); break; // (xce): KEY_C + case 208: KB_CHAR('4', '$'); break; // (xd0): KEY_TR_4 + case 209: KB_CHAR('r', 'R'); break; // (xd1): KEY_R + case 210: KB_CHAR('f', 'F'); break; // (xd2): KEY_F + case 211: KB_CHAR('v', 'V'); break; // (xd3): KEY_V + case 212: KB_CHAR(' ', ' '); break; // (xd4): KEY_SPACE + case 214: KB_CHAR('5', '%'); break; // (xd6): KEY_TR_5 + case 215: KB_CHAR('t', 'T'); break; // (xd7): KEY_T + case 216: KB_CHAR('g', 'G'); break; // (xd8): KEY_G + case 217: KB_CHAR('b', 'B'); break; // (xd9): KEY_B + case 219: KB_CHAR('6', '^'); break; // (xdb): KEY_TR_6 + case 220: KB_CHAR('y', 'Y'); break; // (xdc): KEY_Y + case 221: KB_CHAR('h', 'H'); break; // (xdd): KEY_H + case 222: KB_CHAR('n', 'N'); break; // (xde): KEY_N + case 224: KB_CHAR('7', '&'); break; // (xe0): KEY_TR_7 + case 225: KB_CHAR('u', 'U'); break; // (xe1): KEY_U + case 226: KB_CHAR('j', 'J'); break; // (xe2): KEY_J + case 227: KB_CHAR('m', 'M'); break; // (xe3): KEY_M + case 229: KB_CHAR('8', '*'); break; // (xe5): KEY_TR_8 + case 230: KB_CHAR('i', 'I'); break; // (xe6): KEY_I + case 231: KB_CHAR('k', 'K'); break; // (xe7): KEY_K + case 232: KB_CHAR(',', '<'); break; // (xe8): KEY_COMMA + case 234: KB_CHAR('9', '('); break; // (xea): KEY_TR_9 + case 235: KB_CHAR('o', 'O'); break; // (xeb): KEY_O + case 236: KB_CHAR('l', 'L'); break; // (xec): KEY_L + case 237: KB_CHAR('.', '>'); break; // (xed): KEY_PERIOD + case 239: KB_CHAR('0', ')'); break; // (xef): KEY_TR_0 + case 240: KB_CHAR('p', 'P'); break; // (xf0): KEY_P + case 242: KB_CHAR(';', ':'); break; // (xf2): KEY_SEMICOLON + case 243: KB_CHAR('/', '?'); break; // (xf3): KEY_QMARK + case 245: KB_CHAR('=', '+'); break; // (xf5): KEY_PLUS + case 246: KB_CHAR(']', '}'); break; // (xf6): KEY_RBRACE + case 247: KB_CHAR('\\', '|'); break; // (xf7): KEY_VBAR + case 249: KB_CHAR('-', '_'); break; // (xf9): KEY_UBAR + case 250: KB_CHAR('[', '{'); break; // (xfa): KEY_LBRACE + case 251: KB_CHAR('\'', '"'); break; // (xfb): KEY_QUOTE + /* Key Division 2: 145 - 165 */ + /*146 (x92): KEY_KP_0 +148 (x94): KEY_KP_PERIOD +149 (x95): KEY_KP_ENTER +150 (x96): KEY_KP_1 +151 (x97): KEY_KP_2 +152 (x98): KEY_KP_3 +153 (x99): KEY_KP_4 +154 (x9a): KEY_KP_5 +155 (x9b): KEY_KP_6 +156 (x9c): KEY_KP_COMMA +157 (x9d): KEY_KP_7 +158 (x9e): KEY_KP_8 +159 (x9f): KEY_KP_9 +160 (xa0): KEY_KP_HYPHEN +161 (xa1): KEY_KP_PF1 +162 (xa2): KEY_KP_PF2 +163 (xa3): KEY_KP_PF3 +164 (xa4): KEY_KP_PF4*/ + /* Key Division 3: 188 - 188 */ + case 188: c = 0177; Serial.print(""); break; // (xbc): KEY_DELETE + /* Key Division 4: 189 - 190 */ + case 189: c = '\n'; break; // (xbd): KEY_RETURN + case 190: c = '\t'; break; // (xbe): KEY_TAB + /* Key Division 5: (176 - 178) */ + /*176 (xb0): KEY_LOCK +177 (xb1): KEY_META*/ + /* Key Division 6: (173 - 175) */ + case 174: mod_shift = !mod_shift; Serial.print(""); break; // (xae): KEY_SHIFT + case 175: mod_ctrl = !mod_ctrl; Serial.print(""); break; // (xaf): KEY_CTRL*/ + /* Key Division 7: (166 - 168) */ + case 167: SerialTty.print("\033[D"); Serial.print("LEFT "); break; // (xa7): KEY_LEFT + case 168: SerialTty.print("\033[C"); Serial.print("RIGHT "); break; // (xa8): KEY_RIGHT + /* Key Division 8: (169 - 172) */ + case 169: SerialTty.print("\033[B"); Serial.print("DOWN "); break; // (xa9): KEY_DOWN + case 170: SerialTty.print("\033[A"); Serial.print("UP "); break; // (xaa): KEY_UP + case 171: break; // (xab): KEY_R_SHIFT + + case LK_ALLUP: + mod_shift = false; + mod_ctrl = false; + Serial.print(""); + break; + default: + Serial.print(key); Serial.print(' '); + } + if (c != -1) { + Serial.print((char) c); + SerialTty.print((char) c); + } +} diff --git a/video-terminal/font454.h b/video-terminal/font454.h new file mode 100644 index 0000000..895d46a --- /dev/null +++ b/video-terminal/font454.h @@ -0,0 +1,274 @@ + + +#ifndef FONT454_H +#define FONT454_H + +//#include +//#include + +// generated from https://bitbucket.org/thesheep/font454 + +// 4-pixel-wide matrix of 0-3 intensity, packed AABBCCDD +static const unsigned char font454[] PROGMEM = { + // default ASCII character set + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x30, 0x20, 0x00, 0x30, 0x00, + 0xcc, 0x88, 0x00, 0x00, 0x00, 0x00, + 0x88, 0xfc, 0x88, 0xfc, 0x88, 0x00, + 0x20, 0xb8, 0xf0, 0x3c, 0xb8, 0x00, + 0xcc, 0x08, 0x30, 0x80, 0xcc, 0x00, + 0xb0, 0xcc, 0x34, 0xcc, 0xbc, 0x00, + 0x30, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x80, 0xc0, 0x80, 0x70, 0x00, + 0x34, 0x08, 0x0c, 0x08, 0x34, 0x00, + 0x00, 0x98, 0x74, 0x98, 0x00, 0x00, + 0x00, 0x30, 0xfc, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x30, 0x80, 0x00, + 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x0c, 0x18, 0x30, 0x90, 0xc0, 0x00, + 0x78, 0xcc, 0xcc, 0xcc, 0xb4, 0x00, + 0x30, 0xb0, 0x30, 0x30, 0x30, 0x00, + 0xe4, 0x0c, 0x74, 0x80, 0xfc, 0x00, + 0xb8, 0x0c, 0x34, 0x0c, 0xb8, 0x00, + 0x30, 0x90, 0xcc, 0xfc, 0x0c, 0x00, + 0xfc, 0xc0, 0xf8, 0x0c, 0xf8, 0x00, + 0x78, 0xc0, 0xb8, 0xcc, 0xb8, 0x00, + 0xfc, 0x08, 0x30, 0x80, 0xc0, 0x00, + 0xb8, 0xcc, 0x74, 0xcc, 0xb8, 0x00, + 0xb8, 0xcc, 0xbc, 0x0c, 0xb4, 0x00, + 0x00, 0x30, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x30, 0x80, 0x00, + 0x0c, 0x30, 0xc0, 0x30, 0x0c, 0x00, + 0x00, 0xfc, 0x00, 0xfc, 0x00, 0x00, + 0xc0, 0x30, 0x0c, 0x30, 0xc0, 0x00, + 0xb4, 0x0c, 0x34, 0x00, 0x30, 0x00, + 0x78, 0x8c, 0xcc, 0x80, 0x6c, 0x00, + 0x74, 0x88, 0xcc, 0xec, 0xcc, 0x00, + 0xf4, 0xcc, 0xf4, 0xcc, 0xf4, 0x00, + 0x7c, 0x80, 0xc0, 0x80, 0x7c, 0x00, + 0xe4, 0xc8, 0xcc, 0xc8, 0xe4, 0x00, + 0xfc, 0xc0, 0xf0, 0xc0, 0xfc, 0x00, + 0xfc, 0xc0, 0xf0, 0xc0, 0xc0, 0x00, + 0x7c, 0x80, 0xc0, 0x8c, 0x7c, 0x00, + 0xcc, 0xcc, 0xfc, 0xcc, 0xcc, 0x00, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, + 0x0c, 0x0c, 0x0c, 0xcc, 0xb8, 0x00, + 0xcc, 0xc8, 0xe0, 0xc8, 0xcc, 0x00, + 0xc0, 0xc0, 0xc0, 0xc0, 0xfc, 0x00, + 0x88, 0xec, 0xfc, 0xdc, 0xcc, 0x00, + 0x8c, 0xdc, 0xec, 0xdc, 0xc8, 0x00, + 0x74, 0x88, 0xcc, 0x88, 0x74, 0x00, + 0xe4, 0xcc, 0xcc, 0xe4, 0xc0, 0x00, + 0x74, 0x88, 0xcc, 0x8c, 0x7c, 0x00, + 0xe4, 0xcc, 0xcc, 0xe4, 0xcc, 0x00, + 0xb8, 0xc0, 0xb8, 0x0c, 0xb8, 0x00, + 0xfc, 0x30, 0x30, 0x30, 0x30, 0x00, + 0xcc, 0xcc, 0xcc, 0x8c, 0x68, 0x00, + 0xcc, 0xcc, 0xcc, 0x98, 0x30, 0x00, + 0xcc, 0xcc, 0xdc, 0xfc, 0xec, 0x00, + 0xcc, 0x88, 0x20, 0x88, 0xcc, 0x00, + 0xcc, 0x88, 0x64, 0x30, 0x30, 0x00, + 0xfc, 0x08, 0x30, 0x80, 0xfc, 0x00, + 0xf0, 0xc0, 0xc0, 0xc0, 0xf0, 0x00, + 0xc0, 0x90, 0x30, 0x18, 0x0c, 0x00, + 0x3c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00, + 0x30, 0xdc, 0x88, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, + 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x6c, 0x8c, 0x8c, 0x6c, 0x00, + 0xc0, 0xe4, 0xc8, 0xc8, 0xe4, 0x00, + 0x00, 0x6c, 0x80, 0x80, 0x6c, 0x00, + 0x0c, 0x6c, 0x8c, 0x8c, 0x6c, 0x00, + 0x00, 0x38, 0x8c, 0xe0, 0x7c, 0x00, + 0x1c, 0x30, 0xb8, 0x30, 0x30, 0x30, + 0x00, 0xe4, 0x88, 0x6c, 0x08, 0xe4, + 0xc0, 0xe4, 0xc8, 0xcc, 0xcc, 0x00, + 0x30, 0x00, 0x70, 0x30, 0x2c, 0x00, + 0x0c, 0x00, 0x1c, 0x0c, 0xcc, 0xb8, + 0xc0, 0xcc, 0xe0, 0xc8, 0xcc, 0x00, + 0x70, 0x30, 0x30, 0x20, 0x18, 0x00, + 0x00, 0xf4, 0xfc, 0xec, 0xcc, 0x00, + 0x00, 0xe4, 0xc8, 0xcc, 0xcc, 0x00, + 0x00, 0x74, 0x88, 0x88, 0x74, 0x00, + 0x00, 0xe4, 0xc8, 0xc8, 0xe4, 0xc0, + 0x00, 0x64, 0x88, 0x8c, 0x6c, 0x0c, + 0x00, 0x8c, 0xe0, 0xc0, 0xc0, 0x00, + 0x00, 0x6c, 0x90, 0x18, 0xf4, 0x00, + 0x30, 0xb8, 0x30, 0x20, 0x18, 0x00, + 0x00, 0xcc, 0xcc, 0x8c, 0x78, 0x00, + 0x00, 0xcc, 0xcc, 0x98, 0x20, 0x00, + 0x00, 0xcc, 0xdc, 0xfc, 0xec, 0x00, + 0x00, 0xcc, 0x64, 0x64, 0xcc, 0x00, + 0x00, 0xcc, 0xcc, 0x78, 0x08, 0xa4, + 0x00, 0xfc, 0x18, 0x90, 0xfc, 0x00, + 0x2c, 0x30, 0xd0, 0x30, 0x2c, 0x00, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0xe0, 0x30, 0x1c, 0x30, 0xe0, 0x00, + 0xb0, 0xc0, 0x74, 0x0c, 0x38, 0x00, + 0xec, 0x88, 0x88, 0x88, 0xec, 0x00, + + // line-drawing character set + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x30, 0x20, 0x00, 0x30, 0x00, + 0xcc, 0x88, 0x00, 0x00, 0x00, 0x00, + 0x88, 0xfc, 0x88, 0xfc, 0x88, 0x00, + 0x20, 0xb8, 0xf0, 0x3c, 0xb8, 0x00, + 0xcc, 0x08, 0x30, 0x80, 0xcc, 0x00, + 0xb0, 0xcc, 0x34, 0xcc, 0xbc, 0x00, + 0x30, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x80, 0xc0, 0x80, 0x70, 0x00, + 0x34, 0x08, 0x0c, 0x08, 0x34, 0x00, + 0x00, 0x98, 0x74, 0x98, 0x00, 0x00, + 0x00, 0x30, 0xfc, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x30, 0x80, 0x00, + 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x0c, 0x18, 0x30, 0x90, 0xc0, 0x00, + 0x78, 0xcc, 0xcc, 0xcc, 0xb4, 0x00, + 0x30, 0xb0, 0x30, 0x30, 0x30, 0x00, + 0xe4, 0x0c, 0x74, 0x80, 0xfc, 0x00, + 0xb8, 0x0c, 0x34, 0x0c, 0xb8, 0x00, + 0x30, 0x90, 0xcc, 0xfc, 0x0c, 0x00, + 0xfc, 0xc0, 0xf8, 0x0c, 0xf8, 0x00, + 0x78, 0xc0, 0xb8, 0xcc, 0xb8, 0x00, + 0xfc, 0x08, 0x30, 0x80, 0xc0, 0x00, + 0xb8, 0xcc, 0x74, 0xcc, 0xb8, 0x00, + 0xb8, 0xcc, 0xbc, 0x0c, 0xb4, 0x00, + 0x00, 0x30, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x30, 0x80, 0x00, + 0x0c, 0x30, 0xc0, 0x30, 0x0c, 0x00, + 0x00, 0xfc, 0x00, 0xfc, 0x00, 0x00, + 0xc0, 0x30, 0x0c, 0x30, 0xc0, 0x00, + 0xb4, 0x0c, 0x34, 0x00, 0x30, 0x00, + 0x78, 0x8c, 0xcc, 0x80, 0x6c, 0x00, + 0x74, 0x88, 0xcc, 0xec, 0xcc, 0x00, + 0xf4, 0xcc, 0xf4, 0xcc, 0xf4, 0x00, + 0x7c, 0x80, 0xc0, 0x80, 0x7c, 0x00, + 0xe4, 0xc8, 0xcc, 0xc8, 0xe4, 0x00, + 0xfc, 0xc0, 0xf0, 0xc0, 0xfc, 0x00, + 0xfc, 0xc0, 0xf0, 0xc0, 0xc0, 0x00, + 0x7c, 0x80, 0xc0, 0x8c, 0x7c, 0x00, + 0xcc, 0xcc, 0xfc, 0xcc, 0xcc, 0x00, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, + 0x0c, 0x0c, 0x0c, 0xcc, 0xb8, 0x00, + 0xcc, 0xc8, 0xe0, 0xc8, 0xcc, 0x00, + 0xc0, 0xc0, 0xc0, 0xc0, 0xfc, 0x00, + 0x88, 0xec, 0xfc, 0xdc, 0xcc, 0x00, + 0x8c, 0xdc, 0xec, 0xdc, 0xc8, 0x00, + 0x74, 0x88, 0xcc, 0x88, 0x74, 0x00, + 0xe4, 0xcc, 0xcc, 0xe4, 0xc0, 0x00, + 0x74, 0x88, 0xcc, 0x8c, 0x7c, 0x00, + 0xe4, 0xcc, 0xcc, 0xe4, 0xcc, 0x00, + 0xb8, 0xc0, 0xb8, 0x0c, 0xb8, 0x00, + 0xfc, 0x30, 0x30, 0x30, 0x30, 0x00, + 0xcc, 0xcc, 0xcc, 0x8c, 0x68, 0x00, + 0xcc, 0xcc, 0xcc, 0x98, 0x30, 0x00, + 0xcc, 0xcc, 0xdc, 0xfc, 0xec, 0x00, + 0xcc, 0x88, 0x20, 0x88, 0xcc, 0x00, + 0xcc, 0x88, 0x64, 0x30, 0x30, 0x00, + 0xfc, 0x08, 0x30, 0x80, 0xfc, 0x00, + 0xf0, 0xc0, 0xc0, 0xc0, 0xf0, 0x00, + 0xc0, 0x90, 0x30, 0x18, 0x0c, 0x00, + 0x3c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00, + 0x30, 0xdc, 0x88, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, + 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x6c, 0x8c, 0x8c, 0x6c, 0x00, + 0xc0, 0xe4, 0xc8, 0xc8, 0xe4, 0x00, + 0x00, 0x6c, 0x80, 0x80, 0x6c, 0x00, + 0x0c, 0x6c, 0x8c, 0x8c, 0x6c, 0x00, + 0x00, 0x38, 0x8c, 0xe0, 0x7c, 0x00, + 0x1c, 0x30, 0xb8, 0x30, 0x30, 0x30, + 0x00, 0xe4, 0x88, 0x6c, 0x08, 0xe4, + 0xc0, 0xe4, 0xc8, 0xcc, 0xcc, 0x00, + 0x30, 0x00, 0x70, 0x30, 0x2c, 0x00, + 0x30, 0x30, 0xf0, 0x00, 0x00, 0x00, // 0x6A j + 0x00, 0x00, 0xf0, 0x30, 0x30, 0x30, // 0x6B k + 0x00, 0x00, 0x3f, 0x30, 0x30, 0x30, // 0x6C l + 0x30, 0x30, 0x3f, 0x00, 0x00, 0x00, // 0x6D m + 0x30, 0x30, 0xff, 0x30, 0x30, 0x30, // 0x6E n + 0x00, 0x74, 0x88, 0x88, 0x74, 0x00, + 0x00, 0xe4, 0xc8, 0xc8, 0xe4, 0xc0, + 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, // 0x71 q + 0x00, 0x8c, 0xe0, 0xc0, 0xc0, 0x00, + 0x00, 0x6c, 0x90, 0x18, 0xf4, 0x00, + 0x30, 0x30, 0x3f, 0x30, 0x30, 0x30, // 0x74 t + 0x30, 0x30, 0xf0, 0x30, 0x30, 0x30, // 0x75 u + 0x30, 0x30, 0xff, 0x00, 0x00, 0x00, // 0x76 v + 0x00, 0x00, 0xff, 0x30, 0x30, 0x30, // 0x77 w + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0x78 x + 0x00, 0xcc, 0xcc, 0x78, 0x08, 0xa4, + 0x00, 0xfc, 0x18, 0x90, 0xfc, 0x00, + 0x2c, 0x30, 0xd0, 0x30, 0x2c, 0x00, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0xe0, 0x30, 0x1c, 0x30, 0xe0, 0x00, + 0xb0, 0xc0, 0x74, 0x0c, 0x38, 0x00, + 0xec, 0x88, 0x88, 0x88, 0xec, 0x00 +}; + +#endif diff --git a/video-terminal/tintty.cpp b/video-terminal/tintty.cpp new file mode 100644 index 0000000..7359851 --- /dev/null +++ b/video-terminal/tintty.cpp @@ -0,0 +1,758 @@ +#define TFT_BLACK 0x0000 +#define TFT_BLUE 0x0014 +#define TFT_RED 0xA000 +#define TFT_GREEN 0x0500 +#define TFT_CYAN 0x0514 +#define TFT_MAGENTA 0xA014 +#define TFT_YELLOW 0xA500 +#define TFT_WHITE 0xA514 + +#define TFT_BOLD_BLACK 0x8410 +#define TFT_BOLD_BLUE 0x001F +#define TFT_BOLD_RED 0xF800 +#define TFT_BOLD_GREEN 0x07E0 +#define TFT_BOLD_CYAN 0x07FF +#define TFT_BOLD_MAGENTA 0xF81F +#define TFT_BOLD_YELLOW 0xFFE0 +#define TFT_BOLD_WHITE 0xFFFF + +#include "tintty.h" +#include "font454.h" + +// exported variable for input logic +// @todo refactor +bool tintty_cursor_key_mode_application; + +const uint16_t ANSI_COLORS[] = { + TFT_BLACK, + TFT_RED, + TFT_GREEN, + TFT_YELLOW, + TFT_BLUE, + TFT_MAGENTA, + TFT_CYAN, + TFT_WHITE +}; + +const uint16_t ANSI_BOLD_COLORS[] = { + TFT_BOLD_BLACK, + TFT_BOLD_RED, + TFT_BOLD_GREEN, + TFT_BOLD_YELLOW, + TFT_BOLD_BLUE, + TFT_BOLD_MAGENTA, + TFT_BOLD_CYAN, + TFT_BOLD_WHITE +}; + +// cursor animation +const int16_t IDLE_CYCLE_MAX = 500; +const int16_t IDLE_CYCLE_ON = (IDLE_CYCLE_MAX/2); + +const int16_t TAB_SIZE = 4; + +// cursor and character position is in global buffer coordinate space (may exceed screen height) +struct tintty_state { + // @todo consider storing cursor position as single int offset + int16_t cursor_col, cursor_row; + uint16_t bg_ansi_color, fg_ansi_color; + bool bold; + + // cursor mode + bool cursor_key_mode_application; + + // saved DEC cursor info (in screen coords) + int16_t dec_saved_col, dec_saved_row, dec_saved_bg, dec_saved_fg; + uint8_t dec_saved_g4bank; + bool dec_saved_bold, dec_saved_no_wrap; + + // @todo deal with integer overflow + int16_t top_row; // first displayed row in a logical scrollback buffer + bool no_wrap; + bool cursor_hidden; + + char out_char; + int16_t out_char_col, out_char_row; + uint8_t out_char_g4bank; // current set shift state, G0 to G3 + int16_t out_clear_before, out_clear_after; + + uint8_t g4bank_char_set[4]; + + int16_t idle_cycle_count; // @todo track during blocking reads mid-command +} state; + +struct tintty_rendered { + int16_t cursor_col, cursor_row; + int16_t top_row; +} rendered; + +// @todo support negative cursor_row +void _render(tintty_display *display) { + // expose the cursor key mode state + tintty_cursor_key_mode_application = state.cursor_key_mode_application; + + // if scrolling, prepare the "recycled" screen area + if (state.top_row != rendered.top_row) { + // clear the new piece of screen to be recycled as blank space + // @todo handle scroll-up + if (state.top_row > rendered.top_row) { + // pre-clear the lines at the bottom + // @todo always use black instead of current background colour? + // @todo deal with overflow from multiplication by CHAR_HEIGHT + /*int16_t old_bottom_y = rendered.top_row * FONT_HEIGHT + display->screen_row_count * FONT_HEIGHT; // bottom of text may not align with screen height + int16_t new_bottom_y = state.top_row * FONT_HEIGHT + display->screen_height; // extend to bottom edge of new displayed area + int16_t clear_sbuf_bottom = new_bottom_y % display->screen_height; + int16_t clear_height = min((int)display->screen_height, new_bottom_y - old_bottom_y); + int16_t clear_sbuf_top = clear_sbuf_bottom - clear_height;*/ + + // if rectangle straddles the screen buffer top edge, render that slice at bottom edge + /*if (clear_sbuf_top < 0) { + display->fill_rect( + 0, + clear_sbuf_top + display->screen_height, + display->screen_width, + -clear_sbuf_top, + ANSI_COLORS[state.bg_ansi_color] + ); + }*/ + + // if rectangle is not entirely above top edge, render the normal slice + /*if (clear_sbuf_bottom > 0) { + display->fill_rect( + 0, + max(0, (int)clear_sbuf_top), + display->screen_width, + clear_sbuf_bottom - max(0, (int)clear_sbuf_top), + ANSI_COLORS[state.bg_ansi_color] + ); + }*/ + } + + // update displayed scroll + display->set_vscroll((state.top_row) % display->screen_row_count); // @todo deal with overflow from multiplication + + // save rendered state + rendered.top_row = state.top_row; + } + + // render character if needed + if (state.out_char != 0) { + const uint16_t fg_tft_color = state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]; + const uint16_t bg_tft_color = ANSI_COLORS[state.bg_ansi_color]; + const uint8_t char_set = state.g4bank_char_set[state.out_char_g4bank & 0x03]; // ensure 0-3 value + + display->print_character(state.out_char_col, state.out_char_row, fg_tft_color, bg_tft_color, state.out_char); + + // clear for next render + state.out_char = 0; + state.out_clear_before = 0; + state.out_clear_after = 0; + + // the char draw may overpaint the cursor, in which case + // mark it for repaint + if ( + rendered.cursor_col == state.out_char_col && + rendered.cursor_row == state.out_char_row + ) { + display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); + rendered.cursor_col = -1; + } + } + + // reflect new cursor bar render state + const bool cursor_bar_shown = ( + !state.cursor_hidden && + state.idle_cycle_count < IDLE_CYCLE_ON + ); + + // clear existing rendered cursor bar if needed + // @todo detect if it is already cleared during scroll + if (rendered.cursor_col >= 0) { + if ( + !cursor_bar_shown || + rendered.cursor_col != state.cursor_col || + rendered.cursor_row != state.cursor_row + ) { + display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); + + // record the fact that cursor bar is not on screen + rendered.cursor_col = -1; + } + } + + // render new cursor bar if not already shown + // (sometimes right after clearing existing bar) + if (rendered.cursor_col < 0) { + if (cursor_bar_shown) { + display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); + display->print_cursor(state.cursor_col, state.cursor_row, state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]); + + // save new rendered state + rendered.cursor_col = state.cursor_col; + rendered.cursor_row = state.cursor_row; + } + } +} + +void bell() { + // TODO + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html + // https://github.com/lbernstone/Tone32 +} + +void _ensure_cursor_vscroll(tintty_display *display) { + // move displayed window down to cover cursor + // @todo support scrolling up as well + if (state.cursor_row - state.top_row >= display->screen_row_count) { + state.top_row = state.cursor_row - display->screen_row_count + 1; + } +} + +void _send_sequence( + void (*send_char)(char ch), + char* str +) { + // send zero-terminated sequence character by character + while (*str) { + send_char(*str); + str += 1; + } +} + +char _read_decimal( + char (*peek_char)(), + char (*read_char)() +) { + uint16_t accumulator = 0; + + while (isdigit(peek_char())) { + const char digit_character = read_char(); + const uint16_t digit = digit_character - '0'; + accumulator = accumulator * 10 + digit; + } + + return accumulator; +} + +void _apply_graphic_rendition( + uint16_t* arg_list, + uint16_t arg_count +) { + if (arg_count == 0) { + // special case for resetting to default style + state.bg_ansi_color = 0; + state.fg_ansi_color = 7; + state.bold = false; + + return; + } + + // process commands + // @todo support bold/etc for better colour support + // @todo 39/49? + for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) { + const uint16_t arg_value = arg_list[arg_index]; + + if (arg_value == 0) { + // reset to default style + state.bg_ansi_color = 0; + state.fg_ansi_color = 7; + state.bold = false; + } else if (arg_value == 1) { + // bold + state.bold = true; + } else if (arg_value >= 30 && arg_value <= 37) { + // foreground ANSI colour + state.fg_ansi_color = arg_value - 30; + } else if (arg_value >= 40 && arg_value <= 47) { + // background ANSI colour + state.bg_ansi_color = arg_value - 40; + } + } +} + +void _apply_mode_setting( + bool mode_on, + uint16_t* arg_list, + uint16_t arg_count +) { + // process modes + for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) { + const uint16_t mode_id = arg_list[arg_index]; + + switch (mode_id) { + case 4: + // insert/replace mode + // @todo this should be off for most practical purposes anyway? + // ... otherwise visually shifting line text is expensive + break; + + case 20: + // auto-LF + // ignoring per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.6 + break; + + case 34: + // cursor visibility + state.cursor_hidden = !mode_on; + break; + } + } +} + +void _exec_escape_question_command( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char ch) +) { + // @todo support multiple mode commands + // per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.1, + // ANSI and DEC modes cannot mix; that is, '[?25;20;?7l' is not a valid Esc-command + // (noting this because https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html + // makes it look like the question mark is a prefix) + const uint16_t mode = _read_decimal(peek_char, read_char); + const bool mode_on = (read_char() != 'l'); + + switch (mode) { + case 1: + // cursor key mode (normal/application) + state.cursor_key_mode_application = mode_on; + break; + + case 7: + // auto wrap mode + state.no_wrap = !mode_on; + break; + + case 25: + // cursor visibility + state.cursor_hidden = !mode_on; + break; + } +} + +// @todo cursor position report +void _exec_escape_bracket_command_with_args( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char ch), + tintty_display *display, + uint16_t* arg_list, + uint16_t arg_count +) { + // convenient arg getter + #define ARG(index, default_value) (arg_count > index ? arg_list[index] : default_value) + + // process next character after Escape-code, bracket and any numeric arguments + const char command_character = read_char(); + + switch (command_character) { + case '?': + // question-mark commands + _exec_escape_question_command(peek_char, read_char, send_char); + break; + + case 'A': + // cursor up (no scroll) + state.cursor_row = max((int)state.top_row, state.cursor_row - ARG(0, 1)); + break; + + case 'B': + // cursor down (no scroll) + state.cursor_row = min(state.top_row + display->screen_row_count - 1, state.cursor_row + ARG(0, 1)); + break; + + case 'C': + // cursor right (no scroll) + state.cursor_col = min(display->screen_col_count - 1, state.cursor_col + ARG(0, 1)); + break; + + case 'D': + // cursor left (no scroll) + state.cursor_col = max(0, state.cursor_col - ARG(0, 1)); + break; + + case 'H': + case 'f': + // Direct Cursor Addressing (row;col) + state.cursor_col = max(0, min(display->screen_col_count - 1, ARG(1, 1) - 1)); + state.cursor_row = state.top_row + max(0, min(display->screen_row_count - 1, ARG(0, 1) - 1)); + break; + + case 'J': + // clear screen + state.out_char = ' '; + state.out_char_col = state.cursor_col; + state.out_char_row = state.cursor_row; + + { + const int16_t rel_row = state.cursor_row - state.top_row; + + state.out_clear_before = ARG(0, 0) != 0 + ? rel_row * display->screen_col_count + state.cursor_col + : 0; + state.out_clear_after = ARG(0, 0) != 1 + ? (display->screen_row_count - 1 - rel_row) * display->screen_col_count + (display->screen_col_count - 1 - state.cursor_col) + : 0; + } + + break; + + case 'K': + // clear line + state.out_char = ' '; + state.out_char_col = state.cursor_col; + state.out_char_row = state.cursor_row; + + state.out_clear_before = ARG(0, 0) != 0 + ? state.cursor_col + : 0; + state.out_clear_after = ARG(0, 0) != 1 + ? display->screen_col_count - 1 - state.cursor_col + : 0; + + break; + + case 'm': + // graphic rendition mode + _apply_graphic_rendition(arg_list, arg_count); + break; + + case 'h': + // set mode + _apply_mode_setting(true, arg_list, arg_count); + break; + + case 'l': + // unset mode + _apply_mode_setting(false, arg_list, arg_count); + break; + } +} + +void _exec_escape_bracket_command( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char ch), + tintty_display *display +) { + const uint16_t MAX_COMMAND_ARG_COUNT = 10; + uint16_t arg_list[MAX_COMMAND_ARG_COUNT]; + uint16_t arg_count = 0; + + // start parsing arguments if any + // (this means that '' is treated as no arguments, but '0;' is treated as two arguments, each being zero) + // @todo ignore trailing semi-colon instead of treating it as marking an extra zero arg? + if (isdigit(peek_char())) { + // keep consuming arguments while we have space + while (arg_count < MAX_COMMAND_ARG_COUNT) { + // consume decimal number + arg_list[arg_count] = _read_decimal(peek_char, read_char); + arg_count += 1; + + // stop processing if next char is not separator + if (peek_char() != ';') { + break; + } + + // consume separator before starting next argument + read_char(); + } + } + + _exec_escape_bracket_command_with_args( + peek_char, + read_char, + send_char, + display, + arg_list, + arg_count + ); +} + +// set the characters displayed for given G0-G3 bank +void _exec_character_set( + uint8_t g4bank_index, + char (*read_char)() +) { + switch (read_char()) { + case 'A': + case 'B': + // normal character set (UK/US) + state.g4bank_char_set[g4bank_index] = 0; + break; + + case '0': + // line-drawing + state.g4bank_char_set[g4bank_index] = 1; + break; + + default: + // alternate sets are unsupported + state.g4bank_char_set[g4bank_index] = 0; + break; + } +} + +// @todo terminal reset +// @todo parse modes with arguments even if they are no-op +void _exec_escape_code( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char ch), + tintty_display *display +) { + // read next character after Escape-code + // @todo time out? + char esc_character = read_char(); + + // @todo support for (, ), #, c, cursor save/restore + switch (esc_character) { + case '[': + _exec_escape_bracket_command(peek_char, read_char, send_char, display); + break; + + case 'D': + // index (move down and possibly scroll) + state.cursor_row += 1; + _ensure_cursor_vscroll(display); + break; + + case 'M': + // reverse index (move up and possibly scroll) + state.cursor_row -= 1; + _ensure_cursor_vscroll(display); + break; + + case 'E': + // next line + state.cursor_row += 1; + state.cursor_col = 0; + _ensure_cursor_vscroll(display); + break; + + case 'Z': + // Identify Terminal (DEC Private) + _send_sequence(send_char, "\e[?1;0c"); // DA response: no options + break; + + case '7': + // save cursor + // @todo verify that the screen-relative coordinate approach is valid + state.dec_saved_col = state.cursor_col; + state.dec_saved_row = state.cursor_row - state.top_row; // relative to top + state.dec_saved_bg = state.bg_ansi_color; + state.dec_saved_fg = state.fg_ansi_color; + state.dec_saved_g4bank = state.out_char_g4bank; + state.dec_saved_bold = state.bold; + state.dec_saved_no_wrap = state.no_wrap; + break; + + case '8': + // restore cursor + state.cursor_col = state.dec_saved_col; + state.cursor_row = state.dec_saved_row + state.top_row; // relative to top + state.bg_ansi_color = state.dec_saved_bg; + state.fg_ansi_color = state.dec_saved_fg; + state.out_char_g4bank = state.dec_saved_g4bank; + state.bold = state.dec_saved_bold; + state.no_wrap = state.dec_saved_no_wrap; + break; + + case '=': + case '>': + // keypad mode setting - ignoring + break; + + case '(': + // set G0 + _exec_character_set(0, read_char); + break; + + case ')': + // set G1 + _exec_character_set(1, read_char); + break; + + case '*': + // set G2 + _exec_character_set(2, read_char); + break; + + case '+': + // set G3 + _exec_character_set(3, read_char); + break; + + default: + // unrecognized character, silently ignore + break; + } +} + +void _main( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char str), + tintty_display *display +) { + // start in default idle state + char initial_character = read_char(); + + if (initial_character >= 0x20 && initial_character <= 0x7e) { + // output displayable character + state.out_char = initial_character; + state.out_char_col = state.cursor_col; + state.out_char_row = state.cursor_row; + + // update caret + state.cursor_col += 1; + + if (state.cursor_col >= display->screen_col_count) { + if (state.no_wrap) { + state.cursor_col = display->screen_col_count - 1; + } else { + state.cursor_col = 0; + state.cursor_row += 1; + _ensure_cursor_vscroll(display); + } + } + + // reset idle state + state.idle_cycle_count = 0; + } else { + // @todo bell, answer-back (0x05), delete + switch (initial_character) { + case '\a': + // bell + bell(); + break; + case '\n': + // line-feed + state.cursor_row += 1; + _ensure_cursor_vscroll(display); + break; + + case '\r': + // carriage-return + state.cursor_col = 0; + break; + + case '\b': + // backspace + state.cursor_col -= 1; + + if (state.cursor_col < 0) { + if (state.no_wrap) { + state.cursor_col = 0; + } else { + state.cursor_col = display->screen_col_count - 1; + state.cursor_row -= 1; + _ensure_cursor_vscroll(display); + } + } + + break; + + case '\t': + // tab + { + // @todo blank out the existing characters? not sure if that is expected + const int16_t tab_num = state.cursor_col / TAB_SIZE; + state.cursor_col = min(display->screen_col_count - 1, (tab_num + 1) * TAB_SIZE); + } + break; + + case '\e': + // Escape-command + _exec_escape_code(peek_char, read_char, send_char, display); + break; + + case '\x0f': + // Shift-In (use G0) + // see also the fun reason why these are called this way: + // https://en.wikipedia.org/wiki/Shift_Out_and_Shift_In_characters + state.out_char_g4bank = 0; + break; + + case '\x0e': + // Shift-Out (use G1) + state.out_char_g4bank = 1; + break; + + default: + // nothing, just animate cursor + delay(1); + state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX; + } + } + + _render(display); +} + +void tintty_run( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char str), + tintty_display *display +) { + // set up initial state + state.cursor_col = 0; + state.cursor_row = 0; + state.top_row = 0; + state.no_wrap = 0; + state.cursor_hidden = 0; + state.bg_ansi_color = 0; + state.fg_ansi_color = 7; + state.bold = false; + + state.cursor_key_mode_application = false; + + state.dec_saved_col = 0; + state.dec_saved_row = 0; + state.dec_saved_bg = state.bg_ansi_color; + state.dec_saved_fg = state.fg_ansi_color; + state.dec_saved_g4bank = 0; + state.dec_saved_bold = state.bold; + state.dec_saved_no_wrap = false; + + state.out_char = 0; + state.out_char_g4bank = 0; + state.g4bank_char_set[0] = 0; + state.g4bank_char_set[1] = 0; + state.g4bank_char_set[2] = 0; + state.g4bank_char_set[3] = 0; + + rendered.cursor_col = -1; + rendered.cursor_row = -1; + + // clear screen + display->fill_rect(0, 0, display->screen_width, display->screen_height, TFT_BLACK); + + // reset TFT scroll to default + display->set_vscroll(0); + + // initial render + _render(display); + + // send CR to indicate that the screen is ready + // (this works with the agetty --wait-cr option to help wait until Arduino boots) + send_char('\r'); + + // main read cycle + while (1) { + _main(peek_char, read_char, send_char, display); + } +} + +void tintty_idle( + tintty_display *display +) { + delay(1); + + // animate cursor + state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX; + + // re-render + _render(display); +} diff --git a/video-terminal/tintty.h b/video-terminal/tintty.h new file mode 100644 index 0000000..ebdf066 --- /dev/null +++ b/video-terminal/tintty.h @@ -0,0 +1,37 @@ +#include "Arduino.h" + +extern bool tintty_cursor_key_mode_application; + +/** + * Renderer callbacks. + */ +struct tintty_display { + int16_t screen_width, screen_height; + int16_t screen_col_count, screen_row_count; // width and height divided by char size + + void (*fill_rect)(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color); + void (*draw_pixels)(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t *pixels); + void (*print_character)(int16_t col, int16_t row, uint16_t fg_color, uint16_t bg_color, char character); + void (*print_cursor)(int16_t col, int16_t row, uint16_t color); + void (*set_vscroll)(int16_t offset); // scroll offset for entire screen +}; + +/** + * Main entry point. + * Peek/read callbacks are expected to block until input is available; + * while sketch is waiting for input, it should call the tintty_idle() hook + * to allow animating cursor, etc. + */ +void tintty_run( + char (*peek_char)(), + char (*read_char)(), + void (*send_char)(char str), + tintty_display *display +); + +/** + * Hook to call while e.g. sketch is waiting for input + */ +void tintty_idle( + tintty_display *display +); diff --git a/video-terminal/video-terminal.ino b/video-terminal/video-terminal.ino new file mode 100644 index 0000000..c610e3d --- /dev/null +++ b/video-terminal/video-terminal.ino @@ -0,0 +1,153 @@ +/** + * Video Terminal + * VT100 emulated by ESP32 + TV display + LK201 keyboard + * + * TinTTY main sketch + * by Nick Matantsev 2017 + * + * Original reference: VT100 emulation code written by Martin K. Schroeder + * and modified by Peter Scargill. + */ + +#include + +#include + +#include "tintty.h" + +#define SerialTty Serial1 +#define SerialKbd Serial2 + +#include "Keyboard.h" + +#include "Display.h" +Display display; + +int16_t scrolled = 0; + +#define FONT_SCALE 2 + +uint16_t make_bw_color(uint16_t color) { + return (color >> 8) | (color & 0xFF); +} + +struct tintty_display ili9341_display = { + display.vw,//ILI9341_WIDTH, + display.vh,//(ILI9341_HEIGHT - KEYBOARD_HEIGHT), + display.vw / display.font_w,//ILI9341_WIDTH / TINTTY_CHAR_WIDTH, + display.vh / display.font_h,//(ILI9341_HEIGHT - KEYBOARD_HEIGHT) / TINTTY_CHAR_HEIGHT, + + [=](int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color){ + //tft.fillRect(x, y, w, h, color); + //display.fill_rect(x*FONT_SCALE + display.ow, (y*FONT_SCALE + display.oh, w*FONT_SCALE, h*FONT_SCALE, make_bw_color(color)); + }, + + [=](int16_t x, int16_t y, int16_t w, int16_t h, uint16_t *pixels){ + //tft.setAddrWindow(x, y, x + w - 1, y + h - 1); + //tft.pushColors(pixels, w * h, 1); + for (int px = 0; px < w; px++) { + for (int py = 0; py < h; py++) { + int i = py*w + px; + //display.pixel(x + display.ow + px, y + display.oh + py, pixels[i]); + //display.fill_rect(x*FONT_SCALE + display.ow + px*FONT_SCALE, y*FONT_SCALE + display.oh + py*FONT_SCALE, FONT_SCALE, FONT_SCALE, make_bw_color(pixels[i])); + } + } + }, + + [=](int16_t col, int16_t row, uint16_t fg_color, uint16_t bg_color, char character){ + display.print_character(col, row - scrolled, make_bw_color(fg_color), make_bw_color(bg_color), character); + }, + + [=](int16_t col, int16_t row, uint16_t color){ + display.fill_rect(col*display.font_w + display.ow, (row+1 - scrolled)*display.font_h-1 + display.oh, display.font_w, 1, make_bw_color(color)); + //display.print_character(col, row, make_bw_color(color), 0, '_'); + }, + + [=](int16_t offset){ + //tft.vertScroll(0, (ILI9341_HEIGHT - KEYBOARD_HEIGHT), offset); + int16_t rows = display.vh / display.font_h; + int16_t diff = (rows + offset - (scrolled % rows)) % rows; + display.scroll(diff*display.font_h); + scrolled += diff; + } +}; + +void tty_keyboard_process() +{ + // read keyboard and send it to the host + if (SerialKbd.available() > 0) { + int key = SerialKbd.read(); + keyboard_handle_key(key); + } +} + +// buffer to test various input sequences +char *test_buffer = "-- \e[1mTinTTY\e[m --\r\n"; +uint8_t test_buffer_cursor = 0; + +void input_init() {}; +void input_idle() {}; + +void setup() { + // Debug port + Serial.begin(115200); + Serial.println("Running!"); + + // TTY host + SerialTty.begin(9600, SERIAL_8N1, 18, 19, false, 100); + + // LK201 keyboard connected to pins 16 and 17 + SerialKbd.begin(4800); + + display.setup(); + //uint16_t tftID = tft.readID(); + //tft.begin(tftID); + + input_init(); + + tintty_run( + [=](){ + // peek idle loop + while (true) { + // first peek from the test buffer + if (test_buffer[test_buffer_cursor]) { + return test_buffer[test_buffer_cursor]; + } + + tty_keyboard_process(); + + // fall back to normal blocking serial input + if (SerialTty.available() > 0) { + return (char)SerialTty.peek(); + } + + // idle logic only after peeks failed + tintty_idle(&ili9341_display); + input_idle(); + } + }, + [=](){ + while(true) { + // process at least one idle loop first to allow input to happen + tintty_idle(&ili9341_display); + input_idle(); + tty_keyboard_process(); + + // first read from the test buffer + if (test_buffer[test_buffer_cursor]) { + return test_buffer[test_buffer_cursor++]; + } + + // fall back to normal blocking serial input + if (SerialTty.available() > 0) { + return (char)SerialTty.read(); + } + } + }, + [=](char ch){ SerialTty.print(ch); }, + &ili9341_display + ); +} + +void loop() { +}