diff --git a/emulator/B220Processor.js b/emulator/B220Processor.js index 30d539f..9964ff4 100644 --- a/emulator/B220Processor.js +++ b/emulator/B220Processor.js @@ -224,9 +224,6 @@ function B220Processor(config, devices) { this.TAT = new B220Processor.FlipFlop(this, false); // magnetic tape alarm toggle this.UET = new B220Processor.FlipFlop(this, false); // unequal comparison toggle (HIT=UET=0 => off) - // Mag-Tape Control Unit switch - this.tswSuppressB = 0; // Suppress B-register modification on input - // Left/Right Maintenance Panel this.leftPanelOpen = false; this.rightPanelOpen = false; @@ -245,10 +242,9 @@ function B220Processor(config, devices) { this.boundCardatronOutputFinished = B220Processor.bindMethod(this, B220Processor.prototype.cardatronOutputFinished); this.boundCardatronReceiveWord = B220Processor.bindMethod(this, B220Processor.prototype.cardatronReceiveWord); + this.boundMagTapeComplete = B220Processor.bindMethod(this, B220Processor.prototype.magTapeComplete); this.boundMagTapeReceiveBlock = B220Processor.bindMethod(this, B220Processor.prototype.magTapeReceiveBlock); - this.boundMagTapeInitiateSend = B220Processor.bindMethod(this, B220Processor.prototype.magTapeInitiateSend); this.boundMagTapeSendBlock = B220Processor.bindMethod(this, B220Processor.prototype.magTapeSendBlock); - this.boundMagTapeTerminateSend = B220Processor.bindMethod(this, B220Processor.prototype.magTapeTerminateSend); this.clear(); // Create and initialize the processor state @@ -260,7 +256,7 @@ function B220Processor(config, devices) { * Global Constants * ***********************************************************************/ -B220Processor.version = "0.03"; +B220Processor.version = "0.03a"; B220Processor.tick = 1000/200000; // milliseconds per clock cycle (200KHz) B220Processor.cyclesPerMilli = 1/B220Processor.tick; @@ -1056,7 +1052,7 @@ B220Processor.prototype.setPaperTapeCheck = function setPaperTapeCheck(value) { /**************************************/ B220Processor.prototype.setHighSpeedPrinterCheck = function setHighSpeedPrinterCheck(value) { - /* Sets the Cardatron Check alarm */ + /* Sets the High Speed Printer Check alarm */ if (!this.ALARMSW) { this.HAT.set(value); @@ -2755,11 +2751,11 @@ B220Processor.prototype.cardatronOutputFinished = function cardatronOutputFinish /**************************************/ B220Processor.prototype.cardatronReceiveWord = function cardatronReceiveWord(word) { /* Handles a word coming from the Cardatron input unit. Negative values for - the word indicates this is the last word and the I/O is finished. The word - is stored into the D register and is handled according to the sign digit in - the D register. The last word received (typically a "pusher" word of zeroes) - is abandoned and not acted upon. Returns -1 if further data transfer is to - be terminated, 0 otherwise */ + the word indicates this is the last word and the I/O is finished. Otherwise, + the word is stored into the D register and is handled according to the sign + digit in the D register. The last word received (typically a "pusher" word + of zeroes) is abandoned and not acted upon. Returns -1 if further data + transfer is to be terminated, 0 otherwise */ var returnCode = 0; // default is to continue receiving var sign; // D-register sign digit @@ -2836,95 +2832,70 @@ B220Processor.prototype.cardatronReceiveWord = function cardatronReceiveWord(wor ***********************************************************************/ /**************************************/ -B220Processor.prototype.magTapeInitiateSend = function magTapeInitiateSend(writeInitiate) { - /* Performs the initial memory block-to-loop operation after the tape control - unit has determined the drive is ready and not busy. Once the initial loop is - loaded, calls "writeInitiate" to start tape motion, which in turn will cause - the control to call this.magTapeSendBlock to pass the loop data to the drive - and initiate loading of the alternate loop buffer */ - var that = this; +B220Processor.prototype.magTapeComplete = function magTapeComplete(alarm, control, word) { + /* Call-back routine to signal completion of a magnetic tape operation. If + "alarm" is true, the Magnetic Tape Alarm will be set. If "control" is true, + the contents of "word" are processed as a tape control word and an appropriate + branch is set up. Unconditionally terminates the tape I/O instruction */ + var aaaa; + var bbbb; - if (this.togMT3P) { // if false, we've probably been cleared - if (this.CADDR >= 0x8000) { - writeInitiate(this.boundMagTapeSendBlock, this.boundMagTapeTerminateSend); - } else { - this.execTime += performance.now()*B220Processor.wordsPerMilli; // restore time after I/O - this.blockToLoop((this.togMT1BV4 ? 4 : 5), function initialBlockComplete() { - that.execTime -= performance.now()*B220Processor.wordsPerMilli; // suspend time during I/O - writeInitiate(that.boundMagTapeSendBlock, that.boundMagTapeTerminateSend); - }); + if (alarm) { + this.setMagneticTapeCheck(true); + } else if (control) { + this.D.set(word); + bbbb = word%0x10000; + aaaa = ((word - bbbb)/0x10000)%0x10000; + if ((word - word%0x10000000000)%2) { // if sign bit is 1, + bbbb = this.bcdAdd(bbbb, this.B.value, 4); // B-adjust the low-order 4 digits + } + + this.E.set(aaaa); + this.readMemory(); + if (!this.MET.value) { + this.IB.set(this.IB.value - this.IB.value%0x100000000 + + (this.C.value%0x10000)*0x10000 + this.P.value%0x10000); + this.writeMemory(); + this.P.set(bbbb); } } + + this.ioComplete(true); }; /**************************************/ -B220Processor.prototype.magTapeSendBlock = function magTapeSendBlock(lastBlock) { - /* Sends a block of data from a loop buffer to the tape control unit and - initiates the load of the alternate loop buffer. this.togMT1BV4 and - this.togMT1BV5 control alternation of the loop buffers. "lastBlock" indicates - this will be the last block requested by the control unit and no further - blocks should be buffered. If the C-register address is 8000 or higher, the - loop is not loaded from main memory, and the current contents of the loop - are written to tape. Since tape block writes take 46 ms, they are much - longer than any memory-to-loop transfer, so this routine simply exits after - the next blockToLoop is initiated, and the processor then waits for the tape - control unit to request the next block, by which time the blockToLoop will - have completed. Returns null if the processor has been cleared and the I/O - must be aborted */ - var loop; - var that = this; +B220Processor.prototype.magTapeSendBlock = function magTapeSendBlock(buffer, words) { + /* Sends a block of data from memory to the tape control unit. "buffer" is an + array of words to receive the data to be written to tape. "words" is the number + of words to place in the buffer, starting at the current operand address in the + C register. Returns true if the processor has been cleared or a memory address + error occurs, and the I/O must be aborted */ + var result = false; // return value + var that = this; // local context + var x = 0; // buffer index - function blockFetchComplete() { - that.execTime -= performance.now()*B220Processor.wordsPerMilli; // suspend time again during I/O - } + //console.log("TSU " + this.selectedUnit + " W, Len " + words + + // ", ADDR=" + this.CADDR.toString(16)); - //console.log("TSU " + this.selectedUnit + " W, L" + (this.togMT1BV4 ? 4 : 5) + - // ", ADDR=" + this.CADDR.toString(16) + - // " : " + block[0].toString(16) + ", " + block[19].toString(16)); - - if (!this.togMT3P) { - loop = null; + if (!this.AST.value) { + result = true; } else { - // Select the appropriate loop to send data to the drive - if (this.togMT1BV4) { - loop = this.L4; - this.toggleGlow.glowL4 = 1; // turn on the lamp and let normal decay work - } else { - loop = this.L5; - this.toggleGlow.glowL5 = 1; - } - - if (!lastBlock) { - this.execTime += performance.now()*B220Processor.wordsPerMilli; // restore time after I/O - // Flip the loop-buffer toggles - this.togMT1BV5 = this.togMT1BV4; - this.togMT1BV4 = 1-this.togMT1BV4; - // Block the loop buffer from main memory if appropriate - if (this.CADDR < 0x8000) { - this.blockToLoop((this.togMT1BV4 ? 4 : 5), blockFetchComplete); + while (x < words) { + this.E.set(this.CADDR); + this.CADDR = this.bcdAdd(this.CADDR, 1, 4); + this.readMemory(); + if (this.MET.value) { // invalid address + result = true; + break; // out of do-loop } else { - blockFetchComplete(); + buffer[x] = this.IB.value; + ++x; } } - - this.A.set(loop[loop.length-1]); // for display only - this.D.set(0); // for display only } - return loop; // give the loop data to the control unit -}; - -/**************************************/ -B220Processor.prototype.magTapeTerminateSend = function magTapeTerminateSend() { - /* Called by the tape control unit after the last block has been completely - written to tape. Terminates the write instruction */ - - if (this.togMT3P) { // if false, we've probably been cleared - this.togMT3P = 0; - this.togMT1BV4 = this.togMT1BV5 = 0; - this.execTime += performance.now()*B220Processor.wordsPerMilli; // restore time after I/O - this.schedule(); - } + this.C.set(this.C.value - this.C.value%0x10000 + this.CADDR); + return result; }; /**************************************/ @@ -3676,9 +3647,32 @@ B220Processor.prototype.execute = function execute() { this.operationComplete(); break; - case 0x50: //--------------------- MT* Magnetic tape search/field search/lane select/rewind - this.setProgramCheck(1); - this.operationComplete(); + case 0x50: //--------------------- MTS/MFS/MLS/MRW/MDA Magnetic tape search/field search/lane select/rewind + this.opTime = 0.160; + if (!this.magTape) { + this.setMagneticTapeCheck(true); // no tape control + this.operationComplete(); + } else { + this.selectedUnit = (this.CCONTROL >>> 12) & 0x0F; + switch (this.CCONTROL%0x10) { + case 0: case 1: case 2: case 3: // MTS/MFS: search or field search + this.setProgramCheck(true); // TEMP // + this.operationComplete(); + break; + case 4: case 5: case 6: case 7: // MLS: lane select + this.ioInitiate(); + this.magTape.laneSelect(this.D.value, this.boundMagTapeComplete); + break; + case 8: case 9: // MRW/MDA: rewind, with or without lockout + this.ioInitiate(); + this.magTape.rewind(this.D.value, this.boundMagTapeComplete); + break; + default: // should never happen + this.setProgramCheck(true); + this.operationComplete(); + break; + } // switch on operation variant + } break; case 0x51: //--------------------- MTC/MFC Magnetic tape scan/field scan @@ -3697,13 +3691,31 @@ B220Processor.prototype.execute = function execute() { break; case 0x54: //--------------------- MIW Magnetic tape initial write - this.setProgramCheck(1); - this.operationComplete(); + this.opTime = 0.160; + if (!this.magTape) { + this.setMagneticTapeCheck(true); // no tape control + this.operationComplete(); + } else { + this.selectedUnit = (this.CCONTROL >>> 12) & 0x0F; + this.CCONTROL = this.CADDR; // copy C address into control digits + this.C.set((this.CCONTROL*0x100 + this.COP)*0x10000 + this.CADDR); + this.ioInitiate(); + this.magTape.initialWrite(this.D.value, this.boundMagTapeComplete, this.boundMagTapeSendBlock); + } break; case 0x55: //--------------------- MIR Magnetic tape initial write, record - this.setProgramCheck(1); - this.operationComplete(); + this.opTime = 0.160; + if (!this.magTape) { + this.setMagneticTapeCheck(true); // no tape control + this.operationComplete(); + } else { + this.selectedUnit = (this.CCONTROL >>> 12) & 0x0F; + this.CCONTROL = this.CADDR; // copy C address into control digits + this.C.set((this.CCONTROL*0x100 + this.COP)*0x10000 + this.CADDR); + this.ioInitiate(); + this.magTape.initialWriteRecord(this.D.value, this.boundMagTapeComplete, this.boundMagTapeSendBlock); + } break; case 0x56: //--------------------- MOW Magnetic tape overwrite @@ -3716,13 +3728,47 @@ B220Processor.prototype.execute = function execute() { this.operationComplete(); break; - case 0x58: //--------------------- MPF/MPB Magnetic tape position forward/backward/at end - this.setProgramCheck(1); - this.operationComplete(); + case 0x58: //--------------------- MPF/MPB/MIE Magnetic tape position forward/backward/at end + this.opTime = 0.130; + if (!this.magTape) { + this.setMagneticTapeCheck(true); // no tape control + this.operationComplete(); + } else { + this.selectedUnit = (this.CCONTROL >>> 12) & 0x0F; + this.ioInitiate(); + switch (this.CCONTROL%0x10) { + case 1: // MPB: position tape backward + this.magTape.positionBackward(this.D.value, this.boundMagTapeComplete); + break; + case 2: // MPE: position tape at end + this.magTape.positionAtEnd(this.D.value, this.boundMagTapeComplete); + break; + default: // MPF: position tape forward + this.magTape.positionForward(this.D.value, this.boundMagTapeComplete); + break; + } // switch on operation variant + } break; case 0x59: //--------------------- MIB/MIE Magnetic tape interrogate, branch/end of tape, branch - this.setProgramCheck(1); + if (!this.magTape) { + this.opTime = 0.01; + } else if (this.magTape.controlBusy) { + this.opTime = 0.01; + } else { + opTime = 0.14; + if (this.CCONTROL%0x10 == 1) { // MIE + if (this.magTape.testUnitAtMagneticEOT(this.D.value)) { + this.P.set(this.CADDR); + this.opTime += 0.020; + } + } else { // MIB + if (this.magTape.testUnitReady(this.D.value)) { + this.P.set(this.CADDR); + this.opTime += 0.020; + } + } + } this.operationComplete(); break; @@ -3913,14 +3959,6 @@ B220Processor.prototype.execute = function execute() { } break; - case 0x52: //---------------- MTRW Magnetic Tape Rewind - if (this.magTape) { - this.selectedUnit = (this.CCONTROL >>> 4) & 0x0F; - if (this.magTape.rewind(this.selectedUnit)) { - this.OFT.set(1); // control or tape unit busy/not-ready - } - } - break; default: //---------------- (unimplemented instruction -- alarm) break; } // switch this.COP @@ -4132,6 +4170,7 @@ B220Processor.prototype.stop = function stop() { this.execLimit = 0; // kill the time slice this.SST.set(0); this.RUT.set(0); + this.AST.set(0); // Stop the timers this.clockIn(); @@ -4169,7 +4208,11 @@ B220Processor.prototype.setStop = function setStop() { the end of the Execute cycle, then stop */ if (this.poweredOn) { - this.RUT.set(0); + if (this.RUT.value) { + this.RUT.set(0); + } else { + this.stop(); + } } }; @@ -4250,7 +4293,9 @@ B220Processor.prototype.tcuClear = function tcuClear() { /* Clears the Tape Control Unit */ if (this.poweredOn) { - //?? TBD ?? + if (this.magTape) { + this.magTape.clearUnit(); + } } }; @@ -4297,6 +4342,22 @@ B220Processor.prototype.powerDown = function powerDown() { B220Processor.prototype.loadDefaultProgram = function loadDefaultProgram() { /* Loads a set of default demo programs to the memory drum */ + // TEMP // Tape tests + this.MM[ 0] = 0x1008500000; // MRW 1 + this.MM[ 1] = 0x1002580000; // MPE 1 + this.MM[ 2] = 0x1000540000; // MIW 0,1,10,100 + this.MM[ 3] = 0x1750540100; // MIW 100,1,7,50 + this.MM[ 4] = 0x1500550079; // MIR 79,1,5,00 + this.MM[ 5] = 0x1101540200; // MIW 200,1,1,1 + this.MM[ 6] = 0x1009500000; // MDA 1 + this.MM[ 7] = 0x7777009999; // HLT 9999,7777 + + this.MM[ 79] = 0x1900000000; // preface for 19 words, 80-98 + this.MM[ 99] = 0x4000000000; // preface for 40 words, 100-139 + this.MM[ 140] = 0x5800000000; // preface for 58 words, 141-198 + this.MM[ 199] = 0x9900000000; // preface for 99 words, 200-298 + this.MM[ 299] = 0x0000000000; // preface for 100 words, 300-399 + // Simple counter speed test this.MM[ 80] = 0x0000120082; // ADD 82 this.MM[ 81] = 0x0000300080; // BUN 80 diff --git a/webUI/B220.html b/webUI/B220.html index 8e08fd4..5adf915 100644 --- a/webUI/B220.html +++ b/webUI/B220.html @@ -36,14 +36,15 @@ ---> + diff --git a/webUI/B220.js b/webUI/B220.js index 7558892..da697e2 100644 --- a/webUI/B220.js +++ b/webUI/B220.js @@ -60,13 +60,11 @@ window.addEventListener("load", function() { devices.CardatronControl = null; } - /******************** if (config.getNode("MagTape.hasMagTape")) { devices.MagTapeControl = new B220MagTapeControl(processor); } else { devices.MagTapeControl = null; } - ********************/ // Control Console must be instantiated last devices.ControlConsole = new B220ControlConsole(processor, systemShutDown); diff --git a/webUI/B220CardatronControl.css b/webUI/B220CardatronControl.css index 3d9b443..5147d3c 100644 --- a/webUI/B220CardatronControl.css +++ b/webUI/B220CardatronControl.css @@ -38,7 +38,7 @@ #ClearBtnCaption { bottom: 4px; right: 4px; - width: 40px} + width: 32px} #OutputUnitLamp { top: 8px; diff --git a/webUI/B220Common.css b/webUI/B220Common.css index 7c8f08e..8f0b9e7 100644 --- a/webUI/B220Common.css +++ b/webUI/B220Common.css @@ -445,7 +445,7 @@ DIV.redButton1 { width: 14px; height: 14px; font-size: 4px; - background-image: radial-gradient(circle, #C00, #F00); + background-image: radial-gradient(circle, #C00, #E00, #F00); border-radius: 50%; border: 10px solid #CCC} DIV.redButton2 { @@ -453,7 +453,7 @@ DIV.redButton2 { width: 8px; height: 8px; font-size: 4px; - background-image: radial-gradient(circle, #C00, #F00); + background-image: radial-gradient(circle, #C00, #E00, #F00); border-radius: 50%; border: 6px solid #CCC} DIV.redButton3 { @@ -461,7 +461,7 @@ DIV.redButton3 { width: 20px; height: 20px; font-size: 4px; - background-image: radial-gradient(circle, #C00, #F00); + background-image: radial-gradient(circle, #C00, #E00, #F00); border-radius: 50%; border: 2px solid #CCC} DIV.blackButton1 { @@ -469,7 +469,7 @@ DIV.blackButton1 { width: 14px; height: 14px; font-size: 4px; - background-image: radial-gradient(circle, #000, #333); + background-image: radial-gradient(circle, #333, #111, #000); border-radius: 50%; border: 10px solid #CCC} DIV.blackButton3 { @@ -477,7 +477,7 @@ DIV.blackButton3 { width: 20px; height: 20px; font-size: 4px; - background-image: radial-gradient(circle, #000, #333); + background-image: radial-gradient(circle, #333, #111, #000); border-radius: 50%; border: 2px solid #CCC} diff --git a/webUI/B220ConsolePrinter.css b/webUI/B220ConsolePrinter.css index 9dcadd1..a1f4190 100644 --- a/webUI/B220ConsolePrinter.css +++ b/webUI/B220ConsolePrinter.css @@ -84,12 +84,14 @@ #EndOfPaper.hidden { display: none} +/***** TTY Control Panel *****/ + #FormatControlsDiv { position: absolute; overflow: visible; display: none; - left: calc(50% - 312px); - top: 48px; + left: 2px; + top: 4px; z-index: 10; height: 216px; width: 624px; diff --git a/webUI/B220ConsolePrinter.js b/webUI/B220ConsolePrinter.js index cb61868..6b8aea2 100644 --- a/webUI/B220ConsolePrinter.js +++ b/webUI/B220ConsolePrinter.js @@ -48,7 +48,7 @@ function B220ConsolePrinter(mnemonic, unitIndex, config) { this.printerLine = 0; this.printerCol = 0; this.window = window.open("../webUI/B220ConsolePrinter.html", mnemonic, - "location=no,scrollbars=no,resizable,width=640,height=300," + + "location=no,scrollbars=no,resizable,width=640,height=240," + "left=" + left + ",top=" + top); this.window.addEventListener("load", B220Util.bindMethod(this, B220ConsolePrinter.prototype.printerOnLoad)); diff --git a/webUI/B220MagTapeControl.css b/webUI/B220MagTapeControl.css new file mode 100644 index 0000000..c1ef99d --- /dev/null +++ b/webUI/B220MagTapeControl.css @@ -0,0 +1,57 @@ +/*********************************************************************** +* retro-220/webUI B220MagTapeControl.css +************************************************************************ +* Copyright (c) 2017, Paul Kimpel. +* Licensed under the MIT License, see +* http://www.opensource.org/licenses/mit-license.php +************************************************************************ +* Burroughs 220 emulator Magnetic Tape Control style sheet. +************************************************************************ +* 2017-07-09 P.Kimpel +* Original version, from retro-205 D205MagTapeControl.css. +***********************************************************************/ + +DIV.panelRegCaption { /* Overrides class in B220Common.css */ + border-top: none} + +#mtControlBody { + height: 100%; + min-height: 100%; + overflow: hidden; + padding: 0} + +#PanelSurface { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 0} + +#ClearBtn { + bottom: 60px; + left: 8px; + border: 10px solid #CCC; + box-shadow: 3px 3px 2px #999} +#ClearBtnCaption { + bottom: 48px; + left: 8px; + width: 32px} + +#MiscRegPanel { + left: 52px; + top: 4px; + width: 144px; + height: 128px} + +#CRegPanel { + left: 208px; + top: 4px; + width: 192px; + height: 128px} + +#TRegPanel { + left: 412px; + top: 4px; + width: 288px; + height: 128px} diff --git a/webUI/B220MagTapeControl.html b/webUI/B220MagTapeControl.html new file mode 100644 index 0000000..fb4bf59 --- /dev/null +++ b/webUI/B220MagTapeControl.html @@ -0,0 +1,39 @@ + + + + +retro-220 Mag Tape Control Unit + + + + + + + + + +
+
 
+
CLEAR
+ +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/webUI/B220MagTapeControl.js b/webUI/B220MagTapeControl.js new file mode 100644 index 0000000..69afba8 --- /dev/null +++ b/webUI/B220MagTapeControl.js @@ -0,0 +1,819 @@ +/*********************************************************************** +* retro-220/webUI B220MagTapeControl.js +************************************************************************ +* Copyright (c) 2017, Paul Kimpel. +* Licensed under the MIT License, see +* http://www.opensource.org/licenses/mit-license.php +************************************************************************ +* Burroughs 220 MagTape Control unit module. +************************************************************************ +* 2017-07-09 P.Kimpel +* Original version, from retro-205 D205MagTapeControl.js. +***********************************************************************/ +"use strict"; + +/**************************************/ +function B220MagTapeControl(p) { + /* Constructor for the MagTapeControl object */ + var left = 0; // control window left position + var top = 412; // control window top-from-bottom position + var u; // unit configuration object + var x; // unit index + + this.config = p.config; // System configuration object + this.mnemonic = "MCU"; + this.p = p; // B220Processor object + + // Do not call this.clear() here -- call this.clearUnit() from onLoad instead + + this.doc = null; + this.window = window.open("../webUI/B220MagTapeControl.html", this.mnemonic, + "location=no,scrollbars=no,resizable,width=712,height=144,top=" + + (screen.availHeight - top) + ",left=" + left); + this.window.addEventListener("load", + B220Util.bindMethod(this, B220MagTapeControl.prototype.magTapeOnLoad)); + + this.boundControlFinished = B220Util.bindMethod(this, B220MagTapeControl.prototype.controlFinished); + this.boundTapeUnitFinished = B220Util.bindMethod(this, B220MagTapeControl.prototype.tapeUnitFinished); + this.boundReadReceiveBlock = B220Util.bindMethod(this, B220MagTapeControl.prototype.readReceiveBlock); + this.boundWriteTerminate = B220Util.bindMethod(this, B220MagTapeControl.prototype.writeTerminate); + this.boundWriteSendBlock = B220Util.bindMethod(this, B220MagTapeControl.prototype.writeSendBlock); + this.boundWriteInitiate = B220Util.bindMethod(this, B220MagTapeControl.prototype.writeInitiate); + this.boundSearchComplete = B220Util.bindMethod(this, B220MagTapeControl.prototype.searchComplete); + + this.currentUnit = null; // stashed tape unit object + this.memoryBlockCallback = null; // stashed block-sending/receiving call-back function + this.memoryTerminateCallback = null;// stashed memory-sending terminate call-back function + this.tapeBlock = new Float64Array(101); + // block buffer for tape I/O + + /* Set up the tape units from the system configuration. These can be any + combination of Tape Storage Units (DataReaders) and DataFiles. The indexes + into this array are physical unit numbers used internally -- unit designate + is set on the tape unit itself */ + + this.tapeUnit = [ + null, // 0=not used + null, // tape unit A + null, // tape unit B + null, // tape unit C + null, // tape unit D + null, // tape unit E + null, // tape unit F + null, // tape unit G + null, // tape unit H + null, // tape unit I + null]; // tape unit J + + for (x=1; x=0; --x) { + unit = this.tapeUnit[x]; + if (unit && unit.ready) { + if (unit.unitDesignate == u) { + if (index == -1) { + index = x; + } else { + index = -2; + break; // out of for loop + } + } + } + } // for x + + return index; +}; + +/**************************************/ +B220MagTapeControl.prototype.queuePendingOperation = function queuePendingOperation(callee, args) { + /* Queues a pending tape operation */ + + //console.log(this.mnemonic + " queuePendingOperation: " + args[0].toString(16)); + if (this.pendingCallee !== null) { + throw new Error("More than one pending tape control operation"); + } + + this.pendingCallee = callee; + this.pendingArgs = args; +}; + +/**************************************/ +B220MagTapeControl.prototype.loadCommand = function loadCommand(dReg, releaseProcessor, callee, args) { + /* If the control unit or the tape unit addressed by the unit field in dReg + are currently busy, queues the args parameter (an Arguments object) in + this.pendingCallee and -Args, and returns false. If the control is idle but + the tape unit is not ready or not present, or two units have the same designate, + calls the releaseProcessor call-back and returns false. If the control and + tape unit are ready for their next operation, loads the contents of the processor's + D register passed to the operation routines into the T, C, and MISC registers. + Sets this.unitNr, this.unitIndex, this.blockCount, and this.blockWords from + the digits in T. Then returns true */ + var c; // scratch + var t = dReg%0x10000000000; // scratch + var ux; // internal unit index + var result = false; // return value + + //console.log(this.mnemonic + " loadCommand: " + dReg.toString(16)); + if (this.controlBusy) { + this.queuePendingOperation(callee, args); + } else { + this.MISC = 0; + this.T = t; + this.unitNr = (t - t%0x1000000000)/0x1000000000; + t = (t - t%0x10000)/0x10000; + c = t%0x10; // low-order digit of op code + t = (t - t%0x100)/0x100; // control digits from instruction + this.blockWords = t%0x100; + this.blockCount = ((t - this.blockWords)/0x100)%0x10; + if (this.blockWords == 0) { + this.blockWords = 100; + } else { + this.blockWords = B220Processor.bcdBinary(this.blockWords); + } + + this.C = this.unitNr*0x100000 + t*0x10 + c; + this.regMisc.update(this.MISC); + this.regC.update(this.C); + this.regT.update(this.T); + this.unitIndex = ux = this.findDesignate(this.unitNr); + if (ux < 0) { + setCallback(this.mnemonic, this, 0, releaseProcessor, true); + } else if (this.tapeUnit[ux].busy) { + this.queuePendingOperation(callee, args); + } else { + result = true; + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.controlFinished = function controlFinished(alarm) { + /* Releases the busy status of the control. Typically used as a timed call- + back to simulate the amount of time the control unit is busy with an I/O. + If alarm is true, sets the Processor's Magnetic Tape Check alarm. + If another operation is pending, initiates that operation */ + var args; // pending Arguments object + var callee; // pending method to call + + //console.log(this.mnemonic + " controlFinished: " + alarm + ", busy=" + this.controlBusy); + if (alarm) { + this.p.setMagneticTapeCheck(true); + } + + this.controlBusy = false; + if (this.pendingCallee !== null) { + callee = this.pendingCallee; + args = this.pendingArgs; + this.pendingCallee = this.pendingArgs = null; + callee.apply(this, args); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.tapeUnitFinished = function tapeUnitFinished(alarm) { + /* Call-back function passed to tape unit methods to signal when the unit has + completed its asynchronous operation */ + + if (!this.controlBusy) { // if the control unit is currently idle... + this.controlFinished(alarm); // initiate any pending operation + } +}; + +/**************************************/ +B220MagTapeControl.prototype.decrementBlockCount = function decrementBlockCount() { + /* Decrements the block-count digit in the C register. Returns true if the + remaining block count is non-zero, false otherwise */ + var c1 = this.C; // will hold two high-order digits of C: uu0000 + var c2 = c1%0x10000; // four low-order digits of C: bddd, b=block count + var result = true; + + c1 -= c2; + if (c2 < 0x1000) { + c2 += 0x9000; // decrement 0x0ddd to 0x9ddd as count=0 => 10 + } else { + c2 -= 0x1000; // decrement 0xddd..0x9ddd + if (c2 < 0x1000) { + result = false; // decremented 0x1ddd to 0x0ddd: no more blocks + } + } + + this.C = c1+c2; + this.regC.update(this.C); + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.ClearBtn_onClick = function ClearBtn_onClick(ev) { + /* Handle the click event for the tape control CLEAR button */ + + this.clearUnit(); +}; + +/**************************************/ +B220MagTapeControl.prototype.beforeUnload = function beforeUnload(ev) { + var msg = "Closing this window will make the panel unusable.\n" + + "Suggest you stay on the page and minimize this window instead"; + + ev.preventDefault(); + ev.returnValue = msg; + return msg; +}; + +/**************************************/ +B220MagTapeControl.prototype.magTapeOnLoad = function magTapeOnLoad() { + /* Initializes the MagTape Control window and user interface */ + var body; + var box; + var e; + var x; + + this.doc = this.window.document; + body = this.$$("PanelSurface"); + + // Misc Register + this.regMisc = new PanelRegister(this.$$("MiscRegPanel"), 4*4, 4, "Misc_", " "); + this.regMisc.lamps[15].setCaption("MCL", true); + this.regMisc.lamps[14].setCaption("MC6", true); + this.TYC1Lamp = this.regMisc.lamps[13]; + this.TYC1Lamp.setCaption("TYC", true); + this.TYC2Lamp = this.regMisc.lamps[12]; + this.TYC2Lamp.setCaption("TYC", true); + this.TCFLamp = this.regMisc.lamps[10]; // not in this physical position on a 220 + this.TCFLamp.setCaption("TCF", true); + this.TPCLamp = this.regMisc.lamps[7]; + this.TPCLamp.setCaption("TPC", true); + this.regMisc.lamps[6].setCaption("TSX", true); + this.regMisc.lamps[5].setCaption("1R6", true); + this.TX1Lamp = this.regMisc.lamps[4]; + this.TX1Lamp.setCaption("TX1", true); + this.TX10Lamp = this.regMisc.lamps[3]; + this.TX10Lamp.setCaption("TX10", true); + this.TX8Lamp = this.regMisc.lamps[2]; + this.TX8Lamp.setCaption("TX8", true); + this.TX4Lamp = this.regMisc.lamps[1]; + this.TX4Lamp.setCaption("TX4", true); + this.TX2Lamp = this.regMisc.lamps[0]; + this.TX2Lamp.setCaption("TX2", true); + + // C Register + this.regC = new PanelRegister(this.$$("CRegPanel"), 6*4, 4, "C_", "C"); + + // T Register + this.regT = new PanelRegister(this.$$("TRegPanel"), 10*4, 4, "T_", "T"); + + + // Events + + this.window.addEventListener("beforeunload", B220MagTapeControl.prototype.beforeUnload); + this.$$("ClearBtn").addEventListener("click", + B220Util.bindMethod(this, B220MagTapeControl.prototype.ClearBtn_onClick)); + + this.clearUnit(); +}; + +/**************************************/ +B220MagTapeControl.prototype.readTerminate = function readTerminate() { + /* Terminates the read operation, sets the control to not-busy, and signals + the processor we are finished with the I/O */ + + this.controlBusy = false; + this.currentUnit.readTerminate(); +}; + +/**************************************/ +B220MagTapeControl.prototype.readReceiveBlock = function readReceiveBlock(block, abortRead) { + /* Receives the next block read by the tape unit. Sends the block to the + processor for storage in memory, updates the block counter, and if not + finished, requests the next block from the tape. Termination is a little + tricky here, as readTerminate() must be called to release the drive before + the block is stored in memory (and p.executeComplete() called to advance to + the next instruction, but if the memory call-back tells us the processor + has been cleared, we must release the drive after the attempt to store the + block in memory. Mess with the sequencing below at your peril */ + var lastBlock; + var t = B220Processor.bcdBinary(this.T); + + if (abortRead) { + this.readTerminate(); + this.memoryBlockCallback(null, true); + } else { + // Decrement the block counter in the T register: + t = (t + 990)%1000; // subtract 1 from the counter field without overflow + this.T = B220Processor.binaryBCD(t); + this.regT.update(this.T); + + // If there are more blocks to read, request the next one + lastBlock = (t < 10); + if (lastBlock) { + this.readTerminate(); + abortRead = this.memoryBlockCallback(block, true); + } else { + abortRead = this.memoryBlockCallback(block, false); + if (abortRead) { // processor was cleared + this.readTerminate(); + } else { // at least one block left to go + this.currentUnit.readBlock(this.boundReadReceiveBlock); + } + } + } +}; + +/**************************************/ +B220MagTapeControl.prototype.read = function read(unitNr, blocks, blockSender) { + /* Initiates a read on the designated unit. "blocks" is the number of blocks + to read in BCD; "blockSender" is the call-back function to send a block of data + to the processor. "terminator" is the call-back function to tell the Processor + the I/O is finished. Returns true if the control is still busy with another command + or the unit is busy, and does not do the read */ + var result = false; // return value + var unit; // tape unit object + var ux; // internal unit index + + if (this.controlBusy) { + result = true; + } else { + this.C = unitNr; + this.regC.update(unitNr); + ux = this.findDesignate(unitNr); + if (ux < 0) { + result = true; + } else { + this.controlBusy = true; + this.currentUnit = unit = this.tapeUnit[ux]; + this.MISC = B220Processor.binaryBCD(unit.laneNr); + this.regMisc.update(this.MISC); + this.T = blocks*0x10 + unitNr; + this.regT.update(this.T); + this.memoryBlockCallback = blockSender; + result = unit.readInitiate(this.boundReadReceiveBlock); + if (result) { + this.controlBusy = false; + } + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.writeTerminate = function writeTerminate(abortWrite) { + /* Called by the drive after the last block is written to release the + control unit and terminate the I/O. Note that "abortWrite" is not used, but + exists for signature compatibility with writeSendBlock() */ + + this.recordLamp.set(0); + this.controlBusy = false; + this.memoryTerminateCallback(); +}; + +/**************************************/ +B220MagTapeControl.prototype.writeSendBlock = function writeSendBlock(abortWrite) { + /* Called by the tape drive when it is ready for the next block to be written. + Retrieves the next buffered block from the Processor and passes it to the drive. + Unless this is the last block to write, the drive will call this again after + tape motion is complete. Note that this.memoryBlockCallback() will return true + if the processor has been cleared and the I/O must be aborted */ + var aborted; // true if processor aborted the I/O + var lastBlock = abortWrite; // true if this will be the last block + var t = B220Processor.bcdBinary(this.T); + + // First, decrement the block counter in the T register: + t = (t + 990)%1000; // subtract 1 from the counter field without overflow + this.T = B220Processor.binaryBCD(t); + this.regT.update(this.T); + if (t < 10) { + lastBlock = true; + } + + aborted = this.memoryBlockCallback(this.tapeBlock, lastBlock); + if (abortWrite || aborted) { + this.writeTerminate(false); + } else if (lastBlock) { + this.currentUnit.writeBlock(this.tapeBlock, this.boundWriteTerminate, true); + } else { + this.currentUnit.writeBlock(this.tapeBlock, this.boundWriteSendBlock, false); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.writeInitiate = function writeInitiate(blockReceiver, terminator) { + /* Call-back function called by the Processor once the initial block to be + written is buffered in one of the high-speed loops. Once this block is + buffered, the drive can start tape motion and begin writing to tape */ + + this.memoryBlockCallback = blockReceiver; + this.memoryTerminateCallback = terminator; + this.currentUnit.writeInitiate(this.boundWriteSendBlock); +}; + +/**************************************/ +B220MagTapeControl.prototype.write = function write(unitNr, blocks, receiveInitiate) { + /* Initiates a write on the designated unit. "blocks" is the number of blocks + to write in BCD; "receiveInitiate" is the call-back function to begin memory + transfer from the processor. Returns true if the control is still busy with + another command or the unit is busy, and does not do the write */ + var result = false; // return value + var unit; // tape unit object + var ux; // internal unit index + + if (this.controlBusy) { + result = true; + } else { + this.C = unitNr; + this.regC.update(unitNr); + ux = this.findDesignate(unitNr); + if (ux < 0) { + result = true; + } else { + this.controlBusy = true; + this.currentUnit = unit = this.tapeUnit[ux]; + this.MISC = B220Processor.binaryBCD(unit.laneNr); + this.regMisc.update(this.MISC); + this.T = blocks*0x10 + unitNr; + this.regT.update(this.T); + result = unit.writeReadyTest(); + if (result) { + this.controlBusy = false; + } else { + receiveInitiate(this.boundWriteInitiate); + } + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.searchComplete = function searchComplete(success) { + /* Resets the busy status at the completion of a search */ + var d; // scratch digit + + if (success) { + // rotate T one digit right at end of successful search + d = this.T % 0x10; + this.T = d*0x1000 + (this.T - d)/0x10; + this.regT.update(this.T); + } + + this.controlBusy = false; +}; + +/**************************************/ +B220MagTapeControl.prototype.search = function search(unitNr, laneNr, addr) { + /* Initiates a search on the designated unit. "laneNr" is the lane number in + BCD; "addr" is the number of the block to search for in BCD. The search + Takes place in the control unit and drive independently of the processor. + Returns true if the control is still busy with another command or the unit + is busy, and does not do the search */ + var block = B220Processor.bcdBinary(addr); + var lane = B220Processor.bcdBinary(laneNr); + var result = false; // return value + var unit; // tape unit object + var ux; // internal unit index + + if (this.controlBusy) { + result = true; + } else { + this.C = unitNr; + this.regC.update(unitNr); + this.MISC = laneNr; + this.regMisc.update(laneNr); + this.T = addr; + this.regT.update(addr); + ux = this.findDesignate(unitNr); + if (ux < 0) { + result = true; + } else { + this.controlBusy = true; + unit = this.tapeUnit[ux]; + result = unit.search(lane, block, this.boundSearchComplete, + this.boundDirectionLampSet, this.boundTestDisabled); + if (result) { + this.controlBusy = false; + } + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.initialWrite = function initialWrite(dReg, releaseProcessor, fetchBlock) { + /* Initial-writes the number of blocks and of the size indicated in dReg. + This routine is used by MIW */ + var alarm = false; // error result + var blocksLeft = true; // true => more blocks to process + var that = this; // local self-reference + var unit = null; // selected tape unit + + function blockReady(alarm, writeBlock, completed) { + /* Call-back function when the drive is ready to receive the next block + of data, or when it has encountered an error such as EOT. If there are + more blocks to write, fetches the next block from the Processor and calls + the drive's "writeBlock" function, otherwise calls "completed" to finish + the operation */ + + if (alarm) { + setCallback(that.mnemonic, that, 0, releaseProcessor, true); + completed(true); // drive detected an error + } else if (blocksLeft) { + blocksLeft = that.decrementBlockCount(); // set to false on last block + if (that.blockWords < unit.minBlockWords && that.blockWords > 1) { + completed(true); // invalid block size + } else if (fetchBlock(that.tapeBlock, that.blockWords)) { + completed(true); // Processor cleared or memory address error + } else { + writeBlock(that.tapeBlock, that.blockWords); // write next block + } + } else { + setCallback(that.mnemonic, that, 0, releaseProcessor, false); + completed(false); // normal termination + } + } + + if (this.loadCommand(dReg, releaseProcessor, initialWrite, arguments)) { + this.controlBusy = true; + unit = this.tapeUnit[this.unitIndex]; + alarm = unit.initialWriteBlock(blockReady, this.boundControlFinished); + if (alarm) { + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + controlFinished(true); + } + } +}; + +/**************************************/ +B220MagTapeControl.prototype.initialWriteRecord = function initialWriteRecord(dReg, releaseProcessor, fetchBlock) { + /* Initial-writes the number of blocks indicated in dReg. Block lengths + (preface words) are taken from the word in memory preceding the data to be + written. This routine is used by MIR */ + var alarm = false; // error result + var blocksLeft = true; // true => more blocks to process + var that = this; // local self-reference + var unit = null; // selected tape unit + + function blockReady(alarm, writeBlock, completed) { + /* Call-back function when the drive is ready to receive the next block + of data, or when it has encountered an error such as EOT. If there are + more blocks to write, fetches the next block and its size from the + Processor and calls the drive's "writeBlock" function, otherwise calls + "completed" to finish the operation */ + var words; // words to write for block + + if (alarm) { + setCallback(that.mnemonic, that, 0, releaseProcessor, true); + completed(true); // drive detected an error + } else if (blocksLeft) { + blocksLeft = that.decrementBlockCount(); // set to false on last block + if (fetchBlock(that.tapeBlock, 1)) { // fetch the preface word + completed(true); // Processor cleared or memory address error + } else { + words = that.tapeBlock[0]; // convert preface to binary + words = ((words - words%0x100000000)/0x100000000)%0x100; + if (words) { + words = (words >>> 4)*10 + words%0x10; + } else { + words = 100; // preface == 0 => 100 + } + + if (words < unit.minBlockWords && words > 1) { + completed(true); // invalid block size + } else if (fetchBlock(that.tapeBlock, words)) { + completed(true); // Processor cleared or memory address error + } else { + writeBlock(that.tapeBlock, words); // write next block + } + } + } else { + setCallback(that.mnemonic, that, 0, releaseProcessor, false); + completed(false); // normal termination + } + } + + if (this.loadCommand(dReg, releaseProcessor, initialWriteRecord, arguments)) { + this.controlBusy = true; + unit = this.tapeUnit[this.unitIndex]; + alarm = unit.initialWriteBlock(blockReady, this.boundControlFinished); + if (alarm) { + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + controlFinished(true); + } + } +}; + +/**************************************/ +B220MagTapeControl.prototype.positionForward = function positionForward(dReg, releaseProcessor) { + /* Positions the tape forward the number of blocks indicated in dReg */ + var alarm = false; // error result + var that = this; // local self-reference + var unit = null; // selected tape unit + + function blockFinished(nextBlock, completed) { + /* Call-back function when the drive has finished spacing one block + forward. If there are more blocks to space, calls "nextBlock", otherwise + calls "completed" to finish the operation */ + + if (that.decrementBlockCount()) { + nextBlock(blockFinished); + } else { + completed(false); + } + } + + if (this.loadCommand(dReg, releaseProcessor, positionForward, arguments)) { + this.controlBusy = true; + unit = this.tapeUnit[this.unitIndex]; + alarm = unit.positionForward(blockFinished, this.boundControlFinished); + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.positionBackward = function positionBackward(dReg, releaseProcessor) { + /* Positions the tape backward the number of blocks indicated in dReg */ + var alarm = false; // error result + var that = this; // local self-reference + var unit = null; // selected tape unit + + function blockFinished(nextBlock, completed) { + /* Call-back function when the drive has finished spacing one block + backward. If there are more blocks to space, calls "nextBlock", otherwise + calls "completed" to finish the operation */ + + if (that.decrementBlockCount()) { + nextBlock(blockFinished); + } else { + completed(false); + } + } + + if (this.loadCommand(dReg, releaseProcessor, positionBackward, arguments)) { + this.controlBusy = true; + unit = this.tapeUnit[this.unitIndex]; + alarm = unit.positionBackward(blockFinished, this.boundControlFinished); + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.positionAtEnd = function positionAtEnd(dReg, releaseProcessor) { + /* Positions the tape to the end of recorded information (i.e., when a gap + longer than inter-block gap is detected. Leaves the tape at the end of the + prior recorded block */ + var alarm = false; // error result + + if (this.loadCommand(dReg, releaseProcessor, positionAtEnd, arguments)) { + this.controlBusy = true; + alarm = this.tapeUnit[this.unitIndex].positionAtEnd(this.boundControlFinished); + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.laneSelect = function laneSelect(dReg, releaseProcessor) { + /* Selects the tape lane of the designated unit. Returns an alarm if the + unit does not exist or is not ready */ + var alarm = false; // error result + var laneNr; // lane to select (0, 1) + + if (this.loadCommand(dReg, releaseProcessor,laneSelect, arguments)) { + this.controlBusy = true; + laneNr = ((this.C - this.C%0x100)/0x100)%2; + alarm = this.tapeUnit[this.unitIndex].laneSelect(laneNr, this.boundControlFinished); + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.rewind = function rewind(dReg, releaseProcessor) { + /* Initiates rewind of the designated unit. Returns an alarm if the unit + does not exist or is not ready */ + var alarm = false; // error result + var laneNr; // lane to select (0, 1) + var lockout; // lockout after rewind (0, 1) + + if (this.loadCommand(dReg, releaseProcessor, rewind, arguments)) { + this.controlBusy = true; + laneNr = ((this.C - this.C%0x100)/0x100)%2; + lockout = ((this.C - this.C%0x10)/0x10)%2; + alarm = this.tapeUnit[this.unitIndex].rewind(laneNr, lockout, this.boundTapeUnitFinished); + setCallback(this.mnemonic, this, 50, this.controlFinished, false); + setCallback(this.mnemonic, this, 0, releaseProcessor, alarm); + } +}; + +/**************************************/ +B220MagTapeControl.prototype.testUnitReady = function testUnitReady(dReg) { + /* Interrogates status of the designated unit. Returns true if ready */ + var result = false; // return value + var ux; // internal unit index + + ux = ((dReg - dReg%0x1000000000)/0x1000000000)%0x10; + ux = this.findDesignate(ux); + if (ux >= 0) { + if (this.tapeUnit[ux].ready) { + if (!this.tapeUnit[ux].busy) { + result = true; + } + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.testUnitAtMagneticEOT = function testUnitAtMagneticEOT(dReg) { + /* Interrogates status of the designated unit. Returns true if ready and at + Magnetic-End-of-Tape */ + var result = false; // return value + var ux; // internal unit index + + ux = ((dReg - dReg%0x1000000000)/0x1000000000)%0x10; + ux = this.findDesignate(ux); + if (ux >= 0) { + if (this.tapeUnit[ux].ready) { + if (this.tapeUnit[ux].atEOT) { + result = true; + } + } + } + + return result; +}; + +/**************************************/ +B220MagTapeControl.prototype.clearUnit = function clearUnit() { + /* Clears the internal state of the control unit */ + + this.clear(); + this.regMisc.update(this.MISC); + this.regC.update(this.C); + this.regT.update(this.T); +}; + +/**************************************/ +B220MagTapeControl.prototype.shutDown = function shutDown() { + /* Shuts down the panel */ + var x; + + if (this.tapeUnit) { + for (x=this.tapeUnit.length-1; x>=0; --x) { + if (this.tapeUnit[x]) { + this.tapeUnit[x].shutDown(); + this.tapeUnit[x] = null; + } + } + } + + this.window.removeEventListener("beforeunload", B220MagTapeControl.prototype.beforeUnload); + this.window.close(); +}; \ No newline at end of file diff --git a/webUI/B220MagTapeDrive.css b/webUI/B220MagTapeDrive.css new file mode 100644 index 0000000..a85b146 --- /dev/null +++ b/webUI/B220MagTapeDrive.css @@ -0,0 +1,294 @@ +/*********************************************************************** +* retro-220/webUI B220MagTapeDrive.css +************************************************************************ +* Copyright (c) 2017, Paul Kimpel. +* Licensed under the MIT License, see +* http://www.opensource.org/licenses/mit-license.php +************************************************************************ +* Burroughs 220 emulator Magnetic Tape Drive +* web interface style sheet. +************************************************************************ +* 2017-07-09 P.Kimpel +* Original version, from retro-205 D205MagTapeDrive.css. +***********************************************************************/ + +#magTapeBody { + height: 100%; + min-height: 100%; + overflow: hidden; + padding: 0} + +#MTDiv { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 0px} + +DIV.annunciator { + color: #F93; + padding-top: 4px; + padding-bottom: 4px; + text-align: center; + position: absolute} + +DIV.annunciatorLit { + background-color: black} + +SELECT { + text-align: center} + + +#RWLLamp { + top: 18px; + left: 10px; + box-shadow: 3px 3px 2px #999} +#RWLLampCaption { + top: 10px; + left: 0px; + width: 48px} + +#NotWriteLamp { + top: 18px; + left: 58px; + box-shadow: 3px 3px 2px #999} +#NotWriteLampCaption { + top: 10px; + left: 48px; + width: 48px} + +#NotReadyLamp { + top: 18px; + left: 106px; + box-shadow: 3px 3px 2px #999} +#NotReadyLampCaption { + top: 10px; + left: 96px; + width: 48px} + +#DesignatedLamp { + top: 18px; + left: 154px; + box-shadow: 3px 3px 2px #999} +#DesignatedLampCaption { + top: 3px; + left: 144px; + width: 48px} + +#TransportPowerCaption { + top: 4px; + left: 192px; + width: 96px} + +#TransportOnLamp { + top: 18px; + left: 202px; + box-shadow: 3px 3px 2px #999} +#TransportOnLampCaption { + top: 10px; + left: 192px; + width: 48px} + +#TransportStandbyLamp { + top: 18px; + left: 250px; + box-shadow: 3px 3px 2px #999} +#TransportStandbyLampCaption { + top: 10px; + left: 240px; + width: 48px} + +#LoadBtn { + top: 64px; + left: 12px; + box-shadow: 3px 3px 2px #999} +#LoadBtnCaption { + top: 56px; + left: 0px; + width: 48px} + +#UnloadBtn { + top: 64px; + left: 60px; + box-shadow: 3px 3px 2px #999} +#UnloadBtnCaption { + top: 56px; + left: 48px; + width: 48px} + +#RewindBtn { + top: 64px; + left: 108px; + box-shadow: 3px 3px 2px #999} +#RewindBtnCaption { + top: 56px; + left: 96px; + width: 48px} + +#TransportPowerBtnCaption { + top: 58px; + left: 192px; + width: 96px} + +#TransportOnBtn { + top: 64px; + left: 204px; + box-shadow: 3px 3px 2px #999} +#TransportOnBtnCaption { + top: 56px; + left: 192px; + width: 48px} + +#TransportStandbyBtn { + top: 64px; + left: 252px; + box-shadow: 3px 3px 2px #999} +#TransportStandbyBtnCaption { + top: 56px; + left: 240px; + width: 48px} + +#RWLRBtn { + top: 104px; + left: 12px; + box-shadow: 3px 3px 2px #999} +#RWLRBtnCaption { + top: 96px; + left: 0px; + width: 48px} + +#WriteBtn { + top: 104px; + left: 60px; + box-shadow: 3px 3px 2px #999} +#WriteBtnCaption { + top: 96px; + left: 48px; + width: 48px} + +#NotWriteBtn { + top: 104px; + left: 108px; + box-shadow: 3px 3px 2px #999} +#NotWriteBtnCaption { + top: 96px; + left: 96px; + width: 48px} + +#UnitDesignate { + position: absolute; + top: 106px; + left: 204px; + width: 72px; + box-shadow: 3px 3px 2px #999} +#UnitDesignateCaption { + top: 96px; + left: 192px; + width: 96px} + + +#MTReel { + position: absolute; + height: 48px; + visibility: hidden; + border-radius: 50%; + top: 8px; + right: 16px} + +#MTUnloadedLight { + top: 60px; + right: 8px; + width: 64px} + +#MTAtBOTLight { + top: 78px; + right: 8px; + width: 64px} + +#MTAtEOTLight { + top: 96px; + right: 8px; + width: 64px} + +#MTRewindingLight { + top: 114px; + right: 8px; + width: 64px} + +#MTStatusDiv { + position: absolute; + bottom: 4px; + left: 8px; + right: 8px} + +#MTFileName { + width: 100%; + font-family: DejaVuSansWeb, sans-serif; + color: black; + background-color: inherit; + border: none} + +#MTReelBar { + margin-top: 4px; + width: 100%; + height: 16px; + border: 1px solid white} + +#MTReelBarCaption { + position: absolute; + right: 4px; + top: 24px; + z-index: 1; + height: 12px; + color: black; + font-weight: bold} + +/* Tape Load Panel */ + +#MTLoadFileGroup { + position: absolute; + top: 8px; + left: 8px; + right: 8px} + +#MTLoadFileSelector { + border: 1px solid white; + color: white; + width: 100%} + +#MTLoadFormatGroup { + position: absolute; + top: 32px; + left: 0} + +#MTLoadFormatSelect { + border: 1px solid white} + +#MTLoadWriteEnableCheck { + border: 1px solid white} + +#MTLoadNotes { + position: absolute; + top: 60px; + width: 300px; + left: 0} + +#MTLoadButtonGroup { + position: absolute; + top: 60px; + right: 0} + +#MTLoadCancelBtn { + background-image: none; + background-color: #900; + height: 20px; + bottom: 0; + right: 64px} + +#MTLoadOKBtn { + background-image: none; + background-color: #060; + height: 20px; + bottom: 0; + right: 0} diff --git a/webUI/B220MagTapeDrive.html b/webUI/B220MagTapeDrive.html new file mode 100644 index 0000000..20c6197 --- /dev/null +++ b/webUI/B220MagTapeDrive.html @@ -0,0 +1,102 @@ + + + + +220 Emulator Magnetic Tape Drive + + + + + + + + + + +
+ +
RWL
+ +
NOT WRITE
+ +
NOT READY
+ +
UNIT
DESIGNATE
+ +
TRANSPORT POWER
+ +
ON
+ +
STANDBY
+ +
 
+
LOAD
+ +
 
+
UNLOAD
+ +
 
+
REWIND
+ + +
UNIT DESIGNATE
+ +
 
+
RWLR
+ +
 
+
WRITE
+ +
 
+
NOT WRITE
+ +
 
+
ON
+ +
 
+
ST'DBY
+ + + +
UNLOADED
+
AT BOT
+
AT EOT
+
REWINDING
+ +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/webUI/B220MagTapeDrive.js b/webUI/B220MagTapeDrive.js new file mode 100644 index 0000000..b884352 --- /dev/null +++ b/webUI/B220MagTapeDrive.js @@ -0,0 +1,1899 @@ +/*********************************************************************** +* 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. Each field +* on the line represents one word for the tape image. The first field on the +* line represents the lane number -- only its low-order bit is significant (0/1). +* The second field is the preface word indicating the length of the data. A +* block length of 100 is represented as either 0 or 100. Values greater than 100 +* or less than zero will be treated as 100. +* +* Note that with this representation, it is possible that the 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 remaining words 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. 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. The lane number and preface word will +* written, however. +* +************************************************************************ +* 2017-07-09 P.Kimpel +* Original version, from retro-205 D205MagTapeDrive.js. +***********************************************************************/ +"use strict"; + +/**************************************/ +function B220MagTapeDrive(mnemonic, unitIndex, 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.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.doc = null; + this.window = window.open("../webUI/B220MagTapeDrive.html", mnemonic, + "location=no,scrollbars=no,resizable,width=384,height=184,left=0,top=" + y); + this.window.addEventListener("load", + B220Util.bindMethod(this, B220MagTapeDrive.prototype.tapeDriveOnload), false); +} + +// 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.maxTapeWords = Math.floor(B220MagTapeDrive.prototype.maxTapeInches*B220MagTapeDrive.prototype.density/12); + // max words on a tape (12 digits/word) +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.repositionWords = B220MagTapeDrive.prototype.endOfBlockWords + 2; + // number of words to reposition backwards after a turnaround +B220MagTapeDrive.prototype.startTime = 3; + // tape start time [ms] +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.nil = function nil() { + /* An empty function that just returns */ + + return; +}; + +/**************************************/ +B220MagTapeDrive.prototype.releaseUnit = function releaseUnit(releaseControl, alarm) { + /* Releases the busy status of the unit. Typically used as a timed call- + back to simulate the amount of time the unit is busy with an I/O */ + + this.busy = false; + this.designatedLamp.set(0); + if (releaseControl) { + releaseControl(alarm); + } +}; + +/**************************************/ +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.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) { + /* 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 no parameters. */ + var delayLeft = Math.abs(delay); // milliseconds left to delay + var direction = (inches < 0 ? -1 : 1); + var inchesLeft = inches; // inches left to move tape + var lastStamp = performance.now(); // last timestamp for spinDelay + + function spinFinish() { + this.timer = 0; + if (inchesLeft != 0) { + this.spinReel(inchesLeft); + } + successor.call(this); + } + + 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 ((delayLeft -= interval) > this.spinUpdateInterval) { + lastStamp = stamp; + this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinDelay); + } else { + this.timer = setCallback(this.mnemonic, this, delayLeft, spinFinish); + } + motion = inches*interval/delay; + if (inchesLeft*direction <= 0) { // inchesLeft crossed zero + motion = inchesLeft = 0; + } else if (motion*direction <= inchesLeft*direction) { + inchesLeft -= motion; + } else { + motion = inchesLeft; + inchesLeft = 0; + } + + this.spinReel(motion); + } + + spinDelay.call(this); +}; + +/**************************************/ +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) { + B220Util.removeClass(this.$$("MTAtBOTLight"), "annunciatorLit"); + } else { + this.imgIndex = 0; + this.tapeInches = 0; + this.reelAngle = 0; + B220Util.addClass(this.$$("MTAtBOTLight"), "annunciatorLit"); + this.reelBar.value = this.maxTapeInches; + this.reelIcon.style.transform = "none"; + } + } +}; + +/**************************************/ +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) { + B220Util.removeClass(this.$$("MTAtEOTLight"), "annunciatorLit"); + } else { + B220Util.addClass(this.$$("MTAtEOTLight"), "annunciatorLit"); + this.reelBar.value = 0; + } + } +}; + +/**************************************/ +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.busy = false; // forced reset + this.designatedLamp.set(0); + } + + if (enabled) { + if (this.remote) { + this.tapeState = this.tapeRemote; + } else { + this.busy = false; // forced reset + this.tapeState = this.tapeLocal; + } + } +}; + +/**************************************/ +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.setTapeReady(false); + this.setAtBOT(false); + this.setAtEOT(false); + this.reelBar.value = 0; + this.reelIcon.style.visibility = "hidden"; + this.$$("MTFileName").value = ""; + B220Util.addClass(this.$$("MTUnloadedLight"), "annunciatorLit"); + if (this.timer) { + clearCallback(this.timer); + this.timer = 0; + } + } +}; + +/**************************************/ +B220MagTapeDrive.prototype.findBlockStart = 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 = this.imgLength; // physical end of tape index + var lane = this.image[this.laneNr]; // image data for current lane + var result = this.markerGap; // function result + var state = 1; // FSA state variable + var w; // current image word + var x = this.imgIndex; // lane image word index + + while (x < imgLength) { + w = lane[x++]; + switch (state) { + case 1: // search for inter-block gap word + if (w == this.markerGap) { + state = 2; + } else if (w == this.markerMagEOT) { + result = w; // return the EOT word + this.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 + this.imgIndex = x; // point to first data word in block + x = imgLength; // kill the loop + } else if (w != this.markerGap) { + result = w; // return whatever marker word was found + this.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.findBlockStart, " + state); + break; + } // switch state + } // while x + + return result; +}; + +/**************************************/ +B220MagTapeDrive.prototype.writeBlockStart = 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 this.imgIndex: 3 gap words + the + preface word with the binary block length */ + var x; + + for (x=0; x 0) { + v = parseInt(text, 16); // parse as hex (BCD) + if (!isNaN(v)) { + if (v > 0) { + w = v % 0x100000000000; + } else if (v < 0) { + // The number was specified as negative: if the + // sign bit is not already set, then set it. + w = (-v) % 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 = wx = 0; + mt.laneNr = parseWord()%2; // get the lane number + preface = parseWord(); // get the preface word as hex BCD + mt.imgIndex = lx[mt.laneNr]; // restore current offset for this lane + if (preface > 0x100) { + blockLen = 100; // limit blocks to 100 words + } else if (preface > 0) { + blockLen = B220Processor.bcdBinary(preface); + } else { // if the block length is 0, make it 100 + blockLen = 100; + } + + mt.writeBlockStart(blockLen); + lane = mt.image[mt.laneNr]; + while (tx < chunkLength && wx < blockLen) { + lane[mt.imgIndex+wx] = parseWord(); + ++wx; + } // while + + if (blockLen == 1) { + blockLen = 10; // pad out end-of-tape blocks to 10 words + } + + while (wx < blockLen) { + lane[mt.imgIndex+wx] = 0; + ++wx; + } // while + + mt.imgIndex += blockLen; + mt.writeBlockEnd(); + lx[mt.laneNr] = mt.imgIndex; // save current offset for this lane + } + } + } while (index < bufLength); + + // 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 (x=0; x 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 element to receive tape data + var w; // current image word + var wx; // word index within block + var x = 0; // image data index + + doc = win.document; + doc.title = "retro-220 " + mt.mnemonic + " Unload Tape"; + tape = doc.getElementById("Paper"); + while (tape.firstChild) { // delete any existing
 content
+            tape.removeChild(tape.firstChild);
+        }
+
+        for (lx=0; lx<2; ++lx) {
+            mt.laneNr = lx;
+            lane = mt.image[lx];
+            state = 1;
+            x = 0;
+            while (x < imgLength) {
+                switch (state) {
+                case 1: // Search for start of block
+                    nzw = 0;
+                    mt.imgIndex = x;
+                    w = mt.findBlockStart();
+                    if (w < 0) {        // done with this lane
+                        x = imgLength;  // kill the loop
+                    } else {            // format the preface word
+                        buf = lx.toString(10) + "," + w.toString(10);
+                        x = mt.imgIndex;
+                        state = 2;
+                    }
+                    break;
+                case 2: // Record the block data words
+                    w = lane[x++];
+                    if (w == 0) {
+                        ++nzw;          // suppress trailing zero words
+                    } else if (w >= 0) {
+                        while (nzw > 0) {
+                            --nzw;      // restore non-trailing zero words
+                            buf += ",0";
+                        }
+                        buf += "," + w.toString(16);
+                    } else {
+                        tape.appendChild(doc.createTextNode(buf + "\n"));
+                        state = 1;      // reset for next block
+                    }
+                    break;
+                default:
+                    x = imgLength;      // kill the loop
+                    throw new Error("Invalid state: B220MagTapeDrive.unloadTape, " + state);
+                    break;
+                } // switch state
+            } // while not at end of lane
+        } // for lx
+
+        mt.setTapeUnloaded();
+    }
+
+    function unloadSetup() {
+        /* Loads a status message into the "paper" rendering area, then calls
+        unloadDriver after a short wait to allow the message to appear */
+
+        win.document.getElementById("Paper").appendChild(
+                win.document.createTextNode("Rendering tape image... please wait..."));
+        setTimeout(unloadDriver, 50);
+    }
+
+    // Outer block of unloadTape
+    win.moveTo((screen.availWidth-win.outerWidth)/2, (screen.availHeight-win.outerHeight)/2);
+    win.focus();
+    win.addEventListener("load", unloadSetup, false);
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.tapeRewind = function tapeRewind(laneNr, lockout, unitFinished) {
+    /* 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 */
+    var lastStamp;
+
+    function rewindFinish() {
+        this.timer = 0;
+        this.tapeState = this.tapeLocal;
+        B220Util.removeClass(this.$$("MTRewindingLight"), "annunciatorLit");
+        this.laneNr = laneNr%2;
+        this.rewindLock = (lockout ? true : false);
+        this.rwlLamp.set(this.rewindLock ? 1 : 0);
+        this.setTapeReady(!this.rewindLock);
+        this.releaseUnit(unitFinished, false);
+    }
+
+    function rewindDelay() {
+        var inches;
+        var stamp = performance.now();
+        var interval = stamp - lastStamp;
+
+        if (interval <= 0) {
+            interval = this.spinUpdateInterval/2;
+        }
+        if (this.tapeInches <= 0) {
+            this.setAtBOT(true);
+            this.timer = setCallback(this.mnemonic, this, 1000, rewindFinish);
+        } else {
+            inches = interval*this.rewindSpeed;
+            lastStamp = stamp;
+            this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
+            this.spinReel(-inches);
+        }
+    }
+
+    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);
+        B220Util.addClass(this.$$("MTRewindingLight"), "annunciatorLit");
+        this.timer = setCallback(this.mnemonic, this, 1000, rewindStart);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.tapeReposition = function tapeReposition(successor) {
+    /* Reverses tape direction after a forward tape operation and repositions
+    the head two words from the end of the block just passed, giving room for
+    startup of the next forward operation. At completion, calls the successor
+    function, passing false.
+
+    A real 220 drive repositioned tape about 60 digits (five words) from the end
+    of the data portion of the block, but that was 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 that 2ms
+    delay */
+
+    this.imgIndex -= this.repositionWords;
+    this.moveTape(-this.repositionWords*this.inchesPerWord, this.turnaroundTime, successor);
+};
+
+/**************************************/
+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, null);
+    }
+};
+
+/**************************************/
+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.powerOn = (ev.target.id == "TransportOnBtn") && (this.tapeState != this.tapeUnloaded);
+    this.transportOnLamp.set(this.powerOn ? 1 : 0);
+    this.transportStandbyLamp.set(this.powerOn ? 0 : 1);
+    this.setTapeReady(this.powerOn);
+};
+
+/**************************************/
+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() {
+    /* Initializes the reader window and user interface */
+    var body;
+    var prefs = this.config.getNode("MagTape.units", this.unitIndex);
+
+    this.doc = this.window.document;
+    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",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.LoadBtn_onclick), false);
+    this.$$("UnloadBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.UnloadBtn_onclick), false);
+    this.$$("RewindBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.RewindBtn_onclick), false);
+    this.unitDesignateList.addEventListener("change",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.UnitDesignate_onchange), false);
+    this.$$("RWLRBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.RWLRBtn_onclick), false);
+    this.$$("WriteBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.WriteBtn_onclick), false);
+    this.$$("NotWriteBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.WriteBtn_onclick), false);
+    this.$$("TransportOnBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.TransportOnBtn_onclick), false);
+    this.$$("TransportStandbyBtn").addEventListener("click",
+            B220Util.bindMethod(this, B220MagTapeDrive.prototype.TransportOnBtn_onclick), false);
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.readBlock = function readBlock(receiveBlock) {
+    /* Reads the next block from the tape. If at EOT, makes the drive not ready
+    and terminates the read. After delaying for tape motion, calls the receiveBlock
+    callback function to pass the block to the control unit and thence the processor */
+    var wx = this.blockNr*this.blockWords;
+
+    if (this.blockNr >= this.imgLength) {
+        this.blockNr = this.imgLength;
+        this.setTapeReady(false);
+        this.setAtEOT(true);
+        this.designatedLamp.set(0);
+        this.busy = false;
+        receiveBlock(null, true);       // terminate the I/O
+    } else {
+        if (this.atBOT) {
+            this.setAtBOT(false);
+        }
+        ++this.blockNr;
+        this.moveTape(this.blockLength, this.blockLength/this.tapeSpeed, function readBlockComplete() {
+            receiveBlock(this.image[this.laneNr].subarray(wx, wx+this.blockWords), false);
+        });
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.readTerminate = function readTerminate() {
+    /* Terminates a read operation on the tape drive and makes it not-busy */
+
+    this.designatedLamp.set(0);
+    this.busy = false;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.readInitiate = function readInitiate(receiveBlock) {
+    /* Initiates a read operation on the unit. If the drive is busy or not ready,
+    returns true. Otherwise, delays for the start-up time of the drive and calls
+    readBlock() to read the next block and send it to the Processor */
+    var result = false;
+
+    if (this.busy) {
+        result = true;                  // report unit busy
+    } else if (!this.ready) {
+        result = true;                  // report unit not ready
+    } else {
+        this.busy = true;
+        this.designatedLamp.set(1);
+        setCallback(this.mnemonic, this, this.startTime, this.readBlock, receiveBlock);
+    }
+    //console.log(this.mnemonic + " read:             result=" + result.toString());
+
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.writeBlock = function writeBlock(block, sendBlock, lastBlock) {
+    /* Writes the next block to the tape. If at EOT, makes the drive not ready
+    and terminates the write. Calls the sendBlock callback function to request the
+    next block from the processor */
+    var lane = this.image[this.laneNr];
+    var wx = this.blockNr*this.blockWords;
+    var x;
+
+    if (this.blockNr >= this.imgLength) {
+        this.blockNr = this.imgLength;
+        this.setTapeReady(false);
+        this.setAtEOT(true);
+        this.designatedLamp.set(0);
+        this.busy = false;
+        sendBlock(true);                // terminate the I/O
+    } else {
+        if (this.atBOT) {
+            this.setAtBOT(false);
+        }
+
+        // If the NOT WRITE switch is on, do not copy the block data to the tape image
+        if (!this.notWrite) {
+            this.imgWritten = true;
+            if (this.blockNr > this.imgTopWordNr) {
+                this.imgTopWordNr = this.blockNr;
+            }
+            for (x=0; x= this.imgLength) {
+            this.blockNr = this.imgLength;
+            this.setTapeReady(false);
+            this.setAtEOT(true);
+        }
+        complete(this.blockNr == targetBlock);
+    }
+
+    function searchBackward() {
+        /* While the tape is beyond the target block, moves backward one block and
+        is called by moveTape to evaluate the new block position */
+
+        --this.blockNr;
+        if (this.atEOT) {
+            this.setAtEOT(false);
+        }
+
+        if (testDisabled()) {
+            finishSearch.call(this);
+        } else if (this.blockNr > targetBlock) {
+            this.moveTape(-this.blockLength, delay, searchBackward);
+        } else {
+            finishSearch.call(this);
+        }
+    }
+
+    function searchForward() {
+        /* Searches forward until the tape is beyond the target block, then
+        reverses direction and does a backward search */
+
+        ++this.blockNr;
+        if (this.atBOT) {
+            this.setAtBOT(false);
+        }
+
+        if (testDisabled()) {
+            finishSearch.call(this);
+        } else if (this.blockNr >= this.imgLength) {
+            this.blockNr = this.imgLength;
+            finishSearch.call(this);
+        } else if (this.blockNr <= targetBlock) {
+            this.moveTape(this.blockLength, delay, searchForward);
+        } else {
+            // We have searched forward (or are already forward) of the target block.
+            // Now change direction and search backwards to the target.
+            lampSet(false);             // indicate backward motion
+            this.moveTape(-this.blockLength, this.turnaroundTime, searchBackward);
+        }
+    }
+
+    if (this.busy) {
+        result = true;                  // report unit busy
+    } else if (!this.ready) {
+        result = true;                  // report unit not ready
+    } else {
+        this.busy = true;
+        this.laneNr = laneNr & 0x01;    // TSU uses only low-order lane bit
+        this.designatedLamp.set(1);
+
+        // Begin by searching forward until we are past the target block
+        lampSet(true);                  // indicate forward motion
+        this.moveTape(this.blockLength, delay+this.startTime, searchForward);
+    }
+
+    //console.log(this.mnemonic + " search:           result=" + result.toString());
+    if (result) {
+        complete(false);
+    }
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.initialWriteBlock = function initialWriteBlock(blockReady, controlFinished) {
+    /* Writes one block on edited (blank) tape. "blockReady" is a function
+    to be called after the block is written. "releaseControl" is a function
+    to call after the control signals completion (see release). This routine
+    is used for both MIW and MIR, as block lengths are determined by the
+    tape control unit */
+    var imgLength = this.imgLength;     // physical end of tape index
+    var lane = this.image[this.laneNr]; // image data for current lane
+    var result = false;
+    var startIndex = this.imgIndex;     // initial index into lane buffer
+    var that = this;
+
+    function release() {
+        /* Releases the unit and control */
+
+        that.releaseUnit(controlFinished, false);
+    }
+
+    function finish() {
+        /* Wraps up the positioning and delays before releasing the unit and control */
+        var delay = 20 - that.startTime - that.repositionTime;
+
+        setCallback(that.mnemonic, that, delay, release);
+    }
+
+    function turnaround() {
+        /* Repositions the tape after the last block is written */
+
+        that.tapeReposition(finish);
+    }
+
+    function finalizeWrite() {
+        /* Write a longer inter-block gap so that MPE will work at magnetic
+        end-of-tape, check for end-of-tape, and initiate repositioning prior
+        to terminating the I/O */
+        var count = that.startOfBlockWords*2; // number of gap words
+        var done = false;               // loop control
+        var x = that.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                done = true;            // at end-of-tape
+                that.setAtEOT(true);
+                advance.call(that, x, finish);
+            } else if (count > 0) {
+                --count;                // write extra inter-block gap word
+                lane[x] = that.markerGap;
+                ++x;
+            } else if (lane[x] == that.markerMagEOT) {
+                ++x;                    // space over magnetic EOT words
+            } else {
+                done = true;            // finish write and initiate repositioning
+                advance.call(that, x, turnaround);
+            }
+        } while(!done);
+    }
+
+    function finalizeBlock() {
+        /* Returns to the tape control after completion of one block */
+
+        blockReady(false, writeBlock, finalizeWrite);
+    }
+
+    function abort() {
+        /* Aborts the I/O due to some error */
+
+        blockReady(true, null, release);
+    }
+
+    function setEOT() {
+        /* Sets EOT status after positioning to end-of-tape. Does not release
+        the control unit */
+
+        that.setAtEOT(true);
+        that.releaseUnit(null, false);
+    }
+
+    function advance (index, successor) {
+        /* Advances the tape after a block is passed, then calls the successor */
+        var len = index - that.imgIndex;    // number of words passed
+        var delay = len*that.millisPerWord; // amount of tape spin time
+
+        that.imgIndex = index;
+        if (index > that.imgTopWordNr) {
+            that.imgTopWordNr = index;
+        }
+        that.moveTape(len*that.inchesPerWord, delay, successor);
+    }
+
+    function writeBlock(block, words) {
+        /* Writes the next block from the words in "block" */
+        var count = 0;                  // word counter
+        var done = false;               // completion flag
+        var gapCount = 0;               // count of consecutive inter-block gap words
+        var state = 1;                  // FSA state variable
+        var x = that.imgIndex;          // lane image word index
+
+        if (!that.ready) {
+            done = true;
+            advance.call(that, x, abort); // drive went non-ready
+        } else {
+            do {
+                if (x >= imgLength) {
+                    done = true;                // at EOT: just exit and leave everybody hanging...
+                    that.busy = false;
+                    advance.call(that, x, setEOT);
+                } else {
+                    switch (state) {
+                    case 1: // initial state: skip over flaw and intra-block words
+                        if (lane[x] == that.markerGap) {
+                            state = 2;
+                        } else {
+                            ++x;
+                        }
+                        break;
+                    case 2: // count 3 consecutive inter-block gap words
+                        if (lane[x] == that.markerGap) {
+                            ++gapCount;
+                            if (gapCount < that.startOfBlockWords) {
+                                ++x;
+                            } else {
+                                state = 3;
+                            }
+                        } else if (lane[x] == that.markerMagEOT) {
+                            ++x;
+                        } else {
+                            done = true;
+                            that.imgIndex = x;
+                            advance.call(that, x, abort); // not an edited tape
+                        }
+                        break;
+                    case 3: // write the preface
+                        lane[x] = words;
+                        ++x;
+                        state = 4;
+                        break;
+                    case 4: // write the block words
+                        if (count < words) {
+                            lane[x] = block[count];
+                            ++x;
+                            ++count;
+                        } else if (count < that.minBlockWords) {
+                            lane[x] = 0;
+                            ++x;
+                            ++count;
+                        } else {
+                            count = 0;
+                            state = 5;
+                        }
+                        break;
+                    case 5: // write the erase gap
+                        if (count < that.endOfBlockWords) {
+                            lane[x] = that.markerEOB;
+                            ++x;
+                            ++count;
+                        } else {
+                            done = true;
+                            advance.call(that, x, finalizeBlock);
+                        }
+                        break;
+                    } // switch state
+                }
+            } while (!done);
+        }
+    }
+
+    function firstBlock() {
+        /* Called after the startTime delay to signal the control unit we are
+        ready for the first block of data */
+
+        blockReady(false, writeBlock, release);
+    }
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else if (this.notWrite) {
+        result = true;                  // tape in not-write status
+    } else if (this.atEOT) {
+        result = true;                  // tape at EOT
+    } else {
+        this.busy = true;
+        this.imgWritten = true;
+        this.designatedLamp.set(1);
+        this.setAtBOT(false);
+
+        // Begin with a delay for start-up time
+        setCallback(this.mnemonic, this, this.startTime, firstBlock);
+    }
+
+    //console.log(this.mnemonic + " initialWriteBlock result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.positionForward = function positionForward(blockFinished, releaseControl) {
+    /* Initiates the positioning of tape in a forward direction and spaces the
+    first block. "blockFinished" is a function to be called after the block is
+    spaced. "releaseControl" is a function to call after the last block is spaced
+    (see spaceForward and turnaround) */
+    var lane = this.image[this.laneNr]; // image data for current lane
+    var result = false;
+    var that = this;
+
+    function release() {
+        /* Releases the unit and control */
+
+        that.releaseUnit(releaseControl, false);
+    }
+
+    function finish() {
+        /* Wraps up the positioning and delays before releasing the unit and control */
+        var delay = 16 - that.startTime - that.repositionTime;
+
+        setCallback(that.mnemonic, that, delay, release);
+    }
+
+    function turnaround() {
+        /* Repositions the tape after the last block is spaced over */
+
+        that.tapeReposition(finish);
+    }
+
+    function setEOT() {
+        /* Sets EOT status after positioning to end-of-tape. Does not release
+        the control unit */
+
+        that.setAtEOT(true);
+        that.relaseUnit(null, false);
+    }
+
+    function advance (index, successor) {
+        /* Advances the tape after a block is passed, then calls the successor */
+        var len = index - that.imgIndex;    // number of words passed
+        var delay = len*that.millisPerWord; // amount of tape spin time
+
+        that.imgIndex = index;
+        that.moveTape(len*that.inchesPerWord, delay, successor);
+    }
+
+    function spaceForward(blockFinished) {
+        /* Spaces over the next block */
+        var done = false;                   // completion flag
+        var imgLength = that.imgLength;     // physical end of tape index
+        var state = 1;                      // FSA state variable
+        var w;                              // current image word
+        var x = that.imgIndex;              // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                done = true;                // just exit and leave everybody hanging...
+                that.busy = false;
+                advance.call(that, x, setEOT);
+            } else if (!that.ready) {
+                done = true;
+                that.busy = false;
+                that.releaseUnit(releaseControl, true);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == that.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+                case 2: // skip inter-block gap words
+                    if (w != that.markerGap) {
+                        state = 3;
+                    } else {
+                        ++x;
+                    }
+                    break;
+                case 3: // search for end of block (next inter-block gap word)
+                    if (w != that.markerGap) {
+                        ++x;
+                    } else {
+                        done = true;
+                        advance.call(that, x, function cb() {blockFinished(spaceForward, turnaround)});
+                    }
+                    break;
+                } // switch state
+            }
+        } while (!done);
+
+    }
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else if (this.atEOT) {
+        result = true;                  // tape at EOT, leave control hung
+    } else {
+        this.busy = true;
+        this.designatedLamp.set(1);
+        this.setAtBOT(false);
+
+        // Begin with a delay for start-up time
+        setCallback(this.mnemonic, this, this.startTime, spaceForward, blockFinished);
+    }
+
+    //console.log(this.mnemonic + " positionForward   result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.positionBackward = function positionBackward(blockFinished, releaseControl) {
+    /* Initiates the positioning of tape in a backward direction and spaces the
+    first block. "blockFinished" is a function to be called after the block is
+    spaced. "releaseControl" is a function to call after the last block is spaced
+    (see spaceBackward and finish) */
+    var lane = this.image[this.laneNr]; // image data for current lane
+    var result = false;
+    var that = this;
+
+    function release() {
+        /* Releases the unit and control */
+
+        that.releaseUnit(releaseControl, false);
+    }
+
+    function finish() {
+        /* Wraps up the positioning and delays before releasing the unit and control */
+        var delay = 6 - that.startTime;
+
+        setCallback(that.mnemonic, that, delay, release);
+    }
+
+    function setBOT() {
+        /* Sets BOT status after positioning to beginning-of-tape. Does not
+        release the control unit */
+
+        that.setAtBOT(true);
+        that.releaseUnit(null, false);
+    }
+
+    function advance (index, successor) {
+        /* Advances the tape after a block is passed, then calls the successor */
+        var len = index - that.imgIndex;    // number of words passed
+        var delay = len*that.millisPerWord; // amount of tape spin time
+
+        that.imgIndex = index;
+        that.moveTape(len*that.inchesPerWord, delay, successor);
+    }
+
+    function spaceBackward(blockFinished) {
+        /* Spaces over the current or prior block until the inter-block gap */
+        var done = false;                   // completion flag
+        var imgLength = that.imgLength;     // physical end of tape index
+        var state = 1;                      // FSA state variable
+        var w;                              // current image word
+        var x = that.imgIndex;              // lane image word index
+
+        do {
+            if (x <= 0) {
+                done = true;                // just exit and leave everybody hanging...
+                that.busy = false;
+                advance.call(that, x, setBOT);
+            } else if (!that.ready) {
+                done = true;
+                that.busy = false;
+                that.releaseUnit(releaseControl, true);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw words
+                    if (w == that.markerGap) {
+                        state = 2;
+                    } else if (w != that.markerFlaw) {
+                        state = 3;
+                    } else {
+                        --x;
+                    }
+                    break;
+                case 2: // skip initial inter-block gap words
+                    if (w != that.markerGap) {
+                        state = 3;
+                    } else {
+                        --x;
+                    }
+                    break;
+                case 3: // search for start of block (first prior inter-block gap word)
+                    if (w != that.markerGap) {
+                        --x;
+                    } else {
+                        state = 4;
+                    }
+                    break;
+                case 4: // skip this block's inter-block gap words
+                    if (w != that.markerGap) {
+                        done = true;
+                        x -= that.repositionWords;      // position into end of prior block, as usual
+                        advance.call(that, x, function cb() {blockFinished(spaceBackward, finish)});
+                    } else {
+                        --x;
+                    }
+                    break;
+                } // switch state
+            }
+        } while (!done);
+    }
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else if (this.atBOT) {
+        result = true;                  // tape at BOT, leave control hung
+    } else {
+        this.busy = true;
+        this.designatedLamp.set(1);
+        this.setAtEOT(false);
+
+        // Begin with a delay for start-up time
+        setCallback(this.mnemonic, this, this.startTime, spaceBackward, blockFinished);
+    }
+
+    //console.log(this.mnemonic + " positionBackward  result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.positionAtEnd = function positionAtEnd(releaseControl) {
+    /* Positions the tape to the end of recorded information (i.e., gap longer
+    and inter-block gap */
+    var lane = this.image[this.laneNr]; // image data for current lane
+    var result = false;
+    var that = this;
+
+    function release() {
+        /* Releases the unit and control */
+
+        that.releaseUnit(releaseControl, false);
+    }
+
+    function finish() {
+        /* Wraps up the positioning and delays before releasing the unit and control */
+        var delay = 23 - that.startTime - that.repositionTime;
+
+        setCallback(that.mnemonic, that, delay, release);
+    }
+
+    function turnaround() {
+        /* Repositions the tape after finding end-of-info */
+
+        that.tapeReposition(finish);
+    }
+
+    function setEOT() {
+        /* Sets EOT status after positioning to end-of-tape. Does not release
+        the control unit */
+
+        that.setAtEOT(true);
+        that.relaseUnit(null, false);
+    }
+
+    function advance (index, successor) {
+        /* Advances the tape after a block is passed, then calls the successor */
+        var len = index - that.imgIndex;    // number of words passed
+        var delay = len*that.millisPerWord; // amount of tape spin time
+
+        that.imgIndex = index;
+        that.moveTape(len*that.inchesPerWord, delay, successor);
+    }
+
+    function spaceForward() {
+        /* Spaces over the next block, unless a long gap or physical EOT is
+        encountered */
+        var done = false;                   // completion flag
+        var gapCount = 0;                   // number of consecutive erase-gap words
+        var imgLength = that.imgLength;     // physical end of tape index
+        var state = 1;                      // FSA state variable
+        var w;                              // current image word
+        var x = that.imgIndex;              // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                done = true;                // just exit and leave everybody hanging...
+                that.busy = false;
+                advance.call(that, x, setEOT);
+            } else if (!that.ready) {
+                done = true;
+                that.busy = false;
+                that.releaseUnit(releaseControl, true);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw words
+                    if (w == that.markerGap) {
+                        state = 2;
+                    } else if (w != that.markerFlaw) {
+                        state = 3;
+                    } else {
+                        ++x;
+                    }
+                    break;
+                case 2: // count inter-block gap words
+                    if (w != that.markerGap) {
+                        state = 3;
+                    } else {
+                        ++x;
+                        ++gapCount;
+                        if (gapCount > that.startOfBlockWords) {
+                            done = true;
+                            advance.call(that, x, turnaround);
+                        }
+                    }
+                    break;
+                case 3: // search for end of block (next inter-block gap word)
+                    if (w != that.markerGap) {
+                        ++x;
+                    } else {
+                        done = true;
+                        advance.call(that, x, spaceForward);
+                    }
+                    break;
+                } // switch state
+            }
+        } while (!done);
+
+    }
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else if (this.atEOT) {
+        result = true;                  // tape at EOT, leave control hung
+    } else {
+        this.busy = true;
+        this.designatedLamp.set(1);
+        this.setAtBOT(false);
+
+        // Begin with a delay for start-up time
+        setCallback(this.mnemonic, this, this.startTime, spaceForward);
+    }
+
+    //console.log(this.mnemonic + " positionAtEnd:    result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.laneSelect = function laneSelect(laneNr, releaseControl) {
+    /* Selects the tape lane on the unit. If the drive is busy or not ready,
+    returns true */
+    delay = 3;                          // ms for a no-lane change
+    var lane = laneNr%2;
+    var result = false;
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else {
+        this.busy = true;
+        this.designatedLamp.set(1);
+        if (this.laneNr != lane) {
+            delay += 70;                // additional time (ms) if the lane changes
+            this.laneNr = laneNr%2;
+        }
+
+        setCallback(this.mnemonic, this, delay, this.releaseUnit, releaseControl);
+    }
+
+    //console.log(this.mnemonic + " lane-select:      lane=" + laneNr + ", result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.rewind = function rewind(laneNr, lockout, unitFinished) {
+    /* Initiates a rewind operation on the unit. If the drive is busy or not ready,
+    returns true.  Otherwise, 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 */
+    var result = false;
+
+    if (this.busy) {
+        result = true;                  // unit busy
+    } else if (!this.ready) {
+        result = true;                  // unit not ready
+    } else {
+        this.designatedLamp.set(1);
+        this.tapeRewind(laneNr, lockout, unitFinished);
+    }
+
+    //console.log(this.mnemonic + " rewind:           lane=" + laneNr + ", lockout=" + lockout + ", result=" + result.toString());
+    return result;
+};
+
+/**************************************/
+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();
+    }
+};
diff --git a/webUI/B220MagTapeLoadPanel.html b/webUI/B220MagTapeLoadPanel.html
new file mode 100644
index 0000000..7fba338
--- /dev/null
+++ b/webUI/B220MagTapeLoadPanel.html
@@ -0,0 +1,69 @@
+
+
+
+
+220 Emulator Magnetic Tape Loader
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ Format + +    + + +
+ +
+ To load a blank tape, simply click OK; otherwise + select a tape-image file before clicking OK. +
+ +
+ +   + +
+
+ + + \ No newline at end of file diff --git a/webUI/B220Manifest.appcache b/webUI/B220Manifest.appcache index 60efdb1..b8f7909 100644 --- a/webUI/B220Manifest.appcache +++ b/webUI/B220Manifest.appcache @@ -1,5 +1,5 @@ CACHE MANIFEST -# retro-220 emulator 0.03, 2017-05-29 13:30 +# retro-220 emulator 0.03a, 2017-10-19 08:45 CACHE: ../emulator/B220Processor.js B220.css @@ -30,13 +30,13 @@ B220ControlConsole.js #B220DataFile.js B220DiagMonitor.html B220FramePaper.html -#B220MagTapeControl.css -#B220MagTapeControl.html -#B220MagTapeControl.js -#B220MagTapeDrive.css -#B220MagTapeDrive.html -#B220MagTapeDrive.js -#B220MagTapeLoadPanel.html +B220MagTapeControl.css +B220MagTapeControl.html +B220MagTapeControl.js +B220MagTapeDrive.css +B220MagTapeDrive.html +B220MagTapeDrive.js +B220MagTapeLoadPanel.html B220PanelUtil.js B220PaperTapePunch.css B220PaperTapePunch.html @@ -60,7 +60,7 @@ resources/DejaVuSans-webfont.ttf resources/DejaVuSans-webfont.woff resources/DejaVuSansMono-webfont.ttf resources/DejaVuSansMono-webfont.woff -#resources/MagTapeReel.jpg +resources/MagTapeReel.jpg resources/Organ-Switch-Down.png resources/Organ-Switch-Up.png resources/retro-220-Logo.png diff --git a/webUI/B220PanelUtil.js b/webUI/B220PanelUtil.js index e448014..bba0047 100644 --- a/webUI/B220PanelUtil.js +++ b/webUI/B220PanelUtil.js @@ -824,7 +824,6 @@ PanelRegister.prototype.drawBox = function drawBox(col, lamps, rows, leftStyle, leftStyle and rightStyle specify the left and right borders of the box using standard CSS border syntax. Returns the box element */ var box = document.createElement("div"); - var rightBias = (rightStyle ? 1 : 0); box.style.position = "absolute"; box.style.left = (this.xCoord(col) - (PanelRegister.hSpacing-PanelRegister.lampDiameter)/2 + 1).toString() + "px"; diff --git a/webUI/B220PaperTapePunch.css b/webUI/B220PaperTapePunch.css index 8ece032..b61a94a 100644 --- a/webUI/B220PaperTapePunch.css +++ b/webUI/B220PaperTapePunch.css @@ -44,7 +44,8 @@ position: absolute; width: 24px; left: 56px; - top: 14px} + top: 14px; + box-shadow: 3px 3px 2px #999} #ReadyLampCaption { width: 36px; left: 52px; @@ -57,7 +58,8 @@ right: 8px; top: 18px; color: white; - background-color: #333} + background-color: #333; + box-shadow: 3px 3px 2px #999} #UnitDesignateKnobCaption { width: 64px; right: 8px; diff --git a/webUI/B220PaperTapePunch.js b/webUI/B220PaperTapePunch.js index 6e3d48c..59eca68 100644 --- a/webUI/B220PaperTapePunch.js +++ b/webUI/B220PaperTapePunch.js @@ -15,7 +15,7 @@ /**************************************/ function B220PaperTapePunch(mnemonic, unitIndex, config) { /* Constructor for the Console Paper Tape Punch object */ - var top = unitIndex*32 + 360; + var top = unitIndex*32 + 264; var left = (unitIndex-1)*32; this.config = config; // System configuration object diff --git a/webUI/B220PaperTapeReader.css b/webUI/B220PaperTapeReader.css index 765b553..fe5cc28 100644 --- a/webUI/B220PaperTapeReader.css +++ b/webUI/B220PaperTapeReader.css @@ -44,7 +44,8 @@ position: absolute; width: 24px; left: 56px; - top: 14px} + top: 14px; + box-shadow: 3px 3px 2px #999} #ReadyLampCaption { width: 36px; left: 52px; @@ -75,7 +76,8 @@ right: 8px; top: 18px; color: white; - background-color: #333} + background-color: #333; + box-shadow: 3px 3px 2px #999} #UnitDesignateKnobCaption { width: 64px; right: 8px; diff --git a/webUI/B220PaperTapeReader.js b/webUI/B220PaperTapeReader.js index ec6ba5c..9b2b3a8 100644 --- a/webUI/B220PaperTapeReader.js +++ b/webUI/B220PaperTapeReader.js @@ -18,7 +18,7 @@ /**************************************/ function B220PaperTapeReader(mnemonic, unitIndex, config) { /* Constructor for the PaperTapeReader object */ - var top = unitIndex*32 + 360; + var top = unitIndex*32 + 264; var left = (unitIndex-1)*32 + 250; this.config = config; // System configuration object diff --git a/webUI/B220SetCallback.js b/webUI/B220SetCallback.js index 8fde89b..aa4870d 100644 --- a/webUI/B220SetCallback.js +++ b/webUI/B220SetCallback.js @@ -18,8 +18,8 @@ * minimum they use, and their precision in activating the call-back function * once the actual delay is established varies even more. This module will use * setTimeout() if the requested delay time is above a certain threshold, and -* a setImmediate-like mechanism (based on window.postMessage) if the requested -* delay is below that threshold. +* a setImmediate-like mechanism (based on Promise) if the requested delay is +* below that threshold. * * To help compensate for the fact that the call-back function may be called * sooner than requested, and that due either to other activity or to browser @@ -43,9 +43,9 @@ * earlier or later than the specified delay. The string "category" (which * may be empty, null, or undefined) defines the category under which the * average delay difference will be maintained. setCallBack returns a -* numeric token identifying the call-back event, which can be used -* with clearCallback(). Note that passing a string in lieu of a function -* object is not permitted. +* numeric token identifying the call-back event, which can be used with +* clearCallback() to cancel the callback. Note that passing a string in +* lieu of a function object is not permitted. * * clearCallBack(token) * @@ -77,6 +77,8 @@ * Original version, cloned from retro-205 emulator D205SetCallback.js. * 2017-02-18 P.Kimpel * Redesign yet again the delay adjustment mechanism -- from 205 project. +* 2017-10-16 P.Kimpel +* Replace window.postMessage yield mechanism with one based on Promise(). ***********************************************************************/ "use strict"; @@ -89,7 +91,6 @@ var perf = global.performance; // cached window.performance object var pool = []; // pool of reusable callback objects var poolLength = 0; // length of active entries in pool - var secretPrefix = "retro-205.webUI." + Date.now().toString(16); /**************************************/ function activateCallback(token) { @@ -140,7 +141,7 @@ /* Sets up and schedules a callback for function "fcn", called with context "context", after a delay of "delay" ms. An optional "arg" value will be passed to "fcn". If the delay is less than "minTimeout", a setImmediate-like mechanism - based on window.postsMessage() will be used; otherwise the environment's standard + based on DOM Promise() will be used; otherwise the environment's standard setTimeout mechanism will be used */ var adj = 0; // adjustment to delay and delayDev[] var categoryName = (category || "NUL").toString(); @@ -213,25 +214,12 @@ thisCallback.cancelToken = global.setTimeout(activateCallback, delay, token); } else { thisCallback.cancelToken = 0; - global.postMessage(secretPrefix + tokenName, "*"); + Promise.resolve(token).then(activateCallback); } return token; } - /**************************************/ - function onMessage(ev) { - /* Handler for the global.onmessage event. Activates the callback */ - var payload; - - if (ev.source === global) { - payload = ev.data.toString(); - if (payload.substring(0, secretPrefix.length) === secretPrefix) { - activateCallback(payload.substring(secretPrefix.length)); - } - } - } - /**************************************/ function getCallbackState(optionMask) { /* Diagnostic function. Returns an object that, depending upon bits in @@ -270,7 +258,7 @@ } /********** Outer block of anonymous closure **********/ - if (!global.setCallback && global.postMessage && !global.importScripts) { + if (!global.setCallback && !global.importScripts) { // Attach to the prototype of global, if possible, otherwise to global itself var attachee = global; @@ -282,7 +270,6 @@ } *****/ - global.addEventListener("message", onMessage, false); attachee.setCallback = setCallback; attachee.clearCallback = clearCallback; attachee.getCallbackState = getCallbackState; diff --git a/webUI/B220SystemConfig.css b/webUI/B220SystemConfig.css index 1ac94cf..31a7bb1 100644 --- a/webUI/B220SystemConfig.css +++ b/webUI/B220SystemConfig.css @@ -89,7 +89,6 @@ #ConsoleInputTable, #ConsoleOutputTable, #CardatronTable, -#MagTapeOptionsTable, #MagTapeTable { border-spacing: 0; border-collapse: collapse} @@ -100,8 +99,6 @@ #ConsoleOutputTable TD, #CardatronTable TH, #CardatronTable TD, -#MagTapeOptionsTable TH, -#MagTapeOptionsTable TD, #MagTapeTable TH, #MagTapeTable TD { padding-left: 4px; diff --git a/webUI/B220SystemConfig.html b/webUI/B220SystemConfig.html index 423fe7c..b5c964e 100644 --- a/webUI/B220SystemConfig.html +++ b/webUI/B220SystemConfig.html @@ -925,19 +925,10 @@
Magnetic Tape Unit Selection:
- - - -
- - -
- -
UnitTypeDesignateRemoteRewind-ReadyNot-Write + UnitTypeDesignate
A @@ -959,12 +950,6 @@ - - - - -
B @@ -985,12 +970,6 @@ - - - - -
C @@ -1011,12 +990,6 @@ - - - - -
D @@ -1037,12 +1010,6 @@ - - - - -
E @@ -1063,12 +1030,6 @@ - - - - -
F @@ -1089,12 +1050,6 @@ - - - - -
G @@ -1115,12 +1070,6 @@ - - - - -
H @@ -1141,12 +1090,6 @@ - - - - -
I @@ -1167,12 +1110,6 @@ - - - - -
J @@ -1193,12 +1130,6 @@ - - - - -

 

diff --git a/webUI/B220SystemConfig.js b/webUI/B220SystemConfig.js index 3594e81..537d6a1 100644 --- a/webUI/B220SystemConfig.js +++ b/webUI/B220SystemConfig.js @@ -107,11 +107,11 @@ B220SystemConfig.defaultConfig = { hasMagTape: true, units: [ null, // unit[0] not used - {type: "MTA", designate: 0, remoteSwitch: false, rewindReadySwitch: true, notWriteSwitch: false}, + {type: "MTA", designate: 1}, + {type: "MTB", designate: 2}, + {type: "NONE"}, {type: "NONE"}, {type: "NONE"}, - {type: "MTD", designate: 3, remoteSwitch: false, rewindReadySwitch: true, notWriteSwitch: false}, - {type: "MTE", designate: 4, remoteSwitch: false, rewindReadySwitch: true, notWriteSwitch: false}, {type: "NONE"}, {type: "NONE"}, {type: "NONE"}, @@ -371,15 +371,11 @@ B220SystemConfig.prototype.loadConfigDialog = function loadConfigDialog() { cd.MagTape = B220SystemConfig.defaultConfig.MagTape; } - this.$$("SuppressBMod").checked = !cd.MagTape.suppressBSwitch; for (x=1; x<=10; ++x) { unit = cd.MagTape.units[x]; prefix = "MagTape" + x; this.setListValue(prefix + "Type", unit.type); - this.$$(prefix + "Designate").selectedIndex = unit.designate; - this.$$(prefix + "Remote").checked = unit.remoteSwitch; - this.$$(prefix + "RewindReady").checked = unit.rewindReadySwitch; - this.$$(prefix + "NotWrite").checked = unit.notWriteSwitch; + this.$$(prefix + "Designate").selectedIndex = unit.designate-1; } // for x this.$$("MessageArea").textContent = "220 System Configuration loaded."; @@ -509,17 +505,13 @@ B220SystemConfig.prototype.saveConfigDialog = function saveConfigDialog() { // Magnetic Tape units cd.MagTape.hasMagTape = false; - cd.MagTape.suppressBSwitch = !this.$$("SuppressBMod").checked; for (x=1; x<=10; ++x) { unit = cd.MagTape.units[x]; prefix = "MagTape" + x; e = this.$$(prefix + "Type"); unit.type = (e.selectedIndex < 0 ? "NONE" : e.options[e.selectedIndex].value); - unit.designate = this.$$(prefix + "Designate").selectedIndex; - unit.remoteSwitch = this.$$(prefix + "Remote").checked; - unit.rewindReadySwitch = this.$$(prefix + "RewindReady").checked; - unit.notWriteSwitch = this.$$(prefix + "NotWrite").checked; + unit.designate = this.$$(prefix + "Designate").selectedIndex+1; if (unit.type != "NONE") { cd.MagTape.hasMagTape = true; }