mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Convert cards/disk2.js
to Typescript
This is mostly a straightforward conversion of `cards/disk2.js` to Typescript, with the following exceptions: * `setState()` did not restore the drive light state correctly because the callback was called with the old `on` value. * `setPhase()` did not work for WOZ images. * `getBinary()` did not work for `nib` files. * `getBase64()` did not work for `nib` files and maybe didn't work right at all. Even with these fixes, local storage still doesn't work correctly. I have also added several TODOs where methods don't support WOZ disks.
This commit is contained in:
parent
dc13b6a59a
commit
9d0ec5489c
@ -1,690 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { base64_decode, base64_encode } from '../base64';
|
||||
import { debug, toHex } from '../util';
|
||||
import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
|
||||
|
||||
import { P5_16, P5_13 } from '../roms/cards/disk2';
|
||||
|
||||
import _2MG from '../formats/2mg';
|
||||
import D13 from '../formats/d13';
|
||||
import DOS from '../formats/do';
|
||||
import ProDOS from '../formats/po';
|
||||
import Woz from '../formats/woz';
|
||||
import Nibble from '../formats/nib';
|
||||
|
||||
export const DISK_TYPES = [
|
||||
'2mg',
|
||||
'd13',
|
||||
'do',
|
||||
'dsk',
|
||||
'hdv',
|
||||
'po',
|
||||
'nib',
|
||||
'woz'
|
||||
];
|
||||
|
||||
export default function DiskII(io, callbacks, sectors = 16)
|
||||
{
|
||||
var _drives = [
|
||||
{ // Drive 1
|
||||
format: 'dsk',
|
||||
volume: 254,
|
||||
tracks: [],
|
||||
trackMap: null,
|
||||
rawTracks: null,
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
},
|
||||
{ // Drive 2
|
||||
format: 'dsk',
|
||||
volume: 254,
|
||||
tracks: [],
|
||||
trackMap: null,
|
||||
rawTracks: null,
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
}];
|
||||
|
||||
var _skip = 0;
|
||||
var _bus = 0;
|
||||
var _latch = 0;
|
||||
var _offTimeout = null;
|
||||
var _q6 = 0;
|
||||
var _q7 = 0;
|
||||
var _writeMode = false; // q7
|
||||
var _on = false;
|
||||
var _drive = 1;
|
||||
var _cur = _drives[_drive - 1];
|
||||
|
||||
var LOC = {
|
||||
// Disk II Stuff
|
||||
PHASE0OFF: 0x80,
|
||||
PHASE0ON: 0x81,
|
||||
PHASE1OFF: 0x82,
|
||||
PHASE1ON: 0x83,
|
||||
PHASE2OFF: 0x84,
|
||||
PHASE2ON: 0x85,
|
||||
PHASE3OFF: 0x86,
|
||||
PHASE3ON: 0x87,
|
||||
|
||||
DRIVEOFF: 0x88,
|
||||
DRIVEON: 0x89,
|
||||
DRIVE1: 0x8A,
|
||||
DRIVE2: 0x8B,
|
||||
DRIVEREAD: 0x8C, // Q6L
|
||||
DRIVEWRITE: 0x8D, // Q6H
|
||||
DRIVEREADMODE: 0x8E, // Q7L
|
||||
DRIVEWRITEMODE: 0x8F // Q7H
|
||||
};
|
||||
|
||||
|
||||
// CODE OPERATION BEFORE AFTER
|
||||
// 0 CLR XXXXXXXX 00000000
|
||||
// 8 NOP ABCDEFGH ABCDEFGH
|
||||
// 9 SL0 ABCDEFGH BCDEFGH0
|
||||
// A SR (write protected) ABCDEFGH 11111111
|
||||
// (not write protected) ABCDEFGH 0ABCDEFG
|
||||
// B LOAD XXXXXXXX YYYYYYYY
|
||||
// D SL1 ABCDEFGH BCDEFGH1
|
||||
|
||||
// Q7 Read/Write
|
||||
// Q6 Shift/Load
|
||||
|
||||
var _P6 = [
|
||||
// Q7 L (Read) Q7 H (Write)
|
||||
// Q6 L Q6 H Q6 L (Shift) Q6 H (Load)
|
||||
// QA L QA H QA L QA H QA L QA H QA L QA H
|
||||
//1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
|
||||
0x18, 0x18, 0x18, 0x18, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 0
|
||||
0x2D, 0x2D, 0x38, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, // 1
|
||||
0xD8, 0x38, 0x08, 0x28, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B, // 2
|
||||
0xD8, 0x48, 0x48, 0x48, 0x0A, 0x0A, 0x0A, 0x0A, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, // 3
|
||||
0xD8, 0x58, 0xD8, 0x58, 0x0A, 0x0A, 0x0A, 0x0A, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, // 4
|
||||
0xD8, 0x68, 0xD8, 0x68, 0x0A, 0x0A, 0x0A, 0x0A, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, // 5
|
||||
0xD8, 0x78, 0xD8, 0x78, 0x0A, 0x0A, 0x0A, 0x0A, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, // 6
|
||||
0xD8, 0x88, 0xD8, 0x88, 0x0A, 0x0A, 0x0A, 0x0A, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, // 7
|
||||
0xD8, 0x98, 0xD8, 0x98, 0x0A, 0x0A, 0x0A, 0x0A, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, // 8
|
||||
0xD8, 0x29, 0xD8, 0xA8, 0x0A, 0x0A, 0x0A, 0x0A, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, // 9
|
||||
0xCD, 0xBD, 0xD8, 0xB8, 0x0A, 0x0A, 0x0A, 0x0A, 0xB9, 0xB9, 0xB9, 0xB9, 0xBB, 0xBB, 0xBB, 0xBB, // A
|
||||
0xD9, 0x59, 0xD8, 0xC8, 0x0A, 0x0A, 0x0A, 0x0A, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, // B
|
||||
0xD9, 0xD9, 0xD8, 0xA0, 0x0A, 0x0A, 0x0A, 0x0A, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, // C
|
||||
0xD8, 0x08, 0xE8, 0xE8, 0x0A, 0x0A, 0x0A, 0x0A, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, // D
|
||||
0xFD, 0xFD, 0xF8, 0xF8, 0x0A, 0x0A, 0x0A, 0x0A, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, // E
|
||||
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
|
||||
];
|
||||
|
||||
function _debug() {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
function _init() {
|
||||
debug('Disk ][');
|
||||
}
|
||||
|
||||
var _clock = 0;
|
||||
var _lastCycles = io.cycles();
|
||||
var _state = 0;
|
||||
var _zeros = 0;
|
||||
|
||||
function _moveHead() {
|
||||
if (!_cur.rawTracks) {
|
||||
return;
|
||||
}
|
||||
var track = _cur.rawTracks[_cur.trackMap[_cur.track]] || [0];
|
||||
|
||||
var cycles = io.cycles();
|
||||
var workCycles = (cycles - _lastCycles) * 2;
|
||||
_lastCycles = cycles;
|
||||
|
||||
while (workCycles-- > 0) {
|
||||
var pulse = 0;
|
||||
if (_clock == 4) {
|
||||
pulse = track[_cur.head];
|
||||
if (!pulse) {
|
||||
if (++_zeros > 2) {
|
||||
pulse = Math.random() > 0.5 ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
_zeros = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var idx = 0;
|
||||
idx |= pulse ? 0x00 : 0x01;
|
||||
idx |= _latch & 0x80 ? 0x02 : 0x00;
|
||||
idx |= _q6 ? 0x04 : 0x00;
|
||||
idx |= _q7 ? 0x08 : 0x00;
|
||||
idx |= _state << 4;
|
||||
|
||||
var command = _P6[idx];
|
||||
|
||||
if (_on && _q7) {
|
||||
debug('clock:', _clock, 'command:', toHex(command), 'q6:', _q6);
|
||||
}
|
||||
|
||||
switch (command & 0xf) {
|
||||
case 0x0: // CLR
|
||||
_latch = 0;
|
||||
break;
|
||||
case 0x8: // NOP
|
||||
break;
|
||||
case 0x9: // SL0
|
||||
_latch = (_latch << 1) & 0xff;
|
||||
break;
|
||||
case 0xA: // SR
|
||||
_latch >>= 1;
|
||||
if (_cur.readOnly) {
|
||||
_latch |= 0x80;
|
||||
}
|
||||
break;
|
||||
case 0xB: // LD
|
||||
_latch = _bus;
|
||||
debug('Loading', toHex(_latch), 'from bus');
|
||||
break;
|
||||
case 0xD: // SL1
|
||||
_latch = ((_latch << 1) | 0x01) & 0xff;
|
||||
break;
|
||||
}
|
||||
_state = command >> 4;
|
||||
|
||||
if (_clock == 4) {
|
||||
if (_on) {
|
||||
if (_q7) {
|
||||
track[_cur.head] = _state & 0x8 ? 0x01 : 0x00;
|
||||
debug('Wrote', _state & 0x8 ? 0x01 : 0x00);
|
||||
}
|
||||
|
||||
if (++_cur.head >= track.length) {
|
||||
_cur.head = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (++_clock > 7) {
|
||||
_clock = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _readWriteNext() {
|
||||
if (_skip || _writeMode) {
|
||||
var track = _cur.tracks[_cur.track >> 2];
|
||||
if (track && track.length) {
|
||||
if (_cur.head >= track.length) {
|
||||
_cur.head = 0;
|
||||
}
|
||||
|
||||
if (_writeMode) {
|
||||
if (!_cur.readOnly) {
|
||||
track[_cur.head] = _bus;
|
||||
if (!_cur.dirty) {
|
||||
_updateDirty(_drive, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_latch = track[_cur.head];
|
||||
}
|
||||
|
||||
++_cur.head;
|
||||
}
|
||||
} else {
|
||||
_latch = 0;
|
||||
}
|
||||
_skip = (++_skip % 2);
|
||||
}
|
||||
|
||||
var _phase_delta = [
|
||||
[ 0, 1, 2,-1],
|
||||
[-1, 0, 1, 2],
|
||||
[-2,-1, 0, 1],
|
||||
[ 1,-2,-1, 0]
|
||||
];
|
||||
|
||||
var _q = [false, false, false, false]; // q0-3
|
||||
|
||||
function setPhase(phase, on) {
|
||||
_debug('phase ' + phase + (on ? ' on' : ' off'));
|
||||
if (_cur.rawTracks) {
|
||||
if (on) {
|
||||
var delta = _phase_delta[_cur.phase][phase] * 2;
|
||||
_cur.track += delta;
|
||||
_cur.phase = phase;
|
||||
} else {
|
||||
// foo
|
||||
}
|
||||
} else {
|
||||
if (on) {
|
||||
_cur.track += _phase_delta[_cur.phase][phase] * 2;
|
||||
_cur.phase = phase;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cur.track > _cur.tracks.length * 4 - 1) {
|
||||
_cur.track = _cur.tracks.length * 4 - 1;
|
||||
}
|
||||
if (_cur.track < 0x0) {
|
||||
_cur.track = 0x0;
|
||||
}
|
||||
|
||||
// debug(
|
||||
// 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3),
|
||||
// '(' + toHex(_cur.track) + ')',
|
||||
// '[' + phase + ':' + (on ? 'on' : 'off') + ']');
|
||||
|
||||
_q[phase] = on;
|
||||
}
|
||||
|
||||
function _access(off, val) {
|
||||
var result = 0;
|
||||
var readMode = val === undefined;
|
||||
|
||||
switch (off & 0x8f) {
|
||||
case LOC.PHASE0OFF: // 0x00
|
||||
setPhase(0, false);
|
||||
break;
|
||||
case LOC.PHASE0ON: // 0x01
|
||||
setPhase(0, true);
|
||||
break;
|
||||
case LOC.PHASE1OFF: // 0x02
|
||||
setPhase(1, false);
|
||||
break;
|
||||
case LOC.PHASE1ON: // 0x03
|
||||
setPhase(1, true);
|
||||
break;
|
||||
case LOC.PHASE2OFF: // 0x04
|
||||
setPhase(2, false);
|
||||
break;
|
||||
case LOC.PHASE2ON: // 0x05
|
||||
setPhase(2, true);
|
||||
break;
|
||||
case LOC.PHASE3OFF: // 0x06
|
||||
setPhase(3, false);
|
||||
break;
|
||||
case LOC.PHASE3ON: // 0x07
|
||||
setPhase(3, true);
|
||||
break;
|
||||
|
||||
case LOC.DRIVEOFF: // 0x08
|
||||
if (!_offTimeout) {
|
||||
if (_on) {
|
||||
_offTimeout = window.setTimeout(function() {
|
||||
_debug('Drive Off');
|
||||
_on = false;
|
||||
if (callbacks.driveLight) { callbacks.driveLight(_drive, false); }
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LOC.DRIVEON: // 0x09
|
||||
if (_offTimeout) {
|
||||
window.clearTimeout(_offTimeout);
|
||||
_offTimeout = null;
|
||||
}
|
||||
if (!_on) {
|
||||
_debug('Drive On');
|
||||
_on = true;
|
||||
_lastCycles = io.cycles();
|
||||
if (callbacks.driveLight) { callbacks.driveLight(_drive, true); }
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVE1: // 0x0a
|
||||
_debug('Disk 1');
|
||||
_drive = 1;
|
||||
_cur = _drives[_drive - 1];
|
||||
if (_on && callbacks.driveLight) {
|
||||
callbacks.driveLight(2, false);
|
||||
callbacks.driveLight(1, true);
|
||||
}
|
||||
break;
|
||||
case LOC.DRIVE2: // 0x0b
|
||||
_debug('Disk 2');
|
||||
_drive = 2;
|
||||
_cur = _drives[_drive - 1];
|
||||
if (_on && callbacks.driveLight) {
|
||||
callbacks.driveLight(1, false);
|
||||
callbacks.driveLight(2, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREAD: // 0x0c (Q6L) Shift
|
||||
_q6 = 0;
|
||||
if (_writeMode) {
|
||||
_debug('clearing _q6/SHIFT');
|
||||
}
|
||||
if (!_cur.rawTracks) {
|
||||
_readWriteNext();
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD
|
||||
_q6 = 1;
|
||||
if (_writeMode) {
|
||||
_debug('setting _q6/LOAD');
|
||||
}
|
||||
if (!_cur.rawTracks) {
|
||||
if (readMode && !_writeMode) {
|
||||
if (_cur.readOnly) {
|
||||
_latch = 0xff;
|
||||
_debug('Setting readOnly');
|
||||
} else {
|
||||
_latch = _latch >> 1;
|
||||
_debug('Clearing readOnly');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
|
||||
_debug('Read Mode');
|
||||
_q7 = 0;
|
||||
_writeMode = false;
|
||||
break;
|
||||
case LOC.DRIVEWRITEMODE: // 0x0f (Q7H)
|
||||
_debug('Write Mode');
|
||||
_q7 = 1;
|
||||
_writeMode = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
_moveHead();
|
||||
|
||||
if (readMode) {
|
||||
if ((off & 0x01) === 0) {
|
||||
result = _latch;
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
} else {
|
||||
_bus = val;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _updateDirty(drive, dirty) {
|
||||
_drives[drive - 1].dirty = dirty;
|
||||
if (callbacks.dirty) { callbacks.dirty(_drive, dirty); }
|
||||
}
|
||||
|
||||
var _P5 = sectors == 16 ? P5_16 : P5_13;
|
||||
|
||||
_init();
|
||||
|
||||
return {
|
||||
ioSwitch: function disk2_ioSwitch(off, val) {
|
||||
return _access(off, val);
|
||||
},
|
||||
|
||||
read: function disk2_read(page, off) {
|
||||
return _P5[off];
|
||||
},
|
||||
|
||||
write: function disk2_write() {},
|
||||
|
||||
reset: function disk2_reset() {
|
||||
if (_on) {
|
||||
callbacks.driveLight(_drive, false);
|
||||
_writeMode = false;
|
||||
_on = false;
|
||||
_drive = 1;
|
||||
_cur = _drives[_drive - 1];
|
||||
}
|
||||
for (var idx = 0; idx < 4; idx++) {
|
||||
_q[idx] = false;
|
||||
}
|
||||
},
|
||||
|
||||
tick: function disk2_tick() {
|
||||
_moveHead();
|
||||
},
|
||||
|
||||
getState: function disk2_getState() {
|
||||
function getDriveState(drive) {
|
||||
var result = {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
tracks: [],
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
for (var idx = 0; idx < drive.tracks.length; idx++) {
|
||||
result.tracks.push(base64_encode(drive.tracks[idx]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
var result = {
|
||||
drives: [],
|
||||
skip: _skip,
|
||||
latch: _latch,
|
||||
writeMode: _writeMode,
|
||||
on: _on,
|
||||
drive: _drive
|
||||
};
|
||||
_drives.forEach(function(drive, idx) {
|
||||
result.drives[idx] = getDriveState(drive);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
setState: function disk2_setState(state) {
|
||||
function setDriveState(state) {
|
||||
var result = {
|
||||
format: state.format,
|
||||
volume: state.volume,
|
||||
tracks: [],
|
||||
track: state.track,
|
||||
head: state.head,
|
||||
phase: state.phase,
|
||||
readOnly: state.readOnly,
|
||||
dirty: state.dirty
|
||||
};
|
||||
for (var idx = 0; idx < state.tracks.length; idx++) {
|
||||
result.tracks.push(base64_decode(state.tracks[idx]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
state.drives.forEach(function(drive, idx) {
|
||||
_drives[idx] = setDriveState(drive);
|
||||
callbacks.driveLight(idx, _drive.on);
|
||||
callbacks.dirty(idx, _drive.dirty);
|
||||
});
|
||||
_skip = state.skip;
|
||||
_latch = state.latch;
|
||||
_writeMode = state.writeMode;
|
||||
_on = state.on;
|
||||
_drive = state.drive;
|
||||
_cur = _drives[_drive - 1];
|
||||
},
|
||||
|
||||
getMetadata: function disk_getMetadata(driveNo) {
|
||||
var drive = _drives[driveNo - 1];
|
||||
if (drive.tracks.length) {
|
||||
return {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
rwts: function disk2_rwts(disk, track, sector) {
|
||||
var cur = _drives[disk - 1];
|
||||
return readSector(cur, track, sector);
|
||||
},
|
||||
|
||||
setDisk: function disk2_setDisk(drive, disk) {
|
||||
var fmt = disk.type, readOnly = disk.readOnly;
|
||||
|
||||
var data, t, s;
|
||||
if (disk.encoding == 'base64') {
|
||||
data = [];
|
||||
for (t = 0; t < disk.data.length; t++) {
|
||||
if (fmt == 'nib') {
|
||||
data[t] = base64_decode(disk.data[t]);
|
||||
} else {
|
||||
data[t] = [];
|
||||
for (s = 0; s < disk.data[t].length; s++) {
|
||||
data[t][s] = base64_decode(disk.data[t][s]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = disk.data;
|
||||
}
|
||||
var cur = _drives[drive - 1];
|
||||
|
||||
// var v = (fmt === 'dsk' ? data[0x11][0x00][0x06] : 0xfe);
|
||||
// if (v == 0x00) {
|
||||
var volume = disk.volume || 0xfe;
|
||||
// }
|
||||
|
||||
var options = {
|
||||
volume,
|
||||
readOnly,
|
||||
name,
|
||||
data
|
||||
};
|
||||
|
||||
switch (fmt) {
|
||||
case 'd13':
|
||||
disk = new D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
disk = new DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
disk = new Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
disk = new ProDOS(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(cur, disk);
|
||||
_updateDirty(_drive, false);
|
||||
},
|
||||
|
||||
getJSON: function disk2_getJSON(drive, pretty) {
|
||||
var cur = _drives[drive - 1];
|
||||
return jsonEncode(cur, pretty);
|
||||
},
|
||||
|
||||
setJSON: function disk2_setJSON(drive, data) {
|
||||
var cur = _drives[drive - 1];
|
||||
Object.assign(cur, jsonDecode(data));
|
||||
return true;
|
||||
},
|
||||
|
||||
setBinary: function disk2_setBinary(drive, name, fmt, rawData) {
|
||||
var disk;
|
||||
var cur = _drives[drive - 1];
|
||||
var readOnly = false;
|
||||
var volume = 254;
|
||||
var options = {
|
||||
name,
|
||||
rawData,
|
||||
readOnly,
|
||||
volume
|
||||
};
|
||||
|
||||
switch (fmt) {
|
||||
case '2mg':
|
||||
disk = new _2MG(options);
|
||||
break;
|
||||
case 'd13':
|
||||
disk = new D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
disk = new DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
disk = new Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
disk = new ProDOS(options);
|
||||
break;
|
||||
case 'woz':
|
||||
disk = new Woz(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(cur, disk);
|
||||
_updateDirty(drive, true);
|
||||
return true;
|
||||
},
|
||||
|
||||
getBinary: function disk2_getBinary(drive) {
|
||||
var cur = _drives[drive - 1];
|
||||
var len = (16 * cur.tracks.length * 256);
|
||||
var data = new Uint8Array(len);
|
||||
var idx = 0;
|
||||
|
||||
for (var t = 0; t < cur.tracks.length; t++) {
|
||||
if (cur.format === 'nib') {
|
||||
data[idx++] = cur.tracks[t];
|
||||
} else {
|
||||
for (var s = 0; s < 0x10; s++) {
|
||||
var sector = readSector(cur, t, s);
|
||||
for (var b = 0; b < 256; b++) {
|
||||
data[idx++] = sector[b];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
getBase64: function disk2_getBase64(drive) {
|
||||
var cur = _drives[drive - 1];
|
||||
var data = [];
|
||||
for (var t = 0; t < cur.tracks.length; t++) {
|
||||
data[t] = [];
|
||||
if (cur.format === 'nib') {
|
||||
data += base64_encode(cur.tracks[t]);
|
||||
} else {
|
||||
for (var s = 0; s < 0x10; s++) {
|
||||
data += base64_encode(readSector(cur, t, s));
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
}
|
794
js/cards/disk2.ts
Normal file
794
js/cards/disk2.ts
Normal file
@ -0,0 +1,794 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { base64_decode, base64_encode } from '../base64';
|
||||
import { byte, DiskFormat, MemberOf, memory } from '../types';
|
||||
import { debug, toHex } from '../util';
|
||||
import { Disk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
|
||||
|
||||
import { P5_16, P5_13 } from '../roms/cards/disk2';
|
||||
|
||||
import _2MG from '../formats/2mg';
|
||||
import D13 from '../formats/d13';
|
||||
import DOS from '../formats/do';
|
||||
import ProDOS from '../formats/po';
|
||||
import Woz from '../formats/woz';
|
||||
import Nibble from '../formats/nib';
|
||||
import Apple2IO from '../apple2io';
|
||||
|
||||
|
||||
/** Softswitch locations */
|
||||
const LOC = {
|
||||
// Disk II Controller Commands
|
||||
// See Understanding the Apple IIe, Table 9.1
|
||||
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
|
||||
PHASE0ON: 0x81, // Q0H: Phase 0 ON
|
||||
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
|
||||
PHASE1ON: 0x83, // Q1H: Phase 1 ON
|
||||
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
|
||||
PHASE2ON: 0x85, // Q2H: Phase 2 ON
|
||||
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
|
||||
PHASE3ON: 0x87, // Q3H: Phase 3 ON
|
||||
|
||||
DRIVEOFF: 0x88, // Q4L: Drives OFF
|
||||
DRIVEON: 0x89, // Q4H: Selected drive ON
|
||||
DRIVE1: 0x8A, // Q5L: Select drive 1
|
||||
DRIVE2: 0x8B, // Q5H: Select drive 2
|
||||
DRIVEREAD: 0x8C, // Q6L: Shift while writing; read data
|
||||
DRIVEWRITE: 0x8D, // Q6H: Load while writing; read write protect
|
||||
DRIVEREADMODE: 0x8E, // Q7L: Read
|
||||
DRIVEWRITEMODE: 0x8F // Q7H: Write
|
||||
} as const;
|
||||
|
||||
|
||||
/** Logic state sequencer ROM */
|
||||
// See Understanding the Apple IIe, Table 9.3 Logic State Sequencer Commands
|
||||
// CODE OPERATION BEFORE AFTER
|
||||
// 0 CLR XXXXXXXX 00000000
|
||||
// 8 NOP ABCDEFGH ABCDEFGH
|
||||
// 9 SL0 ABCDEFGH BCDEFGH0
|
||||
// A SR (write protected) ABCDEFGH 11111111
|
||||
// (not write protected) ABCDEFGH 0ABCDEFG
|
||||
// B LOAD XXXXXXXX YYYYYYYY
|
||||
// D SL1 ABCDEFGH BCDEFGH1
|
||||
|
||||
const _P6 = [
|
||||
// See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer
|
||||
// Q7 L (Read) Q7 H (Write)
|
||||
// Q6 L Q6 H Q6 L (Shift) Q6 H (Load)
|
||||
// QA L QA H QA L QA H QA L QA H QA L QA H
|
||||
//1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
|
||||
0x18, 0x18, 0x18, 0x18, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 0
|
||||
0x2D, 0x2D, 0x38, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, // 1
|
||||
0xD8, 0x38, 0x08, 0x28, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B, // 2
|
||||
0xD8, 0x48, 0x48, 0x48, 0x0A, 0x0A, 0x0A, 0x0A, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, // 3
|
||||
0xD8, 0x58, 0xD8, 0x58, 0x0A, 0x0A, 0x0A, 0x0A, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, // 4
|
||||
0xD8, 0x68, 0xD8, 0x68, 0x0A, 0x0A, 0x0A, 0x0A, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, // 5
|
||||
0xD8, 0x78, 0xD8, 0x78, 0x0A, 0x0A, 0x0A, 0x0A, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, // 6
|
||||
0xD8, 0x88, 0xD8, 0x88, 0x0A, 0x0A, 0x0A, 0x0A, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, // 7
|
||||
0xD8, 0x98, 0xD8, 0x98, 0x0A, 0x0A, 0x0A, 0x0A, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, // 8
|
||||
0xD8, 0x29, 0xD8, 0xA8, 0x0A, 0x0A, 0x0A, 0x0A, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, // 9
|
||||
0xCD, 0xBD, 0xD8, 0xB8, 0x0A, 0x0A, 0x0A, 0x0A, 0xB9, 0xB9, 0xB9, 0xB9, 0xBB, 0xBB, 0xBB, 0xBB, // A
|
||||
0xD9, 0x59, 0xD8, 0xC8, 0x0A, 0x0A, 0x0A, 0x0A, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, // B
|
||||
0xD9, 0xD9, 0xD8, 0xA0, 0x0A, 0x0A, 0x0A, 0x0A, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, // C
|
||||
0xD8, 0x08, 0xE8, 0xE8, 0x0A, 0x0A, 0x0A, 0x0A, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, // D
|
||||
0xFD, 0xFD, 0xF8, 0xF8, 0x0A, 0x0A, 0x0A, 0x0A, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, // E
|
||||
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
|
||||
] as const;
|
||||
|
||||
type Phase = 0 | 1 | 2 | 3;
|
||||
|
||||
const _phase_delta = [
|
||||
[0, 1, 2, -1],
|
||||
[-1, 0, 1, 2],
|
||||
[-2, -1, 0, 1],
|
||||
[1, -2, -1, 0]
|
||||
] as const;
|
||||
|
||||
const DRIVE_NUMBERS = [1, 2] as const;
|
||||
type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
|
||||
|
||||
interface Callbacks {
|
||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
||||
}
|
||||
|
||||
interface BaseDrive {
|
||||
format: DiskFormat,
|
||||
volume: byte,
|
||||
track: byte,
|
||||
head: byte,
|
||||
phase: Phase,
|
||||
readOnly: boolean,
|
||||
dirty: boolean,
|
||||
}
|
||||
|
||||
// WOZ format track data from https://applesaucefdc.com/woz/reference2/
|
||||
interface WozDrive extends BaseDrive {
|
||||
// Maps quarter tracks to data is rawTracks; 0xFF = random garbage
|
||||
trackMap: byte[];
|
||||
// Unique tracks. The index is arbitrary—_not_ the track number.
|
||||
rawTracks: memory[];
|
||||
}
|
||||
|
||||
interface NibbleDrive extends BaseDrive {
|
||||
// Nibble data. The index is the track number.
|
||||
tracks: memory[];
|
||||
}
|
||||
|
||||
type Drive = WozDrive | NibbleDrive;
|
||||
|
||||
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
|
||||
return 'tracks' in drive;
|
||||
}
|
||||
|
||||
// Does not support WOZ disks
|
||||
interface DriveState {
|
||||
format: DiskFormat,
|
||||
volume: byte,
|
||||
tracks: string[],
|
||||
track: byte,
|
||||
head: byte,
|
||||
phase: Phase,
|
||||
readOnly: boolean,
|
||||
dirty: boolean,
|
||||
}
|
||||
|
||||
interface State {
|
||||
drives: DriveState[];
|
||||
skip: number;
|
||||
latch: number;
|
||||
writeMode: boolean;
|
||||
on: boolean;
|
||||
drive: DriveNumber;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
function getDriveState(drive: Drive): DriveState {
|
||||
const result: DriveState = {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
tracks: [] as string[],
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
if (!isNibbleDrive(drive)) {
|
||||
throw Error('No tracks.');
|
||||
}
|
||||
for (let idx = 0; idx < drive.tracks.length; idx++) {
|
||||
result.tracks.push(base64_encode(drive.tracks[idx]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
function setDriveState(state: DriveState) {
|
||||
const result: Drive = {
|
||||
format: state.format,
|
||||
volume: state.volume,
|
||||
tracks: [] as memory[],
|
||||
track: state.track,
|
||||
head: state.head,
|
||||
phase: state.phase,
|
||||
readOnly: state.readOnly,
|
||||
dirty: state.dirty
|
||||
};
|
||||
for (let idx = 0; idx < state.tracks.length; idx++) {
|
||||
result.tracks!.push(base64_decode(state.tracks[idx]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates the 16-sector version of the Disk ][ drive and controller.
|
||||
*/
|
||||
export default class DiskII {
|
||||
|
||||
private _drives: Drive[] = [
|
||||
{ // Drive 1
|
||||
format: 'dsk',
|
||||
volume: 254,
|
||||
tracks: [],
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
},
|
||||
{ // Drive 2
|
||||
format: 'dsk',
|
||||
volume: 254,
|
||||
tracks: [],
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
}];
|
||||
|
||||
private _skip = 0;
|
||||
private _bus = 0;
|
||||
private _latch = 0;
|
||||
private _offTimeout: number | null = null;
|
||||
private _q6 = 0;
|
||||
private _q7 = 0;
|
||||
private _writeMode = false; // q7
|
||||
private _on = false;
|
||||
private _drive: DriveNumber = 1;
|
||||
private _cur = this._drives[this._drive - 1];
|
||||
|
||||
private _q = [false, false, false, false]; // q0-3: phase
|
||||
|
||||
private _clock = 0;
|
||||
private _lastCycles = 0;
|
||||
private _state = 0;
|
||||
private _zeros = 0;
|
||||
|
||||
private _P5: memory;
|
||||
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) {
|
||||
this._lastCycles = this.io.cycles();
|
||||
// TODO(flan): This changes the port ROM but does not change the LSS
|
||||
this._P5 = this.sectors == 16 ? P5_16 : P5_13;
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
_debug(..._args: any) {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
_init() {
|
||||
this._debug('Disk ][');
|
||||
}
|
||||
|
||||
// Only used for WOZ disks
|
||||
_moveHead() {
|
||||
if (isNibbleDrive(this._cur)) {
|
||||
return;
|
||||
}
|
||||
const track = this._cur.rawTracks[this._cur.trackMap[this._cur.track]] || [0];
|
||||
|
||||
const cycles = this.io.cycles();
|
||||
let workCycles = (cycles - this._lastCycles) * 2;
|
||||
this._lastCycles = cycles;
|
||||
|
||||
while (workCycles-- > 0) {
|
||||
let pulse = 0;
|
||||
if (this._clock == 4) {
|
||||
pulse = track[this._cur.head];
|
||||
if (!pulse) {
|
||||
if (++this._zeros > 2) {
|
||||
pulse = Math.random() > 0.5 ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
this._zeros = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
idx |= pulse ? 0x00 : 0x01;
|
||||
idx |= this._latch & 0x80 ? 0x02 : 0x00;
|
||||
idx |= this._q6 ? 0x04 : 0x00;
|
||||
idx |= this._q7 ? 0x08 : 0x00;
|
||||
idx |= this._state << 4;
|
||||
|
||||
const command = _P6[idx];
|
||||
|
||||
if (this._on && this._q7) {
|
||||
debug('clock:', this._clock, 'command:', toHex(command), 'q6:', this._q6);
|
||||
}
|
||||
|
||||
switch (command & 0xf) {
|
||||
case 0x0: // CLR
|
||||
this._latch = 0;
|
||||
break;
|
||||
case 0x8: // NOP
|
||||
break;
|
||||
case 0x9: // SL0
|
||||
this._latch = (this._latch << 1) & 0xff;
|
||||
break;
|
||||
case 0xA: // SR
|
||||
this._latch >>= 1;
|
||||
if (this._cur.readOnly) {
|
||||
this._latch |= 0x80;
|
||||
}
|
||||
break;
|
||||
case 0xB: // LD
|
||||
this._latch = this._bus;
|
||||
debug('Loading', toHex(this._latch), 'from bus');
|
||||
break;
|
||||
case 0xD: // SL1
|
||||
this._latch = ((this._latch << 1) | 0x01) & 0xff;
|
||||
break;
|
||||
}
|
||||
this._state = command >> 4;
|
||||
|
||||
if (this._clock == 4) {
|
||||
if (this._on) {
|
||||
if (this._q7) {
|
||||
track[this._cur.head] = this._state & 0x8 ? 0x01 : 0x00;
|
||||
debug('Wrote', this._state & 0x8 ? 0x01 : 0x00);
|
||||
}
|
||||
|
||||
if (++this._cur.head >= track.length) {
|
||||
this._cur.head = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (++this._clock > 7) {
|
||||
this._clock = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only called for non-WOZ disks
|
||||
_readWriteNext() {
|
||||
if (!isNibbleDrive(this._cur)) {
|
||||
return;
|
||||
}
|
||||
if (this._skip || this._writeMode) {
|
||||
const track = this._cur.tracks![this._cur.track >> 2];
|
||||
if (track && track.length) {
|
||||
if (this._cur.head >= track.length) {
|
||||
this._cur.head = 0;
|
||||
}
|
||||
|
||||
if (this._writeMode) {
|
||||
if (!this._cur.readOnly) {
|
||||
track[this._cur.head] = this._bus;
|
||||
if (!this._cur.dirty) {
|
||||
this._updateDirty(this._drive, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._latch = track[this._cur.head];
|
||||
}
|
||||
|
||||
++this._cur.head;
|
||||
}
|
||||
} else {
|
||||
this._latch = 0;
|
||||
}
|
||||
this._skip = (++this._skip % 2);
|
||||
}
|
||||
|
||||
setPhase(phase: Phase, on: boolean) {
|
||||
this._debug('phase ' + phase + (on ? ' on' : ' off'));
|
||||
if (isNibbleDrive(this._cur)) {
|
||||
if (on) {
|
||||
this._cur.track += _phase_delta[this._cur.phase][phase] * 2;
|
||||
this._cur.phase = phase;
|
||||
}
|
||||
} else {
|
||||
if (on) {
|
||||
const delta = _phase_delta[this._cur.phase][phase] * 2;
|
||||
this._cur.track += delta;
|
||||
this._cur.phase = phase;
|
||||
} else {
|
||||
// foo
|
||||
}
|
||||
}
|
||||
|
||||
const maxTrack = isNibbleDrive(this._cur)
|
||||
? this._cur.tracks.length * 4 - 1
|
||||
: this._cur.trackMap.length - 1;
|
||||
if (this._cur.track > maxTrack) {
|
||||
this._cur.track = maxTrack;
|
||||
}
|
||||
if (this._cur.track < 0x0) {
|
||||
this._cur.track = 0x0;
|
||||
}
|
||||
|
||||
// debug(
|
||||
// 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3),
|
||||
// '(' + toHex(_cur.track) + ')',
|
||||
// '[' + phase + ':' + (on ? 'on' : 'off') + ']');
|
||||
|
||||
this._q[phase] = on;
|
||||
}
|
||||
|
||||
_access(off: byte, val: byte) {
|
||||
let result = 0;
|
||||
const readMode = val === undefined;
|
||||
|
||||
switch (off & 0x8f) {
|
||||
case LOC.PHASE0OFF: // 0x00
|
||||
this.setPhase(0, false);
|
||||
break;
|
||||
case LOC.PHASE0ON: // 0x01
|
||||
this.setPhase(0, true);
|
||||
break;
|
||||
case LOC.PHASE1OFF: // 0x02
|
||||
this.setPhase(1, false);
|
||||
break;
|
||||
case LOC.PHASE1ON: // 0x03
|
||||
this.setPhase(1, true);
|
||||
break;
|
||||
case LOC.PHASE2OFF: // 0x04
|
||||
this.setPhase(2, false);
|
||||
break;
|
||||
case LOC.PHASE2ON: // 0x05
|
||||
this.setPhase(2, true);
|
||||
break;
|
||||
case LOC.PHASE3OFF: // 0x06
|
||||
this.setPhase(3, false);
|
||||
break;
|
||||
case LOC.PHASE3ON: // 0x07
|
||||
this.setPhase(3, true);
|
||||
break;
|
||||
|
||||
case LOC.DRIVEOFF: // 0x08
|
||||
if (!this._offTimeout) {
|
||||
if (this._on) {
|
||||
// TODO(flan): This is fragile because it relies on
|
||||
// wall-clock time instead of emulator time.
|
||||
this._offTimeout = window.setTimeout(() => {
|
||||
this._debug('Drive Off');
|
||||
this._on = false;
|
||||
if (this.callbacks.driveLight) {
|
||||
this.callbacks.driveLight(this._drive, false);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LOC.DRIVEON: // 0x09
|
||||
if (this._offTimeout) {
|
||||
// TODO(flan): Fragile—see above
|
||||
window.clearTimeout(this._offTimeout);
|
||||
this._offTimeout = null;
|
||||
}
|
||||
if (!this._on) {
|
||||
this._debug('Drive On');
|
||||
this._on = true;
|
||||
this._lastCycles = this.io.cycles();
|
||||
if (this.callbacks.driveLight) { this.callbacks.driveLight(this._drive, true); }
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVE1: // 0x0a
|
||||
this._debug('Disk 1');
|
||||
this._drive = 1;
|
||||
this._cur = this._drives[this._drive - 1];
|
||||
if (this._on && this.callbacks.driveLight) {
|
||||
this.callbacks.driveLight(2, false);
|
||||
this.callbacks.driveLight(1, true);
|
||||
}
|
||||
break;
|
||||
case LOC.DRIVE2: // 0x0b
|
||||
this._debug('Disk 2');
|
||||
this._drive = 2;
|
||||
this._cur = this._drives[this._drive - 1];
|
||||
if (this._on && this.callbacks.driveLight) {
|
||||
this.callbacks.driveLight(1, false);
|
||||
this.callbacks.driveLight(2, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREAD: // 0x0c (Q6L) Shift
|
||||
this._q6 = 0;
|
||||
if (this._writeMode) {
|
||||
this._debug('clearing _q6/SHIFT');
|
||||
}
|
||||
if (isNibbleDrive(this._cur)) {
|
||||
this._readWriteNext();
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD
|
||||
this._q6 = 1;
|
||||
if (this._writeMode) {
|
||||
this._debug('setting _q6/LOAD');
|
||||
}
|
||||
if (isNibbleDrive(this._cur)) {
|
||||
if (readMode && !this._writeMode) {
|
||||
if (this._cur.readOnly) {
|
||||
this._latch = 0xff;
|
||||
this._debug('Setting readOnly');
|
||||
} else {
|
||||
this._latch = this._latch >> 1;
|
||||
this._debug('Clearing readOnly');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
|
||||
this._debug('Read Mode');
|
||||
this._q7 = 0;
|
||||
this._writeMode = false;
|
||||
break;
|
||||
case LOC.DRIVEWRITEMODE: // 0x0f (Q7H)
|
||||
this._debug('Write Mode');
|
||||
this._q7 = 1;
|
||||
this._writeMode = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this._moveHead();
|
||||
|
||||
if (readMode) {
|
||||
if ((off & 0x01) === 0) {
|
||||
result = this._latch;
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
} else {
|
||||
this._bus = val;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_updateDirty(drive: DriveNumber, dirty: boolean) {
|
||||
this._drives[drive - 1].dirty = dirty;
|
||||
if (this.callbacks.dirty) {
|
||||
this.callbacks.dirty(drive, dirty);
|
||||
}
|
||||
}
|
||||
|
||||
ioSwitch(off: byte, val: byte) {
|
||||
return this._access(off, val);
|
||||
}
|
||||
|
||||
read(_page: byte, off: byte) {
|
||||
return this._P5[off];
|
||||
}
|
||||
|
||||
write() { }
|
||||
|
||||
reset() {
|
||||
if (this._on) {
|
||||
this.callbacks.driveLight(this._drive, false);
|
||||
this._writeMode = false;
|
||||
this._on = false;
|
||||
this._drive = 1;
|
||||
this._cur = this._drives[this._drive - 1];
|
||||
}
|
||||
for (let idx = 0; idx < 4; idx++) {
|
||||
this._q[idx] = false;
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
this._moveHead();
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
getState() {
|
||||
const result = {
|
||||
drives: [] as DriveState[],
|
||||
skip: this._skip,
|
||||
latch: this._latch,
|
||||
writeMode: this._writeMode,
|
||||
on: this._on,
|
||||
drive: this._drive
|
||||
};
|
||||
this._drives.forEach(function (drive, idx) {
|
||||
result.drives[idx] = getDriveState(drive);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
setState(state: State) {
|
||||
this._skip = state.skip;
|
||||
this._latch = state.latch;
|
||||
this._writeMode = state.writeMode;
|
||||
this._on = state.on;
|
||||
this._drive = state.drive;
|
||||
for (const d of DRIVE_NUMBERS) {
|
||||
this._drives[d - 1] = setDriveState(state.drives[d - 1]);
|
||||
this.callbacks.driveLight(d, this._on);
|
||||
this.callbacks.dirty(d, this._drives[d - 1].dirty);
|
||||
}
|
||||
this._cur = this._drives[this._drive - 1];
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
getMetadata(driveNo: DriveNumber) {
|
||||
const drive = this._drives[driveNo - 1];
|
||||
if (isNibbleDrive(drive)) {
|
||||
return {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work on WOZ disks
|
||||
rwts(disk: DriveNumber, track: byte, sector: byte) {
|
||||
const cur = this._drives[disk - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
throw new Error('Can\'t read WOZ disks');
|
||||
}
|
||||
return readSector(cur, track, sector);
|
||||
}
|
||||
|
||||
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
||||
// TODO(flan): This implementation is not very safe.
|
||||
setDisk(drive: DriveNumber, disk: any) {
|
||||
const fmt = disk.type as DiskFormat;
|
||||
const readOnly = disk.readOnly;
|
||||
const name = disk.name;
|
||||
|
||||
let data: memory[] | memory[][];
|
||||
if (disk.encoding == 'base64') {
|
||||
data = [];
|
||||
for (let t = 0; t < disk.data.length; t++) {
|
||||
if (fmt == 'nib') {
|
||||
data[t] = base64_decode(disk.data[t] as string);
|
||||
} else {
|
||||
data[t] = [];
|
||||
for (let s = 0; s < disk.data[t].length; s++) {
|
||||
data[t][s] = base64_decode(disk.data[t][s] as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = disk.data;
|
||||
}
|
||||
const cur = this._drives[drive - 1];
|
||||
|
||||
// var v = (fmt === 'dsk' ? data[0x11][0x00][0x06] : 0xfe);
|
||||
// if (v == 0x00) {
|
||||
const volume = disk.volume || 0xfe;
|
||||
// }
|
||||
|
||||
const options = {
|
||||
volume,
|
||||
readOnly,
|
||||
name,
|
||||
data
|
||||
};
|
||||
|
||||
let newDisk: Disk;
|
||||
switch (fmt) {
|
||||
case 'd13':
|
||||
newDisk = D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
newDisk = DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
newDisk = Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
newDisk = ProDOS(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(cur, newDisk);
|
||||
this._updateDirty(this._drive, false);
|
||||
}
|
||||
|
||||
getJSON(drive: DriveNumber, pretty: boolean) {
|
||||
const cur = this._drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
throw new Error('Can\'t save WOZ disks to JSON');
|
||||
}
|
||||
return jsonEncode(cur, pretty);
|
||||
}
|
||||
|
||||
setJSON(drive: DriveNumber, data: any) {
|
||||
const cur = this._drives[drive - 1];
|
||||
Object.assign(cur, jsonDecode(data));
|
||||
return true;
|
||||
}
|
||||
|
||||
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: memory) {
|
||||
let disk;
|
||||
const cur = this._drives[drive - 1];
|
||||
const readOnly = false;
|
||||
const volume = 254;
|
||||
const options = {
|
||||
name,
|
||||
rawData,
|
||||
readOnly,
|
||||
volume
|
||||
};
|
||||
|
||||
switch (fmt) {
|
||||
case '2mg':
|
||||
disk = _2MG(options);
|
||||
break;
|
||||
case 'd13':
|
||||
disk = D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
disk = DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
disk = Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
disk = ProDOS(options);
|
||||
break;
|
||||
case 'woz':
|
||||
disk = Woz(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(cur, disk);
|
||||
this._updateDirty(drive, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
getBinary(drive: DriveNumber) {
|
||||
const cur = this._drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
return null;
|
||||
}
|
||||
// TODO(flan): Assumes 16-sectors
|
||||
const len = (16 * cur.tracks.length * 256);
|
||||
const data = new Uint8Array(len);
|
||||
|
||||
let idx = 0;
|
||||
for (let t = 0; t < cur.tracks.length; t++) {
|
||||
if (cur.format === 'nib') {
|
||||
data.set(cur.tracks[t], idx);
|
||||
idx += cur.tracks[t].length;
|
||||
} else {
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
const sector = readSector(cur, t, s);
|
||||
data.set(sector, idx);
|
||||
idx += sector.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
getBase64(drive: DriveNumber) {
|
||||
const cur = this._drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
return null;
|
||||
}
|
||||
const data: string[][] | string[] = [];
|
||||
for (let t = 0; t < cur.tracks.length; t++) {
|
||||
if (cur.format === 'nib') {
|
||||
data[t] = base64_encode(cur.tracks[t]);
|
||||
} else {
|
||||
const track: string[] = [];
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
track[s] = base64_encode(readSector(cur, t, s));
|
||||
}
|
||||
data[t] = track;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
@ -9,12 +9,12 @@
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { byte, memory } from '../types';
|
||||
import { byte, DiskFormat, memory } from '../types';
|
||||
import { base64_decode, base64_encode } from '../base64';
|
||||
import { bytify, debug, toHex } from '../util';
|
||||
|
||||
export type Disk = {
|
||||
format: string,
|
||||
format: DiskFormat,
|
||||
name: string,
|
||||
volume: byte,
|
||||
tracks: memory[],
|
||||
@ -22,11 +22,11 @@ export type Disk = {
|
||||
};
|
||||
|
||||
export type Drive = {
|
||||
format: string,
|
||||
volume: 254,
|
||||
format: DiskFormat,
|
||||
volume: byte,
|
||||
tracks: memory[],
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
readOnly: boolean,
|
||||
dirty: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -315,6 +315,7 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b
|
||||
return buf;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work on WOZ disks
|
||||
export function readSector(drive: Drive, track: byte, sector: byte) {
|
||||
const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
|
||||
let val, state = 0;
|
||||
|
23
js/types.ts
23
js/types.ts
@ -1,4 +1,14 @@
|
||||
|
||||
/**
|
||||
* Extracts the members of a constant array as a type. Used as:
|
||||
*
|
||||
* @example
|
||||
* const SOME_VALUES = ['a', 'b', 1, 2] as const;
|
||||
* type SomeValues = MemberOf<typeof SOME_VALUES>; // 'a' | 'b' | 1 | 2
|
||||
*/
|
||||
export type MemberOf<T extends ReadonlyArray<unknown>> =
|
||||
T extends ReadonlyArray<infer E> ? E : never;
|
||||
|
||||
/** A byte (0..255). This is not enforced by the compiler. */
|
||||
export type byte = number;
|
||||
|
||||
@ -20,7 +30,18 @@ export interface Memory {
|
||||
write(page: byte, offset: byte, value: byte): void;
|
||||
}
|
||||
|
||||
export type DiskFormat = '2mg' | 'd13' | 'do' | 'dsk' | 'hdv' | 'po' | 'nib' | 'woz';
|
||||
export const DISK_FORMATS = [
|
||||
'2mg',
|
||||
'd13',
|
||||
'do',
|
||||
'dsk',
|
||||
'hdv',
|
||||
'po',
|
||||
'nib',
|
||||
'woz'
|
||||
] as const;
|
||||
|
||||
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
|
||||
|
||||
export interface Drive {
|
||||
format: DiskFormat,
|
||||
|
@ -2,7 +2,7 @@ import MicroModal from 'micromodal';
|
||||
|
||||
import Audio from './audio';
|
||||
import DriveLights from './drive_lights';
|
||||
import { DISK_TYPES } from '../cards/disk2';
|
||||
import { DISK_FORMATS } from '../types';
|
||||
import { gamepad, configGamepad, initGamepad } from './gamepad';
|
||||
import KeyBoard from './keyboard';
|
||||
import Tape, { TAPE_TYPES } from './tape';
|
||||
@ -180,7 +180,7 @@ export function loadAjax(drive, url) {
|
||||
}).then(function(data) {
|
||||
if (data.type == 'binary') {
|
||||
loadBinary(drive, data);
|
||||
} else if (DISK_TYPES.indexOf(data.type) > -1) {
|
||||
} else if (DISK_FORMATS.indexOf(data.type) > -1) {
|
||||
loadDisk(drive, data);
|
||||
}
|
||||
initGamepad(data.gamepad);
|
||||
@ -245,7 +245,7 @@ export function doDelete(name) {
|
||||
function doLoadLocal(drive, file) {
|
||||
var parts = file.name.split('.');
|
||||
var ext = parts[parts.length - 1].toLowerCase();
|
||||
if (DISK_TYPES.indexOf(ext) > -1) {
|
||||
if (DISK_FORMATS.indexOf(ext) > -1) {
|
||||
doLoadLocalDisk(drive, file);
|
||||
} else if (TAPE_TYPES.indexOf(ext) > -1) {
|
||||
tape.doLoadLocalTape(file);
|
||||
|
Loading…
Reference in New Issue
Block a user