1
0
mirror of https://github.com/pkimpel/retro-220.git synced 2026-01-11 23:52:46 +00:00
pkimpel.retro-220/webUI/B220MagTapeDrive.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

2558 lines
108 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/***********************************************************************
* retro-220/webUI B220MagTapeDrive.js
************************************************************************
* Copyright (c) 2017, Paul Kimpel.
* Licensed under the MIT License, see
* http://www.opensource.org/licenses/mit-license.php
************************************************************************
* Burroughs 220 Magnetic Tape Drive Peripheral Unit module.
*
* Defines a magnetic tape drive peripheral unit type, emulating the
* 220 Tape Storage Unit at 208.333 bits/inch and 120 inches/sec.
*
* Internally, tape images are maintained as a two-dimensional array of 64-bit
* floating-point words, similar to Javascript Number objects. The first dimension
* represents lanes on the tape; the second represents words in tape blocks.
*
* Blocks are formatted internally in a manner similar to their physical layout
* on tape, as follows. This scheme was chosen to allow reasonable behavior after
* a lane change with an MLS command:
*
* * The tape begins with 2083 words of flaw markers (-4 words, see below).
* This represents the magnetic beginning-of-tape area written to
* newly-edited tape.
*
* * Tape words are stored as non-negative floating-point values having 11 BCD
* digits (44 bits) each. The high-order digit is the 220 sign.
*
* * Negative word values are used to designate special control words in the
* tape image:
* -1 Blank (erased) tape/inter-block gap words
* -2 End-of-block and erase-gap words
* -3 Magnetic end-of-tape words
* -4 Flaw markers and magnetic beginning-of-tape words
*
* * A block begins with three inter-block gap (-1) words, followed by the
* preface word. The preface word contains the length of the block in BINARY,
* not BCD. Maximum valid value of a preface word is 100 (not zero). A preface
* value of one indicates an end-of-tape block, although the block will
* contain 10 words, the first of which is the EOT control word. Values 0 and
* 2-9 are invalid block sizes.
*
* * Following the preface word are the data words for the block. In the case
* of an end-of-tape block, the first word will be the EOT control word; the
* remaining nine words will be zeroes.
*
* * Following the data words for the block will be two end-of-block/erase-gap
* (-2) words. The inter-block gap words for the next block (or magnetic
* end-of-tape or flaw marker words) begin immediately after these two words.
*
* * The tape ends with 2916 words of magnetic end-of-tape (-3) words. This
* represents the magnetic end-of-tape area written to newly-edited tape.
*
* External tape images are ordinary text files in comma-separated variable (CSV)
* format. Each line in the file represents one block for one lane. Lines may be
* delimited by ASCII line-feed (hex 0A), carriage-return (hex 0D) or a carriage-
* return/line-feed pair.
*
* The first field on the line contains one or two integers, formatted as "L" or
* "R*L". L represents the lane number. Only its low-order bit (0/1) is significant.
* R is a repeat factor used to compress the size of tape image files. It
* indicates the number of copies of this block that exist consecutively at this
* point in the tape image. If R and its delimiting "*" are not present, R is
* assumed to be one. If R is not present, the "*" must also not be present.
* If R is less than one or greater than 45570 (the maximum number of blocks on
* one lane of a 3500-foot reel of tape), it is considered invalid and aborts
* the load.
*
* The second field on a line is the preface word indicating the length of the
* data. A block length of 100 may be represented as either 0 or 100. Values
* greater than 100 or less than zero will be treated as 100.
*
* The remaining fields on a line represent the data words of the block. Since
* words are stored internally in 4-bit BCD, these fields are interpreted as
* hexadecimal values, although normally they should be composed only using the
* decimal digits. Leading zero digits may be omitted. The digits of a word may
* be preceded by a hyphen ("-"), which will cause a 1 to be OR-ed into the sign
* digit of the word. Spaces may precede or follow the digits of a field, but may
* not appear within the digits or between any leading "-" and the first digit.
*
* If any of the repeat factor, lane number, preface word, or block data words
* is not a valid integer, it is considered invalid and aborts the block. If the
* tape image fills more than 729165 words on a lane (the maximum amount for a
* 3500-foot reel), the load is aborted at that point.
*
* Note that with this representation, it is possible that the word count in
* the preface may not match the actual number of words on the rest of the line.
* While this might be useful at some point to allow construction of invalid
* tape blocks that generate Tape Preface Failure (TPF) halts, at present the
* block is stored in the internal tape image with exactly the number of words
* specified by the preface. The words specified on the line will be truncated
* or padded with zero words as necessary in the internal image to achieve this
* block length.
*
* Also note that the arrangement of blocks with respect to their lane is
* arbitrary. Blocks for a lane can be arranged on the external image separately
* or intermingled with the blocks for the other lane. The only requirement is
* that blocks for a lane be in sequence. When exporting a tape image that has
* been modified, the drive will always dump all of lane 0 and then all of
* lane 1. To save space in the external image, trailing words of zeroes will
* be trimmed from each block, and consecutive blocks containing the same data
* will be compressed using the R*L notation discussed above. The lane number
* and preface fields will always be written, however.
*
************************************************************************
* 2017-07-09 P.Kimpel
* Original version, from retro-205 D205MagTapeDrive.js.
***********************************************************************/
"use strict";
/**************************************/
function B220MagTapeDrive(mnemonic, unitIndex, tcu, config) {
/* Constructor for the MagTapeDrive object */
var y = ((mnemonic.charCodeAt(2) - "A".charCodeAt(0))*32) + 304;
this.config = config; // System configuration object
this.mnemonic = mnemonic; // Unit mnemonic
this.tcu = tcu; // Tape Control Unit object
this.unitIndex = unitIndex; // Internal unit number
this.unitDesignate = -1; // External unit number
this.timer = 0; // setCallback() token
this.remote = false; // Remote/Ready status
this.notWrite = false; // Not-Write status
this.rewindLock = false; // Rewind-Lock status
this.powerOn = false; // Transport power on/standby status
this.clear();
this.loadWindow = null; // handle for the tape loader window
this.reelBar = null; // handle for tape-full meter
this.reelIcon = null; // handle for the reel spinner
this.boundInitialWriteBlock = B220MagTapeDrive.prototype.initialWriteBlock.bind(this);
this.boundOverwriteBlock = B220MagTapeDrive.prototype.overwriteBlock.bind(this);
this.boundReadNextBlock = B220MagTapeDrive.prototype.readNextBlock.bind(this);
this.boundReleaseDelay = B220MagTapeDrive.prototype.releaseDelay.bind(this);
this.boundReleaseUnit = B220MagTapeDrive.prototype.releaseUnit.bind(this);
this.boundReposition = B220MagTapeDrive.prototype.reposition.bind(this);
this.boundReverseDirection = B220MagTapeDrive.prototype.reverseDirection.bind(this);
this.boundSearchBackwardBlock = B220MagTapeDrive.prototype.searchBackwardBlock.bind(this);
this.boundSearchForwardBlock = B220MagTapeDrive.prototype.searchForwardBlock.bind(this);
this.boundSetBOT = B220MagTapeDrive.prototype.setBOT.bind(this);
this.boundSetEOT = B220MagTapeDrive.prototype.setEOT.bind(this);
this.boundSpaceBackwardBlock = B220MagTapeDrive.prototype.spaceBackwardBlock.bind(this);
this.boundSpaceEOIBlock = B220MagTapeDrive.prototype.spaceEOIBlock.bind(this);
this.boundSpaceForwardBlock = B220MagTapeDrive.prototype.spaceForwardBlock.bind(this);
this.boundStartUpBackward = B220MagTapeDrive.prototype.startUpBackward.bind(this);
this.boundStartUpForward = B220MagTapeDrive.prototype.startUpForward.bind(this);
this.doc = null;
this.window = null;
B220Util.openPopup(window, "../webUI/B220MagTapeDrive.html", mnemonic,
"location=no,scrollbars=no,resizable,width=384,height=184,left=0,top=" + y,
this, B220MagTapeDrive.prototype.tapeDriveOnload);
}
// this.tapeState enumerations
B220MagTapeDrive.prototype.tapeUnloaded = 0;
B220MagTapeDrive.prototype.tapeLocal = 1;
B220MagTapeDrive.prototype.tapeRewinding = 2;
B220MagTapeDrive.prototype.tapeRemote = 3;
// Internal tape image control words
B220MagTapeDrive.prototype.markerGap = -1;
B220MagTapeDrive.prototype.markerEOB = -2;
B220MagTapeDrive.prototype.markerMagEOT = -3;
B220MagTapeDrive.prototype.markerFlaw = -4;
B220MagTapeDrive.prototype.density = 208.333;
// bits/inch
B220MagTapeDrive.prototype.tapeSpeed = 120/1000;
// tape motion speed [inches/ms]
B220MagTapeDrive.prototype.inchesPerWord = 12/B220MagTapeDrive.prototype.density;
B220MagTapeDrive.prototype.millisPerWord = B220MagTapeDrive.prototype.inchesPerWord/B220MagTapeDrive.prototype.tapeSpeed;
B220MagTapeDrive.prototype.maxTapeInches = 3500*12;
// length of a standard reel of tape [inches]
B220MagTapeDrive.prototype.minBlockWords = 10;
// min words in a physical block
B220MagTapeDrive.prototype.maxBlockWords = 100;
// max words in a physical block
B220MagTapeDrive.prototype.magBOTWords = Math.floor(10*12*B220MagTapeDrive.prototype.density/12);
// number of words in the magnetic beginning-of-tape area
B220MagTapeDrive.prototype.magEOTWords = Math.floor(14*12*B220MagTapeDrive.prototype.density/12);
// number of words in the magnetic end-of-tape area
B220MagTapeDrive.prototype.startOfBlockWords = 4;
// inter-block tape gap + preface [words]
B220MagTapeDrive.prototype.endOfBlockWords = 2;
// end-of-block + erase gap [words]
B220MagTapeDrive.prototype.maxTapeWords = Math.floor(B220MagTapeDrive.prototype.maxTapeInches*B220MagTapeDrive.prototype.density/12);
// max words on a tape (12 digits/word)
B220MagTapeDrive.prototype.maxTapeBlocks = Math.floor(B220MagTapeDrive.prototype.maxTapeWords/
(B220MagTapeDrive.prototype.minBlockWords+B220MagTapeDrive.prototype.startOfBlockWords+B220MagTapeDrive.prototype.endOfBlockWords));
// max possible blocks on a tape lane
B220MagTapeDrive.prototype.repositionWords = 4;
// number of words to reposition back into the block after a turnaround
B220MagTapeDrive.prototype.startTime = 3;
// tape start time [ms]
B220MagTapeDrive.prototype.startWords = 4;
// number of words traversed during tape start time
B220MagTapeDrive.prototype.stopTime = 3;
// tape stop time [ms]
B220MagTapeDrive.prototype.turnaroundTime = 5;
// tape turnaround time after a direction change [ms]
B220MagTapeDrive.prototype.rewindSpeed = 0.120;
// rewind speed [inches/ms]
B220MagTapeDrive.prototype.reelCircumference = 10*Math.PI;
// max circumference of tape [inches]
B220MagTapeDrive.prototype.spinUpdateInterval = 15;
// milliseconds between reel icon angle updates
B220MagTapeDrive.prototype.maxSpinAngle = 33;
// max angle to rotate reel image [degrees]
/**************************************/
B220MagTapeDrive.prototype.$$ = function $$(e) {
return this.doc.getElementById(e);
};
/**************************************/
B220MagTapeDrive.prototype.clear = function clear() {
/* Initializes (and if necessary, creates) the reader unit state */
this.ready = false; // ready status
this.busy = false; // busy status
this.image = null; // tape drive "reel of tape"
this.imgIndex = 0; // current word index in tape image
this.imgLength = 0; // tape image max length [words] to physical EOT
this.imgTopWordNr = 0; // highest-written word index within image data
this.imgWritten = false; // tape image has been modified (implies writable)
this.atBOT = true; // true if tape at physical beginning-of-tape
this.atEOT = false; // true if tape at physical end-of-tape
this.laneNr = 0; // currently selected lane number
this.reelAngle = 0; // current rotation angle of reel image [degrees]
this.tapeInches = 0; // current distance up-tape [inches]
this.tapeState = this.tapeUnloaded; // tape drive state
};
/**************************************/
B220MagTapeDrive.prototype.releaseUnit = function releaseUnit(param) {
/* Releases the busy status of the unit. Returns but does not use its
parameter so it can be used with Promise.then() */
this.busy = false;
this.designatedLamp.set(0);
return param;
};
/**************************************/
B220MagTapeDrive.prototype.releaseDelay = function releaseDelay(driveState) {
/* Delays the specified number of milliseconds in driveState.completionDelay.
Returns a Promise for completion of the delay */
return new Promise((resolve, reject) => {
setCallback(this.mnemonic, this, driveState.completionDelay, resolve, driveState);
});
};
/**************************************/
B220MagTapeDrive.prototype.setUnitDesignate = function setUnitDesignate(index) {
/* Sets this.unitDesignate from the UNIT DESIGNATE list selectedIndex value */
if (index <= 0) {
this.unitDesignate = -1;
this.remote = false;
this.setTapeReady(false);
} else {
if (index < 10) {
this.unitDesignate = index; // units 1-9
} else {
this.unitDesignate = 0; // unit 10
}
this.remote = true;
this.setTapeReady(true);
}
};
/**************************************/
B220MagTapeDrive.prototype.setImageIndex = function setImageIndex(index) {
/* Updates the image index annunciator with the specified position */
this.$$("MTImageIndexLight").textContent = index.toFixed();
};
/**************************************/
B220MagTapeDrive.prototype.setAtBOT = function setAtBOT(atBOT) {
/* Controls the at-Beginning-of-Tape state of the tape drive */
if (atBOT ^ this.atBOT) {
this.atBOT = atBOT;
if (!atBOT) {
this.$$("MTAtBOTLight").classList.remove("annunciatorLit");
} else {
this.imgIndex = 0;
this.tapeInches = 0;
this.reelAngle = 0;
this.setImageIndex(this.imgIndex);
this.$$("MTAtBOTLight").classList.add("annunciatorLit");
this.reelBar.value = this.maxTapeInches;
this.reelIcon.style.transform = "none";
}
}
};
/**************************************/
B220MagTapeDrive.prototype.setBOT = function setBOT(driveState) {
/* Sets BOT status on the drive and releases the drive after encountering
physical BOT. Return value is designed for use with Promise.then() */
this.setAtBOT(true);
this.releaseUnit(driveState); // release the unit, but leave the control hung:
return driveState; // do not reject the Promise
};
/**************************************/
B220MagTapeDrive.prototype.setAtEOT = function setAtEOT(atEOT) {
/* Controls the at-End-of-Tape state of the tape drive */
if (atEOT ^ this.atEOT) {
this.atEOT = atEOT;
if (!atEOT) {
this.$$("MTAtEOTLight").classList.remove("annunciatorLit");
} else {
this.$$("MTAtEOTLight").classList.add("annunciatorLit");
this.reelBar.value = 0;
}
}
};
/**************************************/
B220MagTapeDrive.prototype.setEOT = function setEOT(driveState) {
/* Sets EOT status on the drive and releases the drive after encountering
physical EOT. Return value is designed for use with Promise.then() */
this.setAtEOT(true);
this.releaseUnit(driveState); // release the unit, but leave the control hung:
return driveState; // do not reject the Promise
};
/**************************************/
B220MagTapeDrive.prototype.setLane = function(laneNr, param) {
/* Sets the lane of the tape drive and updates its annunciator on the drive
control panel. If the lane is changing, introduces a 70ms delay. Returns a
Promise that resolves once the lane is changed and any delay is completed.
"param" is not used by this routine, but is passed as a resolve result for
use with Promise.then() */
var lane = laneNr%2; // make sure it's 0 or 1
this.$$("MTLaneNrLight").textContent = "LANE " + lane;
return new Promise((resolve, reject) => {
if (this.laneNr == lane) {
resolve(param);
} else {
this.laneNr = lane;
setCallback(this.mnemonic, this, 70, resolve, param);
}
});
};
/**************************************/
B220MagTapeDrive.prototype.setTapeReady = function setTapeReady(makeReady) {
/* Controls the ready-state of the tape drive */
var enabled = (this.tapeState != this.tapeUnloaded) && !this.rewindLock &&
(this.tapeState != this.tapeRewinding && this.powerOn);
this.ready = this.remote & makeReady && enabled;
this.notReadyLamp.set(this.ready ? 0 : 1);
if (this.ready) {
this.tapeState = this.tapeRemote;
} else {
this.busy = false; // forced reset
this.designatedLamp.set(0);
if (this.tapeState == this.tapeRemote) {
this.tapeState = this.tapeLocal;
}
}
};
/**************************************/
B220MagTapeDrive.prototype.setTransportPower = function setTransportPower(toOn) {
/* Sets Transport Power for the drive to On or Standby */
this.powerOn = toOn && (this.tapeState != this.tapeUnloaded);
this.transportOnLamp.set(this.powerOn ? 1 : 0);
this.transportStandbyLamp.set(this.powerOn ? 0 : 1);
this.setTapeReady(this.powerOn);
};
/**************************************/
B220MagTapeDrive.prototype.setTapeUnloaded = function setTapeUnloaded() {
/* Controls the loaded/unloaded-state of the tape drive */
if (this.tapeState == this.tapeLocal && this.atBOT) {
this.tapeState = this.tapeUnloaded;
this.image = null; // release the tape image to GC
this.imgLength = 0;
this.imgTopWordNr = 0;
this.busy = false;
this.rewindLock = false;
this.rwlLamp.set(0);
this.setTapeReady(false);
this.setAtBOT(false);
this.setAtEOT(false);
this.reelBar.value = 0;
this.reelIcon.style.visibility = "hidden";
this.$$("MTFileName").value = "";
this.$$("MTLaneNrLight").style.visibility = "hidden";
this.$$("MTImageIndexLight").style.visibility = "hidden";
this.$$("MTUnloadedLight").classList.add("annunciatorLit");
if (this.timer) {
clearCallback(this.timer);
this.timer = 0;
}
}
};
/**************************************/
B220MagTapeDrive.prototype.tapeRewind = function tapeRewind(laneNr, lockout) {
/* Rewinds the tape. Makes the drive not-ready and delays for an appropriate
amount of time depending on how far up-tape we are. Readies the unit again
when the rewind is complete unless lockout is truthy. Returns a Promise that
resolves when the rewind completes */
return new Promise((resolve, reject) => {
var lastStamp;
function rewindFinish() {
this.timer = 0;
this.tapeState = this.tapeLocal;
this.$$("MTRewindingLight").classList.remove("annunciatorLit");
this.setAtBOT(true);
this.setImageIndex(this.imgIndex);
this.rewindLock = (lockout ? true : false);
this.rwlLamp.set(this.rewindLock ? 1 : 0);
this.setTapeReady(!this.rewindLock);
resolve(this.setLane(laneNr, null));
}
function rewindDelay() {
var inches;
var stamp = performance.now();
var interval = stamp - lastStamp;
if (interval <= 0) {
interval = this.spinUpdateInterval/2;
}
if (this.tapeInches <= 0) {
this.imgIndex = 0;
this.timer = setCallback(this.mnemonic, this, 1000, rewindFinish);
} else {
inches = interval*this.rewindSpeed;
this.imgIndex -= Math.floor(inches/this.inchesPerWord);
lastStamp = stamp;
this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
this.spinReel(-inches);
}
this.setImageIndex(this.imgIndex);
}
function rewindStart() {
this.designatedLamp.set(0);
lastStamp = performance.now();
this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
}
if (this.timer) {
clearCallback(this.timer);
this.timer = 0;
}
if (this.tapeState != this.tapeUnloaded && this.tapeState != this.tapeRewinding) {
this.busy = true;
this.tapeState = this.tapeRewinding;
this.setAtEOT(false);
this.$$("MTRewindingLight").classList.add("annunciatorLit");
this.timer = setCallback(this.mnemonic, this, 1000, rewindStart);
}
});
};
/**************************************/
B220MagTapeDrive.prototype.spinReel = function spinReel(inches) {
/* Rotates the reel image icon an appropriate amount based on the "inches"
of tape to be moved. The rotation is limited to this.maxSpinAngle degrees
in either direction so that movement remains apparent to the viewer */
var circumference = this.reelCircumference*(1 - this.tapeInches/this.maxTapeInches/2);
var degrees = inches/circumference*360;
if (degrees > this.maxSpinAngle) {
degrees = this.maxSpinAngle;
} else if (degrees < -this.maxSpinAngle) {
degrees = -this.maxSpinAngle;
}
this.reelAngle = (this.reelAngle + degrees)%360;
this.reelIcon.style.transform = "rotate(" + this.reelAngle.toFixed(0) + "deg)";
this.tapeInches += inches;
if (this.tapeInches < this.maxTapeInches) {
this.reelBar.value = this.maxTapeInches - this.tapeInches;
} else {
this.reelBar.value = 0;
}
};
/**************************************/
B220MagTapeDrive.prototype.moveTape = function moveTape(inches, delay, successor, param) {
/* Delays the I/O during tape motion, during which it animates the reel image
icon. At the completion of the "delay" time in milliseconds, "successor" is
called with "param" as a parameter */
var delayLeft = Math.abs(delay); // milliseconds left to delay
var direction = (inches < 0 ? -1 : 1);
var index = this.imgIndex - inches/this.inchesPerWord;
var inchesLeft = inches; // inches left to move tape
var initiallyReady = this.ready; // remember initial ready state to detect change
var lastStamp = performance.now(); // last timestamp for spinDelay
function spinFinish() {
this.timer = 0;
if (inchesLeft != 0) {
this.spinReel(inchesLeft);
}
this.setImageIndex(this.imgIndex);
successor.call(this, param);
}
function spinDelay() {
var motion;
var stamp = performance.now();
var interval = stamp - lastStamp;
if (interval <= 0) {
interval = this.spinUpdateInterval/2;
if (interval > delayLeft) {
interval = delayLeft;
}
}
if (initiallyReady && !this.ready) { // drive went not ready
inchesLeft = index = 0;
this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinFinish);
} else {
delayLeft -= interval;
if (delayLeft > this.spinUpdateInterval) {
lastStamp = stamp;
this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinDelay);
} else {
this.timer = setCallback(this.mnemonic, this, delayLeft, spinFinish);
}
motion = inchesLeft*interval/delayLeft;
if (inchesLeft*direction <= 0) { // inchesLeft crossed zero
motion = inchesLeft = 0;
} else if (motion*direction <= inchesLeft*direction) {
inchesLeft -= motion;
} else {
motion = inchesLeft;
inchesLeft = 0;
}
index += motion/this.inchesPerWord;
this.setImageIndex(index);
this.spinReel(motion);
}
}
spinDelay.call(this);
};
/**************************************/
B220MagTapeDrive.prototype.moveTapeTo = function moveTapeTo(index, result) {
/* Advances the tape to the specified image index and returns a Promise
that will resolve when tape motion completes */
return new Promise((resolve, reject) => {
var len = index - this.imgIndex; // number of words passed
var delay = len*this.millisPerWord; // amount of tape spin time
this.imgIndex = index;
this.moveTape(len*this.inchesPerWord, delay, resolve, result);
});
};
/**************************************/
B220MagTapeDrive.prototype.loadTape = function loadTape() {
/* Loads a tape into memory based on selections in the MTLoad window */
var $$$ = null; // getElementById shortcut for loader window
var doc = null; // loader window.document
var file = null; // FileReader instance
var fileSelect = null; // file picker element
var mt = this; // this B220MagTapeDrive instance
var win = null; // loader window
function fileSelector_onChange(ev) {
/* Handle the <input type=file> onchange event when a file is selected */
var fileName;
var x;
file = ev.target.files[0];
fileName = file.name;
$$$("MTLoadWriteEnableCheck").checked = false;
}
function finishLoad() {
/* Finishes the tape loading process and closes the loader window */
var x;
mt.notWrite = !$$$("MTLoadWriteEnableCheck").checked;
mt.notWriteLamp.set(mt.notWrite ? 1 : 0);
mt.reelBar.max = mt.maxTapeInches;
mt.reelBar.value = mt.maxTapeInches;
mt.setAtBOT(true);
mt.setAtEOT(false);
mt.setLane(0, null);
mt.$$("MTLaneNrLight").style.visibility = "visible";
mt.$$("MTImageIndexLight").style.visibility = "visible";
mt.reelIcon.style.visibility = "visible";
mt.$$("MTUnloadedLight").classList.remove("annunciatorLit");
// Automatically turn on transport power and make drive ready
mt.tapeState = mt.tapeLocal; // setTapeReady() requires it not be unloaded
mt.setTransportPower($$$("MTLoadTransportPowerOn").checked);
}
function writeBlockStart(length) {
/* Writes the start-of-block words to the tape image buffer in the current
lane number and at the current offset of mt.imgIndex: 3 gap words + the
preface word with the binary block length */
var x;
for (x=0; x<mt.startOfBlockWords-1; ++x) {
mt.image[mt.laneNr][mt.imgIndex+x] = mt.markerGap;
}
mt.image[mt.laneNr][mt.imgIndex+x] = length; // preface word
mt.imgIndex += mt.startOfBlockWords;
}
function writeBlockEnd() {
/* Writes the start-of-block words to the tape image buffer at the current
value of mt.imgIndex: 3 gap words + the preface word with the binary block
length */
var x;
for (x=0; x<mt.endOfBlockWords; ++x) {
mt.image[mt.laneNr][mt.imgIndex+x] = mt.markerEOB;
}
mt.imgIndex += mt.endOfBlockWords;
}
function writeEndOfTapeBlock(controlWord) {
/* Writes an end-of-tape block containing the designated controlWord */
var x;
writeBlockStart(1);
mt.image[mt.laneNr][mt.imgIndex++] = controlWord;
for (x=0; x<9; ++x) {
mt.image[mt.laneNr][mt.imgIndex+x] = 0;
}
mt.imgIndex += 9;
writeBlockEnd();
}
function editedLoader() {
/* Loads an edited (blank) tape image into the drive. If a block length
was chosen on the tape-load panel, initializes the image with blocks of
that size, followed by an end-of-tape block in both lanes having the
controlWord (aaaa=0000, bbbb=0001) */
var blockLen;
var fmt = $$$("MTLoadFormatSelect").selectedIndex;
var lane;
var x;
if (fmt <= 0) {
mt.$$("MTFileName").value = "(Edited tape)";
} else {
blockLen = fmt*10; // words/block
mt.$$("MTFileName").value = "(Formatted as " + blockLen.toFixed(0) + "-word blocks)";
for (mt.laneNr=0; mt.laneNr<2; ++mt.laneNr) {
lane = mt.image[mt.laneNr];
mt.imgIndex = mt.magBOTWords;
while (lane[mt.imgIndex] != mt.markerMagEOT) {
writeBlockStart(blockLen);
for (x=0; x<blockLen; ++x) {
lane[mt.imgIndex+x] = 0;
}
mt.imgIndex += blockLen;
writeBlockEnd();
} // while
writeEndOfTapeBlock(0x00000001); // aaaa=0000, bbbb=0001
// Write a gap so that MPE can sense end-of-info.
for (x=0; x<mt.startOfBlockWords*2; ++x) {
lane[mt.imgIndex+x] = mt.markerGap;
}
mt.imgIndex += mt.startOfBlockWords*2;
} // for mt.laneNr
}
mt.imgTopWordNr = mt.imgIndex;
mt.imgWritten = false;
finishLoad();
}
function textLoader_onload(ev) {
/* Event handler for tape image file onLoad. Loads a text image as
comma-delimited decimal word values. No end-of-tape block is written
unless it is present in the text image */
var aborted = false; // tape load aborted
var blockNr = 0; // current tape block number
var blockWords = 0; // words in current tape block
var chunk = ""; // ANSI text of current chunk
var chunkLength = 0; // length of current ASCII chunk
var buf = ev.target.result; // ANSI tape image buffer
var bufLength = buf.length; // length of ANSI tape image buffer
var dups = 0; // repeat factor for consecutive blocks
var eolRex = /([^\n\r\f]*)((:?\r[\n\f]?)|\n|\f)?/g;
var index = 0; // char index into tape image buffer for next chunk
var lane = 0; // current tape lane image
var lx = [0,0]; // word indexes for each lane
var match = null; // result of eolRex.exec()
var numericRex = /^-?\d+\s*$/; // regex to detect valid integer number
var preface = 0; // preface word: block length in words
var repeatRex = /^\s*(\d+)\s*\*\s*/; // regex to detect and parse repeat factor
var tx = 0; // char index into ANSI chunk text
var w = 0; // current word value
var wx = 0; // word index within current block
function abortLoad(msg, blockNr, chunk) {
/* Displays an alert for a fatal load error */
aborted = true;
mt.window.alert("Abort load: " + msg + " @ block " + blockNr +
"\n\"" + chunk + "\"");
}
function parseRepeatFactor() {
/* Parses the repeat factor, if any, from the first field on the
line and returns its value. If there is no repeat factor, returns 1.
If the repeat factor is not a valid integer, returns NaN.
Leaves "tx" (the index into the line) pointing to the lane number */
var v = 1; // parsed numeric value, default to 1
match = chunk.match(repeatRex);
if (match) {
tx += match[0].length;
if (match[1].search(numericRex) == 0) {
v = parseInt(match[1], 10);
} else {
v = NaN;
}
}
return v;
}
function parseWord(radix) {
/* Parses the next word from the chunk text and returns its value as
determined by "radix". If the comma-delimited word is not a valid
integer, returns NaN */
var cx = 0; // offset to next comma
var text = ""; // text of parsed word
var w = 0; // result BCD word
if (tx < chunkLength) {
cx = chunk.indexOf(",", tx);
if (cx < 0) {
cx = chunkLength;
}
text = chunk.substring(tx, cx).trim();
if (text.length > 0) {
if (text.search(numericRex) != 0) {
w = NaN;
} else {
w = parseInt(text, radix);
if (!isNaN(w)) {
if (w > 0) {
w %= 0x100000000000;
} else if (w < 0) {
// The number was specified as negative: if the
// sign bit is not already set, then set it.
w = (-w) % 0x100000000000;
if (w % 0x20000000000 < 0x10000000000) {
w += 0x10000000000;
}
}
}
}
}
tx = cx+1;
}
return w;
}
lx[0] = lx[1] = mt.magBOTWords;
do {
eolRex.lastIndex = index;
match = eolRex.exec(buf);
if (!match) {
break;
} else {
index += match[0].length;
chunk = match[1].trim();
chunkLength = chunk.length;
if (chunkLength > 0) { // ignore empty lines
tx = 0;
++blockNr;
dups = parseRepeatFactor(); // get the repeat factor, if any
if (isNaN(dups) || dups < 1 || dups > mt.maxTapeBlocks) {
abortLoad("invalid repeat factor", blockNr, chunk);
break;
}
w = parseWord(10); // get the lane number
if (isNaN(w) || w < 0) {
abortLoad("invalid lane number", blockNr, chunk);
break;
}
mt.laneNr = w%2;
preface = parseWord(10); // get the preface word as binary
if (isNaN(preface)) {
abortLoad("invalid preface/length word", blockNr, chunk);
break;
} else if (preface > 100) { // limit blocks to 100 words
preface = 100;
} else if (preface < 1) { // if block length <= 0, make it 100
preface = 100;
}
blockWords = (preface == 1 ? 10 : preface); // pad out end-of-tape blocks to 10 words
lane = mt.image[mt.laneNr];
mt.imgIndex = lx[mt.laneNr]; // restore internal offset for this lane
writeBlockStart(preface);
wx = 0; // load data words from tape image
while (tx < chunkLength && wx < preface) {
w = parseWord(16);
if (isNaN(w)) {
abortLoad("invalid tape block word[" + wx + "]", blockNr, chunk);
break;
}
lane[mt.imgIndex+wx] = w;
++wx;
} // while tx:wx
while (wx < blockWords) { // pad block with zero words, if needed
lane[mt.imgIndex+wx] = 0;
++wx;
} // while wx
mt.imgIndex += blockWords; // update the internal image offset
writeBlockEnd();
wx = lx[mt.laneNr]; // starting offset of block
blockWords = mt.imgIndex - wx; // total words in block, including overhead
while (dups > 1 && mt.imgIndex < mt.maxTapeWords) {
--dups; // repeat the block as necessary
++blockNr;
lane.copyWithin(mt.imgIndex, wx, wx+blockWords);
mt.imgIndex += blockWords;
} // while dups
lx[mt.laneNr] = mt.imgIndex; // save current offset for this lane
if (mt.imgIndex > mt.maxTapeWords) {
abortLoad("maximum tape capacity exceeded", blockNr, chunk);
break;
}
}
}
} while (index < bufLength);
if (!aborted) {
// Write a gap at the end of both lanes so that MPE can sense end-of-info.
for (mt.laneNr=0; mt.laneNr<2; ++mt.laneNr) {
lane = mt.image[mt.laneNr];
mt.imgIndex = lx[mt.laneNr];
for (wx=0; wx<mt.startOfBlockWords*2; ++wx) {
lane[mt.imgIndex+wx] = mt.markerGap;
} // for wx
lx[mt.laneNr] = mt.imgIndex + mt.startOfBlockWords*2;
} // for mt.laneNr
mt.imgTopWordNr = Math.max(lx[0], lx[1]);
mt.imgWritten = false;
finishLoad();
}
}
function tapeLoadRemoveEvents() {
/* Removes the event listeners for the tape load window */
fileSelect.removeEventListener("change", fileSelector_onChange, false);
$$$("MTLoadFormatSelect").removeEventListener("change", selectFormat_onChange, false);
$$$("MTLoadOKBtn").removeEventListener("click", tapeLoadOK, false);
$$$("MTLoadCancelBtn").removeEventListener("click", tapeLoadCancel, false);
}
function selectFormat_onChange(ev) {
/* Handler for the onChange event of the format selection list */
$$$("MTLoadWriteEnableCheck").checked = (ev.target.selectedIndex > 0);
}
function tapeLoadOK(ev) {
/* Handler for the load window's OK button. Does the actual tape load.
If a tape-image file has been selected, loads that file; otherwise loads
a blank tape */
var tape;
var top;
var x;
// Allocate the tape image buffer
mt.image = [new Float64Array(mt.maxTapeWords), // lane 0
new Float64Array(mt.maxTapeWords)]; // lane 1
// Initialize the magnetic beginning-of-tape area
for (x=0; x<mt.magBOTWords; ++x) {
mt.image[0][x] = mt.markerFlaw;
mt.image[1][x] = mt.markerFlaw;
}
// Initialize the bulk of the tape as blank (erased) space
mt.imgIndex = mt.magBOTWords;
mt.imgLength = mt.maxTapeWords;
top = mt.imgLength - mt.magEOTWords;
for (x=mt.imgIndex; x<top; ++x) {
mt.image[0][x] = mt.markerGap;
mt.image[1][x] = mt.markerGap;
}
// Initialize the magnetic end-of-tape area
for (x=top; x<mt.imgLength; ++x) {
mt.image[0][x] = mt.markerMagEOT;
mt.image[1][x] = mt.markerMagEOT;
}
// Load the tape image, if any
if (!file) {
editedLoader();
} else {
mt.$$("MTFileName").value = file.name;
tape = new FileReader();
tape.onload = textLoader_onload;
tape.readAsText(file);
}
win.close();
tapeLoadRemoveEvents();
}
function tapeLoadCancel(ev) {
/* Handler for the load window's Cancel button */
file = null;
mt.$$("MTFileName").value = "";
win.close();
tapeLoadRemoveEvents();
}
function tapeLoadOnload(ev) {
/* Driver for the tape loader window */
var de;
doc = ev.target;
win = doc.defaultView;
doc.title = "retro-220 " + mt.mnemonic + " Tape Loader";
this.loadWindow = win;
de = doc.documentElement;
$$$ = function $$$(id) {
return doc.getElementById(id);
};
win.addEventListener("unload", tapeLoadUnload, false);
fileSelect = $$$("MTLoadFileSelector");
fileSelect.addEventListener("change", fileSelector_onChange, false);
$$$("MTLoadFormatSelect").addEventListener("change", selectFormat_onChange, false);
$$$("MTLoadOKBtn").addEventListener("click", tapeLoadOK, false);
$$$("MTLoadCancelBtn").addEventListener("click", tapeLoadCancel, false);
win.focus();
win.resizeBy(de.scrollWidth - win.innerWidth,
de.scrollHeight - win.innerHeight);
}
function tapeLoadUnload(ev) {
win.removeEventListener("unload", tapeLoadUnload, false);
mt.loadWindow = null;
}
// Outer block of loadTape
if (this.loadWindow && !this.loadWindow.closed) {
this.loadWindow.close();
}
B220Util.openPopup(this.window, "B220MagTapeLoadPanel.html", this.mnemonic + "Load",
"location=no,scrollbars=no,resizable,width=508,height=112,left=" +
(this.window.screenX+16) +",top=" + (this.window.screenY+16),
this, tapeLoadOnload);
};
/**************************************/
B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
/* Reformats the tape image data as ASCII text and displays it in a new
window so the user can save or copy/paste it elsewhere */
var doc = null; // unloader window.document
var image; // <pre> element to receive tape image data
var mt = this; // tape drive object
var win = null; // unloader window
function findBlockStart() {
/* Searches forward in the tape image on the currently-selected lane for the
start of a block or magnetic end-of-tape markers, or physical end-of-tape,
whichever occurs first. If an inter-block (blank) gap word is found, skips
over all of them and reads the preface word. Returns the preface word, or
the magnetic EOT marker (-3) word, or if physical EOT is encountered, an
inter-block gap (-1) word */
var imgLength = mt.imgLength; // physical end of tape index
var lane = mt.image[mt.laneNr]; // image data for current lane
var result = mt.markerGap; // function result
var state = 1; // FSA state variable
var w; // current image word
var x = mt.imgIndex; // lane image word index
while (x < imgLength) {
w = lane[x++];
switch (state) {
case 1: // search for inter-block gap word
if (w == mt.markerGap) {
state = 2;
} else if (w == mt.markerMagEOT) {
result = w; // return the EOT word
mt.imgIndex = x-1; // point to EOT word
x = imgLength; // kill the loop
}
break;
case 2: // search for preface word
if (w >= 0) {
result = w; // found preface, return it
mt.imgIndex = x; // point to first data word in block
x = imgLength; // kill the loop
} else if (w != mt.markerGap) {
result = w; // return whatever marker word was found
mt.imgIndex = x-1; // point to the word found
x = imgLength; // kill the loop
}
break;
default:
x = imgLength; // kill the loop
throw new Error("Invalid state: B220MagTapeDrive.unloadTape.findBlockStart, " + state);
break;
} // switch state
} // while x
return result;
}
function unloadDriver() {
/* Converts the tape image to ASCII once the window has displayed the
waiting message */
var buf; // ANSI tape image buffer
var dups = 0; // block repeat count
var imgLength = mt.imgLength; // active words in tape image
var imgTop = mt.imgTopWordNr; // tape image last block number
var lane; // lane image buffer
var lastBuf = ""; // last block image output
var lx; // lane index
var nzw; // number of consecutive zero words
var state; // lane processing state variable
var w; // current image word
var wx; // word index within block
var x = 0; // image data index
while (image.firstChild) { // delete any existing <pre> content
image.removeChild(image.firstChild);
}
for (lx=0; lx<2; ++lx) {
mt.laneNr = lx;
lane = mt.image[lx];
state = 1;
x = 0;
do {
switch (state) {
case 1: // Search for start of block
nzw = 0;
mt.imgIndex = x;
w = findBlockStart();
if (w < 0) { // done with this lane
x = imgLength; // kill the loop
} else { // format the lane number and preface word
buf = lx.toString(10) + "," + w.toString(10);
x = mt.imgIndex;
state = 2; // switch state to blocking data words
}
break;
case 2: // Record the block data words
w = lane[x++];
if (w == 0) { // suppress trailing zero words
++nzw;
} else if (w >= 0) {// buffer the current word
while (nzw > 0) {
--nzw; // restore non-trailing zero words
buf += ",0";
}
buf += "," + w.toString(16);
} else { // output the last block image(s)
state = 1; // reset state for next block
if (buf == lastBuf) { // compress consecutive duplicate blocks
++dups;
} else {
if (dups > 1) {
image.appendChild(doc.createTextNode(dups.toString(10) + "*" + lastBuf + "\n"));
} else if (dups > 0) {
image.appendChild(doc.createTextNode(lastBuf + "\n"));
}
lastBuf = buf;
dups = 1;
}
}
break;
default:
x = imgLength; // kill the loop
throw new Error("Invalid state: B220MagTapeDrive.unloadTape, " + state);
break;
} // switch state
} while (x < imgLength);
// Output the final block(s) for the lane.
if (dups > 1) {
image.appendChild(doc.createTextNode(dups.toString(10) + "*" + lastBuf + "\n"));
} else if (dups > 0) {
image.appendChild(doc.createTextNode(lastBuf + "\n"));
}
dups = 0;
lastBuf = "";
} // for lx
mt.setTapeUnloaded();
}
function unloadSetup(ev) {
/* Loads a status message into the "paper" rendering area, then calls
unloadDriver after a short wait to allow the message to appear */
doc = ev.target;
win = doc.defaultView;
doc.title = "retro-220 " + mt.mnemonic + " Unload Tape";
win.moveTo((screen.availWidth-win.outerWidth)/2, (screen.availHeight-win.outerHeight)/2);
image = doc.getElementById("Paper");
image.appendChild(win.document.createTextNode(
"Rendering tape image... please wait..."));
setTimeout(unloadDriver, 50);
win.focus();
}
// Outer block of unloadTape
B220Util.openPopup(this.window, "./B220FramePaper.html", "",
"location=no,scrollbars=yes,resizable,width=800,height=600",
this, unloadSetup);
};
/**************************************/
B220MagTapeDrive.prototype.LoadBtn_onclick = function LoadBtn_onclick(ev) {
/* Handle the click event for the LOAD button */
if (!this.busy && !this.powerOn && this.tapeState == this.tapeUnloaded) {
this.loadTape();
}
};
/**************************************/
B220MagTapeDrive.prototype.UnloadBtn_onclick = function UnloadBtn_onclick(ev) {
/* Handle the click event for the UNLOAD button */
if (!this.busy && this.atBOT && !this.powerOn && this.tapeState == this.tapeLocal) {
if (this.imgWritten && this.window.confirm(
"Do you want to save the tape image data?\n(CANCEL discards the image)")) {
this.unloadTape(); // it will do setTapeUnloaded() afterwards
} else {
this.setTapeUnloaded();
}
}
};
/**************************************/
B220MagTapeDrive.prototype.RewindBtn_onclick = function RewindBtn_onclick(ev) {
/* Handle the click event for the REWIND button */
if (!this.busy && !this.powerOn && this.tapeState != this.tapeUnloaded) {
this.tapeRewind(this.laneNr, this.rewindLock)
.then(this.boundReleaseUnit)
.then(this.tcu.boundTapeUnitFinished);
}
};
/**************************************/
B220MagTapeDrive.prototype.UnitDesignate_onchange = function UnitDesignate_onchange(ev) {
/* Handle the change event for the UNIT DESIGNATE select list */
var prefs = this.config.getNode("MagTape.units", this.unitIndex);
var sx = ev.target.selectedIndex;
this.setUnitDesignate(sx);
prefs.designate = sx;
this.setTapeReady(this.remote);
this.config.putNode("MagTape.units", prefs, this.unitIndex);
};
/**************************************/
B220MagTapeDrive.prototype.RWLRBtn_onclick = function RWLRBtn_onclick(ev) {
/* Handle the click event for the RWLR (Rewind-Lock-Release) button */
this.rewindLock = false;
this.rwlLamp.set(0);
this.setTapeReady(true);
};
/**************************************/
B220MagTapeDrive.prototype.WriteBtn_onclick = function WriteBtn_onclick(ev) {
/* Handle the click event for the WRITE and NOT WRITE buttons */
this.notWrite = (ev.target.id == "NotWriteBtn");
this.notWriteLamp.set(this.notWrite ? 1 : 0);
};
/**************************************/
B220MagTapeDrive.prototype.TransportOnBtn_onclick = function TransportOnBtn_onclick(ev) {
/* Handle the click event for the TRANSPORT POWER ON and STANDBY buttons */
this.setTransportPower(ev.target.id == "TransportOnBtn");
};
/**************************************/
B220MagTapeDrive.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;
};
/**************************************/
B220MagTapeDrive.prototype.tapeDriveOnload = function tapeDriveOnload(ev) {
/* Initializes the reader window and user interface */
var body;
var prefs = this.config.getNode("MagTape.units", this.unitIndex);
this.doc = ev.target;
this.window = this.doc.defaultView;
this.doc.title = "retro-220 Tape Drive " + this.mnemonic;
body = this.$$("MTDiv");
this.reelBar = this.$$("MTReelBar");
this.reelIcon = this.$$("MTReel");
this.rwlLamp = new ColoredLamp(body, null, null, "RWLLamp", "blueLamp lampCollar", "blueLit");
this.notWriteLamp = new ColoredLamp(body, null, null, "NotWriteLamp", "blueLamp lampCollar", "blueLit");
this.notReadyLamp = new ColoredLamp(body, null, null, "NotReadyLamp", "blueLamp lampCollar", "blueLit");
this.designatedLamp = new ColoredLamp(body, null, null, "DesignatedLamp", "blueLamp lampCollar", "blueLit");
this.transportOnLamp = new ColoredLamp(body, null, null, "TransportOnLamp", "blueLamp lampCollar", "blueLit");
this.transportStandbyLamp = new ColoredLamp(body, null, null, "TransportStandbyLamp", "blueLamp lampCollar", "blueLit");
this.transportStandbyLamp.set(1);
this.unitDesignateList = this.$$("UnitDesignate");
this.unitDesignateList.selectedIndex = prefs.designate;
this.setUnitDesignate(prefs.designate);
this.tapeState = this.tapeLocal; // setTapeUnloaded() requires it to be in local
this.atBOT = true; // and also at BOT
this.setTapeUnloaded();
this.window.addEventListener("beforeunload",
B220MagTapeDrive.prototype.beforeUnload, false);
this.$$("LoadBtn").addEventListener("click",
B220MagTapeDrive.prototype.LoadBtn_onclick.bind(this), false);
this.$$("UnloadBtn").addEventListener("click",
B220MagTapeDrive.prototype.UnloadBtn_onclick.bind(this), false);
this.$$("RewindBtn").addEventListener("click",
B220MagTapeDrive.prototype.RewindBtn_onclick.bind(this), false);
this.unitDesignateList.addEventListener("change",
B220MagTapeDrive.prototype.UnitDesignate_onchange.bind(this), false);
this.$$("RWLRBtn").addEventListener("click",
B220MagTapeDrive.prototype.RWLRBtn_onclick.bind(this), false);
this.$$("WriteBtn").addEventListener("click",
B220MagTapeDrive.prototype.WriteBtn_onclick.bind(this), false);
this.$$("NotWriteBtn").addEventListener("click",
B220MagTapeDrive.prototype.WriteBtn_onclick.bind(this), false);
this.$$("TransportOnBtn").addEventListener("click",
B220MagTapeDrive.prototype.TransportOnBtn_onclick.bind(this), false);
this.$$("TransportStandbyBtn").addEventListener("click",
B220MagTapeDrive.prototype.TransportOnBtn_onclick.bind(this), false);
};
/**************************************/
B220MagTapeDrive.prototype.startUpForward = function startUpForward(driveState) {
/* Initializes the I/O in a forward direction and provides the start-up
delay for drive acceleration. Returns a Promise that resolves at completion
of the start-up acceleration */
if (this.busy) {
driveState.state = driveState.driveBusy;
return Promise.reject(driveState);
} else if (!this.ready || this.rewindLock) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else if (this.atEOT) {
driveState.state = driveState.driveAtEOT;
return Promise.reject(driveState);
} else {
return new Promise((resolve, reject) => {
this.busy = true;
this.designatedLamp.set(1);
this.setAtBOT(false);
this.imgIndex += this.startWords;
driveState.completionDelay -= this.startTime;
setCallback(this.mnemonic, this, this.startTime, resolve, driveState);
this.moveTape(this.startWords*this.inchesPerWord, this.startTime, resolve, driveState);
});
}
};
/**************************************/
B220MagTapeDrive.prototype.startUpBackward = function startUpBackward(driveState) {
/* Initializes the I/O in a backward direction and provides the start-up
delay for drive acceleration */
if (this.busy) {
driveState.state = driveState.driveBusy;
return Promise.reject(driveState);
} else if (!this.ready || this.rewindLock) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else if (this.atBOT) {
driveState.state = driveState.driveAtBOT;
return Promise.reject(driveState);
} else {
return new Promise((resolve, reject) => {
this.busy = true;
this.designatedLamp.set(1);
this.setAtEOT(false);
this.imgIndex -= this.startWords;
driveState.completionDelay -= this.startTime;
setCallback(this.mnemonic, this, this.startTime, resolve, driveState);
this.moveTape(-this.startWords*this.inchesPerWord, this.startTime, resolve, driveState);
});
}
};
/**************************************/
B220MagTapeDrive.prototype.reverseDirection = function reverseDirection(driveState) {
/* Generates a delay to allow the drive to stop and reverse direction.
Returns a Promise that resolves when the delay is complete */
return new Promise((resolve, reject) => {
setCallback(this.mnemonic, this, this.turnaroundTime, resolve, driveState);
});
};
/**************************************/
B220MagTapeDrive.prototype.reposition = function reposition(driveState) {
/* Reverses tape direction after a forward tape operation and repositions
the head four words from the end of the prior block, giving room for
startup acceleration of the next forward operation. The "prior block" is
located by the first EOB (erase gap) or flaw marker word encountered when
moving in a backward direction. Returns a Promise that resolves when tape
motion is complete.
A real 220 drive repositioned tape about 60 digits (five words) from the end
of the data portion of the block, to allow for tape acceleration of about
3ms, at which point it took about 2ms (50 digits, or just over four words)
to reach the end of the erase gap and start of the inter-block gap for the
next block. this.repositionWords is sized to approximate the 3ms
acceleration delay */
return new Promise((resolve, reject) => {
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var x = this.imgIndex-1; // lane image word index (start with prior word)
do {
if (x <= 0) {
state = 0; // at BOT
} else {
switch (state) {
case 1: // initial state: skip backwards until erase-gap or BOT flaw-marker words
if (lane[x] == this.markerEOB) {
--x;
state = 2;
} else if (lane[x] == this.markerFlaw) {
state = 0;
} else {
--x;
}
break;
case 2: // skip backwards over erase-gap words
if (lane[x] == this.markerEOB) {
--x;
} else {
state = 0;
}
break;
} // switch state
}
} while (state);
x = this.imgIndex - x + this.repositionWords; // words to reposition
if (x < this.imgIndex) {
this.imgIndex -= x;
} else {
x = this.imgIndex;
this.imgIndex = 0;
driveState.state = driveState.driveAtBOT;
this.setAtBOT(true);
}
driveState.completionDelay -= this.turnaroundTime;
this.moveTape(-x*this.inchesPerWord, this.turnaroundTime, resolve, driveState);
});
};
/**************************************/
B220MagTapeDrive.prototype.scanBlock = function scanBlock(driveState, wordIndex) {
/* Scans one block in a forward direction. Terminates with either the control
word from an EOT or control block, or the category word from any other block
stored in the "driveState" structure. "wordIndex" is the 1-relative index
of the category word to match. Returns a Promise that resolves at the end
of the block scan. This routine is used for MTC and MFC */
var scanForward = (resolve, reject) => {
/* Reads the start of the next block in the tape image in a forward
direction to obtain its category word */
var count = 0; // word counter within block
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var preface = 0; // block length read from tape image
var sign = 0; // sign digit of keyword
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (w == this.markerGap) {
++x;
state = 2;
} else {
++x;
}
break;
case 2: // skip over inter-block gap and magnetic EOT words
if (w == this.markerGap) {
++x;
} else if (w == this.markerMagEOT) {
++x;
} else if (w >= 0) {
state = 3; // found the preface
} else {
state = 0; // not a formatted tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
}
break;
case 3: // read the preface and check for EOT block
++x;
preface = w;
if (preface == 1) {
state = 6; // detected end-of-tape block
} else if (preface < this.minBlockWords && preface > 1) {
state = 0; // invalid preface on tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
} else {
state = 4; // normal or control block
}
break;
case 4: // read keyword, detect control block, do not advance beyond keyword (yet)
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
sign = (w - w%0x10000000000)/0x10000000000;
if (sign == 7) {
state = 6; // detected control block
} else {
state = 5; // normal block
}
}
break;
case 5: // read the block words and capture the category word
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
++count;
++x;
if (count >= wordIndex) {
driveState.keyword = w; // return category word to the TCU
state = 7; // finish the block
}
}
break;
case 6: // handle an end-of-tape or control block
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
driveState.state = driveState.driveHasControlWord;
driveState.controlWord = w; // not used by the TCU
++x;
state = 7;
}
break;
case 7: // step through remaining words in the block until normal EOB
if (w == this.markerEOB) {
++x;
state = 8;
} else {
++x;
}
break;
case 8: // step through erase-gap words and finish normally
if (w == this.markerEOB) {
++x;
} else {
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(scanForward);
}
};
/**************************************/
B220MagTapeDrive.prototype.searchForwardBlock = function searchForwardBlock(driveState) {
/* Searches one block in a forward direction. Terminates with either the
control word from an EOT block or the keyword from any other block stored
in the "driveState" structure. Returns a Promise that resolves at the end
of the block search. This routine is used for MTS and MFS */
var searchForward = (resolve, reject) => {
/* Reads the start of the next block in the tape image in a forward
direction to obtain its keyword, leaving the tape positioned after
the keyword */
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var preface = 0; // block length read from tape image
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (w == this.markerGap) {
++x;
state = 2;
} else {
++x;
}
break;
case 2: // skip over inter-block gap and magnetic EOT words
if (w == this.markerGap) {
++x;
} else if (w == this.markerMagEOT) {
++x;
} else if (w >= 0) {
state = 3; // found the preface
} else {
state = 0; // not a formatted tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
}
break;
case 3: // read the preface and check for EOT block
++x;
preface = w;
if (preface == 1) {
state = 5; // detected end-of-tape block
} else if (preface < this.minBlockWords && preface > 1) {
state = 0; // invalid preface on tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
} else {
state = 4; // normal or control block
}
break;
case 4: // read the keyword and finish
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
state = 0; // finish the block
driveState.keyword = w;
resolve(this.moveTapeTo(x+this.repositionWords, driveState));
}
break;
case 5: // handle an end-of-tape block and finish
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
state = 0; // finish the block
driveState.state = driveState.driveHasControlWord;
driveState.controlWord = w; // not used by the TCU
resolve(this.moveTapeTo(x+this.repositionWords, driveState));
}
break;
} // switch
}
} while (state);
}
if (!this.ready || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(searchForward);
}
};
/**************************************/
B220MagTapeDrive.prototype.searchBackwardBlock = function searchBackwardBlock(driveState) {
/* Searches one block in a backward direction. Terminates with the keyword
from block stored in the "driveState" structure. Returns a Promise that
resolves at the end of the block search. This routine is used for MTS and
MFS */
var searchBackward = (resolve, reject) => {
/* Reads the start of the next block in the tape image in a backward
direction to obtain its keyword, leaving the tape positioned in the
prior block */
var count = 0; // contiguous block data word counter
var imgLength = this.imgLength; // physical end of tape index
var keyword = 0; // keyword from block
var lane = this.image[this.laneNr]; // image data for current lane
var preface = 0; // block length read from tape image
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex-1; // lane image word index (start with prior word)
do {
if (x <= 0) {
state = 0; // at BOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtBOT;
this.moveTapeTo(x, driveState).then(this.boundSetBOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over inter-block gap, flaw, and magnetic EOT words
if (w == this.markerGap) {
--x;
} else if (w == this.markerFlaw) {
--x;
} else if (w == this.markerMagEOT) {
--x;
} else {
state = 3;
}
break;
// case 2 deleted //
case 3: // search for start of block (first prior inter-block gap word)
if (w == this.markerGap) {
state = 4; // just passed the preface word
} else if (w < 0) {
count = 0; // if it's not a data word, reset the count
--x;
} else {
keyword = preface; // remember the last two words we've seen
preface = w;
--x;
++count;
}
break;
case 4: // skip this block's inter-block gap words
if (w == this.markerGap) {
--x;
} else if (count < 2) {
state = 1; // saw less than 2 block data words, start over
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-this.repositionWords, driveState));
} else {
state = 0; // position into end of prior block, as usual
driveState.keyword = keyword;
resolve(this.moveTapeTo(x-this.repositionWords, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atBOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(searchBackward);
}
};
/**************************************/
B220MagTapeDrive.prototype.readNextBlock = function readNextBlock(driveState, record, controlEnabled, storeWord) {
/* Reads one block on tape. "record" is true for MRR. "controlEnabled" is true
if control blocks are to be recognized. "storeWord" is a call-back to store
the next word into the Processor's memory. This routine is used for both MRD
and MRR, as block lengths are determined by the tape control unit. Returns
a Promise that resolves when the read completes */
var readBlock = (resolve, reject) => {
/* Reads the next block in the tape image */
var controlBlock = false; // true if control block detected
var count = 0; // word counter within block
var firstWord = false; // flag for initial memory fetch
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var preface = 0; // block length read from tape image
var sign = 0; // sign digit of keyword
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (w == this.markerGap) {
++x;
state = 2;
} else {
++x;
}
break;
case 2: // skip over inter-block gap and magnetic EOT words
if (w == this.markerGap) {
++x;
} else if (w == this.markerMagEOT) {
++x;
} else if (w >= 0) {
state = 3; // found the preface
} else {
state = 0; // not a formatted tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
}
break;
case 3: // read the preface and check for EOT block
++x;
preface = w;
if (preface == 1) {
state = 6; // detected end-of-tape block
} else if (preface < this.minBlockWords && preface > 1) {
state = 0; // invalid preface on tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
} else {
state = 4; // normal or control block
}
break;
case 4: // read the keyword, detect control block, store the preface if necessary, store keyword
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
++x;
if (controlEnabled) {
sign = (w - w%0x10000000000)/0x10000000000;
if (sign == 7) {
controlBlock = true;
// strip sign digit from keyword (not entirely sure this should be done)
w %= 0x10000000000;
}
}
if (record) { // store the preface word (with keyword sign if a control block)
sign = ((sign*0x100 + (preface%100 - preface%10)/10)*0x10 + preface%10)*0x100000000;
if (storeWord(firstWord, sign) < 0) {
state = 10; // memory error storing preface word
driveState.state = driveState.driveMemoryError;
} else {
firstWord = false;
}
}
if (state == 4) { // no error detected yet
if (controlBlock) {
--preface; // decrement block length to prevent storing the control word
}
if (storeWord(firstWord, w) < 0) {
state = 10; // memory error storing keyword from block
driveState.state = driveState.driveMemoryError;
} else {
firstWord = false;
++count;
state = 5;
}
}
}
break;
case 5: // read and store the remaining block words
if (count < preface) {
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
if (storeWord(firstWord, w) < 0) {
state = 10; // memory error storing data word from block
driveState.state = driveState.driveMemoryError;
} else {
firstWord = false;
++x;
++count;
}
}
} else if (controlBlock) {
if (w < 0) {
state = 0; // preface/block-length mismatch
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
driveState.state = driveState.driveHasControlWord;
driveState.controlWord = w;
++x;
state = 7; // deal with the control word after EOB
}
} else {
state = 7; // check for proper EOB
}
break;
case 6: // capture the control word for an end-of-tape block
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
driveState.state = driveState.driveHasControlWord;
driveState.controlWord = w;
++x;
state = 8;
}
break;
case 7: // check for proper end-of-block
if (w == this.markerEOB) {
++x;
state = 9;
} else {
state = 0; // block was longer than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
}
break;
case 8: // step through remaining words in the block until normal EOB
if (w == this.markerEOB) {
++x;
state = 9;
} else {
++x;
}
break;
case 9: // step through erase-gap words and finish normally
if (w == this.markerEOB) {
++x;
} else {
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
case 10: // step through remaining words in the block until EOB for error
if (w == this.markerEOB) {
++x;
state = 11;
} else {
++x;
}
break;
case 11: // step through erase-gap words and finish with error
if (w == this.markerEOB) {
++x;
} else {
state = 0;
reject(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(readBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.overwriteBlock = function overwriteBlock(driveState, record, words, fetchWord) {
/* Overwrites one block on tape. "record" is true for MOR. "words" is the
number of words to write in the block. "fetchWord" is a call-back to
retrieve the next word from the Processor's memory. This routine is used
for both MOW and MOR, as block lengths are determined by the tape
control unit. Returns a Promise that resolves when the write completes */
var writeBlock = (resolve, reject) => {
/* Overwrites the next block in the tape image */
var count = 0; // word counter within block
var firstWord = !record; // flag for initial memory fetch
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (w == this.markerGap) {
++x;
state = 2;
} else {
++x;
}
break;
case 2: // skip over inter-block gap and magnetic EOT words
if (w == this.markerGap) {
++x;
} else if (w == this.markerMagEOT) {
++x;
} else if (w >= 0) {
state = 3; // found the preface
} else {
state = 0; // not a formatted tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
}
break;
case 3: // check the preface
++x;
if (w < this.minBlockWords && w > 1) {
state = 0; // invalid preface on tape
driveState.state = driveState.drivePrefaceCheck;
reject(this.moveTapeTo(x, driveState));
} else if (w == words) {
state = 4; // preface match: overwrite the block
} else if (w == 1) {
state = 5;
} else {
state = 0; // other preface mismatch
driveState.state = driveState.drivePrefaceMismatch;
reject(this.moveTapeTo(x, driveState).then(this.boundReposition));
}
break;
case 4: // overwrite the block words
if (count < words) {
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
w = fetchWord(firstWord);
if (w < 0) {
state = 8; // memory error: gobble rest of block
driveState.state = driveState.driveMemoryError;
} else {
firstWord = false;
lane[x] = w;
++x;
++count;
}
}
} else if (count < this.minBlockWords) {
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
lane[x] = 0; // pad out an EOT block
++x;
++count;
}
} else if (w == this.markerEOB) {
state = 7; // normal block termination
} else {
state = 0; // block was longer than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
}
break;
case 5: // capture control word for EOT block
if (w < 0) {
state = 0; // block was shorter than preface indicated
driveState.state = driveState.driveReadCheck;
reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
} else {
driveState.state = driveState.driveHasControlWord;
driveState.controlWord = w;
++x;
state = 6; // gobble the rest of the block and finish normally
}
break;
case 6: // step through remaining words in the block until normal EOB
if (w == this.markerEOB) {
++x;
state = 7;
} else {
++x;
}
break;
case 7: // step through erase-gap words and finish normally
if (w == this.markerEOB) {
++x;
} else {
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
case 8: // step through remaining words in the block until EOB for error
if (w == this.markerEOB) {
++x;
state = 9;
} else {
++x;
}
break;
case 9: // step through erase-gap words and finish with error
if (w == this.markerEOB) {
++x;
} else {
state = 0;
reject(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atEOT || this.notWrite) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
this.imgWritten = true; // mark the tape image as modified
return new Promise(writeBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.initialWriteFinalize = function initialWriteFinalize(driveState) {
/* Write a longer inter-block gap so that MPE will work at magnetic
end-of-tape and check for end-of-tape prior to terminating the I/O */
return new Promise((resolve, reject) => {
var count = this.startOfBlockWords*2; // number of gap words
var done = false; // loop control
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
done = true; // at end-of-tape
resolve(this.moveTapeTo(x, driveState).then(this.boundSetEOT));
} else if (count > 0) {
--count; // write extra inter-block gap word
lane[x] = this.markerGap;
++x;
} else if (lane[x] == this.markerMagEOT) {
++x; // space over magnetic EOT words
} else {
done = true; // finish write
resolve(this.moveTapeTo(x, driveState));
}
} while(!done);
});
};
/**************************************/
B220MagTapeDrive.prototype.initialWriteBlock = function initialWriteBlock(driveState, record, words, fetchWord) {
/* Initial-writes one block on edited (blank) tape. "record" is true for MIR.
"words" is the number of words to write in the block. "fetchWord" is a call-
back to retrieve the next word from the Processor's memory. This routine is
used for both MIW and MIR, as block lengths are determined by the tape
control unit. Returns a Promise that resolves when the write completes */
var writeBlock = (resolve, reject) => {
/* Initial-writes the next block in the tape image */
var count = 0; // word counter within block
var firstWord = !record; // flag for initial memory fetch
var gapCount = 0; // count of consecutive inter-block gap words
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var w = 0; // memory word value
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (lane[x] == this.markerGap) {
state = 2;
} else {
++x;
}
break;
case 2: // count 3 consecutive inter-block gap words
if (lane[x] == this.markerGap) {
++gapCount;
if (gapCount < this.startOfBlockWords) {
++x;
} else {
state = 3;
}
} else if (lane[x] == this.markerMagEOT) {
++x;
} else {
state = 0; // not an edited tape
driveState.state = driveState.driveNotEditedTape;
reject(this.moveTapeTo(x, driveState));
}
break;
case 3: // write the preface
lane[x] = words;
++x;
state = 4;
break;
case 4: // write the block words
if (count < words) {
w = fetchWord(firstWord);
if (w < 0) {
state = 0; // memory fetch failed
driveState.state = driveState.driveMemoryError;
state = 5;
} else {
firstWord = false;
lane[x] = w;
++x;
++count;
}
} else {
state = 6;
}
break;
case 5: // pad out the block after a memory error
if (count < words) {
lane[x] = 0;
++x;
++count;
} else {
state = 6;
}
break;
case 6: // pad out to the minimum block length (e.g., for an EOT block)
if (count < this.minBlockWords) {
lane[x] = 0;
++x;
++count;
} else {
count = 0;
state = 7;
}
break;
case 7: // write the erase gap
if (count < this.endOfBlockWords) {
lane[x] = this.markerEOB;
++x;
++count;
} else {
state = 0; // finished with the block
this.imgTopWordNr = x;
resolve(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
};
if (!this.ready || this.atEOT || this.notWrite) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
this.imgWritten = true; // mark the tape image as modified
return new Promise(writeBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.spaceForwardBlock = function spaceForwardBlock(driveState) {
/* Positions tape one block in a forward direction. Leaves the block
positioned at the next preface word, ready to space the next block or
reposition into the prior block at the end of the operation. Returns a
Promise that resolves after the block is spaced */
var spaceBlock = (resolve, reject) => {
/* Spaces forward over the next block. Blocks are counted as their
preface words are passed. This routine does this by detecting the
transition between the preface and its immediately preceding gap word */
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw and intra-block words
if (w == this.markerGap) {
++x;
state = 2;
} else {
++x;
}
break;
case 2: // skip inter-block gap words
if (w == this.markerGap) {
++x;
} else {
state = 3; // found preface word
}
break;
case 3: // search for end of block (next erase-gap word)
if (w == this.markerEOB) {
++x;
state = 4;
} else {
++x;
}
break;
case 4: // step through erase-gap words and finish
if (w == this.markerEOB) {
++x;
} else {
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(spaceBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.spaceBackwardBlock = function spaceBackwardBlock(driveState) {
/* Positions tape one block in a backward direction. Leaves the block
positioned before the preface word. Returns a Promise that resolves after
the block is spaced */
var spaceBlock = (resolve, reject) => {
/* Spaces backward over the current or prior block. Blocks are counted
as their preface words are passed. This routine does that by detecting
the transition between the preface and its immediately preceding gap word */
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex-1; // lane image word index (start with prior word)
do {
if (x <= 0) {
state = 0; // at BOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtBOT;
this.moveTapeTo(x, driveState).then(this.boundSetBOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over inter-block gap, flaw, and magnetic EOT words
if (w == this.markerGap) {
--x;
} else if (w == this.markerFlaw) {
--x;
} else if (w == this.markerMagEOT) {
--x;
} else {
state = 3;
}
break;
// case 2 deleted //
case 3: // search for start of block (first prior inter-block gap word)
if (w == this.markerGap) {
state = 4; // just passed the preface word
} else {
--x;
}
break;
case 4: // skip this block's inter-block gap words
if (w == this.markerGap) {
--x;
} else { // position into end of prior block, as usual
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
}
if (!this.ready || this.atBOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(spaceBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.spaceEOIBlock = function spaceEOIBlock(driveState) {
/* Spaces forward one block on tape, detecting end-of-information if encountered
(i.e., gap longer than inter-block gap. Returns a Promise that resolves
after the block is spaced or EOI is encountered */
var spaceBlock = (resolve, reject) => {
/* Spaces over the next block, unless a long gap or physical EOT is
encountered */
var gapCount = 0; // number of consecutive erase-gap words
var imgLength = this.imgLength; // physical end of tape index
var lane = this.image[this.laneNr]; // image data for current lane
var state = 1; // FSA state variable
var w = 0; // current image word
var x = this.imgIndex; // lane image word index
do {
if (x >= imgLength) {
state = 0; // at EOT: just exit and leave the control hanging...
driveState.state = driveState.driveAtEOT;
this.moveTapeTo(x, driveState).then(this.boundSetEOT);
} else {
w = lane[x];
switch (state) {
case 1: // initial state: skip over flaw words
if (w == this.markerGap) {
state = 2;
} else if (w == this.markerFlaw) {
++x;
} else {
state = 3;
}
break;
case 2: // count inter-block gap words
if (w != this.markerGap) {
state = 3;
} else if (gapCount > this.startOfBlockWords) {
state = 0;
driveState.state = driveState.driveAtEOI;
resolve(this.moveTapeTo(x, driveState));
} else {
++x;
++gapCount;
}
break;
case 3: // search for end of block (next erase-gap word)
if (w == this.markerEOB) {
++x;
state = 4;
} else {
++x;
}
break;
case 4: // step through erase-gap words and finish
if (w == this.markerEOB) {
++x;
} else {
state = 0;
resolve(this.moveTapeTo(x, driveState));
}
break;
} // switch state
}
} while (state);
};
if (!this.ready || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
return new Promise(spaceBlock);
}
};
/**************************************/
B220MagTapeDrive.prototype.laneSelect = function laneSelect(driveState, laneNr) {
/* Selects the tape lane on the unit. If the drive is busy or not ready,
returns true */
if (this.busy) {
driveState.state = driveState.driveBusy;
return Promise.reject(driveState);
} else if (!this.ready || this.rewindLock || this.atEOT) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
this.busy = true;
this.designatedLamp.set(1);
return this.setLane(laneNr, driveState);
}
};
/**************************************/
B220MagTapeDrive.prototype.rewind = function rewind(driveState, laneNr, lockout) {
/* Initiates a rewind operation on the unit. Makes the drive not-ready,
delays for an appropriate amount of time depending on how far up-tape we
are, then readies the unit again. Returns a Promise that resolves once the
rewind completes */
if (this.busy) {
driveState.state = driveState.driveBusy;
return Promise.reject(driveState);
} else if (!this.ready || this.rewindLock) {
driveState.state = driveState.driveNotReady;
return Promise.reject(driveState);
} else {
this.designatedLamp.set(1);
return this.tapeRewind(laneNr, lockout);
}
};
/**************************************/
B220MagTapeDrive.prototype.shutDown = function shutDown() {
/* Shuts down the device */
if (this.timer) {
clearCallback(this.timer);
}
this.window.removeEventListener("beforeunload", B220MagTapeDrive.prototype.beforeUnload, false);
this.window.close();
if (this.loadWindow && !this.loadWindow.closed) {
this.loadWindow.close();
}
};