From 8c2299e43e1af4ea8120aecc7fdb645e2700e4d4 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sat, 1 Oct 2022 20:08:58 +0200 Subject: [PATCH] Minor cleanup (#162) * Split handling of nibble disks and WOZ disks into separate drivers Before, the `DiskII` object took care of both nibble disks and WOZ disks at the same time. This made it hard to see which code affected which disks. Now, nibble disks are handled by the `NibbleDiskDriver` and WOZ disks are handled by the `WozDiskDriver`. This separation of code should lead to easeir testing and, perhaps, faster evolution of disk handling. An upcoming change will move the `NibbleDiskDriver` and the `WozDiskDriver` to separate files. This passes all tests, compiles, and runs both nibble disks and WOZ disks. * Rename drive number fields/variables/parameters to `driveNo` Before, the naming of fields, variables, and parameters that took `DriveNumber` was very inconsistent. This changes them all to `driveNo`. * Organize imports in `disk2.ts` The imports were automatically organized using VS Code's organize imports action. --- js/cards/disk2.ts | 761 ++++++++++++++++++++----------- js/cards/smartport.ts | 90 ++-- js/components/BlockDisk.tsx | 6 +- js/components/BlockFileModal.tsx | 4 +- js/components/DiskDragTarget.tsx | 8 +- js/components/DiskII.tsx | 6 +- js/components/DownloadModal.tsx | 8 +- js/components/Drives.tsx | 12 +- js/components/FileModal.tsx | 14 +- js/components/debugger/Disks.tsx | 14 +- js/components/util/files.ts | 46 +- js/formats/types.ts | 8 +- js/ui/apple2.ts | 103 +++-- js/ui/drive_lights.ts | 10 +- test/js/cards/disk2.spec.ts | 12 +- workers/format.worker.ts | 4 +- 16 files changed, 676 insertions(+), 430 deletions(-) diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 1b16d0d..7e44cbc 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -3,32 +3,14 @@ import type { byte, Card, nibble, - ReadonlyUint8Array, + ReadonlyUint8Array } from '../types'; import { - FormatWorkerMessage, - FormatWorkerResponse, - NibbleFormat, - DISK_PROCESSED, - DRIVE_NUMBERS, - DriveNumber, - JSONDisk, - PROCESS_BINARY, - PROCESS_JSON_DISK, - PROCESS_JSON, - MassStorage, - MassStorageData, - SupportedSectors, - FloppyDisk, - FloppyFormat, - WozDisk, - NibbleDisk, - isNibbleDisk, - isWozDisk, - NoFloppyDisk, - isNoFloppyDisk, - NO_DISK, + DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk, + FloppyFormat, FormatWorkerMessage, + FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage, + MassStorageData, NibbleDisk, NibbleFormat, NoFloppyDisk, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk } from '../formats/types'; import { @@ -36,11 +18,11 @@ import { createDiskFromJsonDisk } from '../formats/create_disk'; -import { toHex } from '../util'; import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils'; +import { toHex } from '../util'; -import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2'; import Apple2IO from '../apple2io'; +import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2'; /** Softswitch locations */ const LOC = { @@ -132,15 +114,16 @@ const SEQUENCER_ROM_16 = [ const SEQUENCER_ROM: Record> = { 13: SEQUENCER_ROM_13, 16: SEQUENCER_ROM_16, -}; +} as const; /** Contents of the P5 ROM at 0xCnXX. */ const BOOTSTRAP_ROM: Record = { 13: BOOTSTRAP_ROM_13, 16: BOOTSTRAP_ROM_16, -}; +} as const; type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; +type LssState = nibble; type Phase = 0 | 1 | 2 | 3; /** @@ -175,7 +158,7 @@ const PHASE_DELTA = [ /** * State of the controller. */ - interface ControllerState { +interface ControllerState { /** Sectors supported by the controller. */ sectors: SupportedSectors; @@ -183,7 +166,7 @@ const PHASE_DELTA = [ on: boolean; /** The active drive. */ - drive: DriveNumber; + driveNo: DriveNumber; /** The 8-cycle LSS clock. */ clock: LssClockCycle; @@ -201,18 +184,32 @@ const PHASE_DELTA = [ bus: byte; } +/** Interface for drivers for various disk types. */ +interface DiskDriver { + tick(): void; + onQ6Low(): void; + onQ6High(readMode: boolean): void; + onDriveOn(): void; + onDriveOff(): void; + clampTrack(): void; + getState(): DriverState; + setState(state: DriverState): void; +} + +type DriverState = EmptyDriverState | NibbleDiskDriverState | WozDiskDriverState; + /** Callbacks triggered by events of the drive or controller. */ export interface Callbacks { /** Called when a drive turns on or off. */ - driveLight: (drive: DriveNumber, on: boolean) => void; + driveLight: (driveNo: DriveNumber, on: boolean) => void; /** * Called when a disk has been written to. For performance and integrity, * this is only called when the drive stops spinning or is removed from * the drive. */ - dirty: (drive: DriveNumber, dirty: boolean) => void; + dirty: (driveNo: DriveNumber, dirty: boolean) => void; /** Called when a disk is inserted or removed from the drive. */ - label: (drive: DriveNumber, name?: string, side?: string) => void; + label: (driveNo: DriveNumber, name?: string, side?: string) => void; } /** Common information for Nibble and WOZ disks. */ @@ -231,6 +228,7 @@ interface Drive { interface DriveState { disk: FloppyDisk; + driver: DriverState; readOnly: boolean; track: byte; head: byte; @@ -242,7 +240,7 @@ interface DriveState { // TODO(flan): It's unclear whether reusing ControllerState here is a good idea. interface State { drives: DriveState[]; - skip: number; + driver: DriverState[]; controllerState: ControllerState; } @@ -255,7 +253,7 @@ function getDiskState(disk: FloppyDisk): FloppyDisk { const { encoding, metadata, readOnly } = disk; return { encoding, - metadata: {...metadata}, + metadata: { ...metadata }, readOnly, }; } @@ -296,61 +294,223 @@ function getDiskState(disk: FloppyDisk): FloppyDisk { throw new Error('Unknown drive state'); } -/** - * Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller. - */ -export default class DiskII implements Card, MassStorage { +interface EmptyDriverState { } - private drives: Record = { - 1: { // Drive 1 - track: 0, - head: 0, - phase: 0, - readOnly: false, - dirty: false, - }, - 2: { // Drive 2 - track: 0, - head: 0, - phase: 0, - readOnly: false, - dirty: false, +class EmptyDriver implements DiskDriver { + constructor(private readonly drive: Drive) { } + + tick(): void { + // do nothing + } + + onQ6Low(): void { + // do nothing + } + + onQ6High(_readMode: boolean): void { + // do nothing + } + + onDriveOn(): void { + // do nothing + } + + onDriveOff(): void { + // do nothing + } + + clampTrack(): void { + // For empty drives, the emulator clamps the track to 0 to 34, + // 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. + if (this.drive.track < 0) { + this.drive.track = 0; } - }; - - private disks: Record = { - 1: { - encoding: NO_DISK, - readOnly: false, - metadata: { name: 'Disk 1' }, - }, - 2: { - encoding: NO_DISK, - readOnly: false, - metadata: { name: 'Disk 2' }, + if (this.drive.track > 34) { + this.drive.track = 34; } - }; + } - private state: ControllerState; + getState() { + return {}; + } + setState(_state: EmptyDriverState): void { + // do nothing + } +} + +abstract class BaseDiskDriver implements DiskDriver { + constructor( + protected readonly driveNo: DriveNumber, + protected readonly drive: Drive, + protected readonly disk: NibbleDisk | WozDisk, + protected readonly controller: ControllerState) { } + + /** Called frequently to ensure the disk is spinning. */ + abstract tick(): void; + + /** Called when Q6 is set LOW. */ + abstract onQ6Low(): void; + + /** Called when Q6 is set HIGH. */ + abstract onQ6High(readMode: boolean): void; + + /** + * Called when drive is turned on. This is guaranteed to be called + * only when the associated drive is toggled from off to on. This + * is also guaranteed to be called when a new disk is inserted when + * the drive is already on. + */ + abstract onDriveOn(): void; + + /** + * Called when drive is turned off. This is guaranteed to be called + * only when the associated drive is toggled from on to off. + */ + abstract onDriveOff(): void; + + debug(..._args: unknown[]) { + // debug(...args); + } + + /** + * Called every time the head moves to clamp the track to a valid + * range. + */ + abstract clampTrack(): void; + + isOn(): boolean { + return this.controller.on && this.controller.driveNo === this.driveNo; + } + + isWriteProtected(): boolean { + return this.drive.readOnly; + } + + abstract getState(): DriverState; + + abstract setState(state: DriverState): void; +} + +interface NibbleDiskDriverState { + skip: number; + nibbleCount: number; +} + +class NibbleDiskDriver extends BaseDiskDriver { /** * 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; - /** Drive off timeout id or null. */ - private offTimeout: number | null = null; - /** Current drive object. Must only be set by `updateActiveDrive()`. */ - private curDrive: Drive; - /** Current disk object. Must only be set by `updateActiveDrive()`. */ - private curDisk: FloppyDisk; + private skip: number = 0; + /** Number of nibbles reads since the drive was turned on. */ + private nibbleCount: number = 0; - /** Nibbles read this on cycle */ - private nibbleCount = 0; + constructor( + driveNo: DriveNumber, + drive: Drive, + readonly disk: NibbleDisk, + controller: ControllerState, + private readonly onDirty: () => void) { + super(driveNo, drive, disk, controller); + } + tick(): void { + // do nothing + } + + onQ6Low(): void { + const drive = this.drive; + const disk = this.disk; + if (this.isOn() && (this.skip || this.controller.q7)) { + const track = disk.tracks[drive.track >> 2]; + if (track && track.length) { + if (drive.head >= track.length) { + drive.head = 0; + } + + if (this.controller.q7) { + const writeProtected = disk.readOnly; + if (!writeProtected) { + track[drive.head] = this.controller.bus; + drive.dirty = true; + this.onDirty(); + } + } else { + this.controller.latch = track[drive.head]; + this.nibbleCount++; + } + + ++drive.head; + } + } else { + this.controller.latch = 0; + } + this.skip = (++this.skip % 2); + } + + onQ6High(readMode: boolean): void { + const drive = this.drive; + if (readMode && !this.controller.q7) { + const writeProtected = drive.readOnly; + if (writeProtected) { + this.controller.latch = 0xff; + this.debug('Setting readOnly'); + } else { + this.controller.latch >>= 1; + this.debug('Clearing readOnly'); + } + } + } + + onDriveOn(): void { + this.nibbleCount = 0; + } + + onDriveOff(): void { + this.debug('nibbles read', this.nibbleCount); + } + + clampTrack(): void { + // For NibbleDisks, the emulator clamps the track to the available + // range. + if (this.drive.track < 0) { + this.drive.track = 0; + } + const lastTrack = 35 * 4 - 1; + if (this.drive.track > lastTrack) { + this.drive.track = lastTrack; + } + } + + getState(): NibbleDiskDriverState { + const { skip, nibbleCount } = this; + return { skip, nibbleCount }; + } + + setState(state: NibbleDiskDriverState) { + this.skip = state.skip; + this.nibbleCount = state.nibbleCount; + } +} + +interface WozDiskDriverState { + clock: LssClockCycle; + state: LssState; + lastCycles: number; + zeros: number; +} + +class WozDiskDriver extends BaseDiskDriver { + /** Logic state sequencer clock cycle. */ + private clock: LssClockCycle; + /** Logic state sequencer state. */ + private state: LssState; /** Current CPU cycle count. */ - private lastCycles = 0; + private lastCycles: number = 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 @@ -358,47 +518,30 @@ export default class DiskII implements Card, MassStorage { */ private zeros = 0; - private worker: Worker; + constructor( + driveNo: DriveNumber, + drive: Drive, + readonly disk: WozDisk, + controller: ControllerState, + private readonly onDirty: () => void, + private readonly io: Apple2IO) { + super(driveNo, drive, disk, controller); - /** Builds a new Disk ][ card. */ - constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) { - this.debug('Disk ]['); + // 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.clock = 0; + } + onDriveOn(): void { this.lastCycles = this.io.cycles(); - this.state = { - sectors, - bus: 0, - latch: 0, - drive: 1, - on: false, - q6: false, - q7: false, - clock: 0, - // 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. - state: 2, - }; - - this.updateActiveDrive(); - - this.initWorker(); } - /** Updates the active drive based on the controller state. */ - private updateActiveDrive() { - this.curDrive = this.drives[this.state.drive]; - this.curDisk = this.disks[this.state.drive]; - } - - private debug(..._args: unknown[]) { - // debug(..._args); - } - - public head(): number { - return this.curDrive.head; + onDriveOff(): void { + // nothing } /** @@ -443,22 +586,27 @@ export default class DiskII implements Card, MassStorage { let workCycles = (cycles - this.lastCycles) * 2; this.lastCycles = cycles; - if (!isWozDisk(this.curDisk)) { - return; - } + const drive = this.drive; + const disk = this.disk; + const controller = this.controller; + + // TODO(flan): Improve unformatted track behavior. The WOZ + // documentation suggests using an empty track of 6400 bytes + // (51,200 bits). const track = - this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0]; - - const state = this.state; + disk.rawTracks[disk.trackMap[drive.track]] || [0]; while (workCycles-- > 0) { let pulse: number = 0; - if (state.clock === 4) { - pulse = track[this.curDrive.head]; + if (this.clock === 4) { + pulse = track[drive.head]; if (!pulse) { // More than 2 zeros can not be read reliably. + // TODO(flan): Revisit with the new MC3470 + // suggested 4-bit window behavior. if (++this.zeros > 2) { - pulse = Math.random() >= 0.5 ? 1 : 0; + const r = Math.random(); + pulse = r >= 0.5 ? 1 : 0; } } else { this.zeros = 0; @@ -467,91 +615,197 @@ export default class DiskII implements Card, MassStorage { let idx = 0; idx |= pulse ? 0x00 : 0x01; - idx |= state.latch & 0x80 ? 0x02 : 0x00; - idx |= state.q6 ? 0x04 : 0x00; - idx |= state.q7 ? 0x08 : 0x00; - idx |= state.state << 4; + idx |= controller.latch & 0x80 ? 0x02 : 0x00; + idx |= controller.q6 ? 0x04 : 0x00; + idx |= controller.q7 ? 0x08 : 0x00; + idx |= this.state << 4; - const command = SEQUENCER_ROM[this.sectors][idx]; + const command = SEQUENCER_ROM[controller.sectors][idx]; - this.debug(`clock: ${state.clock} state: ${toHex(state.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${state.q6} latch: ${toHex(state.latch)}`); + this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`); switch (command & 0xf) { case 0x0: // CLR - state.latch = 0; + controller.latch = 0; break; case 0x8: // NOP break; case 0x9: // SL0 - state.latch = (state.latch << 1) & 0xff; + controller.latch = (controller.latch << 1) & 0xff; break; case 0xA: // SR - state.latch >>= 1; - if (this.curDrive.readOnly) { - state.latch |= 0x80; + controller.latch >>= 1; + if (this.isWriteProtected()) { + controller.latch |= 0x80; } break; case 0xB: // LD - state.latch = state.bus; - this.debug('Loading', toHex(state.latch), 'from bus'); + controller.latch = controller.bus; + this.debug('Loading', toHex(controller.latch), 'from bus'); break; case 0xD: // SL1 - state.latch = ((state.latch << 1) | 0x01) & 0xff; + controller.latch = ((controller.latch << 1) | 0x01) & 0xff; break; default: this.debug(`unknown command: ${toHex(command & 0xf)}`); } - state.state = (command >> 4 & 0xF) as nibble; + this.state = (command >> 4 & 0xF) as LssState; - if (state.clock === 4) { - if (state.on) { - if (state.q7) { - track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00; - this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); + if (this.clock === 4) { + if (this.isOn()) { + if (controller.q7) { + // TODO(flan): This assumes that writes are happening in + // a "friendly" way, namely where the track was originally + // written. To do this correctly, the virtual head should + // actually keep track of the current quarter track plus + // the one on each side. Then, when writing, it should + // check that all three are actually the same rawTrack. If + // they aren't, then the trackMap has to be updated as + // well. + track[drive.head] = this.state & 0x8 ? 0x01 : 0x00; + this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00); + drive.dirty = true; + this.onDirty(); } - if (++this.curDrive.head >= track.length) { - this.curDrive.head = 0; + if (++drive.head >= track.length) { + drive.head = 0; } } } - if (++state.clock > 7) { - state.clock = 0; + if (++this.clock > 7) { + this.clock = 0; } } } - // Only called for non-WOZ disks - private readWriteNext() { - if (!isNibbleDisk(this.curDisk)) { - return; - } - const state = this.state; - if (state.on && (this.skip || state.q7)) { - const track = this.curDisk.tracks[this.curDrive.track >> 2]; - if (track && track.length) { - if (this.curDrive.head >= track.length) { - this.curDrive.head = 0; - } + tick(): void { + this.moveHead(); + } - if (state.q7) { - if (!this.curDrive.readOnly) { - track[this.curDrive.head] = state.bus; - if (!this.curDrive.dirty) { - this.updateDirty(state.drive, true); - } - } - } else { - state.latch = track[this.curDrive.head]; - } + onQ6High(_readMode: boolean): void { + // nothing? + } - ++this.curDrive.head; - } - } else { - state.latch = 0; + onQ6Low(): void { + // nothing? + } + + clampTrack(): void { + // For NibbleDisks, the emulator clamps the track to the available + // range. + if (this.drive.track < 0) { + this.drive.track = 0; } - this.skip = (++this.skip % 2); + const lastTrack = this.disk.trackMap.length - 1; + if (this.drive.track > lastTrack) { + this.drive.track = lastTrack; + } + } + + getState(): WozDiskDriverState { + const { clock, state, lastCycles, zeros } = this; + return { clock, state, lastCycles, zeros }; + } + + setState(state: WozDiskDriverState) { + this.clock = state.clock; + this.state = state.state; + this.lastCycles = state.lastCycles; + this.zeros = state.zeros; + } +} + +/** + * Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller. + */ +export default class DiskII implements Card, MassStorage { + + private drives: Record = { + 1: { // Drive 1 + track: 0, + head: 0, + phase: 0, + readOnly: false, + dirty: false, + }, + 2: { // Drive 2 + track: 0, + head: 0, + phase: 0, + readOnly: false, + dirty: false, + } + }; + + private disks: Record = { + 1: { + encoding: NO_DISK, + readOnly: false, + metadata: { name: 'Disk 1' }, + }, + 2: { + encoding: NO_DISK, + readOnly: false, + metadata: { name: 'Disk 2' }, + } + }; + + private driver: Record = { + 1: new EmptyDriver(this.drives[1]), + 2: new EmptyDriver(this.drives[2]), + }; + + private state: ControllerState; + + /** Drive off timeout id or null. */ + private offTimeout: number | null = null; + /** Current drive object. Must only be set by `updateActiveDrive()`. */ + private curDrive: Drive; + /** Current driver object. Must only be set by `updateAcivetDrive()`. */ + private curDriver: DiskDriver; + + private worker: Worker; + + /** Builds a new Disk ][ card. */ + constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) { + this.debug('Disk ]['); + + this.state = { + sectors, + bus: 0, + latch: 0, + driveNo: 1, + on: false, + q6: false, + q7: false, + clock: 0, + // 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. + state: 2, + }; + + this.updateActiveDrive(); + + this.initWorker(); + } + + /** Updates the active drive based on the controller state. */ + private updateActiveDrive() { + this.curDrive = this.drives[this.state.driveNo]; + this.curDriver = this.driver[this.state.driveNo]; + } + + private debug(..._args: unknown[]) { + // debug(..._args); + } + + public head(): number { + return this.curDrive.head; } /** @@ -579,21 +833,7 @@ export default class DiskII implements Card, MassStorage { this.curDrive.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 = isNibbleDisk(this.curDisk) - ? this.curDisk.tracks.length * 4 - 1 - : (isWozDisk(this.curDisk) - ? this.curDisk.trackMap.length - 1 - : 0); - if (this.curDrive.track > maxTrack) { - this.curDrive.track = maxTrack; - } - if (this.curDrive.track < 0x0) { - this.curDrive.track = 0x0; - } + this.curDriver.clampTrack(); // debug( // 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3), @@ -640,8 +880,8 @@ export default class DiskII implements Card, MassStorage { this.offTimeout = window.setTimeout(() => { this.debug('Drive Off'); state.on = false; - this.callbacks.driveLight(state.drive, false); - this.debug('nibbles read', this.nibbleCount); + this.callbacks.driveLight(state.driveNo, false); + this.curDriver.onDriveOff(); }, 1000); } } @@ -654,16 +894,15 @@ export default class DiskII implements Card, MassStorage { } if (!state.on) { this.debug('Drive On'); - this.nibbleCount = 0; state.on = true; - this.lastCycles = this.io.cycles(); - this.callbacks.driveLight(state.drive, true); + this.callbacks.driveLight(state.driveNo, true); + this.curDriver.onDriveOn(); } break; case LOC.DRIVE1: // 0x0a this.debug('Disk 1'); - state.drive = 1; + state.driveNo = 1; this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(2, false); @@ -672,7 +911,7 @@ export default class DiskII implements Card, MassStorage { break; case LOC.DRIVE2: // 0x0b this.debug('Disk 2'); - state.drive = 2; + state.driveNo = 2; this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(1, false); @@ -682,30 +921,12 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVEREAD: // 0x0c (Q6L) Shift state.q6 = false; - if (state.q7) { - this.debug('clearing _q6/SHIFT'); - } - if (isNibbleDisk(this.curDisk)) { - this.readWriteNext(); - } + this.curDriver.onQ6Low(); break; case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD state.q6 = true; - if (state.q7) { - this.debug('setting _q6/LOAD'); - } - if (isNibbleDisk(this.curDisk)) { - if (readMode && !state.q7) { - if (this.curDrive.readOnly) { - state.latch = 0xff; - this.debug('Setting readOnly'); - } else { - state.latch = state.latch >> 1; - this.debug('Clearing readOnly'); - } - } - } + this.curDriver.onQ6High(readMode); break; case LOC.DRIVEREADMODE: // 0x0e (Q7L) @@ -721,7 +942,7 @@ export default class DiskII implements Card, MassStorage { break; } - this.moveHead(); + this.tick(); if (readMode) { // According to UtAIIe, p. 9-13 to 9-14, any even address can be @@ -729,9 +950,6 @@ export default class DiskII implements Card, MassStorage { // also cause conflicts with the disk controller commands. if ((off & 0x01) === 0) { result = state.latch; - if (result & 0x80) { - this.nibbleCount++; - } } else { result = 0; } @@ -744,10 +962,10 @@ export default class DiskII implements Card, MassStorage { return result; } - private updateDirty(drive: DriveNumber, dirty: boolean) { - this.drives[drive].dirty = dirty; + private updateDirty(driveNo: DriveNumber, dirty: boolean) { + this.drives[driveNo].dirty = dirty; if (this.callbacks.dirty) { - this.callbacks.dirty(drive, dirty); + this.callbacks.dirty(driveNo, dirty); } } @@ -766,36 +984,38 @@ export default class DiskII implements Card, MassStorage { reset() { const state = this.state; if (state.on) { - this.callbacks.driveLight(state.drive, false); + this.callbacks.driveLight(state.driveNo, false); state.q7 = false; state.on = false; - state.drive = 1; + state.driveNo = 1; } this.updateActiveDrive(); } tick() { - this.moveHead(); + this.curDriver.tick(); } - private getDriveState(drive: DriveNumber): DriveState { - const curDrive = this.drives[drive]; - const curDisk = this.disks[drive]; + private getDriveState(driveNo: DriveNumber): DriveState { + const curDrive = this.drives[driveNo]; + const curDisk = this.disks[driveNo]; + const curDriver = this.driver[driveNo]; const { readOnly, track, head, phase, dirty } = curDrive; return { disk: getDiskState(curDisk), + driver: curDriver.getState(), readOnly, track, head, phase, dirty, - }; + }; } getState(): State { const result = { drives: [] as DriveState[], - skip: this.skip, + driver: [] as DriverState[], controllerState: { ...this.state }, }; result.drives[1] = this.getDriveState(1); @@ -804,21 +1024,22 @@ export default class DiskII implements Card, MassStorage { return result; } - private setDriveState(drive: DriveNumber, state: DriveState) { + private setDriveState(driveNo: DriveNumber, state: DriveState) { const { track, head, phase, readOnly, dirty } = state; - this.drives[drive] = { + this.drives[driveNo] = { track, head, phase, readOnly, dirty, }; - this.disks[drive] = getDiskState(state.disk); + const disk = getDiskState(state.disk); + this.setDiskInternal(driveNo, disk); + this.driver[driveNo].setState(state.driver); } - + setState(state: State) { - this.skip = state.skip; this.state = { ...state.controllerState }; for (const d of DRIVE_NUMBERS) { this.setDriveState(d, state.drives[d]); @@ -843,8 +1064,8 @@ export default class DiskII implements Card, MassStorage { } /** Reads the given track and physical sector. */ - rwts(disk: DriveNumber, track: byte, sector: byte) { - const curDisk = this.disks[disk]; + rwts(driveNo: DriveNumber, track: byte, sector: byte) { + const curDisk = this.disks[driveNo]; if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t read WOZ disks'); } @@ -853,12 +1074,12 @@ export default class DiskII implements Card, MassStorage { /** 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) { + setDisk(driveNo: DriveNumber, jsonDisk: JSONDisk) { if (this.worker) { const message: FormatWorkerMessage = { type: PROCESS_JSON_DISK, payload: { - drive, + driveNo: driveNo, jsonDisk }, }; @@ -867,39 +1088,39 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDiskFromJsonDisk(jsonDisk); if (disk) { - this.insertDisk(drive, disk); + this.insertDisk(driveNo, disk); return true; } } return false; } - getJSON(drive: DriveNumber, pretty: boolean = false) { - const curDisk = this.disks[drive]; + getJSON(driveNo: DriveNumber, pretty: boolean = false) { + const curDisk = this.disks[driveNo]; if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t save WOZ disks to JSON'); } return jsonEncode(curDisk, pretty); } - setJSON(drive: DriveNumber, json: string) { + setJSON(driveNo: DriveNumber, json: string) { if (this.worker) { const message: FormatWorkerMessage = { type: PROCESS_JSON, payload: { - drive, + driveNo: driveNo, json }, }; this.worker.postMessage(message); } else { const disk = jsonDecode(json); - this.insertDisk(drive, disk); + this.insertDisk(driveNo, disk); } return true; } - setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) { + setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) { const readOnly = false; const volume = 254; const options = { @@ -913,7 +1134,7 @@ export default class DiskII implements Card, MassStorage { const message: FormatWorkerMessage = { type: PROCESS_BINARY, payload: { - drive, + driveNo: driveNo, fmt, options, } @@ -924,7 +1145,7 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDisk(fmt, options); if (disk) { - this.insertDisk(drive, disk); + this.insertDisk(driveNo, disk); return true; } } @@ -944,7 +1165,7 @@ export default class DiskII implements Card, MassStorage { switch (data.type) { case DISK_PROCESSED: { - const { drive, disk } = data.payload; + const { driveNo: drive, disk } = data.payload; if (disk) { this.insertDisk(drive, disk); } @@ -957,13 +1178,39 @@ export default class DiskII implements Card, MassStorage { } } - private insertDisk(drive: DriveNumber, disk: FloppyDisk) { - this.disks[drive] = disk; - this.drives[drive].head = 0; + private setDiskInternal(driveNo: DriveNumber, disk: FloppyDisk) { + this.disks[driveNo] = disk; + if (isNoFloppyDisk(disk)) { + this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]); + } else if (isNibbleDisk(disk)) { + this.driver[driveNo] = + new NibbleDiskDriver( + driveNo, + this.drives[driveNo], + disk, + this.state, + () => this.updateDirty(driveNo, true)); + } else if (isWozDisk(disk)) { + this.driver[driveNo] = + new WozDiskDriver( + driveNo, + this.drives[driveNo], + disk, + this.state, + () => this.updateDirty(driveNo, true), + this.io); + } else { + throw new Error(`Unknown disk format ${disk.encoding}`); + } this.updateActiveDrive(); + } + + private insertDisk(driveNo: DriveNumber, disk: FloppyDisk) { + this.setDiskInternal(driveNo, disk); + this.drives[driveNo].head = 0; const { name, side } = disk.metadata; - this.updateDirty(drive, true); - this.callbacks.label(drive, name, side); + this.updateDirty(driveNo, this.drives[driveNo].dirty); + this.callbacks.label(driveNo, name, side); } /** @@ -976,8 +1223,8 @@ export default class DiskII implements Card, MassStorage { * an error will be thrown. Using `ext == 'nib'` will always return * an image. */ - getBinary(drive: DriveNumber, ext?: Exclude): MassStorageData | null { - const curDisk = this.disks[drive]; + getBinary(driveNo: DriveNumber, ext?: Exclude): MassStorageData | null { + const curDisk = this.disks[driveNo]; if (!isNibbleDisk(curDisk)) { return null; } @@ -1024,8 +1271,8 @@ export default class DiskII implements Card, MassStorage { } // TODO(flan): Does not work with WOZ or D13 disks - getBase64(drive: DriveNumber) { - const curDisk = this.disks[drive]; + getBase64(driveNo: DriveNumber) { + const curDisk = this.disks[driveNo]; if (!isNibbleDisk(curDisk)) { return null; } diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 1137e14..1ffb93a 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -18,9 +18,9 @@ export interface SmartPortOptions { } export interface Callbacks { - driveLight: (drive: DriveNumber, on: boolean) => void; - dirty: (drive: DriveNumber, dirty: boolean) => void; - label: (drive: DriveNumber, name?: string, side?: string) => void; + driveLight: (driveNo: DriveNumber, on: boolean) => void; + dirty: (driveNo: DriveNumber, dirty: boolean) => void; + label: (driveNo: DriveNumber, name?: string, side?: string) => void; } class Address { @@ -152,15 +152,15 @@ export default class SmartPort implements Card, MassStorage, Restor // debug.apply(this, arguments); } - private driveLight(drive: DriveNumber) { - if (!this.busy[drive]) { - this.busy[drive] = true; - this.callbacks?.driveLight(drive, true); + private driveLight(driveNo: DriveNumber) { + if (!this.busy[driveNo]) { + this.busy[driveNo] = true; + this.callbacks?.driveLight(driveNo, true); } - clearTimeout(this.busyTimeout[drive]); - this.busyTimeout[drive] = setTimeout(() => { - this.busy[drive] = false; - this.callbacks?.driveLight(drive, false); + clearTimeout(this.busyTimeout[driveNo]); + this.busyTimeout[driveNo] = setTimeout(() => { + this.busy[driveNo] = false; + this.callbacks?.driveLight(driveNo, false); }, 100); } @@ -168,7 +168,7 @@ export default class SmartPort implements Card, MassStorage, Restor * dumpBlock */ - dumpBlock(drive: DriveNumber, block: number) { + dumpBlock(driveNo: DriveNumber, block: number) { let result = ''; let b; let jdx; @@ -176,7 +176,7 @@ export default class SmartPort implements Card, MassStorage, Restor for (let idx = 0; idx < 32; idx++) { result += toHex(idx << 4, 4) + ': '; for (jdx = 0; jdx < 16; jdx++) { - b = this.disks[drive].blocks[block][idx * 16 + jdx]; + b = this.disks[driveNo].blocks[block][idx * 16 + jdx]; if (jdx === 8) { result += ' '; } @@ -184,7 +184,7 @@ export default class SmartPort implements Card, MassStorage, Restor } result += ' '; for (jdx = 0; jdx < 16; jdx++) { - b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f; + b = this.disks[driveNo].blocks[block][idx * 16 + jdx] & 0x7f; if (jdx === 8) { result += ' '; } @@ -203,9 +203,9 @@ export default class SmartPort implements Card, MassStorage, Restor * getDeviceInfo */ - getDeviceInfo(state: CpuState, drive: DriveNumber) { - if (this.disks[drive]) { - const blocks = this.disks[drive].blocks.length; + getDeviceInfo(state: CpuState, driveNo: DriveNumber) { + if (this.disks[driveNo]) { + const blocks = this.disks[driveNo].blocks.length; state.x = blocks & 0xff; state.y = blocks >> 8; @@ -221,23 +221,23 @@ export default class SmartPort implements Card, MassStorage, Restor * readBlock */ - readBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) { - this.debug(`read drive=${drive}`); + readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) { + this.debug(`read drive=${driveNo}`); this.debug(`read buffer=${buffer.toString()}`); this.debug(`read block=$${toHex(block)}`); - if (!this.disks[drive]?.blocks.length) { - debug('Drive', drive, 'is empty'); + if (!this.disks[driveNo]?.blocks.length) { + debug('Drive', driveNo, 'is empty'); state.a = DEVICE_OFFLINE; state.s |= flags.C; return; } // debug('read', '\n' + dumpBlock(drive, block)); - this.driveLight(drive); + this.driveLight(driveNo); for (let idx = 0; idx < 512; idx++) { - buffer.writeByte(this.disks[drive].blocks[block][idx]); + buffer.writeByte(this.disks[driveNo].blocks[block][idx]); buffer = buffer.inc(1); } @@ -249,30 +249,30 @@ export default class SmartPort implements Card, MassStorage, Restor * writeBlock */ - writeBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) { - this.debug(`write drive=${drive}`); + writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) { + this.debug(`write drive=${driveNo}`); this.debug(`write buffer=${buffer.toString()}`); this.debug(`write block=$${toHex(block)}`); - if (!this.disks[drive]?.blocks.length) { - debug('Drive', drive, 'is empty'); + if (!this.disks[driveNo]?.blocks.length) { + debug('Drive', driveNo, 'is empty'); state.a = DEVICE_OFFLINE; state.s |= flags.C; return; } - if (this.disks[drive].readOnly) { - debug('Drive', drive, 'is write protected'); + if (this.disks[driveNo].readOnly) { + debug('Drive', driveNo, 'is write protected'); state.a = WRITE_PROTECTED; state.s |= flags.C; return; } // debug('write', '\n' + dumpBlock(drive, block)); - this.driveLight(drive); + this.driveLight(driveNo); for (let idx = 0; idx < 512; idx++) { - this.disks[drive].blocks[block][idx] = buffer.readByte(); + this.disks[driveNo].blocks[block][idx] = buffer.readByte(); buffer = buffer.inc(1); } state.a = 0; @@ -283,25 +283,25 @@ export default class SmartPort implements Card, MassStorage, Restor * formatDevice */ - formatDevice(state: CpuState, drive: DriveNumber) { - if (!this.disks[drive]?.blocks.length) { - debug('Drive', drive, 'is empty'); + formatDevice(state: CpuState, driveNo: DriveNumber) { + if (!this.disks[driveNo]?.blocks.length) { + debug('Drive', driveNo, 'is empty'); state.a = DEVICE_OFFLINE; state.s |= flags.C; return; } - if (this.disks[drive].readOnly) { - debug('Drive', drive, 'is write protected'); + if (this.disks[driveNo].readOnly) { + debug('Drive', driveNo, 'is write protected'); state.a = WRITE_PROTECTED; state.s |= flags.C; return; } - for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) { - this.disks[drive].blocks[idx] = new Uint8Array(); + for (let idx = 0; idx < this.disks[driveNo].blocks.length; idx++) { + this.disks[driveNo].blocks[idx] = new Uint8Array(); for (let jdx = 0; jdx < 512; jdx++) { - this.disks[drive].blocks[idx][jdx] = 0; + this.disks[driveNo].blocks[idx][jdx] = 0; } } @@ -549,18 +549,18 @@ export default class SmartPort implements Card, MassStorage, Restor ); } - setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) { + setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) { let volume = 254; let readOnly = false; if (fmt === '2mg') { const header = read2MGHeader(rawData); - this.metadata[drive] = header; + this.metadata[driveNo] = header; const { bytes, offset } = header; volume = header.volume; readOnly = header.readOnly; rawData = rawData.slice(offset, offset + bytes); } else { - this.metadata[drive] = null; + this.metadata[driveNo] = null; } const options = { rawData, @@ -569,9 +569,9 @@ export default class SmartPort implements Card, MassStorage, Restor volume, }; - this.ext[drive] = fmt; - this.disks[drive] = createBlockDisk(fmt, options); - this.callbacks?.label(drive, name); + this.ext[driveNo] = fmt; + this.disks[driveNo] = createBlockDisk(fmt, options); + this.callbacks?.label(driveNo, name); return true; } diff --git a/js/components/BlockDisk.tsx b/js/components/BlockDisk.tsx index 18053d9..17a5119 100644 --- a/js/components/BlockDisk.tsx +++ b/js/components/BlockDisk.tsx @@ -63,19 +63,19 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => { void; } -export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFileModalProps) => { +export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen } : BlockFileModalProps) => { const [handles, setHandles] = useState(); const [busy, setBusy] = useState(false); const [empty, setEmpty] = useState(true); diff --git a/js/components/DiskDragTarget.tsx b/js/components/DiskDragTarget.tsx index 6494795..6afea68 100644 --- a/js/components/DiskDragTarget.tsx +++ b/js/components/DiskDragTarget.tsx @@ -6,7 +6,7 @@ import { spawn } from './util/promises'; export interface DiskDragTargetProps extends JSX.HTMLAttributes { storage: MassStorage | undefined; - drive?: DriveNumber; + driveNo?: DriveNumber; formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS; @@ -16,7 +16,7 @@ export interface DiskDragTargetProps extends JSX.HTMLAttributes { event.preventDefault(); event.stopPropagation(); - const targetDrive = drive ?? 1; //TODO(whscullin) Maybe pick available drive + const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive const dt = event.dataTransfer; if (dt?.files.length === 1 && storage) { @@ -87,7 +87,7 @@ export const DiskDragTarget = ({ div.removeEventListener('drop', onDrop); }; } - }, [drive, dropRef, formats, onError, storage]); + }, [driveNo, dropRef, formats, onError, storage]); return (
diff --git a/js/components/DiskII.tsx b/js/components/DiskII.tsx index 400aa83..442ea4d 100644 --- a/js/components/DiskII.tsx +++ b/js/components/DiskII.tsx @@ -65,13 +65,13 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => { - + ; - number: DriveNumber; + driveNo: DriveNumber; onClose: (closeBox?: boolean) => void; } -export const DownloadModal = ({ massStorage, number, onClose, isOpen } : DownloadModalProps) => { +export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen } : DownloadModalProps) => { const [href, setHref] = useState(''); const [downloadName, setDownloadName] = useState(''); const doCancel = useCallback(() => onClose(true), [onClose]); useEffect(() => { if (isOpen) { - const storageData = massStorage.getBinary(number); + const storageData = massStorage.getBinary(driveNo); if (storageData) { const { ext, data } = storageData; const { name } = storageData.metadata; @@ -37,7 +37,7 @@ export const DownloadModal = ({ massStorage, number, onClose, isOpen } : Downloa setHref(''); setDownloadName(''); } - }, [isOpen, number, massStorage]); + }, [isOpen, driveNo, massStorage]); return ( <> diff --git a/js/components/Drives.tsx b/js/components/Drives.tsx index c44d6e2..6a617f5 100644 --- a/js/components/Drives.tsx +++ b/js/components/Drives.tsx @@ -10,7 +10,7 @@ import { ErrorModal } from './ErrorModal'; import { ProgressModal } from './ProgressModal'; import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files'; import { useHash } from './hooks/useHash'; -import { DISK_FORMATS, DriveNumber, SupportedSectors } from 'js/formats/types'; +import { DISK_FORMATS, DRIVE_NUMBERS, SupportedSectors } from 'js/formats/types'; import { spawn, Ready } from './util/promises'; import styles from './css/Drives.module.css'; @@ -90,9 +90,9 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => { const hashParts = getHashParts(hash); const controllers: AbortController[] = []; let loading = 0; - for (const drive of [1, 2] as DriveNumber[]) { - if (hashParts && hashParts[drive]) { - const hashPart = decodeURIComponent(hashParts[drive]); + for (const driveNo of DRIVE_NUMBERS) { + if (hashParts && hashParts[driveNo]) { + const hashPart = decodeURIComponent(hashParts[driveNo]); const isHttp = hashPart.match(/^https?:/i); const isJson = hashPart.match(/\.json$/i); if (isHttp && !isJson) { @@ -101,7 +101,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => { try { await loadHttpUnknownFile( smartStorageBroker, - drive, + driveNo, hashPart, signal, onProgress); @@ -116,7 +116,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => { })); } else { const url = isHttp ? hashPart : `json/disks/${hashPart}.json`; - loadJSON(disk2, drive, url).catch((e) => setError(e)); + loadJSON(disk2, driveNo, url).catch((e) => setError(e)); } } } diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index 3a636d7..bc1eb82 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -28,7 +28,7 @@ export type NibbleFileCallback = ( interface FileModalProps { isOpen: boolean; disk2: DiskII; - number: DriveNumber; + driveNo: DriveNumber; onClose: (closeBox?: boolean) => void; } @@ -38,7 +38,7 @@ interface IndexEntry { category: string; } -export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => { +export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => { const [busy, setBusy] = useState(false); const [empty, setEmpty] = useState(true); const [category, setCategory] = useState(); @@ -69,13 +69,13 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => try { if (handles?.length === 1) { - hashParts[number] = ''; - await loadLocalNibbleFile(disk2, number, await handles[0].getFile()); + hashParts[driveNo] = ''; + await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile()); } if (filename) { const name = filename.match(/\/([^/]+).json$/) || ['', '']; - hashParts[number] = name[1]; - await loadJSON(disk2, number, filename); + hashParts[driveNo] = name[1]; + await loadJSON(disk2, driveNo, filename); } } catch (e) { setError(e); @@ -86,7 +86,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => } setHashParts(hashParts); - }, [disk2, filename, number, onClose, handles, hash]); + }, [disk2, filename, driveNo, onClose, handles, hash]); const onChange = useCallback((handles: FileSystemFileHandle[]) => { setEmpty(handles.length === 0); diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index b2f535c..7ffea55 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -235,7 +235,7 @@ const Catalog = ({ dos, setFileData }: CatalogProps) => { */ interface DiskInfoProps { massStorage: MassStorage; - drive: DriveNumber; + driveNo: DriveNumber; setFileData: (fileData: FileData) => void; } @@ -250,9 +250,9 @@ interface DiskInfoProps { * @param drive The drive number * @returns DiskInfo component */ -const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { +const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => { const disk = useMemo(() => { - const massStorageData = massStorage.getBinary(drive, 'po'); + const massStorageData = massStorage.getBinary(driveNo, 'po'); if (massStorageData) { const { data, readOnly, ext } = massStorageData; const { name } = massStorageData.metadata; @@ -265,7 +265,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { volume: 254, }); } else if (data.byteLength < 800 * 1024) { - const doData = massStorage.getBinary(drive, 'do'); + const doData = massStorage.getBinary(driveNo, 'do'); if (doData) { if (isMaybeDOS33(doData)) { disk = createDiskFromDOS({ @@ -288,7 +288,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { return disk; } return null; - }, [massStorage, drive]); + }, [massStorage, driveNo]); if (disk) { try { @@ -409,11 +409,11 @@ export const Disks = ({ apple2 }: DisksProps) => {
{card.constructor.name} - 1
- +
{card.constructor.name} - 2
- +
))} diff --git a/js/components/util/files.ts b/js/components/util/files.ts index d365c35..b4caac1 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -48,7 +48,7 @@ export const getNameAndExtension = (url: string) => { export const loadLocalFile = ( storage: MassStorage, formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, - number: DriveNumber, + driveNo: DriveNumber, file: File, ) => { return new Promise((resolve, reject) => { @@ -58,7 +58,7 @@ export const loadLocalFile = ( const { name, ext } = getNameAndExtension(file.name); if (includes(formats, ext)) { initGamepad(); - if (storage.setBinary(number, name, ext, result)) { + if (storage.setBinary(driveNo, name, ext, result)) { resolve(true); } else { reject(`Unable to load ${name}`); @@ -76,12 +76,12 @@ export const loadLocalFile = ( * selection form element to be loaded. * * @param smartPort SmartPort object - * @param number Drive number + * @param driveNo Drive number * @param file Browser File object to load * @returns true if successful */ -export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, file: File) => { - return loadLocalFile(smartPort, BLOCK_FORMATS, number, file); +export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, file: File) => { + return loadLocalFile(smartPort, BLOCK_FORMATS, driveNo, file); }; /** @@ -89,12 +89,12 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi * selection form element to be loaded. * * @param disk2 Disk2 object - * @param number Drive number + * @param driveNo Drive number * @param file Browser File object to load * @returns true if successful */ -export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { - return loadLocalFile(disk2, FLOPPY_FORMATS, number, file); +export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: File) => { + return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file); }; /** @@ -103,13 +103,13 @@ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: Fil * as the emulator. * * @param disk2 Disk2 object - * @param number Drive number + * @param driveNo Drive number * @param url URL, relative or absolute to JSON file * @returns true if successful */ export const loadJSON = async ( disk2: Disk2, - number: DriveNumber, + driveNo: DriveNumber, url: string, ) => { const response = await fetch(url); @@ -120,7 +120,7 @@ export const loadJSON = async ( if (!includes(FLOPPY_FORMATS, data.type)) { throw new Error(`Type "${data.type}" not recognized.`); } - disk2.setDisk(number, data); + disk2.setDisk(driveNo, data); initGamepad(data.gamepad); }; @@ -166,13 +166,13 @@ export const loadHttpFile = async ( * as the emulator. * * @param smartPort SmartPort object - * @param number Drive number + * @param driveNo Drive number * @param url URL, relative or absolute to JSON file * @returns true if successful */ export const loadHttpBlockFile = async ( smartPort: SmartPort, - number: DriveNumber, + driveNo: DriveNumber, url: string, signal?: AbortSignal, onProgress?: ProgressCallback @@ -182,7 +182,7 @@ export const loadHttpBlockFile = async ( throw new Error(`Extension "${ext}" not recognized.`); } const data = await loadHttpFile(url, signal, onProgress); - smartPort.setBinary(number, name, ext, data); + smartPort.setBinary(driveNo, name, ext, data); initGamepad(); return true; @@ -194,55 +194,55 @@ export const loadHttpBlockFile = async ( * as the emulator. * * @param disk2 Disk2 object - * @param number Drive number + * @param driveNo Drive number * @param url URL, relative or absolute to JSON file * @returns true if successful */ export const loadHttpNibbleFile = async ( disk2: Disk2, - number: DriveNumber, + driveNo: DriveNumber, url: string, signal?: AbortSignal, onProgress?: ProgressCallback ) => { if (url.endsWith('.json')) { - return loadJSON(disk2, number, url); + return loadJSON(disk2, driveNo, url); } const { name, ext } = getNameAndExtension(url); if (!includes(FLOPPY_FORMATS, ext)) { throw new Error(`Extension "${ext}" not recognized.`); } const data = await loadHttpFile(url, signal, onProgress); - disk2.setBinary(number, name, ext, data); + disk2.setBinary(driveNo, name, ext, data); initGamepad(); return loadHttpFile(url, signal, onProgress); }; export const loadHttpUnknownFile = async ( smartStorageBroker: SmartStorageBroker, - number: DriveNumber, + driveNo: DriveNumber, url: string, signal?: AbortSignal, onProgress?: ProgressCallback, ) => { const data = await loadHttpFile(url, signal, onProgress); const { name, ext } = getNameAndExtension(url); - smartStorageBroker.setBinary(number, name, ext, data); + smartStorageBroker.setBinary(driveNo, name, ext, data); }; export class SmartStorageBroker implements MassStorage { constructor(private disk2: Disk2, private smartPort: SmartPort) {} - setBinary(drive: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean { + setBinary(driveNo: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean { if (includes(DISK_FORMATS, ext)) { if (data.byteLength >= 800 * 1024) { if (includes(BLOCK_FORMATS, ext)) { - this.smartPort.setBinary(drive, name, ext, data); + this.smartPort.setBinary(driveNo, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); } } else if (includes(FLOPPY_FORMATS, ext)) { - this.disk2.setBinary(drive, name, ext, data); + this.disk2.setBinary(driveNo, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); } diff --git a/js/formats/types.ts b/js/formats/types.ts index a3553b9..73fe547 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -234,7 +234,7 @@ export const PROCESS_JSON = 'PROCESS_JSON'; export interface ProcessBinaryMessage { type: typeof PROCESS_BINARY; payload: { - drive: DriveNumber; + driveNo: DriveNumber; fmt: FloppyFormat; options: DiskOptions; }; @@ -244,7 +244,7 @@ export interface ProcessBinaryMessage { export interface ProcessJsonDiskMessage { type: typeof PROCESS_JSON_DISK; payload: { - drive: DriveNumber; + driveNo: DriveNumber; jsonDisk: JSONDisk; }; } @@ -253,7 +253,7 @@ export interface ProcessJsonDiskMessage { export interface ProcessJsonMessage { type: typeof PROCESS_JSON; payload: { - drive: DriveNumber; + driveNo: DriveNumber; json: string; }; } @@ -272,7 +272,7 @@ export const DISK_PROCESSED = 'DISK_PROCESSED'; export interface DiskProcessedResponse { type: typeof DISK_PROCESSED; payload: { - drive: DriveNumber; + driveNo: DriveNumber; disk: FloppyDisk | null; }; } diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 498b5dc..db081d5 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -84,7 +84,7 @@ let joystick: JoyStick; let system: System; let keyboard: KeyBoard; let io: Apple2IO; -let _currentDrive: DriveNumber = 1; +let driveNo: DriveNumber = 1; let _e: boolean; let ready: Promise<[void, void]>; @@ -103,14 +103,13 @@ export function compileApplesoftProgram(program: string) { } export function openLoad(driveString: string, event: MouseEvent) { - const drive = parseInt(driveString, 10) as DriveNumber; - _currentDrive = drive; - if (event.metaKey && includes(DRIVE_NUMBERS, drive)) { + driveNo = parseInt(driveString, 10) as DriveNumber; + if (event.metaKey && includes(DRIVE_NUMBERS, driveNo)) { openLoadHTTP(); } else { - if (disk_cur_cat[drive]) { + if (disk_cur_cat[driveNo]) { const element = document.querySelector('#category_select')!; - element.value = disk_cur_cat[drive]; + element.value = disk_cur_cat[driveNo]; selectCategory(); } MicroModal.show('load-modal'); @@ -118,27 +117,27 @@ export function openLoad(driveString: string, event: MouseEvent) { } export function openSave(driveString: string, event: MouseEvent) { - const drive = parseInt(driveString, 10) as DriveNumber; + const driveNo = parseInt(driveString, 10) as DriveNumber; const mimeType = 'application/octet-stream'; - const storageData = _disk2.getBinary(drive); + const storageData = _disk2.getBinary(driveNo); const a = document.querySelector('#local_save_link')!; if (!storageData) { - alert(`No data from drive ${drive}`); + alert(`No data from drive ${driveNo}`); return; } const { data } = storageData; const blob = new Blob([data], { 'type': mimeType }); a.href = window.URL.createObjectURL(blob); - a.download = driveLights.label(drive) + '.dsk'; + a.download = driveLights.label(driveNo) + '.dsk'; if (event.metaKey) { - dumpDisk(drive); + dumpDisk(driveNo); } else { const saveName = document.querySelector('#save_name')!; - saveName.value = driveLights.label(drive); + saveName.value = driveLights.label(driveNo); MicroModal.show('save-modal'); } } @@ -154,12 +153,12 @@ export function openAlert(msg: string) { * Drag and Drop */ -export function handleDragOver(_drive: number, event: DragEvent) { +export function handleDragOver(_driveNo: number, event: DragEvent) { event.preventDefault(); event.dataTransfer!.dropEffect = 'copy'; } -export function handleDragEnd(_drive: number, event: DragEvent) { +export function handleDragEnd(_driveNo: number, event: DragEvent) { const dt = event.dataTransfer!; if (dt.items) { for (let i = 0; i < dt.items.length; i++) { @@ -170,23 +169,23 @@ export function handleDragEnd(_drive: number, event: DragEvent) { } } -export function handleDrop(drive: number, event: DragEvent) { +export function handleDrop(driveNo: number, event: DragEvent) { event.preventDefault(); event.stopPropagation(); - if (drive < 1) { + if (driveNo < 1) { if (!_disk2.getMetadata(1)) { - drive = 1; + driveNo = 1; } else if (!_disk2.getMetadata(2)) { - drive = 2; + driveNo = 2; } else { - drive = 1; + driveNo = 1; } } const dt = event.dataTransfer!; if (dt.files.length === 1) { const runOnLoad = event.shiftKey; - doLoadLocal(drive as DriveNumber, dt.files[0], { runOnLoad }); + doLoadLocal(driveNo as DriveNumber, dt.files[0], { runOnLoad }); } else if (dt.files.length === 2) { doLoadLocal(1, dt.files[0]); doLoadLocal(2, dt.files[1]); @@ -195,7 +194,7 @@ export function handleDrop(drive: number, event: DragEvent) { if (dt.items[idx].type === 'text/uri-list') { dt.items[idx].getAsString(function (url) { const parts = hup().split('|'); - parts[drive - 1] = url; + parts[driveNo - 1] = url; document.location.hash = parts.join('|'); }); } @@ -228,7 +227,7 @@ function loadingStop() { } } -export function loadAjax(drive: DriveNumber, url: string) { +export function loadAjax(driveNo: DriveNumber, url: string) { loadingStart(); fetch(url).then(function (response: Response) { @@ -241,7 +240,7 @@ export function loadAjax(drive: DriveNumber, url: string) { if (data.type === 'binary') { loadBinary(data ); } else if (includes(DISK_FORMATS, data.type)) { - loadDisk(drive, data); + loadDisk(driveNo, data); } initGamepad(data.gamepad); loadingStop(); @@ -269,7 +268,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) { const files = localFile.files; if (files && files.length === 1) { const runOnLoad = event.shiftKey; - doLoadLocal(_currentDrive, files[0], { runOnLoad }); + doLoadLocal(driveNo, files[0], { runOnLoad }); } else if (url) { let filename; MicroModal.close('load-modal'); @@ -278,7 +277,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) { if (filename === '__manage') { openManage(); } else { - loadLocalStorage(_currentDrive, filename); + loadLocalStorage(driveNo, filename); } } else { const r1 = /json\/disks\/(.*).json$/.exec(url); @@ -288,7 +287,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) { filename = url; } const parts = hup().split('|'); - parts[_currentDrive - 1] = filename; + parts[driveNo - 1] = filename; document.location.hash = parts.join('|'); } } @@ -297,7 +296,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) { export function doSave() { const saveName = document.querySelector('#save_name')!; const name = saveName.value; - saveLocalStorage(_currentDrive, name); + saveLocalStorage(driveNo, name); MicroModal.close('save-modal'); window.setTimeout(() => openAlert('Saved'), 0); } @@ -313,7 +312,7 @@ interface LoadOptions { runOnLoad?: boolean; } -function doLoadLocal(drive: DriveNumber, file: File, options: Partial = {}) { +function doLoadLocal(driveNo: DriveNumber, file: File, options: Partial = {}) { const parts = file.name.split('.'); const ext = parts[parts.length - 1].toLowerCase(); const matches = file.name.match(CIDERPRESS_EXTENSION); @@ -322,7 +321,7 @@ function doLoadLocal(drive: DriveNumber, file: File, options: Partial= 800 * 1024) { if ( includes(BLOCK_FORMATS, ext) && - _massStorage.setBinary(drive, name, ext, result) + _massStorage.setBinary(driveNo, name, ext, result) ) { initGamepad(); } else { @@ -393,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) { } else { if ( includes(FLOPPY_FORMATS, ext) && - _disk2.setBinary(drive, name, ext, result) + _disk2.setBinary(driveNo, name, ext, result) ) { initGamepad(); } else { @@ -406,7 +405,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) { fileReader.readAsArrayBuffer(file); } -export function doLoadHTTP(drive: DriveNumber, url?: string) { +export function doLoadHTTP(driveNo: DriveNumber, url?: string) { if (!url) { MicroModal.close('http-modal'); } @@ -454,13 +453,13 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { if (includes(DISK_FORMATS, ext)) { if (data.byteLength >= 800 * 1024) { if (includes(BLOCK_FORMATS, ext)) { - _massStorage.setBinary(drive, name, ext, data); + _massStorage.setBinary(driveNo, name, ext, data); initGamepad(); } } else { if ( includes(FLOPPY_FORMATS, ext) && - _disk2.setBinary(drive, name, ext, data) + _disk2.setBinary(driveNo, name, ext, data) ) { initGamepad(); } @@ -547,11 +546,11 @@ function updateSoundButton(on: boolean) { } } -function dumpDisk(drive: DriveNumber) { +function dumpDisk(driveNo: DriveNumber) { const wind = window.open('', '_blank')!; - wind.document.title = driveLights.label(drive); + wind.document.title = driveLights.label(driveNo); wind.document.write('
');
-    wind.document.write(_disk2.getJSON(drive, true));
+    wind.document.write(_disk2.getJSON(driveNo, true));
     wind.document.write('
'); wind.document.close(); } @@ -586,7 +585,7 @@ export function selectCategory() { option.value = file.filename; option.innerText = name; diskSelect.append(option); - if (disk_cur_name[_currentDrive] === name) { + if (disk_cur_name[driveNo] === name) { option.selected = true; } } @@ -603,7 +602,7 @@ export function clickDisk(event: MouseEvent|KeyboardEvent) { } /** Called to load disks from the local catalog. */ -function loadDisk(drive: DriveNumber, disk: JSONDisk) { +function loadDisk(driveNo: DriveNumber, disk: JSONDisk) { let name = disk.name; const category = disk.category!; // all disks in the local catalog have a category @@ -611,10 +610,10 @@ function loadDisk(drive: DriveNumber, disk: JSONDisk) { name += ' - ' + disk.disk; } - disk_cur_cat[drive] = category; - disk_cur_name[drive] = name; + disk_cur_cat[driveNo] = category; + disk_cur_name[driveNo] = name; - _disk2.setDisk(drive, disk); + _disk2.setDisk(driveNo, disk); initGamepad(disk.gamepad); } @@ -654,16 +653,16 @@ type LocalDiskIndex = { [name: string]: string; }; -function saveLocalStorage(drive: DriveNumber, name: string) { +function saveLocalStorage(driveNo: DriveNumber, name: string) { const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; - const json = _disk2.getJSON(drive); + const json = _disk2.getJSON(driveNo); diskIndex[name] = json; window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex)); - driveLights.label(drive, name); - driveLights.dirty(drive, false); + driveLights.label(driveNo, name); + driveLights.dirty(driveNo, false); updateLocalStorage(); } @@ -677,12 +676,12 @@ function deleteLocalStorage(name: string) { updateLocalStorage(); } -function loadLocalStorage(drive: DriveNumber, name: string) { +function loadLocalStorage(driveNo: DriveNumber, name: string) { const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; if (diskIndex[name]) { - _disk2.setJSON(drive, diskIndex[name]); - driveLights.label(drive, name); - driveLights.dirty(drive, false); + _disk2.setJSON(driveNo, diskIndex[name]); + driveLights.label(driveNo, name); + driveLights.dirty(driveNo, false); } } diff --git a/js/ui/drive_lights.ts b/js/ui/drive_lights.ts index 04399a9..396a652 100644 --- a/js/ui/drive_lights.ts +++ b/js/ui/drive_lights.ts @@ -2,8 +2,8 @@ import { Callbacks } from '../cards/disk2'; import type { DriveNumber } from '../formats/types'; export default class DriveLights implements Callbacks { - public driveLight(drive: DriveNumber, on: boolean) { - const disk = document.querySelector(`#disk${drive}`); + public driveLight(driveNo: DriveNumber, on: boolean) { + const disk = document.querySelector(`#disk${driveNo}`); if (disk) { disk.style.backgroundImage = on ? 'url(css/red-on-16.png)' : @@ -11,12 +11,12 @@ export default class DriveLights implements Callbacks { } } - public dirty(_drive: DriveNumber, _dirty: boolean) { + public dirty(_driveNo: DriveNumber, _dirty: boolean) { // document.querySelector('#disksave' + drive).disabled = !dirty; } - public label(drive: DriveNumber, label?: string, side?: string) { - const labelElement = document.querySelector(`#disk-label${drive}`); + public label(driveNo: DriveNumber, label?: string, side?: string) { + const labelElement = document.querySelector(`#disk-label${driveNo}`); let labelText = ''; if (labelElement) { labelText = labelElement.innerText; diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 9e31cd0..a0938ce 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -66,8 +66,8 @@ describe('DiskII', () => { const state = diskII.getState(); // These are just arbitrary changes, not an exhaustive list of fields. - state.skip = 1; - state.controllerState.drive = 2; + (state.drives[1].driver as {skip:number}).skip = 1; + state.controllerState.driveNo = 2; state.controllerState.latch = 0x42; state.controllerState.on = true; state.controllerState.q7 = true; @@ -97,7 +97,7 @@ describe('DiskII', () => { expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined); expect(callbacks.dirty).toHaveBeenCalledTimes(2); - expect(callbacks.dirty).toHaveBeenCalledWith(1, true); + expect(callbacks.dirty).toHaveBeenCalledWith(1, false); expect(callbacks.dirty).toHaveBeenCalledWith(2, false); }); @@ -760,11 +760,11 @@ class TestDiskReader { nibbles = 0; diskII: DiskII; - constructor(drive: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) { + constructor(driveNo: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) { mocked(apple2IO).cycles.mockImplementation(() => this.cycles); this.diskII = new DiskII(apple2IO, callbacks); - this.diskII.setBinary(drive, label, 'woz', image); + this.diskII.setBinary(driveNo, label, 'woz', image); } readNibble(): byte { @@ -824,7 +824,7 @@ class TestDiskReader { rawTracks() { // NOTE(flan): Hack to access private properties. - const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk; + const disk = (this.diskII as unknown as { disks: WozDisk[] }).disks[1]; const result: Uint8Array[] = []; for (let i = 0; i < disk.rawTracks.length; i++) { result[i] = disk.rawTracks[i].slice(0); diff --git a/workers/format.worker.ts b/workers/format.worker.ts index 21df0e0..e8c95ad 100644 --- a/workers/format.worker.ts +++ b/workers/format.worker.ts @@ -19,7 +19,7 @@ debug('Worker loaded'); addEventListener('message', (message: MessageEvent) => { debug('Worker started', message.type); const data = message.data; - const { drive } = data.payload; + const { driveNo } = data.payload; let disk: FloppyDisk | null = null; switch (data.type) { @@ -45,7 +45,7 @@ addEventListener('message', (message: MessageEvent) => { const response: DiskProcessedResponse = { type: DISK_PROCESSED, payload: { - drive, + driveNo, disk } };