From 9d0ec5489c002834d71c68e0745ace3ae1d5336f Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sun, 31 Jan 2021 12:31:43 +0100 Subject: [PATCH] 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. --- js/cards/disk2.js | 690 -------------------------------- js/cards/disk2.ts | 794 +++++++++++++++++++++++++++++++++++++ js/formats/format_utils.ts | 13 +- js/types.ts | 23 +- js/ui/apple2.js | 6 +- 5 files changed, 826 insertions(+), 700 deletions(-) delete mode 100644 js/cards/disk2.js create mode 100644 js/cards/disk2.ts diff --git a/js/cards/disk2.js b/js/cards/disk2.js deleted file mode 100644 index 094ae40..0000000 --- a/js/cards/disk2.js +++ /dev/null @@ -1,690 +0,0 @@ -/* Copyright 2010-2019 Will Scullin - * - * 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; - } - }; -} diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts new file mode 100644 index 0000000..b77f415 --- /dev/null +++ b/js/cards/disk2.ts @@ -0,0 +1,794 @@ +/* Copyright 2010-2019 Will Scullin + * + * 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; + +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; + } +} \ No newline at end of file diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index e43c4c4..2382f10 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -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; diff --git a/js/types.ts b/js/types.ts index bab5194..ca0bb20 100644 --- a/js/types.ts +++ b/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; // 'a' | 'b' | 1 | 2 + */ +export type MemberOf> = + T extends ReadonlyArray ? 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; export interface Drive { format: DiskFormat, diff --git a/js/ui/apple2.js b/js/ui/apple2.js index fd58f32..5e67a82 100644 --- a/js/ui/apple2.js +++ b/js/ui/apple2.js @@ -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);