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