import { base64_encode } from '../base64'; import type { byte, Card, memory, nibble, rom, } from '../types'; import { FormatWorkerMessage, FormatWorkerResponse, NibbleFormat, DISK_PROCESSED, DRIVE_NUMBERS, DriveNumber, JSONDisk, ENCODING_NIBBLE, PROCESS_BINARY, PROCESS_JSON_DISK, PROCESS_JSON, ENCODING_BITSTREAM, MassStorageData, } from '../formats/types'; import { createDisk, createDiskFromJsonDisk } from '../formats/create_disk'; import { toHex } from '../util'; import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2'; 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 SEQUENCER_ROM_13 = [ // See Understanding the Apple IIe, Figure 9.10 The DOS 3.2 Logic State Sequencer // Note that the column order here is NOT the same as in Figure 9.10 for Q7 H (Write). // // Q7 L (Read) Q7 H (Write) // Q6 L (Shift) Q6 H (Load) 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 0xD8, 0x18, 0x18, 0x08, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 0 0xD8, 0x2D, 0x28, 0x28, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, // 1 0xD8, 0x38, 0x38, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B, // 2 0xD8, 0x48, 0xD8, 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, 0x09, 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, 0x39, 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 0x1D, 0x0D, 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; const SEQUENCER_ROM_16 = [ // See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer // Note that the column order here is NOT the same as in Figure 9.11 for Q7 H (Write). // // Q7 L (Read) Q7 H (Write) // Q6 L (Shift) Q6 H (Load) 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 LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type Phase = 0 | 1 | 2 | 3; /** * How far the head moves, in quarter tracks, when in phase X and phase Y is * activated. For example, if in phase 0 (top row), turning on phase 3 would * step backwards a quarter track while turning on phase 2 would step forwards * a half track. * * Note that this emulation is highly simplified as it only takes into account * the order that coils are powered on and ignores when they are powered off. * The actual hardware allows for multiple coils to be powered at the same time * providing different levels of torque on the head arm. Along with that, the * RWTS uses a complex delay system to drive the coils faster based on expected * head momentum. * * Examining the https://computerhistory.org/blog/apple-ii-dos-source-code/, * one finds that the SEEK routine on line 4831 of `appdos31.lst`. It uses * `ONTABLE` and `OFFTABLE` (each 12 bytes) to know exactly how many * microseconds to power on/off each coil as the head accelerates. At the end, * the final coil is left powered on 9.5 milliseconds to ensure the head has * settled. * * https://embeddedmicro.weebly.com/apple-2iie.html shows traces of the boot * seek (which is slightly different) and a regular seek. */ const PHASE_DELTA = [ [0, 1, 2, -1], [-1, 0, 1, 2], [-2, -1, 0, 1], [1, -2, -1, 0] ] as const; export interface Callbacks { driveLight: (drive: DriveNumber, on: boolean) => void; dirty: (drive: DriveNumber, dirty: boolean) => void; label: (drive: DriveNumber, name?: string, side?: string) => void; } /** Common information for Nibble and WOZ disks. */ interface BaseDrive { /** Current disk format. */ format: NibbleFormat; /** Current disk volume number. */ volume: byte; /** Displayed disk name */ name: string; /** (Optional) Disk side (Front/Back, A/B) */ side?: string | undefined; /** Quarter track position of read/write head. */ track: byte; /** Position of the head on the track. */ head: byte; /** Current active coil in the head stepper motor. */ phase: Phase; /** Whether the drive write protect is on. */ readOnly: boolean; /** Whether the drive has been written to since it was loaded. */ dirty: boolean; } /** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ interface WozDrive extends BaseDrive { /** Woz encoding */ encoding: typeof ENCODING_BITSTREAM; /** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */ trackMap: byte[]; /** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */ rawTracks: Uint8Array[]; } /** Nibble format track data. */ interface NibbleDrive extends BaseDrive { /** Nibble encoding */ encoding: typeof ENCODING_NIBBLE; /** Nibble data. The index is the track number. */ tracks: memory[]; } type Drive = WozDrive | NibbleDrive; function isNibbleDrive(drive: Drive): drive is NibbleDrive { return drive.encoding === ENCODING_NIBBLE; } function isWozDrive(drive: Drive): drive is WozDrive { return drive.encoding === ENCODING_BITSTREAM; } interface DriveState { format: NibbleFormat; encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE; volume: byte; name: string; side?: string | undefined; tracks: memory[]; track: byte; head: byte; phase: Phase; readOnly: boolean; dirty: boolean; trackMap: number[]; rawTracks: Uint8Array[]; } interface State { drives: DriveState[]; skip: number; latch: number; writeMode: boolean; on: boolean; drive: DriveNumber; } function getDriveState(drive: Drive): DriveState { const result: DriveState = { format: drive.format, encoding: drive.encoding, volume: drive.volume, name: drive.name, side: drive.side, tracks: [], track: drive.track, head: drive.head, phase: drive.phase, readOnly: drive.readOnly, dirty: drive.dirty, trackMap: [], rawTracks: [], }; if (isNibbleDrive(drive)) { for (let idx = 0; idx < drive.tracks.length; idx++) { result.tracks.push(new Uint8Array(drive.tracks[idx])); } } if (isWozDrive(drive)) { result.trackMap = [...drive.trackMap]; for (let idx = 0; idx < drive.rawTracks.length; idx++) { result.rawTracks.push(new Uint8Array(drive.rawTracks[idx])); } } return result; } function setDriveState(state: DriveState) { let result: Drive; if (state.encoding === ENCODING_NIBBLE) { result = { format: state.format, encoding: ENCODING_NIBBLE, volume: state.volume, name: state.name, side: state.side, tracks: [], 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(new Uint8Array(state.tracks[idx])); } } else { result = { format: state.format, encoding: ENCODING_BITSTREAM, volume: state.volume, name: state.name, side: state.side, track: state.track, head: state.head, phase: state.phase, readOnly: state.readOnly, dirty: state.dirty, trackMap: [...state.trackMap], rawTracks: [], }; for (let idx = 0; idx < state.rawTracks.length; idx++) { result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); } } return result; } /** * Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller. */ export default class DiskII implements Card { private drives: Drive[] = [ { // Drive 1 format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, name: 'Disk 1', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, }, { // Drive 2 format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, name: 'Disk 2', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, }]; /** * When `1`, the next nibble will be available for read; when `0`, * the card is pretending to wait for data to be shifted in by the * sequencer. */ private skip = 0; /** Last data written by the CPU to card softswitch 0x8D. */ private bus = 0; /** Drive data register. */ private latch = 0; /** Drive off timeout id or null. */ private offTimeout: number | null = null; /** Q6 (Shift/Load): Used by WOZ disks. */ private q6 = 0; /** Q7 (Read/Write): Used by WOZ disks. */ private q7: boolean = false; /** Q7 (Read/Write): Used by Nibble disks. */ private writeMode = false; /** Whether the selected drive is on. */ private on = false; /** Current drive number (1, 2). */ private drive: DriveNumber = 1; /** Current drive object. */ private cur = this.drives[this.drive - 1]; /** Nibbles read this on cycle */ private nibbleCount = 0; /** Q0-Q3: Coil states. */ private q = [false, false, false, false]; /** The 8-cycle LSS clock. */ private clock: LssClockCycle = 0; /** Current CPU cycle count. */ private lastCycles = 0; /** Current state of the Logic State Sequencer. */ private state: nibble = 0; /** * Number of zeros read in a row. The Disk ][ can only read two zeros in a * row reliably; above that and the drive starts reporting garbage. See * "Freaking Out Like a MC3470" in the WOZ spec. */ private zeros = 0; /** Contents of the P5 ROM at 0xCnXX. */ private bootstrapRom: rom; /** Contents of the P6 ROM. */ private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13; private worker: Worker; /** Builds a new Disk ][ card. */ constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) { this.debug('Disk ]['); this.lastCycles = this.io.cycles(); this.bootstrapRom = this.sectors === 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13; this.sequencerRom = this.sectors === 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13; // From the example in UtA2e, p. 9-29, col. 1, para. 1., this is // essentially the start of the sequencer loop and produces // correctly synced nibbles immediately. Starting at state 0 // would introduce a spurrious 1 in the latch at the beginning, // which requires reading several more sync bytes to sync up. this.state = 2; this.initWorker(); } private debug(..._args: unknown[]) { // debug(..._args); } public head(): number { return this.cur.head; } /** * Spin the disk under the read/write head for WOZ images. * * This implementation emulates every clock cycle of the 2 MHz * sequencer since the last time it was called in order to * determine the current state. Because this is called on * every access to the softswitches, the data in the latch * will be correct on every read. * * The emulation of the disk makes a few simplifying assumptions: * * * The motor turns on instantly. * * The head moves tracks instantly. * * The length (in bits) of each track of the WOZ image * represents one full rotation of the disk and that each * bit is evenly spaced. * * Writing will not change the track length. This means * that short tracks stay short. * * The read head picks up the next bit when the sequencer * clock === 4. * * Head position X on track T is equivalent to head position * X on track T′. (This is not the recommendation in the WOZ * spec.) * * Unspecified tracks contain a single zero bit. (A very * short track, indeed!) * * Two zero bits are sufficient to cause the MC3470 to freak * out. When freaking out, it returns 0 and 1 with equal * probability. * * Any softswitch changes happen before `moveHead`. This is * important because it means that if the clock is ever * advanced more than one cycle between calls, the * softswitch changes will appear to happen at the very * beginning, not just before the last cycle. */ private moveHead() { // TODO(flan): Short-circuit if the drive is not on. const cycles = this.io.cycles(); // Spin the disk the number of elapsed cycles since last call let workCycles = (cycles - this.lastCycles) * 2; this.lastCycles = cycles; if (!isWozDrive(this.cur)) { return; } const track = this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; while (workCycles-- > 0) { let pulse: number = 0; if (this.clock === 4) { pulse = track[this.cur.head]; if (!pulse) { // More than 2 zeros can not be read reliably. 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 = this.sequencerRom[idx]; this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${this.q6} latch: ${toHex(this.latch)}`); 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; this.debug('Loading', toHex(this.latch), 'from bus'); break; case 0xD: // SL1 this.latch = ((this.latch << 1) | 0x01) & 0xff; break; default: this.debug(`unknown command: ${toHex(command & 0xf)}`); } this.state = (command >> 4 & 0xF) as nibble; if (this.clock === 4) { if (this.on) { if (this.q7) { track[this.cur.head] = this.state & 0x8 ? 0x01 : 0x00; this.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 private readWriteNext() { if (!isNibbleDrive(this.cur)) { return; } if (this.on && (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); } /** * Sets whether the head positioning stepper motor coil for the given * phase is on or off. Normally, the motor must be stepped two phases * per track. Half tracks can be written by stepping only once; quarter * tracks by activating two neighboring coils at once. */ private setPhase(phase: Phase, on: boolean) { // According to Sather, UtA2e, p. 9-12, Drive On/Off and Drive // Select: // Turning a drive on ($C089,X) [...]: // 1. [...] // 5. [...] enables head positioning [...] // // Therefore do nothing if no drive is on. if (!this.on) { this.debug(`ignoring phase ${phase}${on ? ' on' : ' off'}`); return; } this.debug(`phase ${phase}${on ? ' on' : ' off'}`); if (on) { this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2; this.cur.phase = phase; } // The emulator clamps the track to the valid track range available // in the image, but real Disk II drives can seek past track 34 by // at least a half track, usually a full track. Some 3rd party // drives can seek to track 39. 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; } private 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; this.callbacks.driveLight(this.drive, false); this.debug('nibbles read', this.nibbleCount); }, 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.nibbleCount = 0; this.on = true; this.lastCycles = this.io.cycles(); 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(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(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 = false; this.writeMode = false; break; case LOC.DRIVEWRITEMODE: // 0x0f (Q7H) this.debug('Write Mode'); this.q7 = true; this.writeMode = true; break; default: break; } this.moveHead(); if (readMode) { // According to UtAIIe, p. 9-13 to 9-14, any even address can be // used to read the data register onto the CPU bus, although some // also cause conflicts with the disk controller commands. if ((off & 0x01) === 0) { result = this.latch; if (result & 0x80) { this.nibbleCount++; } } else { result = 0; } } else { // It's not explicitly stated, but writes to any address set the // data register. this.bus = val; } return result; } private 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.bootstrapRom[off]; } write() { // not writable } 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(); } getState(): State { // TODO(flan): This does not accurately save state. It's missing // all of the state for WOZ disks and the current status of the // bus. 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; } 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) { const idx = d - 1; this.drives[idx] = setDriveState(state.drives[idx]); const { name, side, dirty } = state.drives[idx]; this.callbacks.label(d, name, side); this.callbacks.driveLight(d, this.on); this.callbacks.dirty(d, dirty); } this.cur = this.drives[this.drive - 1]; } getMetadata(driveNo: DriveNumber) { const drive = this.drives[driveNo - 1]; return { format: drive.format, volume: drive.volume, track: drive.track, head: drive.head, phase: drive.phase, readOnly: drive.readOnly, dirty: drive.dirty }; } // 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, jsonDisk: JSONDisk) { if (this.worker) { const message: FormatWorkerMessage = { type: PROCESS_JSON_DISK, payload: { drive, jsonDisk }, }; this.worker.postMessage(message); return true; } else { const disk = createDiskFromJsonDisk(jsonDisk); if (disk) { const cur = this.drives[drive - 1]; Object.assign(cur, disk); this.updateDirty(drive, false); this.callbacks.label(drive, disk.name, disk.side); return true; } } return false; } getJSON(drive: DriveNumber, pretty: boolean = false) { 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, json: string) { if (this.worker) { const message: FormatWorkerMessage = { type: PROCESS_JSON, payload: { drive, json }, }; this.worker.postMessage(message); } else { const cur = this.drives[drive - 1]; Object.assign(cur, jsonDecode(json)); } return true; } setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) { const readOnly = false; const volume = 254; const options = { name, rawData, readOnly, volume, }; if (this.worker) { const message: FormatWorkerMessage = { type: PROCESS_BINARY, payload: { drive, fmt, options, } }; this.worker.postMessage(message); return true; } else { const disk = createDisk(fmt, options); if (disk) { const cur = this.drives[drive - 1]; const { name, side } = cur; Object.assign(cur, disk); this.updateDirty(drive, true); this.callbacks.label(drive, name, side); return true; } } return false; } initWorker() { if (!window.Worker) { return; } this.worker = new Worker('dist/format_worker.bundle.js'); this.worker.addEventListener('message', (message: MessageEvent) => { const { data } = message; switch (data.type) { case DISK_PROCESSED: { const { drive, disk } = data.payload; if (disk) { const cur = this.drives[drive - 1]; Object.assign(cur, disk); const { name, side } = cur; this.updateDirty(drive, true); this.callbacks.label(drive, name, side); } } break; } }); } // TODO(flan): Does not work with WOZ disks getBinary(drive: DriveNumber): MassStorageData | null { 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 { ext: 'dsk', name: cur.name, data: data.buffer }; } // 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; } }