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