/**
* @fileoverview Implements the PC8080 SerialPort 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 State = require("../../shared/lib/state");
var PC8080 = require("./defines");
var Messages8080= require("./messages");
}
/**
* SerialPort8080(parmsSerial)
*
* The SerialPort8080 component has the following component-specific (parmsSerial) properties:
*
* adapter: 0 if not defined
*
* binding: name of a control (based on its "binding" attribute) to bind to this port's I/O
*
* tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to
* the above binding); default is 0 (no conversion)
*
* 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.
*
* @constructor
* @extends Component
* @param {Object} parmsSerial
*/
function SerialPort8080(parmsSerial) {
this.iAdapter = +parmsSerial['adapter'];
switch (this.iAdapter) {
case 0:
this.portBase = 0;
this.nIRQ = 2;
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).
*
* @type {string|null}
*/
this.consoleOutput = null;
/**
* controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()).
*
* @type {Object}
*/
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.
*
* charBOL, if nonzero, is 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'];
this.charBOL = parmsSerial['charBOL'];
this.iLogicalCol = 0;
/*
* fAutoXOFF enables some experimental auto-XOFF/XON processing. It assumes if the VT100 firmware
* issues an XOFF, receiveByte() should stop accepting more data until the firmware issues an XOFF.
*
* The downside is that this doesn't really do anything to stem the flow of incoming data; it just
* prevents the VT100's internal buffer from overflowing. TODO: Eliminate the need for this hack
* and add some *real* flow-control interfaces between connected SerialPort components.
*/
this.fAutoXOFF = true;
this.fAutoStop = false;
Component.call(this, "SerialPort", parmsSerial, SerialPort8080, Messages8080.SERIAL);
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, SerialPort8080.sIOBuffer);
}
/*
* No connection until initConnection() is called.
*/
this.sDataReceived = "";
this.connection = this.sendData = null;
/*
* Export all functions required by initConnection(); currently, this is the bare minimum (no flow control yet).
*/
this['exports'] = {
'connect': this.initConnection,
'receiveData': this.receiveData
};
}
/*
* class SerialPort8080
* property {number} iAdapter
* property {number} portBase
* property {number} nIRQ
* property {Object} controlIOBuffer is a DOM element bound to the port (for rudimentary output; see transmitByte())
*
* NOTE: This class declaration 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.
*/
Component.subclass(SerialPort8080);
SerialPort8080.UART8251 = {
/*
* Format of MODE byte written to CONTROL port 0x1
*/
MODE: {
BAUD_FACTOR: 0x03, // 00=SYNC, 01=1x, 10=16x, 11=64x
DATA_BITS: 0x0C, // 00=5, 01=6, 10=7, 11=8
PARITY_ENABLE: 0x10,
EVEN_PARITY: 0x20,
STOP_BITS: 0xC0, // 00=invalid, 01=1, 10=1.5, 11=2
INIT: 0x8E // 16x baud rate, 8 data bits, no parity, 1.5 stop bits
},
/*
* Format of COMMAND byte written to CONTROL port 0x1
*/
COMMAND: {
XMIT_ENABLE: 0x01,
DTR: 0x02, // Data Terminal Ready
RECV_ENABLE: 0x04,
SEND_BREAK: 0x08,
ERROR_RESET: 0x10,
RTS: 0x20, // Request To Send
INTERNAL_RESET: 0x40,
HUNT_MODE: 0x80,
INIT: 0x27 // XMIT_ENABLE | DTR | RECV_ENABLE | RTS
},
/*
* Format of STATUS byte read from CONTROL port 0x1
*/
STATUS: {
XMIT_READY: 0x01,
RECV_FULL: 0x02,
XMIT_EMPTY: 0x04,
PARITY_ERROR: 0x08,
OVERRUN_ERROR: 0x10,
FRAMING_ERROR: 0x20,
BREAK_DETECT: 0x40,
DSR: 0x80, // Data Set Ready
INIT: 0x85 // XMIT_READY | XMIT_EMPTY | DSR
},
/*
* Format of BAUDRATES byte written to port 0x2
*
* Each nibble is an index (0x0-0xF) into a set of internal CPU clock divisors that yield the
* following baud rates:
*
* Index Divisor Baud Rate
* ----- ------- ---------
* 0x0 3456 50
* 0x1 2304 75
* 0x2 1571 110
* 0x3 1285 134.5
* 0x4 1152 150
* 0x5 864 200
* 0x6 576 300
* 0x7 288 600
* 0x8 144 1200
* 0x9 96 1800
* 0xA 86 2000
* 0xB 72 2400
* 0xC 48 3600
* 0xD 36 4800
* 0xE 18 9600 (default)
* 0xF 9 19200
*
* NOTE: This is a VT100-specific port and baud rate table.
*/
BAUDRATES: {
RECV_RATE: 0x0F,
XMIT_RATE: 0xF0,
INIT: 0xEE // default to 9600 (0xE) for both XMIT and RECV
},
BAUDTABLE: [
50, 75, 110, 134.5, 150, 200, 300, 600, 1200, 1800, 2000, 2400, 3600, 4800, 9600, 19200
]
};
SerialPort8080.UART8251.INIT = [
false,
0,
0,
SerialPort8080.UART8251.STATUS.INIT,
SerialPort8080.UART8251.MODE.INIT,
SerialPort8080.UART8251.COMMAND.INIT,
SerialPort8080.UART8251.BAUDRATES.INIT
];
/*
* Internal name used for the I/O buffer control, if any, that we bind to a SerialPort8080.
*
* Alternatively, if SerialPort8080 wants to use another component's control (eg, the Panel's
* "print" control), it can specify the name of that control with the 'binding' property.
*
* For that binding to succeed, we also need to know the target component; for now, that's
* been hard-coded to "Panel", in part because that's one of the few components we can rely
* upon initializing before we do, but it would be a simple matter to include a component type
* or ID as part of the 'binding' property as well, if we need more flexibility later.
*/
SerialPort8080.sIOBuffer = "buffer";
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {SerialPort8080}
* @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 {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
*/
SerialPort8080.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
{
var serial = this;
switch (sBinding) {
case SerialPort8080.sIOBuffer:
this.bindings[sBinding] = this.controlIOBuffer = 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).
*/
control.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.receiveByte(keyCode);
}
return true;
};
control.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.receiveByte(keyCode);
/*
* Since we're going to remove the "readonly" attribute from the