Groundwork for loading local disk images in PCjs

This commit is contained in:
Jeff Parsons 2014-11-09 18:57:13 -08:00 committed by jeffpar
commit 43860a588c
20 changed files with 1355 additions and 1157 deletions

View file

@ -163,18 +163,18 @@ C1PComputer.prototype.stop = function(msStart, nCycles)
/**
* @this {C1PComputer}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PComputer.prototype.setBinding = function(c, t, s, e)
C1PComputer.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
switch(s) {
switch(sBinding) {
case "reset":
this.bindings[s] = e;
e.onclick = function(computer) {
this.bindings[sBinding] = control;
control.onclick = function(computer) {
return function() {
computer.reset();
};

View file

@ -502,19 +502,19 @@ C1PCPU.prototype.reset = function(fPowerOn)
/**
* @this {C1PCPU}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PCPU.prototype.setBinding = function(c, t, s, e)
C1PCPU.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
var fBound = false;
switch(s) {
switch(sBinding) {
case "run":
this.bindings[s] = e;
e.onclick = function(cpu) {
this.bindings[sBinding] = control;
control.onclick = function(cpu) {
return function() {
if (!cpu.aFlags.fRunning)
cpu.run();
@ -527,12 +527,12 @@ C1PCPU.prototype.setBinding = function(c, t, s, e)
case "A": case "X": case "Y": case "S": case "PC":
case "C": case "Z": case "I": case "D": case "B": case "V": case "N":
case "speed":
this.bindings[s] = e;
this.bindings[sBinding] = control;
fBound = true;
break;
case "setSpeed":
this.bindings[s] = e;
e.onclick = function(cpu) {
this.bindings[sBinding] = control;
control.onclick = function(cpu) {
return function() {
var speed = (cpu.speed >= cpu.SPEED_MAX? cpu.SPEED_SLOW : cpu.speed+1);
cpu.setSpeed(speed, true);

View file

@ -657,21 +657,23 @@ C1PDiskController.prototype.resetDrive = function(iDrive, iDriveType, nMaxTracks
/**
* @this {C1PDiskController}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisk")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisk")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PDiskController.prototype.setBinding = function(c, t, s, e)
C1PDiskController.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
switch(s) {
switch(sBinding) {
case "listDisk":
this.bindings[s] = e;
this.bindings[sBinding] = control;
return true;
case "loadDisk":
this.bindings[s] = e;
e.onclick = function(controller) {
this.bindings[sBinding] = control;
control.onclick = function(controller) {
return function() {
if (controller.bindings["listDisk"]) {
var sFilePath = controller.bindings["listDisk"].value;
@ -698,6 +700,7 @@ C1PDiskController.prototype.setBinding = function(c, t, s, e)
};
}(this);
return true;
default:
break;
}

View file

@ -388,38 +388,38 @@ C1PKeyboard.prototype.reset = function()
/**
* @this {C1PKeyboard}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc", "ctrl-c")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc", "ctrl-c")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PKeyboard.prototype.setBinding = function(c, t, s, e)
C1PKeyboard.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
/*
* I want to bind to the first caller (ie, the Screen), not subsequent ones (eg, the Panel)
*/
if (this.bindings[s] === undefined) {
switch(s) {
if (this.bindings[sBinding] === undefined) {
switch(sBinding) {
case "keyDown":
this.bindings[s] = e;
e.onkeydown = function(kbd) {
this.bindings[sBinding] = control;
control.onkeydown = function(kbd) {
return function(event) {
return kbd.keyEvent(event, true);
};
}(this);
return true;
case "keyPress":
this.bindings[s] = e;
e.onkeypress = function(kbd) {
this.bindings[sBinding] = control;
control.onkeypress = function(kbd) {
return function(event) {
return kbd.keyPress(event);
};
}(this);
return true;
case "keyUp":
this.bindings[s] = e;
e.onkeyup = function(kbd) {
this.bindings[sBinding] = control;
control.onkeyup = function(kbd) {
return function(event) {
return kbd.keyEvent(event, false);
};
@ -436,8 +436,8 @@ C1PKeyboard.prototype.setBinding = function(c, t, s, e)
* other injected keys, I'll need to avoid clearing the injection buffer on a reset;
* currently, reset() resets everything.
*/
this.bindings[s] = e;
e.onclick = function(kbd) {
this.bindings[sBinding] = control;
control.onclick = function(kbd) {
return function(event) {
if (DEBUG) kbd.println("keyPressSimulate(break)");
if (kbd.cmp) kbd.cmp.reset(true);
@ -445,15 +445,15 @@ C1PKeyboard.prototype.setBinding = function(c, t, s, e)
}(this);
return true;
default:
if (this.aButtonCodeMap[s] !== undefined) {
this.bindings[s] = e;
e.onclick = function(kbd, sButton, charCode) {
if (this.aButtonCodeMap[sBinding] !== undefined) {
this.bindings[sBinding] = control;
control.onclick = function(kbd, sButton, charCode) {
return function(event) {
if (DEBUG) kbd.println("keyPressSimulate(" + sButton + ")");
if (kbd.cpu) kbd.cpu.setFocus();
return !kbd.keyPressSimulate(charCode);
};
}(this, s, this.aButtonCodeMap[s]);
}(this, sBinding, this.aButtonCodeMap[sBinding]);
return true;
}
break;

View file

@ -100,64 +100,73 @@ C1PSerialPort.prototype.start = function()
/**
* @this {C1PSerialPort}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listSerial")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listSerial")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PSerialPort.prototype.setBinding = function(c, t, s, e)
C1PSerialPort.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
switch(s) {
case "listSerial":
this.bindings[s] = e;
return true;
case "loadSerial":
this.bindings[s] = e;
e.onclick = function(serial) {
return function() {
if (serial.bindings["listSerial"]) {
var sFile = serial.bindings["listSerial"].value;
// serial.println("loading " + sFile + "...");
web.loadResource(sFile, true, null, serial, serial.loadFile);
}
};
}(this);
return true;
case "uploadSerial":
// Check for availability of FileReader
if (window.FileReader && window.File && window.FileList && window.Blob ) {
var serial = this;
this.bindings[s] = e;
var serial = this;
// Enable "Load Local File" button only if a file is actually selected
e.addEventListener('change', function () {
var fieldset = e.children[0];
switch(sBinding) {
case "listSerial":
this.bindings[sBinding] = control;
return true;
case "loadSerial":
this.bindings[sBinding] = control;
control.onclick = function(event) {
if (serial.bindings["listSerial"]) {
var sFile = serial.bindings["listSerial"].value;
// serial.println("loading " + sFile + "...");
web.loadResource(sFile, true, null, serial, serial.loadFile);
}
};
return true;
case "uploadSerial":
/*
* Check for availability of FileReader
*/
if (window.FileReader && window.File && window.FileList && window.Blob) {
this.bindings[sBinding] = control;
/*
* Enable "Load Local File" button only if a file is actually selected
*/
control.addEventListener('change', function() {
var fieldset = control.children[0];
var files = fieldset.children[0].files;
var submit = fieldset.children[1];
submit.disabled = (files.length == 0);
});
e.onsubmit = function (event) {
control.onsubmit = function(event) {
var file = event.currentTarget[1].files[0];
var reader = new FileReader();
reader.onload = function () {
// serial.println("uploading " + file.name + "...");
reader.onload = function() {
// serial.println("loading " + file.name + "...");
serial.loadFile(file.name, reader.result.toString(), 0);
};
reader.readAsText(file);
// Prevent reloading of web page after form submission
/*
* Prevent reloading of web page after form submission
*/
return false;
};
}
else {
this.println("FileReader support not available, disabling local file upload");
e.parentNode.removeChild(e);
this.println("FileReader support not available, disabling local file load");
control.parentNode.removeChild(control);
}
return true;
default:
break;
}

View file

@ -172,18 +172,18 @@ C1PVideo.prototype.reset = function(fPowerOn)
/**
* @this {C1PVideo}
* @param {string|null} c is the class of the HTML control (eg, "input", "output")
* @param {string|null} t is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} s is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "refresh")
* @param {Object} e is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "refresh")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
C1PVideo.prototype.setBinding = function(c, t, s, e)
C1PVideo.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
switch(s) {
switch(sBinding) {
case "refresh":
this.bindings[s] = e;
e.onclick = function(video) {
this.bindings[sBinding] = control;
control.onclick = function(video) {
return function() {
if (DEBUG) video.println("refreshScreen()");
video.initScreen();

View file

@ -45,6 +45,7 @@ var net = require("../../shared/lib/netlib");
var proc = require("../../shared/lib/proclib");
var str = require("../../shared/lib/strlib");
var usr = require("../../shared/lib/usrlib");
var DiskAPI = require("../../shared/lib/diskapi");
var DumpAPI = require("../../shared/lib/dumpapi");
var X86 = require("../../pcjs-client/lib/x86");
@ -177,38 +178,6 @@ DiskDump.sCopyright = "© 2012-2014 by Jeff Parsons (@jeffpar)";
DiskDump.sNotice = DiskDump.sAPIURL + " " + DiskDump.sCopyright;
DiskDump.sUsage = "Usage: " + DiskDump.sAPIURL + "?" + DumpAPI.QUERY.PATH + "={url}&" + DumpAPI.QUERY.FORMAT + "=json|data|hex|bytes|img";
/*
* TODO: The following descriptions of common disk data structures should be moved to a common (shared) module,
* so that client code (eg, PCjs components) can access them, too (eg, something like /my_modules/shared/lib/bpbdefs.js)
*/
DiskDump.DisketteFormats = {
163840: [1,40,8], // media type 0xFE: 1 head (single-sided), 40 cylinders, 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)
184320: [1,40,9], // media type 0xFC: 1 head (single-sided), 40 cylinders, 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)
327680: [2,40,8], // media type 0xFF: 2 heads (double-sided), 40 cylinders, 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)
368640: [2,40,9], // media type 0xFD: 2 heads (double-sided), 40 cylinders, 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)
737280: [2,80,9], // media type 0xF9: 2 heads (double-sided), 80 cylinders, 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)
1228800: [2,80,15], // media type 0xF9: 2 heads (double-sided), 80 cylinders, 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)
1474560: [2,80,18], // media type 0xF0: 2 heads (double-sided), 80 cylinders, 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)
2949120: [2,80,36] // media type 0xF0: 2 heads (double-sided), 80 cylinders, 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)
};
DiskDump.BPB = {
JMP_OPCODE: 0x00, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset
OEM_STRING: 0x03, // 8 bytes
SECTOR_BYTES: 0x0B, // 2 bytes: bytes per sector (eg, 0x200 or 512)
CLUSTER_SECS: 0x0D, // 1 byte: sectors per cluster (eg, 1)
RESERVED_SECS: 0x0E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1)
FAT_TOTAL: 0x10, // 1 byte: FAT copies (eg, 2)
ROOT_ENTRIES: 0x11, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors)
SECTOR_TOTAL: 0x13, // 2 bytes: number of sectors (eg, 0x140 or 320)
MEDIA_TYPE: 0x15, // 1 byte: media type (eg, 0xFF: 320Kb, 0xFE: 160Kb, 0xFD: 360Kb, 0xFC: 180Kb)
FAT_SECS: 0x16, // 2 bytes: sectors per FAT (eg, 1)
TRACK_SECS: 0x18, // 2 bytes: sectors per track (eg, 8)
HEAD_TOTAL: 0x1A, // 2 bytes: number of heads (eg, 1)
HIDDEN_SECS: 0x1C, // 4 bytes: number of hidden sectors (always 0 for non-partitioned media)
LARGE_SECS: 0x20 // 4 bytes: number of sectors if SECTOR_TOTAL is zero
};
/*
* MY_VOL_LABEL is our default label, used whenever a more suitable label (eg, based on the disk image's folder name)
* is not available or not supplied, and MY_OEM_STRING is inserted into any DiskDump-generated diskette images.
@ -2129,8 +2098,8 @@ DiskDump.prototype.convertToJSON = function()
*/
}
var bByte0 = this.bufDisk.readUInt8(offBootSector + DiskDump.BPB.JMP_OPCODE);
var cbSectorBPB = this.bufDisk.readUInt16LE(offBootSector + DiskDump.BPB.SECTOR_BYTES);
var bByte0 = this.bufDisk.readUInt8(offBootSector + DiskAPI.BPB.JMP_OPCODE);
var cbSectorBPB = this.bufDisk.readUInt16LE(offBootSector + DiskAPI.BPB.SECTOR_BYTES);
/*
* These checks are not only necessary for DOS 1.x diskette images (and other pre-BPB images),
@ -2145,11 +2114,11 @@ DiskDump.prototype.convertToJSON = function()
* image whose logical format doesn't agree with its physical structure.
*/
var fXDFOutput = false;
var oDisketteFormat = DiskDump.DisketteFormats[cbDiskData];
if (oDisketteFormat) {
nHeads = oDisketteFormat[0];
nCylinders = oDisketteFormat[1];
nSectorsPerTrack = oDisketteFormat[2];
var disketteFormat = DiskAPI.DISKETTE_FORMATS[cbDiskData];
if (disketteFormat) {
nCylinders = disketteFormat[0];
nHeads = disketteFormat[1];
nSectorsPerTrack = disketteFormat[2];
}
else {
/*
@ -2159,10 +2128,13 @@ DiskDump.prototype.convertToJSON = function()
* 0xE9 is JMP with a 2-byte displacement). What else?
*/
if ((bByte0 == X86.OPCODE.JMP || bByte0 == X86.OPCODE.JMPS) && cbSectorBPB == cbSector) {
var nHeadsBPB = this.bufDisk.readUInt16LE(offBootSector + DiskDump.BPB.HEAD_TOTAL);
var nSectorsTotalBPB = this.bufDisk.readUInt16LE(offBootSector + DiskDump.BPB.SECTOR_TOTAL);
var nSectorsPerTrackBPB = this.bufDisk.readUInt16LE(offBootSector + DiskDump.BPB.TRACK_SECS);
var nHeadsBPB = this.bufDisk.readUInt16LE(offBootSector + DiskAPI.BPB.HEAD_TOTAL);
var nSectorsTotalBPB = this.bufDisk.readUInt16LE(offBootSector + DiskAPI.BPB.SECTOR_TOTAL);
var nSectorsPerTrackBPB = this.bufDisk.readUInt16LE(offBootSector + DiskAPI.BPB.TRACK_SECS);
if (nSectorsPerTrackBPB && nHeadsBPB) {
var nSectorsPerCylinderBPB = nSectorsPerTrackBPB * nHeadsBPB;
nHeads = nHeadsBPB;
nCylinders = Math.floor(nSectorsTotalBPB / nSectorsPerCylinderBPB);
@ -2727,13 +2699,13 @@ DiskDump.prototype.convertToIMG = function()
/*
* Mimic the BPB test in convertToJSON(), because we don't want to blast an OEM string into non-DOS diskette images
*/
var bByte0 = buf.readUInt8(DiskDump.BPB.JMP_OPCODE);
var cbSectorBPB = buf.readUInt16LE(DiskDump.BPB.SECTOR_BYTES);
var bByte0 = buf.readUInt8(DiskAPI.BPB.JMP_OPCODE);
var cbSectorBPB = buf.readUInt16LE(DiskAPI.BPB.SECTOR_BYTES);
if ((bByte0 == X86.OPCODE.JMP || bByte0 == X86.OPCODE.JMPS) && cbSectorBPB == 512) {
/*
* Overwrite the OEM string with our own, so that people know how the image originated
*/
buf.write(DiskDump.MY_OEM_STRING, DiskDump.BPB.OEM_STRING, DiskDump.MY_OEM_STRING.length);
buf.write(DiskDump.MY_OEM_STRING, DiskAPI.BPB.OEM_STRING, DiskDump.MY_OEM_STRING.length);
}
}
} catch(err) {

View file

@ -869,10 +869,10 @@ if (DEBUGGER) {
];
Debugger.aaOp0FDescs = {
0x00: [Debugger.INS.GRP6, Debugger.TYPE_MODRM | Debugger.TYPE_WORD | Debugger.TYPE_BOTH],
0x01: [Debugger.INS.GRP7, Debugger.TYPE_MODRM | Debugger.TYPE_WORD | Debugger.TYPE_BOTH],
0x02: [Debugger.INS.LAR, Debugger.TYPE_REG | Debugger.TYPE_WORD | Debugger.TYPE_IN | Debugger.TYPE_286, Debugger.TYPE_MEM | Debugger.TYPE_WORD | Debugger.TYPE_IN],
0x03: [Debugger.INS.LSL, Debugger.TYPE_REG | Debugger.TYPE_WORD | Debugger.TYPE_IN | Debugger.TYPE_286, Debugger.TYPE_MEM | Debugger.TYPE_WORD | Debugger.TYPE_IN],
0x00: [Debugger.INS.GRP6, Debugger.TYPE_MODRM | Debugger.TYPE_WORD | Debugger.TYPE_BOTH],
0x01: [Debugger.INS.GRP7, Debugger.TYPE_MODRM | Debugger.TYPE_WORD | Debugger.TYPE_BOTH],
0x02: [Debugger.INS.LAR, Debugger.TYPE_REG | Debugger.TYPE_WORD | Debugger.TYPE_IN | Debugger.TYPE_286, Debugger.TYPE_MEM | Debugger.TYPE_WORD | Debugger.TYPE_IN],
0x03: [Debugger.INS.LSL, Debugger.TYPE_REG | Debugger.TYPE_WORD | Debugger.TYPE_IN | Debugger.TYPE_286, Debugger.TYPE_MEM | Debugger.TYPE_WORD | Debugger.TYPE_IN],
0x05: [Debugger.INS.LOADALL,Debugger.TYPE_286]
};
@ -2747,6 +2747,43 @@ if (DEBUGGER) {
/**
* parseInstruction(sOp, sOperand, addr)
*
* This generally requires an exact match of both the operation code (sOp) and mode operand
* (sOperand) against the aOps[] and aOpMods[] arrays, respectively; however, the regular
* expression built from aOpMods and stored in regexOpModes does relax the matching criteria
* slightly; ie, a 4-digit hex value ("nnnn") will be satisfied with either 3 or 4 digits, and
* similarly, a 2-digit hex address (nn) will be satisfied with either 1 or 2 digits.
*
* Note that this function does not actually store the instruction into memory, even though it requires
* a target address (addr); that parameter is currently needed ONLY for "branch" instructions, because in
* order to calculate the branch displacement, it needs to know where the instruction will ultimately be
* stored, relative to its target address.
*
* Another handy feature of this function is its ability to display all available modes for a particular
* operation. For example, while in "assemble mode", if one types:
*
* ldy?
*
* the Debugger will display:
*
* supported opcodes:
* A0: LDY nn
* A4: LDY [nn]
* AC: LDY [nnnn]
* B4: LDY [nn+X]
* BC: LDY [nnnn+X]
*
* Use of a trailing "?" on any opcode will display all variations of that opcode; no instruction will be
* assembled, and the operand parameter, if any, will be ignored.
*
* Although this function is capable of reporting numerous errors, roughly half of them indicate internal
* consistency errors, not user errors; the former should really be asserts, but I'm not comfortable bombing
* out because of my error as opposed to their error. The only errors a user should expect to see:
*
* "unknown operation": sOp is not a valid operation (per aOps)
* "unknown operand": sOperand is not a valid operand (per aOpMods)
* "unknown instruction": the combination of sOp + sOperand does not exist (per aaOpDescs)
* "branch out of range": the branch address, relative to addr, is too far away
*
* @this {Debugger}
* @param {string} sOp
* @param {string|undefined} sOperand

View file

@ -35,14 +35,13 @@
*
* 1) creating an empty disk: create()
* 2) loading a disk image: load()
* 3) mounting a disk image: mount()
* 4) getting disk information: info()
* 5) dumping disk contents: dump()
* 6) seeking a disk sector: seek()
* 7) reading data from a sector: read()
* 8) writing data to a sector: write()
* 9) save disk deltas: save()
* 10) restore disk deltas: restore()
* 3) getting disk information: info()
* 4) dumping disk contents: dump()
* 5) seeking a disk sector: seek()
* 6) reading data from a sector: read()
* 7) writing data to a sector: write()
* 8) save disk deltas: save()
* 9) restore disk deltas: restore()
*
* More functionality may be factored out of the FDC and HDC components later and moved here, to
* further reduce some of the duplication between them, but the above functionality is a good start.
@ -349,7 +348,7 @@ Disk.prototype.powerUp = function(data, fRepower) {
if (!fRepower) {
if (this.fOnDemand && !this.fRemote) {
this.setReady(false);
this.load(this.sDiskName, this.sDiskPath, this.donePowerUp, this);
this.load(this.sDiskName, this.sDiskPath, null, this.donePowerUp, this);
}
}
return true;
@ -467,12 +466,16 @@ Disk.prototype.create = function()
};
/**
* load(sDiskName, sDiskPath, fnNotify, controller)
* load(sDiskName, sDiskPath, file, fnNotify)
*
* TODO: Figure out how we can strongly type fnNotify, because the Closure Compiler has issues with:
*
* param {function(Component,Object,Disk,string,string)} fnNotify
*
* for:
*
* this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
*
* Also, while we're at it, learn if there are ways to:
*
* 1) declare a function taking NO parameters (ie, generate a warning if any parameters are specified)
@ -481,10 +484,11 @@ Disk.prototype.create = function()
* @this {Disk}
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {File} [file] is set if there's an associated File object
* @param {function(...)} [fnNotify]
* @param {Component} [controller]
*/
Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify, controller)
Disk.prototype.load = function(sDiskName, sDiskPath, file, fnNotify, controller)
{
var sDiskURL = sDiskPath;
@ -497,8 +501,6 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify, controller)
this.messageDebugger(sMessage);
}
Component.assert(!this.fnNotify);
if (this.fnNotify) {
if (DEBUG) this.controller.log('too many load requests for "' + sDiskName + '" (' + sDiskPath + ')');
return;
@ -509,6 +511,16 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify, controller)
this.fnNotify = fnNotify;
this.controllerNotify = controller || this.controller;
if (file) {
var disk = this;
var reader = new FileReader();
reader.onload = function() {
disk.build(reader.result, true);
};
reader.readAsArrayBuffer(file);
return;
}
/*
* If there's an occurrence of API_ENDPOINT anywhere in the path, we assume we can use it as-is;
* ie, that the user has already formed a URL of the type we use ourselves for unconverted disk images.
@ -563,6 +575,60 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify, controller)
web.loadResource(sDiskURL, true, null, this, this.doneLoad, sDiskPath);
};
/**
*
* build(buffer, fDirty)
*
* Builds a disk image from an ArrayBuffer (eg, from a FileReader object), rather than from JSON-encoded data.
*
* @this {Disk}
* @param {?} buffer (we KNOW this is an ArrayBuffer, but we can't seem to convince the Closure Compiler)
* @param {boolean} [fDirty] is true if we should mark the entire disk dirty (to ensure that we save/restore it)
*/
Disk.prototype.build = function(buffer, fDirty)
{
var disk;
var cbDiskData = buffer? buffer.byteLength : 0;
var disketteFormat = DiskAPI.DISKETTE_FORMATS[cbDiskData];
if (disketteFormat) {
var ib = 0;
var dwChecksum = 0;
var cbSector = 512, dwPattern = 0;
var dv = new DataView(buffer, 0, cbDiskData);
this.aDiskData = new Array(disketteFormat[0]);
for (var iCylinder = 0; iCylinder < this.aDiskData.length; iCylinder++) {
var cylinder = this.aDiskData[iCylinder] = new Array(disketteFormat[1]);
for (var iHead = 0; iHead < cylinder.length; iHead++) {
var head = cylinder[iHead] = new Array(disketteFormat[2]);
for (var iSector = 0; iSector < head.length; iSector++) {
var sector = this.initSector(null, iCylinder, iHead, iSector + 1, cbSector, dwPattern);
var cdw = cbSector >> 2;
var adw = sector['data'];
for (var idw = 0; idw < cdw; idw++, ib += 4) {
var dw = adw[idw] = dv.getInt32(ib, true);
dwChecksum = (dwChecksum + dw) & 0xffffffff;
}
if (fDirty) {
sector.cModify = cdw;
sector.fDirty = true;
}
head[iSector] = sector;
}
}
}
this.dwChecksum = dwChecksum;
disk = this;
} else {
this.notice("Unrecognized diskette format (" + cbDiskData + " bytes)");
}
if (this.fnNotify) {
this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
this.fnNotify = null;
}
};
/**
* doneLoad(sDiskFile, sDiskData, nErrorCode, sDiskPath)
*
@ -749,27 +815,12 @@ Disk.prototype.doneLoad = function(sDiskFile, sDiskData, nErrorCode, sDiskPath)
}
delete sector['bytes'];
}
/*
* The current sector should now have ALL the properties of a proper Sector object; ie:
*
* 'sector': sector number
* 'length': size of the sector, in bytes
* 'data': array of dwords
* 'pattern': dword pattern to use for empty or partial sectors
*
* In addition, we will maintain the following information on a per-sector basis,
* as sectors are modified:
*
* iModify: index of first modified dword in sector
* cModify: number of modified dwords in sector
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
*
* And for the disk as a whole, we maintain a checksum of the original unmodified data:
*
* dwChecksum: summation of all dwords in all non-empty sectors
*/
this.initSector(sector, iCylinder, iHead);
/*
* For the disk as a whole, we maintain a checksum of the original unmodified data:
*
* dwChecksum: summation of all dwords in all non-empty sectors
*
* Pattern-filling of sectors is deferred until absolutely necessary (eg, when a sector is
* being written). So all we need to do at this point is checksum all the initial sector data.
*/
@ -797,6 +848,20 @@ Disk.prototype.doneLoad = function(sDiskFile, sDiskData, nErrorCode, sDiskPath)
/**
* initSector(sector, iCylinder, iHead, iSector, cbSector, dwPattern)
*
* Ensures every sector has ALL the properties of a proper Sector object; ie:
*
* 'sector': sector number
* 'length': size of the sector, in bytes
* 'data': array of dwords
* 'pattern': dword pattern to use for empty or partial sectors (null for unread remote sectors)
*
* In addition, we will maintain the following information on a per-sector basis,
* as sectors are modified:
*
* iModify: index of first modified dword in sector
* cModify: number of modified dwords in sector
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
*
* @param {Object} sector
* @param {number} iCylinder
* @param {number} iHead

View file

@ -143,19 +143,19 @@ function FDC(parmsFDC) {
this['dmaWrite'] = this.dmaWrite;
this['dmaFormat'] = this.dmaFormat;
this.pAutoMount = null;
this.configAutoMount = null;
if (parmsFDC['autoMount']) {
this.pAutoMount = parmsFDC['autoMount'];
if (typeof this.pAutoMount == "string") {
this.configAutoMount = parmsFDC['autoMount'];
if (typeof this.configAutoMount == "string") {
try {
/*
* The most likely source of any exception will be right here, where we're parsing
* the JSON-encoded diskette data.
*/
this.pAutoMount = eval("(" + parmsFDC['autoMount'] + ")");
this.configAutoMount = eval("(" + parmsFDC['autoMount'] + ")");
} catch (e) {
Component.error("FDC auto-mount error: " + e.message + " (" + parmsFDC['autoMount'] + ")");
this.pAutoMount = null;
this.configAutoMount = null;
}
}
}
@ -412,117 +412,109 @@ FDC.aCmdInfo = {
*/
FDC.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
{
var fdc = this;
switch (sBinding) {
case "listDisks":
this.bindings[sBinding] = control;
/*
* Add the special path of "?" to the list, which will prompt the user for a URL.
*/
var controlOption = window.document.createElement("option");
controlOption['value'] = "?";
controlOption.innerHTML = "User-defined URL...";
case "listDisks":
this.bindings[sBinding] = control;
var addControlOption = function(sValue, sDisplay) {
var controlOption;
controlOption = window.document.createElement("option");
controlOption['value'] = sValue;
controlOption.innerHTML = sDisplay;
control.appendChild(controlOption);
/*
* Now add an 'onchange' handler.
*/
control.onchange = function(fdc, controlDisks) {
return function onChangeListDisks() {
var controlDesc = fdc.bindings["descDisk"];
if (controlDesc) {
var controlOption = controlDisks.options[controlDisks.selectedIndex];
if (controlOption) {
var dataValue = {};
var sValue = controlOption.getAttribute("data-value");
if (sValue) {
try {
dataValue = eval("({" + sValue + "})");
} catch (e) {
Component.error("FDC option error: " + (e.message || e));
}
}
var sDesc = dataValue['desc'];
if (sDesc === undefined) sDesc = "";
var sHRef = dataValue['href'];
if (sHRef !== undefined) sDesc = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sDesc + "</a>";
controlDesc.innerHTML = sDesc;
}
};
addControlOption("?", "Remote Disk");
control.onchange = function onChangeListDisks(event) {
var controlDesc = fdc.bindings["descDisk"];
var controlOption = control.options[control.selectedIndex];
if (controlDesc && controlOption) {
var dataValue = {};
var sValue = controlOption.getAttribute("data-value");
if (sValue) {
try {
dataValue = eval("({" + sValue + "})");
} catch (e) {
Component.error("FDC option error: " + e.message);
}
};
}(this, control);
return true;
}
var sDesc = dataValue['desc'];
if (sDesc === undefined) sDesc = "";
var sHRef = dataValue['href'];
if (sHRef !== undefined) sDesc = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sDesc + "</a>";
controlDesc.innerHTML = sDesc;
}
};
return true;
case "descDisk":
case "listDrives":
case "descDisk":
case "listDrives":
this.bindings[sBinding] = control;
/*
* I tried going with onclick instead of onchange, so that if you wanted to confirm what's
* loaded in a particular drive, you could click the drive control without having to change it.
* However, that doesn't seem to work for all browsers, so I've reverted to onchange.
*/
control.onchange = function onChangeListDrives(event) {
var iDrive = str.parseInt(control.value, 10);
if (iDrive != null) fdc.displayDiskette(iDrive);
};
return true;
case "loadDrive":
this.bindings[sBinding] = control;
control.onclick = function onClickLoadDrive(event) {
var controlDisks = fdc.bindings["listDisks"];
if (controlDisks) {
var sDisketteName = controlDisks.options[controlDisks.selectedIndex].text;
var sDiskettePath = controlDisks.value;
fdc.loadSelectedDrive(sDisketteName, sDiskettePath);
}
};
return true;
case "loadLocal":
/*
* Check for availability of FileReader
*/
if (window.FileReader && window.File && window.FileList && window.Blob ) {
this.bindings[sBinding] = control;
/*
* I tried going with onclick instead of onchange, so that if you wanted to confirm what's
* loaded in a particular drive, you could click the drive control without having to change it.
* However, that doesn't seem to work for all browsers, so I've reverted to onchange.
* Enable "Load Local File" button only if a file is actually selected
*/
control.onchange = function(fdc, controlDrives) {
return function onChangeListDrives() {
var iDrive = parseInt(controlDrives.value, 10);
if (!isNaN(iDrive)) fdc.displayDiskette(iDrive);
};
}(this, control);
return true;
control.addEventListener('change', function() {
var fieldset = control.children[0];
var files = fieldset.children[0].files;
var submit = fieldset.children[1];
submit.disabled = (files.length == 0);
});
case "loadDrive":
this.bindings[sBinding] = control;
control.onclick = function(fdc) {
return function onClickLoadDrive() {
var iDrive;
var controlDisks = fdc.bindings["listDisks"];
var controlDrives = fdc.bindings["listDrives"];
if (controlDisks && controlDrives && !isNaN(iDrive = parseInt(controlDrives.value, 10)) && iDrive >= 0 && iDrive < fdc.aDrives.length) {
var sDiskettePath = controlDisks.value;
if (!sDiskettePath) {
fdc.unloadDrive(iDrive);
return;
}
var sDisketteName = controlDisks.options[controlDisks.selectedIndex].text;
control.onsubmit = function(event) {
var file = event.currentTarget[1].files[0];
if (file) {
var sDiskettePath = file.name;
var sDisketteName = str.getBaseName(sDiskettePath, true);
fdc.loadSelectedDrive(sDisketteName, sDiskettePath, file);
}
/*
* Prevent reloading of web page after form submission
*/
return false;
};
}
else {
this.println("FileReader support not available, disabling local file load");
control.parentNode.removeChild(control);
}
return true;
/*
* If the special path of "?" is selected, then we want to prompt the user for a URL. Oh, and
* make sure we pass an empty string as the 2nd parameter to prompt(), so that IE won't display
* "undefined" -- because after all, undefined and "undefined" are EXACTLY the same thing, right?
*
* TODO: This is literally all I've done to support external disk images. There's probably more
* I should do, like dynamically updating "listDisks" to include new entries, and adding new entries
* to the save/restore data.
*/
if (sDiskettePath == "?") {
sDiskettePath = window.prompt("Enter the URL of a disk image to load.", "");
if (!sDiskettePath)
return;
sDisketteName = str.getBaseName(sDiskettePath);
fdc.println("Attempting to load " + sDiskettePath + " as \"" + sDisketteName + "\"");
}
while (fdc.loadDiskette(iDrive, sDisketteName, sDiskettePath, false)) {
if (!window.confirm("Click OK to reload the original disk.\n(WARNING: All disk changes will be discarded)")) {
return;
}
/*
* So here's the story: loadDiskette() returned true, which it does ONLY if the specified disk is already
* mounted, AND the user clicked OK to reload the original disk image. So we must toss any history we have
* for the disk, unload it, and then loop back around to loadDiskette().
*
* loadDiskette() should NEVER return true the second time, since no disk is loaded. In other words, this
* isn't really a loop so much as a one-time retry operation.
*/
fdc.removeDiskHistory(sDisketteName, sDiskettePath);
fdc.unloadDrive(iDrive, false, true);
}
return;
}
fdc.notice("Nothing to load");
};
}(this);
return true;
default:
break;
default:
break;
}
return false;
};
@ -1143,17 +1135,17 @@ FDC.prototype.seekDrive = function(drive, iSector, nSectors)
FDC.prototype.autoMount = function(fRemount)
{
if (!fRemount) this.cAutoMount = 0;
if (this.pAutoMount) {
for (var sDrive in this.pAutoMount) {
var pDriveConfig = this.pAutoMount[sDrive];
if (pDriveConfig['name'] && pDriveConfig['path']) {
if (this.configAutoMount) {
for (var sDrive in this.configAutoMount) {
var configDrive = this.configAutoMount[sDrive];
if (configDrive['name'] && configDrive['path']) {
/*
* WARNING: This conversion of drive letter to drive number, starting with A:, is very simplistic
* and is not guaranteed to match the drive mapping that DOS ultimately uses.
*/
var iDrive = sDrive.charCodeAt(0) - 0x41;
if (iDrive >= 0 && iDrive < this.aDrives.length) {
if (!this.loadDiskette(iDrive, pDriveConfig['name'], pDriveConfig['path'], true) && fRemount)
if (!this.loadDiskette(iDrive, configDrive['name'], configDrive['path'], true) && fRemount)
this.setReady(false);
continue;
}
@ -1165,18 +1157,75 @@ FDC.prototype.autoMount = function(fRemount)
};
/**
* loadDiskette(iDrive, sDisketteName, sDiskettePath, fAutoMount)
* loadSelectedDrive(sDisketteName, sDiskettePath, file)
*
* @this {FDC}
* @param {string} sDisketteName
* @param {string} sDiskettePath
* @param {File} [file] is set if there's an associated File object
*/
FDC.prototype.loadSelectedDrive = function(sDisketteName, sDiskettePath, file)
{
var iDrive;
var controlDrives = this.bindings["listDrives"];
if (controlDrives && !isNaN(iDrive = parseInt(controlDrives.value, 10)) && iDrive >= 0 && iDrive < this.aDrives.length) {
if (!sDiskettePath) {
this.unloadDrive(iDrive);
return;
}
/*
* If the special path of "?" is selected, then we want to prompt the user for a URL. Oh, and
* make sure we pass an empty string as the 2nd parameter to prompt(), so that IE won't display
* "undefined" -- because after all, undefined and "undefined" are EXACTLY the same thing, right?
*
* TODO: This is literally all I've done to support remote disk images. There's probably more
* I should do, like dynamically updating "listDisks" to include new entries, and adding new entries
* to the save/restore data.
*/
if (sDiskettePath == "?") {
sDiskettePath = window.prompt("Enter the URL of a remote disk image.", "") || "";
if (!sDiskettePath) return;
sDisketteName = str.getBaseName(sDiskettePath);
this.println("Attempting to load " + sDiskettePath + " as \"" + sDisketteName + "\"");
}
this.println("loading disk " + sDiskettePath + "...");
while (this.loadDiskette(iDrive, sDisketteName, sDiskettePath, false, file)) {
if (!window.confirm("Click OK to reload the original disk.\n(WARNING: All disk changes will be discarded)")) {
return;
}
/*
* So here's the story: loadDiskette() returned true, which it does ONLY if the specified disk is already
* mounted, AND the user clicked OK to reload the original disk image. So we must toss any history we have
* for the disk, unload it, and then loop back around to loadDiskette().
*
* loadDiskette() should NEVER return true the second time, since no disk is loaded. In other words,
* this isn't really a loop so much as a one-time retry operation.
*/
this.removeDiskHistory(sDisketteName, sDiskettePath);
this.unloadDrive(iDrive, false, true);
}
return;
}
this.notice("Nothing to load");
};
/**
* loadDiskette(iDrive, sDisketteName, sDiskettePath, fAutoMount, file)
*
* NOTE: If sDiskettePath is already loaded in the drive, nothing needs to be done.
*
* @this {FDC}
* @param {number} iDrive (pre-validated)
* @param {number} iDrive
* @param {string} sDisketteName
* @param {string|null} sDiskettePath
* @param {boolean} fAutoMount
* @param {string} sDiskettePath
* @param {boolean} [fAutoMount]
* @param {File} [file] is set if there's an associated File object
* @return {boolean} true if diskette (already) loaded, false if queued up (or busy)
*/
FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAutoMount)
FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAutoMount, file)
{
var drive = this.aDrives[iDrive];
if (sDiskettePath && drive.sDiskettePath != sDiskettePath) {
@ -1192,7 +1241,7 @@ FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAut
this.messageDebugger("loading diskette '" + sDisketteName + "'");
}
var disk = new Disk(this, drive, DiskAPI.MODE.PRELOAD);
disk.load(sDisketteName, sDiskettePath, this.doneLoadDiskette);
disk.load(sDisketteName, sDiskettePath, file, this.doneLoadDiskette);
return false;
}
return true;

View file

@ -1151,7 +1151,7 @@ HDC.prototype.loadDisk = function(iDrive, sDiskName, sDiskPath, fAutoMount)
this.messageDebugger("loading " + sDiskName);
}
var disk = drive.disk || new Disk(this, drive, drive.mode);
disk.load(sDiskName, sDiskPath, this.doneLoadDisk);
disk.load(sDiskName, sDiskPath, null, this.doneLoadDisk);
return false;
};

View file

@ -135,9 +135,6 @@ function Memory(addr, size, fReadOnly, controller) {
*/
if (TYPEDARRAYS) {
this.buffer = new ArrayBuffer(size);
/**
* @type {DataView}
*/
this.dv = new DataView(this.buffer, 0, size);
/*
* We could also use dv.getUint8() and dv.setUint8(), but using ab[] to get/set bytes

View file

@ -356,6 +356,14 @@
<xsl:when test="@type = 'heading'">
<div><xsl:value-of select="."/></div>
</xsl:when>
<xsl:when test="@type = 'file'">
<form class="{$APPCLASS}-{@class}" data-value="{$type},{$binding}">
<fieldset>
<input type="file"/>
<input type="submit" value="Mount" disabled="true"/>
</fieldset>
</form>
</xsl:when>
<xsl:when test="@type = 'separator'">
<hr/>
</xsl:when>

View file

@ -72,4 +72,38 @@ var DiskAPI = {
}
};
if (typeof module !== 'undefined') module.exports = DiskAPI;
/*
* Common (supported) diskette formats
*/
DiskAPI.DISKETTE_FORMATS = {
163840: [40,1,8], // media type 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)
184320: [40,1,9], // media type 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)
327680: [40,2,8], // media type 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)
368640: [40,2,9], // media type 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)
737280: [80,2,9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)
1228800: [80,2,15], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)
1474560: [80,2,18], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)
2949120: [80,2,36] // media type 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)
};
/*
* BIOS Parameter Block (BPB) offsets in DOS-compatible boot sectors
*/
DiskAPI.BPB = {
JMP_OPCODE: 0x00, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset
OEM_STRING: 0x03, // 8 bytes
SECTOR_BYTES: 0x0B, // 2 bytes: bytes per sector (eg, 0x200 or 512)
CLUSTER_SECS: 0x0D, // 1 byte: sectors per cluster (eg, 1)
RESERVED_SECS: 0x0E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1)
FAT_TOTAL: 0x10, // 1 byte: FAT copies (eg, 2)
ROOT_ENTRIES: 0x11, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors)
SECTOR_TOTAL: 0x13, // 2 bytes: number of sectors (eg, 0x140 or 320)
MEDIA_TYPE: 0x15, // 1 byte: media type (eg, 0xFF: 320Kb, 0xFE: 160Kb, 0xFD: 360Kb, 0xFC: 180Kb)
FAT_SECS: 0x16, // 2 bytes: sectors per FAT (eg, 1)
TRACK_SECS: 0x18, // 2 bytes: sectors per track (eg, 8)
HEAD_TOTAL: 0x1A, // 2 bytes: number of heads (eg, 1)
HIDDEN_SECS: 0x1C, // 4 bytes: number of hidden sectors (always 0 for non-partitioned media)
LARGE_SECS: 0x20 // 4 bytes: number of sectors if SECTOR_TOTAL is zero
};
if (typeof module !== 'undefined') module.exports = DiskAPI;