1
0
mirror of https://github.com/pkimpel/retro-220.git synced 2026-01-11 23:52:46 +00:00
pkimpel.retro-220/webUI/B220PaperTapeReader.js
Paul Kimpel bca1b7881a Release emulator version 1.04.
. Adjust (hopefully for the last time) the RGB code for the 220
Sundland Beige panel color: it's now #C5B1A0. This has been a really
difficult color to pin down.
. Implement Clear Memory button on Console; implement "hover captions"
for the Clear Memory and Load Card buttons.
. Implement current block-number display on magnetic tape panel.
. Implement "word index" counter on the B220MagTapeDrive panel to
indicate the tape position in terms of 11-digit 220 words.
. Implement Rewind and Unload buttons on the paper-tape reader.
. Implement "word index" counter on the B220PaperTapeReader panel.
. When a Cardatron or paper-tape reader encounters a sign=6 control
word, do not reschedule the Processor if it has been halted.
. Correct handling of reload-lockout in B220CardatronInput; change
method of reporting Cardatron end-of-I/O signal to the Processor.
. Correct this.pendingFinish timing race in B220CardatronOutput.
. Correct validation when loading tape images in B220MagTapeDrive so
that the drive will continue to be usable after an invalid image is
detected.
. Correct logic for spacing and searching magnetic tape blocks
backwards.
. Correct output of non-printing characters in B220PaperTapePunch.
. Correct handling of invalid tape image characters in
B220PaperTapeReader.
. Implement additional tracing for paper-tape and magnetic tape I/Os
(currently disabled).
2021-09-06 17:28:48 -07:00

536 lines
22 KiB
JavaScript

/***********************************************************************
* retro-220/webUI B220PaperTapeReader.js
************************************************************************
* Copyright (c) 2017, Paul Kimpel.
* Licensed under the MIT License, see
* http://www.opensource.org/licenses/mit-license.php
************************************************************************
* Burroughs 220 Paper Tape Reader module.
*
* Defines the paper tape input devices.
*
************************************************************************
* 2017-05-27 P.Kimpel
* Original version, from retro-205 D205ConsoleInput.js.
***********************************************************************/
"use strict";
/**************************************/
function B220PaperTapeReader(mnemonic, unitIndex, config) {
/* Constructor for the PaperTapeReader object */
var top = unitIndex*32 + 264;
var left = (unitIndex-1)*32 + 250;
this.config = config; // System configuration object
this.mnemonic = mnemonic; // Unit mnemonic
this.unitIndex = unitIndex; // Unit index into console output units
this.inTimer = 0; // input setCallback() token
this.charPeriod = B220PaperTapeReader.highSpeedPeriod;
// paper-tape reader speed [ms/char]
this.nextCharTime = 0; // next time a character can be read
this.unitMask = 0; // unit selection mask
this.readerRewinding = false; // unit is currently rewinding
this.boundFlipSwitch = B220PaperTapeReader.prototype.flipSwitch.bind(this);
this.boundReadTapeChar = B220PaperTapeReader.prototype.readTapeChar.bind(this);
this.readResult = { // object passed back to Processor for each read
error: false, // ANSI character read from tape
code: 0, // 220 char code sent to processor
signDigit: 0, // copy of first digit read for word
frameCount: 0, // count of tape frames read for word
readChar: this.boundReadTapeChar // callback function for next char
};
this.clear();
// Create the reader window and onload event
this.doc = null;
this.window = null;
this.tapeSupplyBar = null;
this.tapeView = null;
this.wordCountDiv = null;
B220Util.openPopup(window, "../webUI/B220PaperTapeReader.html", mnemonic,
"location=no,scrollbars=no,resizable,width=420,height=160,left=" + left + ",top=" + top,
this, B220PaperTapeReader.prototype.readerOnload);
}
/**************************************/
B220PaperTapeReader.offSwitchImage = "./resources/ToggleDown.png";
B220PaperTapeReader.onSwitchImage = "./resources/ToggleUp.png";
B220PaperTapeReader.lowSpeedPeriod = 1000/500;
// low reader speed @ 500 cps [ms/char]
B220PaperTapeReader.highSpeedPeriod = 1000/1000;
// high reader speed @ 1000 cps [ms/char]
B220PaperTapeReader.startupTime = 5; // reader start-up delay [ms]
B220PaperTapeReader.idleTime = 50; // idle time before reader requires start-up delay [ms]
B220PaperTapeReader.rewindSpeedFactor = 10;
// Rewind speed over read speed factor
B220PaperTapeReader.spinUpdateInterval = 15;
// milliseconds between tape motion updates
// Translate ANSI character codes to B220 charater codes.
// Note that ANSI new-line sequences are used for end-of-word characters,
// so "|" translates to B220 carriage-return (16). To avoid space-expansion
// of tabs, "~" translates to tab (26). "_" translates to the "blank" code (02).
// "^" translates to form-feed (15).
B220PaperTapeReader.xlate220 = [
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x26, 0x35, 0x17, 0x15, 0x16, 0x17, 0x17, // 00-0F
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // 10-1F
0x00, 0x17, 0x36, 0x33, 0x13, 0x24, 0x10, 0x34, 0x24, 0x04, 0x14, 0x10, 0x23, 0x20, 0x03, 0x21, // 20-2F
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x27, 0x13, 0x04, 0x33, 0x05, 0x17, // 30-3F
0x34, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, // 40-4F
0x57, 0x58, 0x59, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x32, 0x06, 0x12, 0x15, 0x02, // 50-5F
0x37, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, // 60-6F
0x57, 0x58, 0x59, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x22, 0x16, 0x25, 0x26, 0x17, // 70-7F
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // 80-8F
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // 90-9F
0x17, 0x17, 0x17, 0x17, 0x04, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // A0-AF
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // B0-BF
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // C0-CF
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // D0-DF
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, // E0-EF
0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17];// F0-FF
/**************************************/
B220PaperTapeReader.prototype.clear = function clear() {
/* Initializes (and if necessary, creates) the reader unit state */
this.ready = false; // a tape has been loaded into the reader
this.busy = false; // a request for a digit is outstanding
this.pendingReceiver = null; // stashed digit-receiver function
this.buffer = ""; // reader input buffer (paper-tape reel)
this.bufLength = 0; // current input buffer length (characters)
this.bufIndex = 0; // 0-relative offset to next character to be read
this.wordCount = 0; // count of full words read from tape
};
/**************************************/
B220PaperTapeReader.prototype.$$ = function $$(e) {
return this.doc.getElementById(e);
};
/**************************************/
B220PaperTapeReader.prototype.fileSelector_onChange = function fileSelector_onChange(ev) {
/* Handle the <input type=file> onchange event when files are selected. For each
file, load it and add it to the input buffer of the reader */
var tape;
var f = ev.target.files;
var that = this;
var x;
function fileLoader_onLoad(ev) {
/* Handle the onload event for a Text FileReader */
if (that.bufIndex >= that.bufLength) {
that.buffer = ev.target.result;
that.bufIndex = 0;
that.setWordCount(0);
} else {
switch (that.buffer.charAt(that.buffer.length-1)) {
case "\r":
case "\n":
break; // do nothing -- the last word has a delimiter
default:
that.buffer += "\n"; // so the next tape starts on a new line
break;
}
that.buffer += ev.target.result;
}
that.bufLength = that.buffer.length;
that.$$("PRTapeSupplyBar").max = that.bufLength;
that.$$("PRTapeSupplyBar").value = that.bufLength - that.bufIndex;
that.setReaderReady(true);
if (that.busy && that.ready) { // reinitiate the pending read
that.readTapeChar(that.pendingReceiver);
that.receiver = null;
}
}
this.tapeView.value = "";
for (x=f.length-1; x>=0; x--) {
tape = new FileReader();
tape.onload = fileLoader_onLoad;
tape.readAsText(f[x]);
}
};
/**************************************/
B220PaperTapeReader.prototype.setReaderReady = function setReaderReady(ready) {
/* Sets the reader to a ready or not-ready status */
this.ready = ready && this.remoteSwitch.state && !this.readerRewinding &&
(this.bufIndex < this.bufLength);
this.readyLamp.set(this.ready ? 1 : 0);
};
/**************************************/
B220PaperTapeReader.prototype.setReaderEmpty = function setReaderEmpty() {
/* Sets the reader to a not-ready status and empties the buffer */
this.setReaderReady(false);
this.tapeSupplyBar.value = 0;
this.fileSelector.value = null; // reset the control so the same file can be reloaded
};
/**************************************/
B220PaperTapeReader.prototype.setWordCount = function setWordCount(c) {
/* Sets the value of word counter on the reader panel */
this.wordCount = c;
this.wordCountDiv.textContent = c.toString();
};
/**************************************/
B220PaperTapeReader.prototype.beforeUnload = function beforeUnload(ev) {
var msg = "Closing this window will make the device unusable.\n" +
"Suggest you stay on the page and minimize this window instead";
ev.preventDefault();
ev.returnValue = msg;
return msg;
};
/**************************************/
B220PaperTapeReader.prototype.flipSwitch = function flipSwitch(ev) {
/* Handler for switch clicks */
var id = ev.target.id;
var prefs = this.config.getNode("ConsoleInput.units", this.unitIndex);
var x = 0;
switch (id) {
case "RemoteSwitch":
this.remoteSwitch.flip();
prefs.remote = this.remoteSwitch.state;
this.setReaderReady(true);
this.fileSelector.disabled = (this.remoteSwitch.state != 0);
break;
case "SpeedSwitch":
this.speedSwitch.flip();
prefs.speed = this.speedSwitch.state;
this.charPeriod = (this.speedSwitch.state ? B220PaperTapeReader.highSpeedPeriod : B220PaperTapeReader.lowSpeedPeriod);
break;
case "UnitDesignateKnob":
x = this.unitDesignateKnob.selectedIndex;
if (x < 0) {
this.unitMask = 0;
} else {
this.unitMask = B220Processor.pow2[x+1];
}
prefs.unitMask = this.unitMask;
break;
}
this.config.putNode("ConsoleInput.units", prefs, this.unitIndex);
ev.preventDefault();
ev.stopPropagation();
};
/**************************************/
B220PaperTapeReader.prototype.unloadTape = function unloadTape(ev) {
/* Unloads the reader tape buffer */
var bufLeft = this.bufLength - this.bufIndex;
var proceed = this.remoteSwitch.state == 0;
if (proceed) {
if (bufLeft > 0) {
proceed = this.window.confirm(bufLeft.toString() + " of " + this.bufLength.toString() +
" characters remaining to read.\nDo you want to unload the reader?");
}
}
if (proceed) {
this.buffer = ""; // discard the input buffer
this.bufLength = 0;
this.bufIndex = 0;
this.setWordCount(0);
this.setReaderEmpty();
this.tapeView.value = "";
}
};
/**************************************/
B220PaperTapeReader.prototype.rewindTape = function rewindTape() {
/* Rewinds the paper tape if the reader is in a not-ready status */
var lastStamp = 0;
function rewindFinish() {
this.readerRewinding = false;
this.setWordCount(0);
this.tapeView.value = "";
this.setReaderReady(true);
this.fileSelector.disabled = (this.remoteSwitch.state != 0);
}
function rewindDelay() {
var chars;
var stamp = performance.now();
var interval = stamp - lastStamp;
if (interval <= 0) {
interval = B220PaperTapeReader.spinUpdateInterval/2;
}
this.$$("PRTapeSupplyBar").value = this.bufLength - this.bufIndex;
if (this.bufIndex <= 0) {
setCallback(this.mnemonic, this, B220PaperTapeReader.spinUpdateInterval, rewindFinish);
} else {
lastStamp = stamp;
chars = interval/this.charPeriod*B220PaperTapeReader.rewindSpeedFactor;
if (chars > this.bufIndex) {
this.bufIndex = 0;
} else {
this.bufIndex -= chars;
}
setCallback(this.mnemonic, this, B220PaperTapeReader.spinUpdateInterval, rewindDelay);
}
}
function rewindStart() {
lastStamp = performance.now();
setCallback(this.mnemonic, this, B220PaperTapeReader.spinUpdateInterval, rewindDelay);
}
if (this.bufIndex > 0 && this.remoteSwitch.state == 0) { // reader must be in LOCAL
this.readerRewinding = true;
this.fileSelector.disabled = true;
setCallback(this.mnemonic, this, 100, rewindStart);
}
};
/**************************************/
B220PaperTapeReader.prototype.readerOnload = function readerOnload(ev) {
/* Initializes the reader window and user interface */
var body;
var boundUnloadTape = B220PaperTapeReader.prototype.unloadTape.bind(this);
var mask;
var prefs = this.config.getNode("ConsoleInput.units", this.unitIndex);
var x;
this.doc = ev.target;
this.window = this.doc.defaultView;
//de = this.doc.documentElement;
this.doc.title = "retro-220 - Reader - " + this.mnemonic;
this.fileSelector = this.$$("PRFileSelector");
this.tapeSupplyBar = this.$$("PRTapeSupplyBar");
this.tapeView = this.$$("PRTapeView");
this.wordCountDiv = this.$$("PRWordCountDiv");
this.setWordCount(0);
body = this.$$("PaperTapeReader")
this.remoteSwitch = new ToggleSwitch(body, null, null, "RemoteSwitch",
B220PaperTapeReader.offSwitchImage, B220PaperTapeReader.onSwitchImage);
this.remoteSwitch.set(0); // ignore prefs.remote, always initialize as LOCAL
this.readyLamp = new ColoredLamp(body, null, null, "ReadyLamp", "blueLamp lampCollar", "blueLit");
this.setReaderReady(true);
this.speedSwitch = new ToggleSwitch(body, null, null, "SpeedSwitch",
B220PaperTapeReader.offSwitchImage, B220PaperTapeReader.onSwitchImage);
this.speedSwitch.set(prefs.speed);
this.charPeriod = (this.speedSwitch.state ? B220PaperTapeReader.highSpeedPeriod : B220PaperTapeReader.lowSpeedPeriod);
this.unitDesignateKnob = this.$$("UnitDesignateKnob");
this.unitMask = prefs.unitMask;
if (this.unitMask == 0) {
this.unitDesignateKnob.selectedIndex = this.unitDesignateKnob.length-1; // OFF
} else {
mask = 0x002; // ignore the 1-bit (used for SPO on output devices)
for (x=0; x<this.unitDesignateKnob.length; ++x) {
if (this.unitMask & mask) {
this.unitDesignateKnob.selectedIndex = x;
break; // out of for loop
} else {
mask <<= 1;
}
}
}
this.fileSelector.disabled = (this.remoteSwitch.state != 0);
// Events
this.window.addEventListener("beforeunload",
B220PaperTapeReader.prototype.beforeUnload, false);
this.fileSelector.addEventListener("change",
B220PaperTapeReader.prototype.fileSelector_onChange.bind(this), false);
this.$$("RewindBtn").addEventListener("click",
B220PaperTapeReader.prototype.rewindTape.bind(this), false);
this.$$("UnloadBtn").addEventListener("click", boundUnloadTape, false);
this.tapeSupplyBar.addEventListener("click", boundUnloadTape, false);
this.remoteSwitch.addEventListener("click", this.boundFlipSwitch);
this.speedSwitch.addEventListener("click", this.boundFlipSwitch);
this.unitDesignateKnob.addEventListener("change", this.boundFlipSwitch);
//this.window.resizeBy(de.scrollWidth - this.window.innerWidth + 4, // kludge for right-padding/margin
// de.scrollHeight - this.window.innerHeight);
};
/***********************************************************************
* Input Entry Points *
***********************************************************************/
/**************************************/
B220PaperTapeReader.prototype.initiateInput = function initiateInput(successor) {
/* Initiates input to the reader. This simply calls this.readTapeChar(),
which returns the first tape character (which should be the sign digit) to
the processor and gets the ball rolling */
var stamp = performance.now();
this.readResult.error = false;
this.readResult.code = 0;
this.readResult.signDigit = 0;
this.readResult.frameCount = 0;
if (stamp-this.nextCharTime < B220PaperTapeReader.idleTime) {
this.readTapeChar(successor);
} else {
setCallback(this.mnemonic, this, B220PaperTapeReader.startupTime, this.readTapeChar, successor);
}
};
/**************************************/
B220PaperTapeReader.prototype.sendTapeChar = function sendTapeChar(c, code, receiver) {
/* Sends the character or EOW code read from the tape back to the
Processor after an appropriate delay. Updates the TapeView display with the
character sent */
var delay;
var length;
var stamp = performance.now();
var text = this.tapeView.value;
this.readResult.code = code;
if (code == 0x17 && c != 0x3F) { // if it's the error code but not a literal "?" character
this.readResult.error = true;
}
++this.readResult.frameCount;
if (this.readResult.frameCount == 1) {
this.readResult.signDigit = code;
}
if (this.nextCharTime < stamp) {
delay = 0;
this.nextCharTime = stamp + this.charPeriod;
} else {
delay = this.nextCharTime - stamp;
this.nextCharTime += this.charPeriod;
}
this.inTimer = setCallback(this.mnemonic, this, delay, receiver, this.readResult);
length = text.length;
if (length < 120) {
this.tapeView.value = text + String.fromCharCode(c);
++length;
} else {
this.tapeView.value = text.substring(length-119) + String.fromCharCode(c);
}
this.tapeView.setSelectionRange(length-1, length);
};
/**************************************/
B220PaperTapeReader.prototype.conditionalSendEOW = function conditionalSendEOW(receiver) {
/* Checks for the special case of sending an alphanumeric word (sign=2) with
less than five alpha characters. Since text editors may trim trailing blanks
from the end of lines, alpha words with trailing blanks may be trimmed in the
tape image file. If the sign digit was 2 and fewer than six characters (sign
plus five alpha codes) have been sent to the processor, sends a space code to
pad the alpha word and returns false, causing readTapeChar() to loop until a
full word has been accumulated. Otherwise, sends an End-of-Word code and
returns true */
var code = 0x35; // EOW code
var result = true;
if (this.readResult.signDigit == 0x82) { // sign digit was a "2"
if (this.readResult.frameCount < 6) {
code = 0x00; // space code
result = false;
}
}
this.sendTapeChar(0x20, code, receiver);
return result;
};
/**************************************/
B220PaperTapeReader.prototype.readTapeChar = function readTapeChar(receiver) {
/* Reads one character frame from the paper-tape buffer and passes it to the
"receiver" function. If at end-of-line, passes an end-of-word (0x35) code;
an error or invalid character is passed as a 0x17 code. Otherwise, the ANSI
character is translated to B220 code and that code is passed.
If the buffer is empty (this.ready=false) or we are at end of buffer,
simply exits after stashing a reference to the receiver function in
this.pendingReceiver. This will leave the processor hanging until more tape
is loaded into the reader or the processor is cleared. Once more tape is
loaded, the fileSelector_onChange event will set ready, notice the hanging
read (this.busy=true) and restart the read */
var bufLength = this.bufLength; // current buffer length
var c = 0; // current character ANSI code
var x = this.bufIndex; // current buffer index
if (!this.ready) {
this.busy = true;
this.pendingReceiver = receiver;// stash the receiver until tape is loaded
this.window.focus(); // call attention to the tape reader
} else {
this.busy = false;
if (x >= bufLength) { // end of buffer -- send finish
if (this.conditionalSendEOW(receiver)) {
this.setWordCount(this.wordCount+1);
this.setReaderEmpty();
}
} else {
c = this.buffer.charCodeAt(x) % 0x100;
if (c == 0x0D) { // carriage return -- send EOW and check for LF
if (this.conditionalSendEOW(receiver)) {
this.setWordCount(this.wordCount+1);
if (++x < bufLength && this.buffer.charCodeAt(x) == 0x0A) {
++x;
}
if (x >= bufLength) {
this.setReaderEmpty();
}
}
} else if (c == 0x0A) { // line feed -- send EOW
if (this.conditionalSendEOW(receiver)) {
this.setWordCount(this.wordCount+1);
if (++x >= bufLength) {
this.setReaderEmpty();
}
}
} else { // translate character and send its code
++x;
this.sendTapeChar(c, B220PaperTapeReader.xlate220[c], receiver);
}
}
this.bufIndex = x;
this.tapeSupplyBar.value = bufLength-x;
}
};
/**************************************/
B220PaperTapeReader.prototype.shutDown = function shutDown() {
/* Shuts down the device */
if (this.inTimer) {
clearCallback(this.inTimer);
}
if (this.window) {
this.window.removeEventListener("beforeunload",
B220PaperTapeReader.prototype.beforeUnload, false);
this.window.close();
this.window = null;
}
};