/*********************************************************************** * retro-220/webUI B220CardatronInput.js ************************************************************************ * Copyright (c) 2017, Paul Kimpel. * Licensed under the MIT License, see * http://www.opensource.org/licenses/mit-license.php ************************************************************************ * Burroughs 220 Cardatron Input Unit module. * * Defines a card reader peripheral unit type. * ************************************************************************ * 2017-05-19 P.Kimpel * Original version, from retro-205 D205CardatronInput.js. ***********************************************************************/ "use strict"; /**************************************/ function B220CardatronInput(mnemonic, unitIndex, config) { /* Constructor for the Cardatron Input object */ var h = 160; // window height var left = 0; // (temporary window x-offset) var tks = B220CardatronInput.trackSize; var w = 560; // window width var x; this.config = config; // System configuration object this.mnemonic = mnemonic; // Unit mnemonic this.unitIndex = unitIndex; // Input unit number this.timer = 0; // setCallback() token this.clear(); // Buffer drum: information band is [0], format bands are [1]-[6], [7] is a dummy for indexing only this.bufferDrum = new ArrayBuffer(tks*8); this.info = new Uint8Array(this.bufferDrum, 0, tks); // information band this.formatBand = [ null, // no format band 0 new Uint8Array(this.bufferDrum, tks*1, tks), // format band 1 new Uint8Array(this.bufferDrum, tks*2, tks), // format band 2 new Uint8Array(this.bufferDrum, tks*3, tks), // format band 3 new Uint8Array(this.bufferDrum, tks*4, tks), // format band 4 new Uint8Array(this.bufferDrum, tks*5, tks), // format band 5 new Uint8Array(this.bufferDrum, tks*6, tks), // format band 6 (fixed) new Uint8Array(this.bufferDrum, tks*7, tks)]; // format band 7 (dummy) // Initialize format band 6 for all-numeric transfer // (note that ArrayBuffer storage is initialized to zero, so band[160..233] == 0) for (x=0; x<160; x+=2) { this.formatBand[6][x] = 1; this.formatBand[6][x+1] = 3; } for (x=234; x=0; --x) { this.info[x] = 0; } }; /**************************************/ B220CardatronInput.prototype.setReaderReady = function setReaderReady(ready) { /* Controls the ready-state of the card reader */ this.$$("CIFileSelector").disabled = ready; if (ready && !this.ready) { B220Util.addClass(this.$$("CIStartBtn"), "greenLit") B220Util.removeClass(this.$$("CIStopBtn"), "redLit"); this.ready = true; } else if (this.ready && !ready) { B220Util.removeClass(this.$$("CIStartBtn"), "greenLit") B220Util.addClass(this.$$("CIStopBtn"), "redLit"); this.ready = false; } }; /**************************************/ B220CardatronInput.prototype.setNoFormatAlarm = function setNoFormatAlarm(noFormat) { /* Controls the state of the No Format alarm and lamp */ this.noFormatAlarm = noFormat; if (noFormat) { this.setReaderReady(false); this.noFormatLamp.set(1); } else { this.noFormatLamp.set(0); } }; /**************************************/ B220CardatronInput.prototype.setReloadLockout = function setReloadLockout(lockout) { /* Controls the state of the Reload Lockout (RLO) toggle and lamp */ this.reloadLockout = lockout; if (lockout) { this.reloadLockoutLamp.set(1); } else { this.bufferReady = false; this.setFormatLockout(false); this.reloadLockoutLamp.set(0); } }; /**************************************/ B220CardatronInput.prototype.setFormatLockout = function setFormatLockout(lockout) { /* Controls the state of the Format Lockout (FLO) toggle and lamp. Setting Format Lockout implicitly sets Reload Lockout */ this.formatLockout = lockout; if (lockout) { this.setReloadLockout(true); this.formatLockoutLamp.set(1); } else { this.formatLockoutLamp.set(0); } }; /**************************************/ B220CardatronInput.prototype.setFormatSelectLamps = function setFormatSelectLamps(format) { /* Sets the FS lamps on the panel from the low-order three bits of "format" */ this.formatSelect1Lamp.set(format & 0x01); this.formatSelect2Lamp.set((format >>> 1) & 0x01); this.formatSelect4Lamp.set((format >>> 2) & 0x01); }; /**************************************/ B220CardatronInput.prototype.CIStartBtn_onClick = function CIStartBtn_onClick(ev) { /* Handle the click event for the START button */ if (!this.ready) { this.setNoFormatAlarm(false); if (this.bufIndex < this.bufLength) { this.setReaderReady(true); if (!this.bufferReady) { this.initiateCardRead(); } } } }; /**************************************/ B220CardatronInput.prototype.CIStopBtn_onClick = function CIStopBtn_onClick(ev) { /* Handle the click event for the STOP button */ this.$$("CIFileSelector").value = null; // reset the control so the same file can be reloaded if (this.ready) { this.setReaderReady(false); this.startMachineLamp.set(0); } }; /**************************************/ B220CardatronInput.prototype.ClearBtn_onClick = function ClearBtn_onClick(ev) { /* Handle the click event for the CLEAR button */ this.clearUnit(); }; /**************************************/ B220CardatronInput.prototype.CIHopperBar_onClick = function CIHopperBar_onClick(ev) { /* Handle the click event for the "input hopper" meter bar */ if (this.bufIndex < this.bufLength && !this.ready) { if (this.window.confirm((this.bufLength-this.bufIndex).toString() + " of " + this.bufLength.toString() + " characters remaining to read.\nDo you want to clear the input hopper?")) { this.buffer = ""; this.bufLength = 0; this.bufIndex = 0; this.hopperBar.value = 0; this.$$("CIFileSelector").value = null; // reset the control while (this.outHopper.childNodes.length > 0) { this.outHopper.removeChild(this.outHopper.firstChild); } } } }; /**************************************/ B220CardatronInput.prototype.fileSelector_onChange = function fileSelector_onChange(ev) { /* Handle the onchange event when files are selected. For each file, load it and add it to the "input hopper" of the reader */ var deck; var f = ev.target.files; var that = this; var x; function fileLoader_onLoad(ev) { /* Handle the onLoad event for a Text FileReader */ if (that.bufIndex >= that.bufLength) { that.buffer = ev.target.result; } else { switch (that.buffer.charAt(that.buffer.length-1)) { case "\r": case "\n": case "\f": break; // do nothing -- the last card has a delimiter default: that.buffer += "\n"; // so the next deck starts on a new line break; } that.buffer = that.buffer.substring(that.bufIndex) + ev.target.result; } that.bufIndex = 0; that.bufLength = that.buffer.length; that.$$("CIHopperBar").value = that.bufLength; that.$$("CIHopperBar").max = that.bufLength; } for (x=f.length-1; x>=0; x--) { deck = new FileReader(); deck.onload = fileLoader_onLoad; deck.readAsText(f[x]); } }; /**************************************/ B220CardatronInput.prototype.format_onChange = function format_onChange(ev) { /* Event handler for changes to the FormatColumn and FormatSelect lists */ var prefs = this.config.getNode("Cardatron.units", this.unitIndex); var x; switch (ev.target.id) { case "FormatColumn": x = this.formatColumnList.selectedIndex; this.setFormatColumn(x); prefs.formatCol = x; this.config.putNode("Cardatron.units", prefs, this.unitIndex); break; case "FormatSelect": x = this.formatSelectList.selectedIndex; prefs.formatSelect = this.formatSelect = x; this.config.putNode("Cardatron.units", prefs, this.unitIndex); break; } }; /**************************************/ B220CardatronInput.prototype.readCardImage = function readCardImage() { /* Reads one card image from the buffer; pads or trims the image as necessary to 80 characters. Detects empty buffer (hopper) and sets the reader not ready after reading the last card. Updates the progress bar. Returns the raw ASCII card image as a string */ var card; // card image var match; // result of eolRex.exec() this.eolRex.lastIndex = this.bufIndex; match = this.eolRex.exec(this.buffer); if (!match) { card = ""; } else { this.bufIndex += match[0].length; card = match[1].toUpperCase(); } if (card.length > 80) { card = card.substring(0, 80); } else { while (card.length <= 70) { card += " "; // pad with spaces } while (card.length < 80) { card += " "; } } if (this.bufIndex < this.bufLength) { this.hopperBar.value = this.bufLength-this.bufIndex; } else { this.hopperBar.value = 0; this.buffer = ""; // discard the input buffer this.bufLength = 0; this.bufIndex = 0; this.setReaderReady(false); this.$$("CIFileSelector").value = null; // reset the control so the same file can be reloaded } while (this.outHopper.childNodes.length > 1) { this.outHopper.removeChild(this.outHopper.firstChild); } this.outHopper.appendChild(this.doc.createTextNode("\n")); this.outHopper.appendChild(this.doc.createTextNode(card)); return card; }; /**************************************/ B220CardatronInput.prototype.determineFormatBand = function determineFormatBand(card) { /* Determines the format band number to be applied to the current card. Returns the format band number or zero if No Format Alarm is set */ var c; // selected column character code var format; // selected format band // Check for format override on the reader panel format = this.formatSelect; if (format > 6) { format = 0; this.setNoFormatAlarm(true); } else if (format <= 0) { // No format override, so determine format from a card column c = card.charAt(this.formatCol); switch (c) { case "1": format = 1; break; case "2": format = 2; break; case "3": format = 3; break; case "4": format = 4; break; case "5": format = 5; break; case "6": format = 6; break; case "7": format = 7; break; case "`": // 1-8 punch format = 1; this.setFormatLockout(true); break; case ":": // 2-8 punch format = 2; this.setFormatLockout(true); break; case "#": // 3-8 punch format = 3; this.setFormatLockout(true); break; case "@": // 4-8 punch format = 4; this.setFormatLockout(true); break; case "'": // 5-8 punch case "|": // translates to a 5-numeric digit format = 5; this.setFormatLockout(true); break; case "=": // 6-8 punch case "}": // translates to a 6-numeric digit format = 6; this.setFormatLockout(true); break; case "\"": // 7-8 punch -- reject plus lockout case "~": // translates to a 7-numeric digit format = 7+8; this.setFormatLockout(true); break; default: format = 0; this.setNoFormatAlarm(true); break; } // switch c } this.setFormatSelectLamps(format); return format; }; /**************************************/ B220CardatronInput.prototype.finishCardRead = function finishCardRead() { /* Processes a card image after the delay to read the card. Establishes the format band to be used and encodes the data from the card image onto the info band of the buffer drum per Figure 2 in US Patent 3,000,556 */ var band; // local copy of selected format band var c; // current character code var card = this.readCardImage(); // the card image in ASCII var col; // current card column var format; // selected format band var fmax; // length of format band var info = this.info; // local copy of info band on buffer drum var nu; // numeric (true), zone (false) toggle var x; // info/format band digit index this.startMachineLamp.set(0); format = this.determineFormatBand(card); if ((format & 0x07) == 7) { // Reject format -- clear the information band and read next card this.selectedFormat = 7; // If reject+lockout was imposed by a 7-8 punch, read next card and lock out its successor if ((format & 0x08) || !this.reloadLockout) { this.initiateCardRead(); } } else if (format > 0) { this.selectedFormat = format; band = this.formatBand[format]; fmax = band.length; col = card.length-1; // start with last column on card nu = true; // start with the numeric digit for (x=0; x>> 4) & 0x0F; // take the zone half nu = true; // next will be the numeric digit if (col > 0) { // advance to next card column --col; } } } // switch } // for x this.bufferReady = true; if (this.readRequested) { // fire up any pending read from Processor this.readRequested = false; this.pendingCall.apply(this, this.pendingParams); this.pendingCall = null; } } }; /**************************************/ B220CardatronInput.prototype.initiateCardRead = function initiateCardRead() { /* Initiates the read of the next card into the buffer drum */ if (this.ready) { this.startMachineLamp.set(1); this.setNoFormatAlarm(false); this.clearInfoBand(); this.timer = setCallback(this.mnemonic, this, 60000/this.cardsPerMinute, this.finishCardRead); } }; /**************************************/ B220CardatronInput.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; }; /**************************************/ B220CardatronInput.prototype.readerOnLoad = function readerOnLoad() { /* Initializes the reader window and user interface */ var body; var de; var prefs = this.config.getNode("Cardatron.units", this.unitIndex); this.doc = this.window.document; de = this.doc.documentElement; this.doc.title = "retro-220 Cardatron Reader " + this.mnemonic; body = this.$$("CIDiv"); this.hopperBar = this.$$("CIHopperBar"); this.outHopperFrame = this.$$("CIOutHopperFrame"); this.outHopper = this.outHopperFrame.contentDocument.getElementById("Paper"); this.formatColumnList = this.$$("FormatColumn"); this.formatColumnList.selectedIndex = prefs.formatCol; this.setFormatColumn(prefs.formatCol); this.formatSelectList = this.$$("FormatSelect"); this.formatSelectList.selectedIndex = this.formatSelect = prefs.formatSelect; this.noFormatLamp = new ColoredLamp(body, null, null, "NoFormatLamp", "redLamp", "redLit"); this.noFormatLamp.setCaption("NO FORMAT", true); this.startMachineLamp = new NeonLamp(body, null, null, "StartMachineLamp"); this.startMachineLamp.setCaption("SM", true); this.reloadLockoutLamp = new NeonLamp(body, null, null, "ReloadLockoutLamp"); this.reloadLockoutLamp.setCaption("RLO", true); this.formatLockoutLamp = new NeonLamp(body, null, null, "FormatLockoutLamp"); this.formatLockoutLamp.setCaption("FLO", true); this.formatSelect1Lamp = new NeonLamp(body, null, null, "FormatSelect1Lamp"); this.formatSelect1Lamp.setCaption("FS1", true); this.formatSelect2Lamp = new NeonLamp(body, null, null, "FormatSelect2Lamp"); this.formatSelect2Lamp.setCaption("FS2", true); this.formatSelect4Lamp = new NeonLamp(body, null, null, "FormatSelect4Lamp"); this.formatSelect4Lamp.setCaption("FS4", true); this.ready = true; // so that setReaderReady called from clearUnit this.clearUnit(); // will actually set the state and lamps correctly this.window.addEventListener("beforeunload", B220CardatronInput.prototype.beforeUnload); this.$$("CIFileSelector").addEventListener("change", B220Util.bindMethod(this, B220CardatronInput.prototype.fileSelector_onChange)); this.$$("FormatColumn").addEventListener("change", B220Util.bindMethod(this, B220CardatronInput.prototype.format_onChange)); this.$$("FormatSelect").addEventListener("change", B220Util.bindMethod(this, B220CardatronInput.prototype.format_onChange)); this.$$("CIStartBtn").addEventListener("click", B220Util.bindMethod(this, B220CardatronInput.prototype.CIStartBtn_onClick)); this.$$("CIStopBtn").addEventListener("click", B220Util.bindMethod(this, B220CardatronInput.prototype.CIStopBtn_onClick)); this.$$("ClearBtn").addEventListener("click", B220Util.bindMethod(this, B220CardatronInput.prototype.ClearBtn_onClick)); this.hopperBar.addEventListener("click", B220Util.bindMethod(this, B220CardatronInput.prototype.CIHopperBar_onClick)); this.window.resizeBy(de.scrollWidth - this.window.innerWidth + 4, // kludge for right-padding/margin de.scrollHeight - this.window.innerHeight); this.window.moveTo(0, screen.availHeight - this.window.outerHeight - (this.unitIndex-1)*32); }; /**************************************/ B220CardatronInput.prototype.inputWord = function inputWord(wordReceiver) { /* Reads the next word of digits from the info band of the buffer drum, translating the digits to Datatron Processor code and sending the word to the Processor via the wordReceiver callback function. If at end of band, returns any partially-accumulated word minus 0x900000000000 to signal end of I/O. Note that sign digits must be translated from zone digits specially. Also note that a completed word is not returned until the next digit is obtained from the info band */ var band; // local copy of format band var d; // result digit var eod; // finished with current digit var eow = false; // finished with word var info = this.info; // local reference to info band var ix = this.infoIndex; // current info/format band index var lastNumeric = this.lastNumericDigit; var nu = this.togNumeric; // numeric/zone digit toggle var word = this.pendingInputWord; // word being assembled var x = this.digitCount; // word-digit index band = this.formatBand[this.selectedFormat]; do { eod = false; do { if (ix >= info.length) { // At end of info band -- finish the I/O d = 0; word = word + this.eodBias; // flag this as the final word of the I/O eow = eod = true; } else { // Translate or delete the current digit switch (band[ix]) { case 0: // insert 0 digit d = 0; ++ix; eod = true; break; case 1: // translate zone/numeric digit d = info[ix]; if (nu) { // Numeric digit: straight translation except for 3-8 and 4-8 punches nu = false; // next is a zone digit lastNumeric = d; if (d > 9) { d -= 8; } } else { // Zone digit: requires special handling in the sign-digit position nu = true; // next is a numeric digit d = this.zoneXlate[d][lastNumeric]; if (x == 10) { d &= 0x0B; // zero the 4-bit in the sign digit } } ++ix; eod = true; break; case 2: // insert 2 digit d = 2; ++ix; eod = true; break; default: // (3) delete the digit if (nu) { nu = false; // next is a zone digit lastNumeric = info[ix]; } else { nu = true; // next is a numeric digit } ++ix; // We are not yet done producing the next digit... break; } // switch band[ix] } } while (!eod); if (x < 11) { // Increment the digit index and shift this digit into the word ++x; word = word/0x10 + d*0x10000000000; } else { // Word has overflowed -- send the word to the Processor and save // this digit for the next word eow = true; this.pendingInputWord = d*0x10000000000; this.digitCount = 1; } } while (!eow); this.lastNumericDigit = lastNumeric; this.togNumeric = nu; // Send the word to the Processor if (wordReceiver(word) < 0) { this.infoIndex = info.length; // stop the I/O } else { this.infoIndex = ix; } }; /**************************************/ B220CardatronInput.prototype.inputStop = function inputStop() { /* Terminates data transfer from the input unit and releases the card */ this.setFormatSelectLamps(0); if (this.rDigit % 2) { // set reload-lockout if (!this.reloadLockout) { this.setReloadLockout(true); } } else if (this.reloadLockout) { // reset reload-lockout this.setReloadLockout(false); } if (!this.reloadLockout) { this.bufferReady = false; this.initiateCardRead(); } }; /**************************************/ B220CardatronInput.prototype.inputTransfer = function inputTransfer(wordReceiver) { /* Driver for sending words assembled from the info band to the Processor */ while (this.infoIndex < this.info.length) { this.inputWord(wordReceiver); } this.inputStop(); }; /**************************************/ B220CardatronInput.prototype.inputInitiate = function inputInitiate(rDigit, wordReceiver) { /* Initiates a read against the buffer drum on this unit. rDigit is the (41) numeric digit from the instruction word, with odd values indicating reload-lockout should be imposed at the end of the read. wordReceiver is the callback function that will receive one word at a time for return to the Processor. If the buffer is not ready, simply sets the readRequested flag and exits after stashing rDigit and the wordReceiver callback */ if (!this.bufferReady) { this.readRequested = true; // wait for the buffer to be filled this.pendingCall = inputInitiate; this.pendingParams = [rDigit, wordReceiver]; if (!this.ready) { this.window.focus(); } } else { this.rDigit = rDigit; this.infoIndex = 0; // start at the beginning of the info band this.digitCount = 0; this.pendingInputWord = 0; this.togNumeric = true; this.lastNumericDigit = 0; setCallback(this.mnemonic, this, B220CardatronInput.drumTransferTime*(Math.random()+1), this.inputTransfer, wordReceiver); } }; /**************************************/ B220CardatronInput.prototype.inputReadyInterrogate = function inputReadyInterrogate() { /* Returns the current ready status of the input unit */ return this.bufferReady; }; /**************************************/ B220CardatronInput.prototype.inputFormatTransfer = function inputFormatTransfer(requestNextWord) { /* Receives input format band words from the Processor and stores the digits from each word into the next 11 format band digits */ var band = this.formatBand[this.selectedFormat]; var d; // current format digit var ix = 0; // current format band digit index var word; // band word from Processor var x; // word-digit index while (ix < B220CardatronInput.trackSize) { word = requestNextWord(); if (word < 0) { // transfer terminated ix = B220CardatronInput.tracksize; } else { for (x=0; x<11; ++x) { d = word % 0x10; word = (word-d)/0x10; if (ix < B220CardatronInput.trackSize) { band[ix++] = d % 4; } else { break; // out of for loop } } // for x } } this.pendingFinish(); // call signalFinished(); this.pendingFinish = null; this.inputStop(); }; /**************************************/ B220CardatronInput.prototype.inputFormatInitiate = function inputFormatInitiate( rDigit, requestNextWord, signalFinished) { /* Initiates the loading of a format band on this unit. rDigit is the (41) numeric digit from the instruction word, with odd values indicating reload-lockout should be imposed at the end of the read and the remaining three bits indicating the format band to be loaded. requestNextWord is the callback function that will trigger the Processor to send the next word. signalFinished is the callback function that will signal the Processor to terminate the I/O */ if (rDigit > 9) { signalFinished(); } else { this.rDigit = rDigit; this.selectedFormat = ((rDigit >>> 1) & 0x07) + 1; this.pendingFinish = signalFinished; // stash the call-back function this.setFormatSelectLamps(this.selectedFormat); setCallback(this.mnemonic, this, B220CardatronInput.drumTransferTime*(Math.random()+2), this.inputFormatTransfer, requestNextWord); } }; /**************************************/ B220CardatronInput.prototype.clearUnit = function clearUnit() { /* Clears the input unit and resets all internal state */ this.$$("CIFileSelector").value = null; // reset the control so the same file can be reloaded this.bufferReady = false; this.setReaderReady(false); this.setNoFormatAlarm(false); this.setReloadLockout(false); this.setFormatLockout(false); this.startMachineLamp.set(0); this.setFormatSelectLamps(7); // If there is a pending read, confirm that this.pendingParams[1] is a // function and call it with the end-of-data signal. We assume it's the // Processor's wordReceiver function. This will prevent the Processor // from hanging on an I/O to a cleared input unit. if (this.readRequested) { if (Object.prototype.toString.call(this.pendingParams) === "[object Array]") { if (Object.prototype.toString.call(this.pendingParams[1]) === "[object Function]") { this.pendingParams[1](this.eodBias); } } } this.clear(); if (this.timer) { clearCallback(this.timer); this.timer = 0; } }; /**************************************/ B220CardatronInput.prototype.shutDown = function shutDown() { /* Shuts down the device */ if (this.timer) { clearCallback(this.timer); } this.window.removeEventListener("beforeunload", B220CardatronInput.prototype.beforeUnload); this.window.close(); };