| /* pdp18b_g2tty.c: PDP-7/9 Bell Labs "GRAPHIC-2" subsystem as a TTY via TELNET | |
| from 13-Sep-15 version of pdp18b_tt1.c | |
| Copyright (c) 1993-2015, Robert M Supnik | |
| Copyright (c) 2016, Philip L Budne | |
| Permission is hereby granted, free of charge, to any person obtaining a | |
| copy of this software and associated documentation files (the "Software"), | |
| to deal in the Software without restriction, including without limitation | |
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| and/or sell copies of the Software, and to permit persons to whom the | |
| Software is furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in | |
| all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| ROBERT M SUPNIK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
| IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| Except as contained in this notice, the name of Robert M Supnik shall not be | |
| used in advertising or otherwise to promote the sale, use or other dealings | |
| in this Software without prior written authorization from Robert M Supnik. | |
| Doug McIlroy had this to say about the Bell Labs PDP-7 Ken Thompson | |
| created UNIX on: | |
| The pdp7 was cast off by the visual and acoustics research department. | |
| Bill Ninke et al. built graphic II on it -- a graphics attachment as big | |
| as the pdp7 itself. The disk was an amazing thing about 2' in diameter, | |
| mounted on a horizontal axis. Mystery crashes bedeviled it until somebody | |
| realized that the axis was perpendicular to the loading dock 4 floors | |
| below. A 90-degree turn solved the problem. | |
| GRAPHICS-2 was a command list based graphics display system, | |
| and included a light pen, a "button box" and status bits | |
| for a "dataphone" interface to speak to a GECOS system. | |
| The UNIX-7 system driver only uses text display, and reserves 269 | |
| words (holding two characters each; the buffer is 273 words, but | |
| three contain display "setup" commands, and the final word in the | |
| buffer must be a display "TRAP" instruction that ends the display | |
| list). | |
| The UNIX system code triggers a refresh every 10 60Hz "ticks" of | |
| the real time clock. This driver attempts to do detect new text | |
| and send it to a user who has TELNETed in. | |
| Thoughts on implementing a web interface: | |
| 538 characters redisplaying at 6Hz (every 10 "ticks") gives a | |
| bandwith requirement of only 26Kbit, and most refreshes won't | |
| change the screen and could be suppressed. So it seems like it | |
| would be reasonable to create a web interface. | |
| Make a SIMH TCP server port which implements a tiny HTTP server. | |
| The base URL serves up a skeletal page with (lighted) buttons, | |
| and a "display window". | |
| And either: | |
| 1) Use "AJAX": an (invisible) <iframe> on in the HTML served by the | |
| "base URL" keeps a never-ending connection that is sent "script" | |
| tags to alter the "screen" (div) contents, and light push buttons. | |
| Keypresses and buttons could be implemented using "onclick" | |
| actions which trigger a GET (or a POST) on a URLs which | |
| open new (temporary) HTTP connections to SIMH. The key/button | |
| press URL could contain a session UUID which has to match | |
| a value sent in the initial page. | |
| 2) Have the "home" page HTML establish a bi-directional WebSocket | |
| connection to Encapsulate all the traffic (screen contents, | |
| button lighting, key & button presses). | |
| The graphics system responds as ten PDP-7 "devices"; | |
| UNIX only uses six, and only three of the six are simulated here | |
| (and *JUST* enough of those to figure out the text being displayed), | |
| as two SIMH DEVICES, G2OUT and G2IN: | |
| G2OUT: | |
| G2D1 005 GRAPHICS-2 display output | |
| G2IN: | |
| G2KB 043 GRAPHICS-2 keyboard | |
| G2BB 044 GRAPHICS-2 button box (lighted bush buttons) | |
| 20-Mar-16 PLB Works, mostly | |
| 19-Mar-16 PLB Working (up to a screen full) | |
| 17-Mar-16 PLB Cloned from 13-Sep-15 version of pdp18b_tt1.c | |
| */ | |
| /* | |
| * GRAPHICS-2 was vector graphics hardware, UNIX-7 uses it as a | |
| * "Glass TTY" for a "second seat". | |
| * | |
| * This simulation ONLY handles text display; the one program | |
| * found that uses the "capt" (capture?) call to set a user | |
| * supplied display list will not work here. | |
| * | |
| * When the display buffer or screen is filled, the UNIX "display" | |
| * driver lights "push button 7" (PB7), and waits for the user to | |
| * press the button. UNIX then clears the screen, and output | |
| * continues. If the program outputs a "Form Feed" character the | |
| * display is also cleared. | |
| * | |
| * This simulation automatically presses PB7 when lit, without | |
| * bothering the user (a more accurate simulation might prompt | |
| * the user to press any key)! | |
| */ | |
| #include "pdp18b_defs.h" | |
| #ifdef GRAPHICS2 | |
| #include "sim_tmxr.h" | |
| int debug = 0; | |
| /* hardware registers */ | |
| uint8 g2kb_done = 0; /* keyboard flag */ | |
| uint32 g2kb_buf = 0; /* keyboard buffer */ | |
| uint8 g2bb_flag = 0; /* button flag */ | |
| uint32 g2bb_bbuf = 0; /* button buffer */ | |
| uint32 g2bb_lbuf = 0; /* button lights buffer */ | |
| uint32 g2out_addr = 0; /* display address */ | |
| #define PB7 02000 | |
| /* not hardware registers: */ | |
| uint32 g2out_count = 0; | |
| uint8 g2out_stuffcr = 0; /* need to stuff a CR */ | |
| /* keep old and new version of characters to display | |
| * a count & checksum of the "old" screen contents might suffice, | |
| * time will tell.... | |
| */ | |
| uint8 g2out_which = 0; | |
| #define OLD g2out_which | |
| #define NEW (g2out_which ^ 1) | |
| #define MAXBUFCHARS 700 /* larger than kernel display list */ | |
| static struct dspbuf { | |
| uint16 count; | |
| char buffer[MAXBUFCHARS]; /* 7-bit ASCII */ | |
| } g2out_dspbufs[2]; | |
| /* terminal mux data */ | |
| TMLN g2_ldsc = { 0 }; /* line descriptor */ | |
| TMXR g2_desc = { 1, 0, 0, &g2_ldsc }; /* mux descriptor */ | |
| /* kernel display lists always start like this: */ | |
| static const int32 g2_expect[3] = { | |
| 0065057, /* PARAM: clear blink, clear light pen, scale=1, intensity=3 */ | |
| 0147740, /* X-Y: invisible, no delay, Y=01740 (992) */ | |
| 0160000 /* X-Y: invisible, settling delay, X=0 */ | |
| }; | |
| extern int32 *M; | |
| extern int32 int_hwre[API_HLVL+1]; | |
| extern int32 api_vec[API_HLVL][32]; | |
| extern int32 tmxr_poll; | |
| extern int32 stop_inst; | |
| /* SIMH G2IN DEVICE */ | |
| t_bool g2kb_test_done (); | |
| void g2kb_set_done (); | |
| void g2kb_clr_done (); | |
| int32 g2kb_iot (int32 dev, int32 pulse, int32 dat); /* device 043 */ | |
| t_bool g2bb_test_flag (); | |
| void g2bb_set_flag (); | |
| void g2bb_clr_flag (); | |
| int32 g2bb_iot (int32 dev, int32 pulse, int32 dat); /* device 044 */ | |
| t_stat g2in_svc (UNIT *uptr); | |
| /* SIMH G2OUT DEVICE */ | |
| int32 g2d1_iot (int32 dev, int32 pulse, int32 dat); /* device 05 */ | |
| static void g2out_clear (); | |
| static void g2out_process_display_list (); | |
| static int g2out_send_new (); | |
| /* both G2IN/G2OUT: */ | |
| t_stat g2_attach (UNIT *uptr, char *cptr); | |
| t_stat g2_detach (UNIT *uptr); | |
| t_stat g2_reset (DEVICE *dptr); | |
| /**************************************************************** | |
| * SIMH G2IN (keyboard/buttons) DEVICE data structures | |
| * | |
| * g2in_dev G2IN device descriptor | |
| * g2in_unit G2IN unit descriptor | |
| * g2in_reg G2IN register list | |
| * g2in_mod G2IN modifiers list | |
| */ | |
| DIB g2in_dib = { DEV_G2KB, 2, NULL, { &g2kb_iot, &g2bb_iot } }; | |
| UNIT g2in_unit = { | |
| UDATA (&g2in_svc, UNIT_IDLE|UNIT_ATTABLE, 0), KBD_POLL_WAIT | |
| }; | |
| REG g2in_reg[] = { | |
| { ORDATA (KBBUF, g2kb_buf, 1) }, | |
| { ORDATA (KBDONE, g2kb_done, 1) }, | |
| { FLDATA (INT, int_hwre[API_G2], INT_V_G2) }, | |
| { DRDATA (TIME, g2in_unit.wait, 24), REG_NZ + PV_LEFT }, | |
| { ORDATA (BBBBUF, g2bb_bbuf, 1) }, /* button box button buffer */ | |
| { ORDATA (BBFLAG, g2bb_flag, 1) }, /* button box IRQ */ | |
| { ORDATA (BBLBUF, g2bb_lbuf, 1) }, /* button box lights buffer */ | |
| { NULL } | |
| }; | |
| MTAB g2in_mod[] = { | |
| { UNIT_ATT, UNIT_ATT, "summary", NULL, | |
| NULL, &tmxr_show_summ, (void *) &g2_desc }, | |
| { MTAB_XTD | MTAB_VDV, 1, NULL, "DISCONNECT", | |
| &tmxr_dscln, NULL, (void *) &g2_desc }, | |
| { MTAB_XTD | MTAB_VDV | MTAB_NMO, 1, "CONNECTIONS", NULL, | |
| NULL, &tmxr_show_cstat, (void *) &g2_desc }, | |
| { MTAB_XTD | MTAB_VDV | MTAB_NMO, 0, "STATISTICS", NULL, | |
| NULL, &tmxr_show_cstat, (void *) &g2_desc }, | |
| { MTAB_XTD|MTAB_VUN|MTAB_NC, 0, "LOG", "LOG", | |
| &tmxr_set_log, &tmxr_show_log, &g2_desc }, | |
| { MTAB_XTD|MTAB_VUN|MTAB_NC, 0, NULL, "NOLOG", | |
| &tmxr_set_nolog, NULL, &g2_desc }, | |
| { MTAB_XTD|MTAB_VDV, 0, "DEVNO", "DEVNO", | |
| NULL, &show_devno, NULL }, | |
| { 0 } | |
| }; | |
| /* SIMH G2IN device descriptor (GRAPHICS-2 keyboard & button box) */ | |
| DEVICE g2in_dev = { | |
| "G2IN", /* name */ | |
| &g2in_unit, /* units */ | |
| g2in_reg, g2in_mod, /* registers, modifiers */ | |
| 1, /* numunits */ | |
| 10, 31, /* aradix, awidth */ | |
| 1, 8, 8, /* aincr, dradix, dwidth */ | |
| &tmxr_ex, &tmxr_dep, &g2_reset, /* examine, deposit, reset */ | |
| NULL, &g2_attach, &g2_detach, /* boot, attach, detach */ | |
| &g2in_dib, DEV_MUX | DEV_DISABLE /* ctxt, flags */ | |
| }; | |
| /**************************************************************** | |
| * SIMH G2OUT (display output) DEVICE data structures | |
| * Only needed to hold "iot" routine, since DIB's can't represent | |
| * devices with register sets as sparse as GRAPHICS-2 | |
| * | |
| * g2out_dev G2OUT device descriptor | |
| * g2out_unit G2OUT unit descriptor | |
| * g2out_reg G2OUT register list | |
| * g2out_mod G2OUT modifiers list | |
| */ | |
| DIB g2out_dib = { DEV_G2D1, 1, NULL, { &g2d1_iot } }; | |
| UNIT g2out_unit = { UDATA (NULL, 0, 0) }; | |
| REG g2out_reg[] = { | |
| { ORDATA (DPYADDR, g2out_addr, 1) }, | |
| { NULL } | |
| }; | |
| MTAB g2out_mod[] = { | |
| { MTAB_XTD|MTAB_VUN, 0, NULL, "DISCONNECT", | |
| &tmxr_dscln, NULL, &g2_desc }, | |
| { MTAB_XTD|MTAB_VDV, 0, "DEVNO", "DEVNO", | |
| NULL, &show_devno, NULL }, | |
| { 0 } | |
| }; | |
| /* SIMH G2OUT device descriptor (simulates just one of many display IOTs!) */ | |
| DEVICE g2out_dev = { | |
| "G2OUT", /* name */ | |
| &g2out_unit, /* units */ | |
| g2out_reg, g2out_mod, /* registers, modifiers */ | |
| 1, /* numunits */ | |
| 10, 31, /* aradix, awidth */ | |
| 1, 8, 8, /* aincr, dradix, dwidth */ | |
| NULL, NULL, &g2_reset, /* examine, deposit, reset */ | |
| NULL, NULL, NULL, /* boot, attach, detach */ | |
| &g2out_dib, DEV_DISABLE /* ctxt, flags */ | |
| }; | |
| /**************************************************************** | |
| * IOT routines | |
| */ | |
| /* Keyboard input IOT routine */ | |
| /* real device could have done bitwise decode?! */ | |
| int32 g2kb_iot (int32 dev, int32 pulse, int32 dat) | |
| { | |
| if (pulse == 001) { /* sck */ | |
| if (g2kb_done) { | |
| dat = dat | IOT_SKP; | |
| } | |
| } | |
| else if (pulse == 002) { /* lck */ | |
| dat = dat | g2kb_buf; /* return buffer */ | |
| } | |
| else if (pulse == 004) { /* cck */ | |
| g2kb_clr_done (); /* clear flag */ | |
| } | |
| return dat; | |
| } | |
| /* Button Box IOT routine */ | |
| int32 g2bb_iot (int32 dev, int32 pulse, int32 dat) | |
| { | |
| if (pulse == 001) { /* "spb" -- skip on push button flag */ | |
| if (g2bb_flag) | |
| dat = dat | IOT_SKP; | |
| } | |
| else if (pulse == 002) /* "lpb"/"opb" -- or push buttons */ | |
| dat = dat | g2bb_bbuf; /* return buttons */ | |
| else if (pulse == 004) { /* "cpb" -- clear push button flag */ | |
| g2bb_clr_flag (); /* clear flag */ | |
| } | |
| else if (pulse == 024) { /* "wbl" -- write buttons lights */ | |
| if (dat == 0) | |
| g2out_clear(); /* UNIX has ack'ed button press */ | |
| g2bb_lbuf = dat; | |
| } | |
| return dat; | |
| } | |
| /* Input side Unit service */ | |
| t_stat g2in_svc (UNIT *uptr) | |
| { | |
| int32 ln, c; | |
| if ((uptr->flags & UNIT_ATT) == 0) /* attached? */ | |
| return SCPE_OK; | |
| if (g2bb_lbuf & PB7) { /* button 7 lit? */ | |
| /* yes: try sending anything new */ | |
| g2out_process_display_list(); | |
| g2out_send_new(); | |
| g2bb_bbuf |= PB7; /* press it to clear screen! */ | |
| g2bb_set_flag (); | |
| } | |
| sim_clock_coschedule (uptr, tmxr_poll); /* continue poll */ | |
| ln = tmxr_poll_conn (&g2_desc); /* look for connect */ | |
| if (ln >= 0) /* got one? rcv enab */ | |
| g2_ldsc.rcve = 1; | |
| tmxr_poll_rx (&g2_desc); /* poll for input */ | |
| if (g2_ldsc.conn) { /* connected? */ | |
| tmxr_poll_tx (&g2_desc); /* PLB: poll xmt */ | |
| if ((c = tmxr_getc_ln (&g2_ldsc))) { /* get char */ | |
| if (c & SCPE_BREAK) /* break? */ | |
| c = 0; | |
| else { | |
| c &= 0177; | |
| if (c == '\r') /* translate CR but not ESC! */ | |
| c = '\n'; | |
| else if ((c & 0155) == 055) /* kernel swaps around -?/= */ | |
| c ^= 020; /* pre-swap!! */ | |
| } | |
| g2kb_buf = c; | |
| g2kb_set_done (); | |
| } | |
| } /* connected */ | |
| else { | |
| /* not connected; next connection sees entire "screen" */ | |
| g2out_stuffcr = 0; | |
| } | |
| return SCPE_OK; | |
| } | |
| /* Interrupt handling routines */ | |
| t_bool g2kb_test_done () | |
| { | |
| return g2kb_done != 0; | |
| } | |
| void g2kb_set_done () | |
| { | |
| g2kb_done = 1; | |
| SET_INT (G2); | |
| return; | |
| } | |
| void g2kb_clr_done () | |
| { | |
| g2kb_done = 0; | |
| CLR_INT (G2); | |
| return; | |
| } | |
| t_bool g2bb_test_flag () | |
| { | |
| return g2bb_flag != 0; | |
| } | |
| void g2bb_set_flag () | |
| { | |
| g2bb_flag = 1; | |
| SET_INT (G2); | |
| return; | |
| } | |
| void g2bb_clr_flag () | |
| { | |
| g2bb_flag = 0; | |
| CLR_INT (G2); | |
| return; | |
| } | |
| /**************************************************************** | |
| * SIMH G2OUT (Display Output) DEVICE routines | |
| */ | |
| /* helper to put 7-bit display character */ | |
| static void g2pc(char c) { | |
| //if (debug) putchar(c); | |
| tmxr_putc_ln (&g2_ldsc, c); | |
| } | |
| /* send a character from the display. adds CR after LF */ | |
| /* returns 1 if "c" was sent; 0 means try again later */ | |
| static int g2out_putchar(char c) | |
| { | |
| if (!g2_ldsc.conn || !g2_ldsc.xmte) /* connected, tx enabled? */ | |
| return 0; | |
| if (g2out_stuffcr) { /* need to stuff a CR? */ | |
| g2pc ('\r'); | |
| g2out_stuffcr = 0; | |
| if (!g2_ldsc.xmte) /* full? */ | |
| return 0; /* yes: wait until next time */ | |
| } | |
| g2pc (c); | |
| if (c == '\n') { /* was it a NL? */ | |
| if (g2_ldsc.xmte) /* transmitter enabled? */ | |
| g2pc ('\r'); /* send CR now */ | |
| else | |
| g2out_stuffcr = 1; /* wait until next time */ | |
| } | |
| return 1; | |
| } | |
| /* Device 05 IOT routine */ | |
| int32 g2d1_iot (int32 dev, int32 pulse, int32 dat) | |
| { | |
| /* | |
| * UNIX text display command lists always end with a TRAP | |
| * and display output is restarted periodicly in timer PI service code | |
| */ | |
| if (g2_ldsc.conn && g2_ldsc.xmte && pulse == 047) { /* conn&ready, "beg" */ | |
| g2out_addr = dat & 017777; | |
| g2out_process_display_list(); | |
| g2out_send_new(); | |
| g2out_which ^= 1; /* swap buffers */ | |
| } /* beg IOT */ | |
| return dat; | |
| } | |
| /**************** | |
| * display buffer management/process | |
| * we're informed when UNIX wants to clear the screen (PB7 lit) | |
| * we then press the button | |
| * UNIX does a "cpb" to ACK/clear the interrupt. | |
| * | |
| * *BUT* UNIX clears the screen when a FF (014) char is output, | |
| * which just resets the buffer (and not issuing any IOTs) | |
| */ | |
| static void g2out_clear() { | |
| g2out_stuffcr = 0; | |
| g2out_which = 0; | |
| g2out_count = 0; | |
| g2out_dspbufs[0].count = g2out_dspbufs[1].count = 0; | |
| } | |
| /* interpret display list; save characters into "new" dspbuf | |
| * quits early if display list doesn't conform to what's expected | |
| */ | |
| static void g2out_process_display_list() { | |
| uint32 i; | |
| struct dspbuf *dp = g2out_dspbufs + NEW; | |
| dp->count = 0; | |
| for (i = g2out_addr; i < 020000; i++) { | |
| uint32 w = M[i] & 0777777; | |
| int offset = i - g2out_addr; | |
| char c; | |
| if (w & 0400000) /* TRAP (end of display list) */ | |
| return; | |
| /* check first three words for expected setup commands */ | |
| if (offset < sizeof(g2_expect)/sizeof(g2_expect[0])) { | |
| if (w != g2_expect[offset]) | |
| return; | |
| continue; | |
| } | |
| if (w & 0300000) /* not characters? */ | |
| return; | |
| c = (w >> 7) & 0177; | |
| if (c) | |
| dp->buffer[dp->count++] = c; | |
| c = w & 0177; | |
| if (c) | |
| dp->buffer[dp->count++] = c; | |
| } | |
| } | |
| /* figure out what to send on TELNET connection | |
| * truncates new->count to the number sent so far | |
| * returns number of new characters sent | |
| */ | |
| static int g2out_send_new() { | |
| struct dspbuf *old = g2out_dspbufs + OLD; | |
| struct dspbuf *new = g2out_dspbufs + NEW; | |
| char *cp = new->buffer; | |
| int cur = 0; | |
| int start; | |
| /* nothing in newest refresh? | |
| * COULD have had undisplayed stuff on last screen before it was cleared?? | |
| * would need to have a transmit queue?? | |
| */ | |
| if (new->count == 0) | |
| return 0; | |
| if (old->count && /* have old chars */ | |
| memcmp(old->buffer, new->buffer, old->count) == 0) { /* and a prefix */ | |
| cur = old->count; | |
| cp += cur; | |
| } | |
| /* loop for chars while connected & tx enabled */ | |
| start = cur; | |
| while (cur < new->count && g2_ldsc.conn && g2_ldsc.xmte) { | |
| if (g2out_putchar(*cp)) { | |
| cp++; | |
| cur++; | |
| } | |
| } | |
| new->count = cur; /* only remember what's been sent */ | |
| return cur - start; /* remember number sent */ | |
| } | |
| /**************************************************************** | |
| * subsystem common routines (used by both G2IN and G2OUT SIMH DEVICEs) | |
| */ | |
| /* Reset routine */ | |
| t_stat g2_reset (DEVICE *dptr) | |
| { | |
| if (dptr->flags & DEV_DIS) { /* sync enables */ | |
| g2in_dev.flags = g2in_dev.flags | DEV_DIS; | |
| g2out_dev.flags = g2out_dev.flags | DEV_DIS; | |
| } | |
| else { | |
| g2in_dev.flags = g2in_dev.flags & ~DEV_DIS; | |
| g2out_dev.flags = g2out_dev.flags & ~DEV_DIS; | |
| } | |
| if (g2in_unit.flags & UNIT_ATT) /* if attached, */ | |
| sim_activate (&g2in_unit, tmxr_poll); /* activate */ | |
| else sim_cancel (&g2in_unit); /* else stop */ | |
| g2kb_buf = 0; /* clear buf */ | |
| g2kb_clr_done (); /* clear done */ | |
| g2bb_bbuf = 0; /* clear buttons */ | |
| g2bb_lbuf = 0; /* clear lights */ | |
| g2bb_clr_flag (); | |
| g2out_addr = 0; | |
| g2out_clear(); | |
| sim_cancel (&g2out_unit); /* stop poll */ | |
| return SCPE_OK; | |
| } | |
| /* Attach master unit */ | |
| t_stat g2_attach (UNIT *uptr, char *cptr) | |
| { | |
| t_stat r; | |
| r = tmxr_attach (&g2_desc, uptr, cptr); /* attach */ | |
| if (r != SCPE_OK) /* error */ | |
| return r; | |
| sim_activate (uptr, 0); /* start poll at once */ | |
| return SCPE_OK; | |
| } | |
| /* Detach master unit */ | |
| t_stat g2_detach (UNIT *uptr) | |
| { | |
| t_stat r = tmxr_detach (&g2_desc, uptr); /* detach */ | |
| sim_cancel (uptr); /* stop poll */ | |
| g2_ldsc.rcve = 0; | |
| return r; | |
| } | |
| #endif |