/** * @fileoverview Implements the PCx86 SerialPort component. * @author Jeff Parsons * @copyright © Jeff Parsons 2012-2017 * * 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 State = require("../../shared/lib/state"); var PCX86 = require("./defines"); var Messages = require("./messages"); var ChipSet = require("./chipset"); } /** * SerialPort class * * The class property declarations below started as a way of informing the code inspector of the controlIOBuffer * property, which remained undefined until a setBinding() call set it later, but I've since decided that explicitly * initializing such properties in the constructor is a better way to go -- even though it's more code -- because * JavaScript compilers are supposed to be happier when the underlying object structures aren't constantly changing. * * Besides, I'm not sure I want to get into documenting every property this way, for this or any/every other class, * let alone getting into which ones should be considered private or protected, because PCjs isn't really a library * for third-party apps. * * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, which would force us * to declare all class properties in the constructor, as well as prevent us from defining any named properties. * So, for now, we mark all our classes as 'unrestricted'. * * @class SerialPort * @property {number} iAdapter * @property {number} portBase * @property {number} nIRQ * @property {string|null} consoleOutput * @property {HTMLTextAreaElement} controlIOBuffer (DOM element bound to the port for rudimentary output; see transmitByte()) * @unrestricted */ class SerialPort extends Component { /** * SerialPort(parmsSerial) * * The SerialPort component has the following component-specific (parmsSerial) properties: * * adapter: 1 (port 0x3F8) or 2 (port 0x2F8); 0 if not defined * * binding: name of a control (based on its "binding" attribute) to bind to this port's I/O; * as a special case, it can be set to "console" to direct all output to the component's default * println() handler (eg, the Control Panel's "print" control, if any, or console.log() if using * a DEBUG or non-COMPILED machine) * * tabSize: a non-zero number specifies the tab-stop multiple to use for automatic tab-to-space * conversion; it applies only to the above binding, and the default is 0 (no tab conversion) * * charBOL: a non-zero number specifies the ASCII code of a character to display at the beginning * of every line; it applies only to the above binding, and the default is 0 (no BOL character) * * In the future, we may support 'port' and 'irq' properties that allow the machine to define a non-standard * serial port configuration, instead of only our pre-defined 'adapter' configurations. * * NOTE: Since the XSL file defines 'adapter' as a number, not a string, there's no need to use parseInt(), * and as an added benefit, we don't need to worry about whether a hex or decimal format was used. * * This hard-coded approach mimics the original IBM PC Asynchronous Adapter configuration, which contained a * pair of "shunt modules" that allowed the user to select a port address of either 0x3F8 ("Primary") or 0x2F8 * ("Secondary"). * * DOS typically names the Primary adapter "COM1" and the Secondary adapter "COM2", but I prefer to stick to * adapter numbers, since not all operating systems follow those naming conventions. * * @this {SerialPort} * @param {Object} parmsSerial */ constructor(parmsSerial) { super("SerialPort", parmsSerial, Messages.SERIAL); this.iAdapter = parmsSerial['adapter']; switch (this.iAdapter) { case 1: this.portBase = 0x3F8; this.nIRQ = ChipSet.IRQ.COM1; break; case 2: this.portBase = 0x2F8; this.nIRQ = ChipSet.IRQ.COM2; break; default: Component.warning("Unrecognized serial adapter #" + this.iAdapter); return; } /** * consoleOutput becomes a string that records serial port output if the 'binding' property is set to the * reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output * or the string length reaches a threshold (currently, 1024 characters). */ this.consoleOutput = null; /** * controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()). * * Example: CTTY COM2 * * The CTTY DOS command redirects all CON I/O to the specified serial port (eg, COM2), which it assumes is * connected to a serial terminal, and therefore anything it *transmits* via COM2 will be displayed by the * terminal. It further assumes that anything typed on such a terminal is NOT displayed, so as DOS *receives* * serial input, DOS *transmits* the appropriate characters back to the terminal via COM2. * * As a result, controlIOBuffer only needs to be updated by the transmitByte() function. */ this.controlIOBuffer = null; /* * If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters * being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate * number of spaces. * * Another controlIOBuffer feature is charBOL, which, if nonzero, specifies a character to automatically output * at the beginning of every line. This probably isn't generally useful; I use it internally to preformat serial * output. */ this.tabSize = parmsSerial['tabSize'] || 0; this.charBOL = parmsSerial['charBOL'] || 0; this.charPrev = 0; this.iLogicalCol = 0; this.bMSRInit = SerialPort.MSR.CTS | SerialPort.MSR.DSR; this.fNullModem = true; var sBinding = parmsSerial['binding']; if (sBinding == "console") { this.consoleOutput = ""; } else { /* * NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing. */ Component.bindExternalControl(this, sBinding, SerialPort.sIOBuffer); } /* * No connection until initConnection() is called. */ this.sDataReceived = ""; this.connection = this.sendData = this.updateStatus = null; /* * Export all functions required by initConnection(). */ this['exports'] = { 'connect': this.initConnection, 'receiveData': this.receiveData, 'receiveStatus': this.receiveStatus }; } /** * attachMouse(id, mouse, fnUpdate) * * @this {SerialPort} * @param {string} id * @param {Mouse} mouse * @param {function(number)} fnUpdate * @return {Component|null} */ attachMouse(id, mouse, fnUpdate) { var component = null; if (id == this.idComponent && !this.connection) { this.connection = mouse; this.updateStatus = fnUpdate; this.fNullModem = false; component = this; } return component; } /** * setBinding(sHTMLType, sBinding, control, sValue) * * @this {SerialPort} * @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, "buffer") * @param {HTMLElement} 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 */ setBinding(sHTMLType, sBinding, control, sValue) { var serial = this; switch (sBinding) { case SerialPort.sIOBuffer: this.bindings[sBinding] = this.controlIOBuffer = /** @type {HTMLTextAreaElement} */ (control); /* * By establishing an onkeypress handler here, we make it possible for DOS commands like * "CTTY COM1" to more or less work (use "CTTY CON" to restore control to the DOS console). */ this.controlIOBuffer.onkeydown = function onKeyDown(event) { /* * This is required in addition to onkeypress, because it's the only way to prevent * BACKSPACE (keyCode 8) from being interpreted by the browser as a "Back" operation; * moreover, not all browsers generate an onkeypress notification for BACKSPACE. * * A related problem exists for Ctrl-key combinations in most Windows-based browsers * (eg, IE, Edge, Chrome for Windows, etc), because keys like Ctrl-C and Ctrl-S have * special meanings (eg, Copy, Save). To the extent the browser will allow it, we * attempt to disable that default behavior when this control receives an onkeydown * event for one of those keys (probably the only event the browser generates for them). */ event = event || window.event; var keyCode = event.keyCode; if (keyCode === 0x08 || event.ctrlKey && keyCode >= 0x41 && keyCode <= 0x5A) { if (event.preventDefault) event.preventDefault(); if (keyCode > 0x40) keyCode -= 0x40; serial.receiveData(keyCode); } return true; }; this.controlIOBuffer.onkeypress = function onKeyPress(event) { /* * Browser-independent keyCode extraction; refer to onKeyPress() and the other key event * handlers in keyboard.js. */ event = event || window.event; var keyCode = event.which || event.keyCode; serial.receiveData(keyCode); /* * Since we're going to remove the "readonly" attribute from the