/** * @fileoverview Implements the PC8080 Keyboard component. * @author Jeff Parsons * @copyright © Jeff Parsons 2012-2016 * * This file is part of PCjs, a computer emulation software project at . * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every modified copy of this work * and to display that copyright notice when the software starts running; see COPYRIGHT in * . * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ "use strict"; if (NODE) { var str = require("../../shared/lib/strlib"); var web = require("../../shared/lib/weblib"); var Component = require("../../shared/lib/component"); var Keys = require("../../shared/lib/keys"); var PC8080 = require("./defines"); var ChipSet8080 = require("./chipset"); var Messages8080= require("./messages"); } /** * Keyboard8080(parmsKbd) * * The Keyboard8080 component has the following component-specific (parmsKbd) properties: * * model: eg, "VT100" (should be a member of Keyboard8080.MODELS) * * @constructor * @extends Component * @param {Object} parmsKbd */ function Keyboard8080(parmsKbd) { Component.call(this, "Keyboard", parmsKbd, Keyboard8080, Messages8080.KEYBOARD); var model = parmsKbd['model']; if (model && !Keyboard8080.MODELS[model]) { Component.notice("Unrecognized Keyboard8080 model: " + model); } this.config = Keyboard8080.MODELS[model] || {}; this.reset(); this.setReady(); } Component.subclass(Keyboard8080); Keyboard8080.MINPRESSTIME = 100; // 100ms /** * Alternate keyCode mappings to support popular "WASD"-style directional-key mappings. * * TODO: ES6 computed property name support may now be in all mainstream browsers, allowing us to use * a simple object literal for this and all other object initializations. */ Keyboard8080.WASDCODES = {}; Keyboard8080.WASDCODES[Keys.ASCII.A] = Keys.KEYCODE.LEFT; Keyboard8080.WASDCODES[Keys.ASCII.D] = Keys.KEYCODE.RIGHT; Keyboard8080.WASDCODES[Keys.ASCII.L] = Keys.KEYCODE.SPACE; /* * Supported configurations */ Keyboard8080.SI1978 = { MODEL: 1978.1, KEYMAP: {}, ALTCODES: Keyboard8080.WASDCODES, LEDCODES: {}, SOFTCODES: { '1p': Keys.KEYCODE.ONE, '2p': Keys.KEYCODE.TWO, 'coin': Keys.KEYCODE.THREE, 'left': Keys.KEYCODE.LEFT, 'right': Keys.KEYCODE.RIGHT, 'fire': Keys.KEYCODE.SPACE } }; Keyboard8080.VT100 = { MODEL: 100.0, KEYMAP: {}, ALTCODES: {}, LEDCODES: {}, SOFTCODES: { 'setup': Keys.KEYCODE.F9 }, /* * Reading port 0x82 returns a key address from the VT100 keyboard's UART data output. * * Every time a keyboard scan is initiated (by setting the START bit of the status byte), * our internal address index (iKeyNext) is set to zero, and an interrupt is generated for * each entry in the aKeysActive array, along with a final interrupt for KEYLAST. */ ADDRESS: { PORT: 0x82, INIT: 0x7F }, /* * Writing port 0x82 updates the VT100's keyboard status byte via the keyboard's UART data input. */ STATUS: { PORT: 0x82, // write-only LED4: 0x01, LED3: 0x02, LED2: 0x04, LED1: 0x08, LOCKED: 0x10, LOCAL: 0x20, LEDS: 0x3F, // all LEDs START: 0x40, // set to initiate a scan /* * From p. 4-38 of the VT100 Technical Manual (July 1982): * * A bit (CLICK) in the keyboard status word controls the bell.... When a single status word contains * the bell bit, flip-flop E3 toggles and turns on E1, generating a click. If the bell bit is set for * many words in succession, the UART latch holds the data output constant..., allowing the circuit to * produce an 800 hertz tone. Bell is generated by setting the bell bit for 0.25 seconds. Each cycle of * the tone is at a reduced amplitude compared with the single keyclick.... The overall effect of the * tone burst on the ear is that of a beep. */ CLICK: 0x80, INIT: 0x00 }, KEYLAST: 0x7F // special end-of-scan key address (all valid key addresses are < KEYLAST) }; /* * Table to map host key codes to VT100 key addresses (ie, unique 7-bit values representing key positions on the VT100) */ Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.DEL] = 0x03; Keyboard8080.VT100.KEYMAP[Keys.ASCII.P] = 0x05; Keyboard8080.VT100.KEYMAP[Keys.ASCII.O] = 0x06; Keyboard8080.VT100.KEYMAP[Keys.ASCII.Y] = 0x07; Keyboard8080.VT100.KEYMAP[Keys.ASCII.T] = 0x08; Keyboard8080.VT100.KEYMAP[Keys.ASCII.W] = 0x09; Keyboard8080.VT100.KEYMAP[Keys.ASCII.Q] = 0x0A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.RIGHT] = 0x10; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.RBRACK] = 0x14; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.LBRACK] = 0x15; Keyboard8080.VT100.KEYMAP[Keys.ASCII.I] = 0x16; Keyboard8080.VT100.KEYMAP[Keys.ASCII.U] = 0x17; Keyboard8080.VT100.KEYMAP[Keys.ASCII.R] = 0x18; Keyboard8080.VT100.KEYMAP[Keys.ASCII.E] = 0x19; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.ONE] = 0x1A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.LEFT] = 0x20; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.DOWN] = 0x22; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F6] = 0x23; // aka BREAK Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.PAUSE] = 0x23; // aka BREAK Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.BQUOTE] = 0x24; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.DASH] = 0x25; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NINE] = 0x26; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.SEVEN] = 0x27; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.FOUR] = 0x28; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.THREE] = 0x29; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.ESC] = 0x2A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.UP] = 0x30; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F3] = 0x31; // aka PF3 Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F1] = 0x32; // aka PF1 Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.BS] = 0x33; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.EQUALS] = 0x34; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.ZERO] = 0x35; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.EIGHT] = 0x36; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.SIX] = 0x37; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.FIVE] = 0x38; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.TWO] = 0x39; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.TAB] = 0x3A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_7] = 0x40; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F4] = 0x41; // aka PF4 Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F2] = 0x42; // aka PF2 Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_0] = 0x43; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F7] = 0x44; // aka LINE FEED Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.BSLASH] = 0x45; Keyboard8080.VT100.KEYMAP[Keys.ASCII.L] = 0x46; Keyboard8080.VT100.KEYMAP[Keys.ASCII.K] = 0x47; Keyboard8080.VT100.KEYMAP[Keys.ASCII.G] = 0x48; Keyboard8080.VT100.KEYMAP[Keys.ASCII.F] = 0x49; Keyboard8080.VT100.KEYMAP[Keys.ASCII.A] = 0x4A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_8] = 0x50; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_CR] = 0x51; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_2] = 0x52; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_1] = 0x53; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.QUOTE] = 0x55; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.SEMI] = 0x56; Keyboard8080.VT100.KEYMAP[Keys.ASCII.J] = 0x57; Keyboard8080.VT100.KEYMAP[Keys.ASCII.H] = 0x58; Keyboard8080.VT100.KEYMAP[Keys.ASCII.D] = 0x59; Keyboard8080.VT100.KEYMAP[Keys.ASCII.S] = 0x5A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_DEL] = 0x60; // keypad period Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F5] = 0x61; // aka KEYPAD COMMA Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_5] = 0x62; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_4] = 0x63; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.CR] = 0x64; // TODO: Figure out why the Technical Manual lists CR at both 0x04 and 0x64 Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.PERIOD] = 0x65; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.COMMA] = 0x66; Keyboard8080.VT100.KEYMAP[Keys.ASCII.N] = 0x67; Keyboard8080.VT100.KEYMAP[Keys.ASCII.B] = 0x68; Keyboard8080.VT100.KEYMAP[Keys.ASCII.X] = 0x69; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F8] = 0x6A; // aka NO SCROLL Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_9] = 0x70; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_3] = 0x71; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_6] = 0x72; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.NUM_SUB] = 0x73; // aka KEYPAD MINUS Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.SLASH] = 0x75; Keyboard8080.VT100.KEYMAP[Keys.ASCII.M] = 0x76; Keyboard8080.VT100.KEYMAP[Keys.ASCII[' ']] = 0x77; Keyboard8080.VT100.KEYMAP[Keys.ASCII.V] = 0x78; Keyboard8080.VT100.KEYMAP[Keys.ASCII.C] = 0x79; Keyboard8080.VT100.KEYMAP[Keys.ASCII.Z] = 0x7A; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.F9] = 0x7B; // aka SET-UP Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.CTRL] = 0x7C; Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.SHIFT] = 0x7D; // either shift key (doesn't matter) Keyboard8080.VT100.KEYMAP[Keys.KEYCODE.CAPS_LOCK] = 0x7E; Keyboard8080.VT100.LEDCODES = { 'l4': Keyboard8080.VT100.STATUS.LED4, 'l3': Keyboard8080.VT100.STATUS.LED3, 'l2': Keyboard8080.VT100.STATUS.LED2, 'l1': Keyboard8080.VT100.STATUS.LED1, 'locked': Keyboard8080.VT100.STATUS.LOCKED, 'local': Keyboard8080.VT100.STATUS.LOCAL, 'online': ~Keyboard8080.VT100.STATUS.LOCAL }; /* * Supported models and their configurations */ Keyboard8080.MODELS = { "SI1978": Keyboard8080.SI1978, "VT100": Keyboard8080.VT100 }; /** * setBinding(sHTMLType, sBinding, control, sValue) * * @this {Keyboard8080} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ Keyboard8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) { /* * There's a special binding that the Video component uses ("kbd") to effectively bind its * screen to the entire keyboard, in Video.powerUp(); ie: * * video.kbd.setBinding("canvas", "kbd", video.canvasScreen); * or: * video.kbd.setBinding("textarea", "kbd", video.textareaScreen); * * However, it's also possible for the keyboard XML definition to define a control that serves * a similar purpose; eg: * * Kbd * * The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on * certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will * still be enabled (there's currently no way to configure the Video component to not bind its screen, * but we could certainly add one if the need ever arose). */ var kbd = this; var id = sHTMLType + '-' + sBinding; if (this.bindings[id] === undefined) { if (sHTMLType == "led" && this.config.LEDCODES[sBinding]) { this.bindings[id] = control; return true; } switch (sBinding) { case "kbd": /* * Recording the binding ID prevents multiple controls (or components) from attempting to erroneously * bind a control to the same ID, but in the case of a "dual display" configuration, we actually want * to allow BOTH video components to call setBinding() for "kbd", so that it doesn't matter which * display the user gives focus to. * * this.bindings[id] = control; */ control.onkeydown = function onKeyDown(event) { return kbd.onKeyDown(event, true); }; control.onkeyup = function onKeyUp(event) { return kbd.onKeyDown(event, false); }; return true; default: if (this.config.SOFTCODES && this.config.SOFTCODES[sBinding] !== undefined) { this.bindings[id] = control; var fnDown = function(kbd, softCode) { return function onKeyboardBindingDown(event) { /* * iOS Usability Improvement: Calling preventDefault() prevents rapid clicks from * also being (mis)interpreted as a desire to "zoom" in on the machine. */ event.preventDefault(); kbd.onSoftKeyDown(softCode, true); /* * I'm assuming we only need to give focus back on the "up" event... * * if (kbd.cmp) kbd.cmp.updateFocus(); */ }; }(this, this.config.SOFTCODES[sBinding]); var fnUp = function (kbd, softCode) { return function onKeyboardBindingUp(event) { kbd.onSoftKeyDown(softCode, false); /* * Give focus back to the machine (since clicking the button takes focus away). * * if (kbd.cmp) kbd.cmp.updateFocus(); * * iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down * (assuming it was already down). */ }; }(this, this.config.SOFTCODES[sBinding]); if ('ontouchstart' in window) { control.ontouchstart = fnDown; control.ontouchend = fnUp; } else { control.onmousedown = fnDown; control.onmouseup = control.onmouseout = fnUp; } return true; } break; } } return false; }; /** * initBus(cmp, bus, cpu, dbg) * * @this {Keyboard8080} * @param {Computer8080} cmp * @param {Bus8080} bus * @param {CPUState8080} cpu * @param {Debugger8080} dbg */ Keyboard8080.prototype.initBus = function(cmp, bus, cpu, dbg) { this.cmp = cmp; this.cpu = cpu; this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work this.chipset = /** @type {ChipSet8080} */ (cmp.getMachineComponent("ChipSet")); bus.addPortInputTable(this, this.config.portsInput); bus.addPortOutputTable(this, this.config.portsOutput); }; /** * powerUp(data, fRepower) * * @this {Keyboard8080} * @param {Object|null} data * @param {boolean} [fRepower] * @return {boolean} true if successful, false if failure */ Keyboard8080.prototype.powerUp = function(data, fRepower) { if (!fRepower) { if (!data) { this.reset(); } else { if (!this.restore(data)) return false; } } return true; }; /** * powerDown(fSave, fShutdown) * * @this {Keyboard8080} * @param {boolean} [fSave] * @param {boolean} [fShutdown] * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure */ Keyboard8080.prototype.powerDown = function(fSave, fShutdown) { return fSave? this.save() : true; }; Keyboard8080.VT100.INIT = [ [ Keyboard8080.VT100.STATUS.INIT, // bVT100Status Keyboard8080.VT100.ADDRESS.INIT, // bVT100Address false, // fVT100UARTBusy 0, // nVT100UARTCycleSnap -1 // iKeyNext ] ]; /** * reset() * * @this {Keyboard8080} */ Keyboard8080.prototype.reset = function() { /* * As keyDown events are encountered, a corresponding "softCode" is looked up. If one is found, * then an entry for the key is added to the aKeysActive array. Each "key" entry in aKeysActive contains: * * softCode: number or string representing the key pressed * msDown: timestamp of the most recent "down" event * fAutoRelease: true to auto-release the key after MINPRESSTIME (set when "up" occurs too quickly) * * When the key is finally released (or auto-released), its entry is removed from the array. */ this.aKeysActive = []; if (this.config.INIT && !this.restore(this.config.INIT)) { this.notice("reset error"); } }; /** * save() * * This implements save support for the Keyboard component. * * @this {Keyboard8080} * @return {Object} */ Keyboard8080.prototype.save = function() { var state = new State(this); switch(this.config.MODEL) { case Keyboard8080.SI1978.MODEL: break; case Keyboard8080.VT100.MODEL: state.set(0, [this.bVT100Status, this.bVT100Address, this.fVT100UARTBusy, this.nVT100UARTCycleSnap, -1]); break; } return state.data(); }; /** * restore(data) * * This implements restore support for the Keyboard component. * * @this {Keyboard8080} * @param {Object} data * @return {boolean} true if successful, false if failure */ Keyboard8080.prototype.restore = function(data) { var a; if (data && (a = data[0]) && a.length) { switch(this.config.MODEL) { case Keyboard8080.SI1978.MODEL: return true; case Keyboard8080.VT100.MODEL: this.bVT100Status = a[0]; this.updateLEDs(this.bVT100Status & Keyboard8080.VT100.STATUS.LEDS); this.bVT100Address = a[1]; this.fVT100UARTBusy = a[2]; this.nVT100UARTCycleSnap = a[3]; this.iKeyNext = a[4]; return true; } } return false; }; /** * setLED(control, f) * * @this {Keyboard8080} * @param {Object} control is an HTML control DOM object * @param {boolean} f is true if the LED represented by control should be "on", false if "off" */ Keyboard8080.prototype.setLED = function(control, f) { /* * TODO: Add support for user-definable LED colors */ control.style.backgroundColor = (f? "#ff0000" : "#000000"); }; /** * updateLEDs(bLEDs) * * @this {Keyboard8080} * @param {number} bLEDs */ Keyboard8080.prototype.updateLEDs = function(bLEDs) { this.bLEDs = bLEDs; for (var sBinding in this.config.LEDCODES) { var id = "led-" + sBinding; var control = this.bindings[id]; if (control) { var bitLED = this.config.LEDCODES[sBinding]; var fOn = !!(bLEDs & bitLED); if (bitLED & (bitLED-1)) { fOn = !(bLEDs & ~bitLED); } this.setLED(control, fOn); } } }; /** * getSoftCode(keyCode) * * Returns a number if the keyCode exists in the KEYMAP, or a string if the keyCode has a soft-code string. * * @this {Keyboard8080} * @return {string|number|null} */ Keyboard8080.prototype.getSoftCode = function(keyCode) { keyCode = this.config.ALTCODES[keyCode] || keyCode; if (this.config.KEYMAP[keyCode]) { return keyCode; } for (var sSoftCode in this.config.SOFTCODES) { if (this.config.SOFTCODES[sSoftCode] === keyCode) { return sSoftCode; } } return null; }; /** * onKeyDown(event, fDown) * * @this {Keyboard8080} * @param {Object} event * @param {boolean} fDown is true for a keyDown event, false for up * @return {boolean} true to pass the event along, false to consume it */ Keyboard8080.prototype.onKeyDown = function(event, fDown) { var fPass = true; var keyCode = event.keyCode; var softCode = this.getSoftCode(keyCode); if (softCode) { fPass = this.onSoftKeyDown(softCode, fDown); event.preventDefault(); } if (!COMPILED && this.messageEnabled(Messages8080.KEYS)) { this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): softCode=" + softCode + ", pass=" + (fPass? "true" : "false"), true); } return fPass; }; /** * indexOfSoftKey(softCode) * * @this {Keyboard8080} * @param {number|string} softCode * @return {number} index of softCode in aKeysActive, or -1 if not found */ Keyboard8080.prototype.indexOfSoftKey = function(softCode) { var i; for (i = 0; i < this.aKeysActive.length; i++) { if (this.aKeysActive[i].softCode == softCode) return i; } return -1; }; /** * onSoftKeyDown(softCode, fDown) * * @this {Keyboard8080} * @param {number|string} softCode * @param {boolean} fDown is true for a down event, false for up * @return {boolean} true to pass the event along, false to consume it */ Keyboard8080.prototype.onSoftKeyDown = function(softCode, fDown) { var i = this.indexOfSoftKey(softCode); if (fDown) { // this.println(softCode + " down"); if (i < 0) { this.aKeysActive.push({ softCode: softCode, msDown: Date.now(), fAutoRelease: false }); } else { this.aKeysActive[i].msDown = Date.now(); this.aKeysActive[i].fAutoRelease = false; } } else if (i >= 0) { // this.println(softCode + " up"); if (!this.aKeysActive[i].fAutoRelease) { var msDown = this.aKeysActive[i].msDown; if (msDown) { var msElapsed = Date.now() - msDown; if (msElapsed < Keyboard8080.MINPRESSTIME) { // this.println(softCode + " released after only " + msElapsed + "ms"); this.aKeysActive[i].fAutoRelease = true; this.checkSoftKeysToRelease(); return true; } } } this.aKeysActive.splice(i, 1); } else { // this.println(softCode + " up with no down?"); } if (this.chipset) { var bit = 0; switch(softCode) { case '1p': bit = ChipSet8080.SI1978.STATUS1.P1; break; case '2p': bit = ChipSet8080.SI1978.STATUS1.P2; break; case 'coin': bit = ChipSet8080.SI1978.STATUS1.CREDIT; break; case 'left': bit = ChipSet8080.SI1978.STATUS1.P1_LEFT; break; case 'right': bit = ChipSet8080.SI1978.STATUS1.P1_RIGHT; break; case 'fire': bit = ChipSet8080.SI1978.STATUS1.P1_FIRE; break; } if (bit) { this.chipset.updateStatus1(bit, fDown); } } return true; }; /** * checkSoftKeysToRelease() * * @this {Keyboard8080} */ Keyboard8080.prototype.checkSoftKeysToRelease = function() { var i = 0; var msDelayMin = -1; while (i < this.aKeysActive.length) { if (this.aKeysActive[i].fAutoRelease) { var softCode = this.aKeysActive[i].softCode; var msDown = this.aKeysActive[i].msDown; var msElapsed = Date.now() - msDown; var msDelay = Keyboard8080.MINPRESSTIME - msElapsed; if (msDelay > 0) { if (msDelayMin < 0 || msDelayMin > msDelay) { msDelayMin = msDelay; } } else { /* * Because the key is already in the auto-release state, this next call guarantees that the * key will be removed from the array; a consequence of that removal, however, is that we must * reset our array index to zero. */ this.onSoftKeyDown(softCode, false); i = 0; continue; } } i++; } if (msDelayMin >= 0) { var kbd = this; setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin); } }; /** * isVT100TransmitterReady() * * Called whenever the VT100 ChipSet circuit needs the Keyboard UART's transmitter status. * * From p. 4-32 of the VT100 Technical Manual (July 1982): * * The operating clock for the keyboard interface comes from an address line in the video processor (LBA4). * This signal has an average period of 7.945 microseconds. Each data byte is transmitted with one start bit * and one stop bit, and each bit lasts 16 clock periods. The total time for each data byte is 160 times 7.945 * or 1.27 milliseconds. Each time the Transmit Buffer Empty flag on the terminal's UART gets set (when the * current byte is being transmitted), the microprocessor loads another byte into the transmit buffer. In this * way, the stream of status bytes to the keyboard is continuous. * * We used to always return true (after all, what's wrong with an infinitely fast UART?), but unfortunately, * the VT100 firmware relies on the UART's slow transmission speed to drive cursor blink rate. We have several * options: * * 1) Snapshot the CPU cycle count each time a byte is transmitted (see outVT100UARTStatus()) and then every * time this is polled, see if the cycle count has exceeded the snapshot value by the necessary threshold; * if we assume 361.69ns per CPU cycle, there are 22 CPU cycles for every 1 LBA4 cycle, and since transmission * time is supposed to last for 160 LBA4 cycles, the threshold is 22*160 CPU cycles, or 3520 cycles. * * 2) Set a CPU timer using the new setTimer() interface, which can be passed the number of milliseconds to * wait before firing (in this case, roughly 1.27ms). * * 3) Call the ChipSet's getVT100LBA(4) function for the state of the simulated LBA4, and count 160 LBA4 * transitions; however, that would be the worst solution, because there's no guarantee that the firmware's * UART polling will occur regularly and/or frequently enough for us to catch every LBA4 transition. * * I'm going with solution #1 because it's less overhead. * * @this {Keyboard8080} * @return {boolean} (true if ready, false if not) */ Keyboard8080.prototype.isVT100TransmitterReady = function() { if (this.fVT100UARTBusy) { /* * NOTE: getMSCycles(1.2731488) should work out to 3520 cycles for a CPU clocked at 361.69ns per cycle, * which is roughly 2.76Mhz. We could just hard-code 3520 instead of calling getMSCycles(), but this helps * maintain a reasonable blink rate for the cursor even when the user cranks up the CPU speed. */ if (this.cpu.getCycles() >= this.nVT100UARTCycleSnap + this.cpu.getMSCycles(1.2731488)) { this.fVT100UARTBusy = false; } } return !this.fVT100UARTBusy; }; /** * inVT100UARTAddress(port, addrFrom) * * We take our cue from iKeyNext. If it's -1 (default), we simply return the last value latched * in bVT100Address. Otherwise, if iKeyNext is a valid index into aKeysActive, we look up the key * in the VT100.KEYMAP, latch it, and increment iKeyNext. Failing that, we latch Keyboard8080.VT100.KEYLAST * and reset iKeyNext to -1. * * @this {Keyboard8080} * @param {number} port (0x82) * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) * @return {number} simulated port value */ Keyboard8080.prototype.inVT100UARTAddress = function(port, addrFrom) { var b = this.bVT100Address; if (this.iKeyNext >= 0) { if (this.iKeyNext < this.aKeysActive.length) { var key = this.aKeysActive[this.iKeyNext]; if (!MAXDEBUG) { this.iKeyNext++; } else { /* * In MAXDEBUG builds, this code removes the key as soon as it's been reported, because * when debugging, it's easy for the window to lose focus and never receive the keyUp event, * thereby leaving us with a stuck key. However, this may cause more problems than it solves, * because the VT100's ROM seems to require that key presses persist for more than a single poll. */ this.aKeysActive.splice(this.iKeyNext, 1); } b = Keyboard8080.VT100.KEYMAP[key.softCode]; if (b & 0x80) { /* * TODO: This code is supposed to be accompanied by a SHIFT key; make sure that it is. */ b &= 0x7F; } } else { this.iKeyNext = -1; b = Keyboard8080.VT100.KEYLAST; } this.bVT100Address = b; this.cpu.requestINTR(1); } this.printMessageIO(port, null, addrFrom, "KBDUART.ADDRESS", b); return b; }; /** * outVT100UARTStatus(port, b, addrFrom) * * @this {Keyboard8080} * @param {number} port (0x82) * @param {number} b * @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port) */ Keyboard8080.prototype.outVT100UARTStatus = function(port, b, addrFrom) { this.printMessageIO(port, b, addrFrom, "KBDUART.STATUS"); this.bVT100Status = b; this.fVT100UARTBusy = true; this.nVT100UARTCycleSnap = this.cpu.getCycles(); this.updateLEDs(b & Keyboard8080.VT100.STATUS.LEDS); if (b & Keyboard8080.VT100.STATUS.START) { this.iKeyNext = 0; this.cpu.requestINTR(1); } }; /* * Port notification tables */ Keyboard8080.VT100.portsInput = { 0x82: Keyboard8080.prototype.inVT100UARTAddress }; Keyboard8080.VT100.portsOutput = { 0x82: Keyboard8080.prototype.outVT100UARTStatus }; /** * Keyboard8080.init() * * This function operates on every HTML element of class "keyboard", extracting the * JSON-encoded parameters for the Keyboard constructor from the element's "data-value" * attribute, invoking the constructor to create a Keyboard component, and then binding * any associated HTML controls to the new component. */ Keyboard8080.init = function() { var aeKbd = Component.getElementsByClass(document, PC8080.APPCLASS, "keyboard"); for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) { var eKbd = aeKbd[iKbd]; var parmsKbd = Component.getComponentParms(eKbd); var kbd = new Keyboard8080(parmsKbd); Component.bindComponentControls(kbd, eKbd, PC8080.APPCLASS); } }; /* * Initialize every Keyboard module on the page. */ web.onInit(Keyboard8080.init); if (NODE) module.exports = Keyboard8080;