/**
* @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