Initial commit

This commit is contained in:
Dejvino 2021-03-13 18:25:12 +01:00
parent 02deaf49ba
commit 5b40a9b083
7 changed files with 1562 additions and 0 deletions

View File

@ -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 - [TinTTY](https://github.com/unframework/tintty) for VT100 emulation
- [ESP32Lib](https://github.com/bitluni/ESP32Lib/tree/development) for composite video output - [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 ## Resources
- [LK201 Interface](http://www.netbsd.org/docs/Hardware/Machines/DEC/lk201.html) for keyboard specification - [LK201 Interface](http://www.netbsd.org/docs/Hardware/Machines/DEC/lk201.html) for keyboard specification

119
video-terminal/Display.h Normal file
View File

@ -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) R2R 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 <ESP32Lib.h>
#include <Ressources/Font6x8.h>
//#include <Ressources/CodePage437_8x8.h>
//#include <Ressources/CodePage437_8x14.h>
//#include <Ressources/CodePage437_8x16.h>
//#include <Ressources/CodePage437_8x19.h>
#include <Ressources/CodePage437_9x16.h>
//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; }
};

216
video-terminal/Keyboard.h Normal file
View File

@ -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("<DELETE>"); 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("<SHIFT "); Serial.print(mod_shift); Serial.print(">"); break; // (xae): KEY_SHIFT
case 175: mod_ctrl = !mod_ctrl; Serial.print("<CTRL "); Serial.print(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("<ALLUP>");
break;
default:
Serial.print(key); Serial.print(' ');
}
if (c != -1) {
Serial.print((char) c);
SerialTty.print((char) c);
}
}

274
video-terminal/font454.h Normal file
View File

@ -0,0 +1,274 @@
#ifndef FONT454_H
#define FONT454_H
//#include <avr/io.h>
//#include <avr/pgmspace.h>
// 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

758
video-terminal/tintty.cpp Normal file
View File

@ -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);
}

37
video-terminal/tintty.h Normal file
View File

@ -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
);

View File

@ -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 <SPI.h>
#include <Adafruit_GFX.h>
#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() {
}