From 4bbfac5a5a1544043774e0cf24d732dd90758a91 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Mon, 19 Sep 2022 09:07:27 +0200 Subject: [PATCH 1/3] 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`. --- js/cards/disk2.ts | 128 +++++++++++++++---------------- 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 | 6 +- workers/format.worker.ts | 4 +- 16 files changed, 233 insertions(+), 234 deletions(-) diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 6062324..1446abd 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -184,7 +184,7 @@ interface ControllerState { on: boolean; /** The active drive. */ - drive: DriveNumber; + driveNo: DriveNumber; /** The 8-cycle LSS clock. */ clock: LssClockCycle; @@ -219,15 +219,15 @@ 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. */ @@ -361,7 +361,7 @@ class EmptyDriver implements DiskDriver { abstract class BaseDiskDriver implements DiskDriver { constructor( - protected readonly driveNumber: DriveNumber, + protected readonly driveNo: DriveNumber, protected readonly drive: Drive, protected readonly disk: NibbleDisk | WozDisk, protected readonly controller: ControllerState) { } @@ -400,7 +400,7 @@ abstract class BaseDiskDriver implements DiskDriver { abstract clampTrack(): void; isOn(): boolean { - return this.controller.on && this.controller.drive === this.driveNumber; + return this.controller.on && this.controller.driveNo === this.driveNo; } isWriteProtected(): boolean { @@ -428,12 +428,12 @@ class NibbleDiskDriver extends BaseDiskDriver { private nibbleCount: number = 0; constructor( - driveNumber: DriveNumber, + driveNo: DriveNumber, drive: Drive, readonly disk: NibbleDisk, controller: ControllerState, private readonly onDirty: () => void) { - super(driveNumber, drive, disk, controller); + super(driveNo, drive, disk, controller); } tick(): void { @@ -537,13 +537,13 @@ class WozDiskDriver extends BaseDiskDriver { private zeros = 0; constructor( - driveNumber: DriveNumber, + driveNo: DriveNumber, drive: Drive, readonly disk: WozDisk, controller: ControllerState, private readonly onDirty: () => void, private readonly io: Apple2IO) { - super(driveNumber, drive, disk, controller); + super(driveNo, drive, disk, controller); // From the example in UtA2e, p. 9-29, col. 1, para. 1., this is // essentially the start of the sequencer loop and produces @@ -794,7 +794,7 @@ export default class DiskII implements Card, MassStorage { sectors, bus: 0, latch: 0, - drive: 1, + driveNo: 1, on: false, q6: false, q7: false, @@ -814,8 +814,8 @@ export default class DiskII implements Card, MassStorage { /** Updates the active drive based on the controller state. */ private updateActiveDrive() { - this.curDrive = this.drives[this.state.drive]; - this.curDriver = this.driver[this.state.drive]; + this.curDrive = this.drives[this.state.driveNo]; + this.curDriver = this.driver[this.state.driveNo]; } private debug(..._args: unknown[]) { @@ -898,7 +898,7 @@ 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.callbacks.driveLight(state.driveNo, false); this.curDriver.onDriveOff(); }, 1000); } @@ -913,14 +913,14 @@ export default class DiskII implements Card, MassStorage { if (!state.on) { this.debug('Drive On'); state.on = true; - 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); @@ -929,7 +929,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); @@ -980,10 +980,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); } } @@ -1002,10 +1002,10 @@ 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(); } @@ -1014,10 +1014,10 @@ export default class DiskII implements Card, MassStorage { this.curDriver.tick(); } - private getDriveState(drive: DriveNumber): DriveState { - const curDrive = this.drives[drive]; - const curDisk = this.disks[drive]; - const curDriver = this.driver[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), @@ -1042,9 +1042,9 @@ 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, @@ -1052,8 +1052,8 @@ export default class DiskII implements Card, MassStorage { dirty, }; const disk = getDiskState(state.disk); - this.setDiskInternal(drive, disk); - this.driver[drive].setState(state.driver); + this.setDiskInternal(driveNo, disk); + this.driver[driveNo].setState(state.driver); } @@ -1082,8 +1082,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'); } @@ -1092,12 +1092,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 }, }; @@ -1106,39 +1106,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 = { @@ -1152,7 +1152,7 @@ export default class DiskII implements Card, MassStorage { const message: FormatWorkerMessage = { type: PROCESS_BINARY, payload: { - drive, + driveNo: driveNo, fmt, options, } @@ -1163,7 +1163,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; } } @@ -1183,7 +1183,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); } @@ -1196,26 +1196,26 @@ export default class DiskII implements Card, MassStorage { } } - private setDiskInternal(drive: DriveNumber, disk: FloppyDisk) { - this.disks[drive] = disk; + private setDiskInternal(driveNo: DriveNumber, disk: FloppyDisk) { + this.disks[driveNo] = disk; if (isNoFloppyDisk(disk)) { - this.driver[drive] = new EmptyDriver(this.drives[drive]); + this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]); } else if (isNibbleDisk(disk)) { - this.driver[drive] = + this.driver[driveNo] = new NibbleDiskDriver( - drive, - this.drives[drive], + driveNo, + this.drives[driveNo], disk, this.state, - () => this.updateDirty(drive, true)); + () => this.updateDirty(driveNo, true)); } else if (isWozDisk(disk)) { - this.driver[drive] = + this.driver[driveNo] = new WozDiskDriver( - drive, - this.drives[drive], + driveNo, + this.drives[driveNo], disk, this.state, - () => this.updateDirty(drive, true), + () => this.updateDirty(driveNo, true), this.io); } else { throw new Error(`Unknown disk format ${disk.encoding}`); @@ -1223,12 +1223,12 @@ export default class DiskII implements Card, MassStorage { this.updateActiveDrive(); } - private insertDisk(drive: DriveNumber, disk: FloppyDisk) { - this.setDiskInternal(drive, disk); - this.drives[drive].head = 0; + private insertDisk(driveNo: DriveNumber, disk: FloppyDisk) { + this.setDiskInternal(driveNo, disk); + this.drives[driveNo].head = 0; const { name, side } = disk.metadata; - this.updateDirty(drive, this.drives[drive].dirty); - this.callbacks.label(drive, name, side); + this.updateDirty(driveNo, this.drives[driveNo].dirty); + this.callbacks.label(driveNo, name, side); } /** @@ -1241,8 +1241,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; } @@ -1289,8 +1289,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 1fc218a..a0938ce 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -67,7 +67,7 @@ describe('DiskII', () => { const state = diskII.getState(); // These are just arbitrary changes, not an exhaustive list of fields. (state.drives[1].driver as {skip:number}).skip = 1; - state.controllerState.drive = 2; + state.controllerState.driveNo = 2; state.controllerState.latch = 0x42; state.controllerState.on = true; state.controllerState.q7 = true; @@ -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 { 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 } }; From 630a3e9d3863084e232b166bee54efd44037210f Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sat, 24 Sep 2022 08:32:47 +0200 Subject: [PATCH 2/3] Organize imports in `disk2.ts` The imports were automatically organized using VS Code's organize imports action. --- js/cards/disk2.ts | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 1446abd..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 = { From a7bf5d025d384b0d06aba872e853586fa4e2cbf6 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sat, 24 Sep 2022 09:45:46 +0200 Subject: [PATCH 3/3] Split drivers into different files Before, all of the disk drivers were in the `disk2.ts` file. With this change, they are now in separate files with a common `types.ts` file for shared types. Now, none of the drives depend on the `disk2.ts` except the WOZ driver that needs access to the sequencer rom. (This may be moved in a future refactoring.) --- js/cards/disk2.ts | 503 +-------------------------- js/cards/drivers/BaseDiskDriver.ts | 63 ++++ js/cards/drivers/EmptyDriver.ts | 53 +++ js/cards/drivers/NibbleDiskDriver.ts | 106 ++++++ js/cards/drivers/WozDiskDriver.ts | 225 ++++++++++++ js/cards/drivers/types.ts | 66 ++++ 6 files changed, 521 insertions(+), 495 deletions(-) create mode 100644 js/cards/drivers/BaseDiskDriver.ts create mode 100644 js/cards/drivers/EmptyDriver.ts create mode 100644 js/cards/drivers/NibbleDiskDriver.ts create mode 100644 js/cards/drivers/WozDiskDriver.ts create mode 100644 js/cards/drivers/types.ts diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 7e44cbc..810ea98 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -2,7 +2,6 @@ import { base64_encode } from '../base64'; import type { byte, Card, - nibble, ReadonlyUint8Array } from '../types'; @@ -10,7 +9,7 @@ import { 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 + MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk } from '../formats/types'; import { @@ -19,11 +18,16 @@ import { } from '../formats/create_disk'; import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils'; -import { toHex } from '../util'; import Apple2IO from '../apple2io'; + import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2'; +import { EmptyDriver } from './drivers/EmptyDriver'; +import { NibbleDiskDriver } from './drivers/NibbleDiskDriver'; +import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types'; +import { WozDiskDriver } from './drivers/WozDiskDriver'; + /** Softswitch locations */ const LOC = { // Disk II Controller Commands @@ -111,7 +115,7 @@ const SEQUENCER_ROM_16 = [ ] as const; /** Contents of the P6 sequencer ROM. */ -const SEQUENCER_ROM: Record> = { +export const SEQUENCER_ROM: Record> = { 13: SEQUENCER_ROM_13, 16: SEQUENCER_ROM_16, } as const; @@ -122,10 +126,6 @@ const BOOTSTRAP_ROM: Record = { 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; - /** * 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 @@ -155,49 +155,6 @@ const PHASE_DELTA = [ [1, -2, -1, 0] ] as const; -/** - * State of the controller. - */ -interface ControllerState { - /** Sectors supported by the controller. */ - sectors: SupportedSectors; - - /** Is the active drive powered on? */ - on: boolean; - - /** The active drive. */ - driveNo: DriveNumber; - - /** The 8-cycle LSS clock. */ - clock: LssClockCycle; - /** Current state of the Logic State Sequencer. */ - state: nibble; - - /** Q6 (Shift/Load) */ - q6: boolean; - /** Q7 (Read/Write) */ - q7: boolean; - - /** Last data from the disk drive. */ - latch: byte; - /** Last data written by the CPU to card softswitch 0x8D. */ - 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. */ @@ -212,20 +169,6 @@ export interface Callbacks { label: (driveNo: DriveNumber, name?: string, side?: string) => void; } -/** Common information for Nibble and WOZ disks. */ -interface Drive { - /** Whether the drive write protect is on. */ - readOnly: boolean; - /** 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 has been written to since it was loaded. */ - dirty: boolean; -} - interface DriveState { disk: FloppyDisk; driver: DriverState; @@ -237,17 +180,11 @@ interface DriveState { } /** State of the controller for saving/restoring. */ -// TODO(flan): It's unclear whether reusing ControllerState here is a good idea. interface State { drives: DriveState[]; - driver: DriverState[]; controllerState: ControllerState; } -function getDiskState(disk: NoFloppyDisk): NoFloppyDisk; -function getDiskState(disk: NibbleDisk): NibbleDisk; -function getDiskState(disk: WozDisk): WozDisk; -function getDiskState(disk: FloppyDisk): FloppyDisk; function getDiskState(disk: FloppyDisk): FloppyDisk { if (isNoFloppyDisk(disk)) { const { encoding, metadata, readOnly } = disk; @@ -294,429 +231,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk { throw new Error('Unknown drive state'); } -interface EmptyDriverState { } - -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; - } - if (this.drive.track > 34) { - this.drive.track = 34; - } - } - - 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: number = 0; - /** Number of nibbles reads since the drive was turned on. */ - private nibbleCount: number = 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: 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 - * "Freaking Out Like a MC3470" in the WOZ spec. - */ - private zeros = 0; - - constructor( - driveNo: DriveNumber, - drive: Drive, - readonly disk: WozDisk, - controller: ControllerState, - private readonly onDirty: () => void, - private readonly io: Apple2IO) { - super(driveNo, drive, disk, controller); - - // 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(); - } - - onDriveOff(): void { - // nothing - } - - /** - * 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; - - 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 = - disk.rawTracks[disk.trackMap[drive.track]] || [0]; - - while (workCycles-- > 0) { - let pulse: number = 0; - 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) { - const r = Math.random(); - pulse = r >= 0.5 ? 1 : 0; - } - } else { - this.zeros = 0; - } - } - - let idx = 0; - idx |= pulse ? 0x00 : 0x01; - idx |= controller.latch & 0x80 ? 0x02 : 0x00; - idx |= controller.q6 ? 0x04 : 0x00; - idx |= controller.q7 ? 0x08 : 0x00; - idx |= this.state << 4; - - const command = SEQUENCER_ROM[controller.sectors][idx]; - - 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 - controller.latch = 0; - break; - case 0x8: // NOP - break; - case 0x9: // SL0 - controller.latch = (controller.latch << 1) & 0xff; - break; - case 0xA: // SR - controller.latch >>= 1; - if (this.isWriteProtected()) { - controller.latch |= 0x80; - } - break; - case 0xB: // LD - controller.latch = controller.bus; - this.debug('Loading', toHex(controller.latch), 'from bus'); - break; - case 0xD: // SL1 - controller.latch = ((controller.latch << 1) | 0x01) & 0xff; - break; - default: - this.debug(`unknown command: ${toHex(command & 0xf)}`); - } - this.state = (command >> 4 & 0xF) as LssState; - - 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 (++drive.head >= track.length) { - drive.head = 0; - } - } - } - - if (++this.clock > 7) { - this.clock = 0; - } - } - } - - tick(): void { - this.moveHead(); - } - - onQ6High(_readMode: boolean): void { - // nothing? - } - - 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; - } - 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. */ @@ -1015,7 +529,6 @@ export default class DiskII implements Card, MassStorage { getState(): State { const result = { drives: [] as DriveState[], - driver: [] as DriverState[], controllerState: { ...this.state }, }; result.drives[1] = this.getDriveState(1); diff --git a/js/cards/drivers/BaseDiskDriver.ts b/js/cards/drivers/BaseDiskDriver.ts new file mode 100644 index 0000000..f7957a3 --- /dev/null +++ b/js/cards/drivers/BaseDiskDriver.ts @@ -0,0 +1,63 @@ +import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types'; +import { ControllerState, DiskDriver, Drive, DriverState } from './types'; + + +/** + * Common logic for both `NibbleDiskDriver` and `WozDiskDriver`. + */ +export 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; + + /** Returns `true` if the controller is on and this drive is selected. */ + isOn(): boolean { + return this.controller.on && this.controller.driveNo === this.driveNo; + } + + /** Returns `true` if the drive's write protect switch is enabled. */ + isWriteProtected(): boolean { + return this.drive.readOnly; + } + + /** Returns the current state of the driver as a serializable object. */ + abstract getState(): DriverState; + + /** Sets the state of the driver from the given `state`. */ + abstract setState(state: DriverState): void; +} diff --git a/js/cards/drivers/EmptyDriver.ts b/js/cards/drivers/EmptyDriver.ts new file mode 100644 index 0000000..a5265f1 --- /dev/null +++ b/js/cards/drivers/EmptyDriver.ts @@ -0,0 +1,53 @@ +import { DiskDriver, Drive, DriverState } from './types'; + +/** Returned state for an empty drive. */ +export interface EmptyDriverState extends DriverState { } + +/** + * Driver for empty drives. This implementation does nothing except keep + * the head clamped between tracks 0 and 34. + */ +export 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; + } + if (this.drive.track > 34) { + this.drive.track = 34; + } + } + + getState() { + return {}; + } + + setState(_state: EmptyDriverState): void { + // do nothing + } +} diff --git a/js/cards/drivers/NibbleDiskDriver.ts b/js/cards/drivers/NibbleDiskDriver.ts new file mode 100644 index 0000000..bf04fea --- /dev/null +++ b/js/cards/drivers/NibbleDiskDriver.ts @@ -0,0 +1,106 @@ +import { DriveNumber, NibbleDisk } from '../../formats/types'; +import { BaseDiskDriver } from './BaseDiskDriver'; +import { ControllerState, Drive, DriverState } from './types'; + +interface NibbleDiskDriverState extends DriverState { + skip: number; + nibbleCount: number; +} + +export 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: number = 0; + /** Number of nibbles reads since the drive was turned on. */ + private nibbleCount: number = 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; + } +} diff --git a/js/cards/drivers/WozDiskDriver.ts b/js/cards/drivers/WozDiskDriver.ts new file mode 100644 index 0000000..f118a5e --- /dev/null +++ b/js/cards/drivers/WozDiskDriver.ts @@ -0,0 +1,225 @@ +import Apple2IO from '../../apple2io'; +import { DriveNumber, WozDisk } from '../../formats/types'; +import { toHex } from '../../util'; +import { SEQUENCER_ROM } from '../disk2'; +import { BaseDiskDriver } from './BaseDiskDriver'; +import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types'; + +interface WozDiskDriverState extends DriverState { + clock: LssClockCycle; + state: LssState; + lastCycles: number; + zeros: number; +} + +export 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: 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 + * "Freaking Out Like a MC3470" in the WOZ spec. + */ + private zeros = 0; + + constructor( + driveNo: DriveNumber, + drive: Drive, + readonly disk: WozDisk, + controller: ControllerState, + private readonly onDirty: () => void, + private readonly io: Apple2IO) { + super(driveNo, drive, disk, controller); + + // 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(); + } + + onDriveOff(): void { + // nothing + } + + /** + * 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; + + 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 = disk.rawTracks[disk.trackMap[drive.track]] || [0]; + + while (workCycles-- > 0) { + let pulse: number = 0; + 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) { + const r = Math.random(); + pulse = r >= 0.5 ? 1 : 0; + } + } else { + this.zeros = 0; + } + } + + let idx = 0; + idx |= pulse ? 0x00 : 0x01; + idx |= controller.latch & 0x80 ? 0x02 : 0x00; + idx |= controller.q6 ? 0x04 : 0x00; + idx |= controller.q7 ? 0x08 : 0x00; + idx |= this.state << 4; + + const command = SEQUENCER_ROM[controller.sectors][idx]; + + 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 + controller.latch = 0; + break; + case 0x8: // NOP + break; + case 0x9: // SL0 + controller.latch = (controller.latch << 1) & 0xff; + break; + case 0xA: // SR + controller.latch >>= 1; + if (this.isWriteProtected()) { + controller.latch |= 0x80; + } + break; + case 0xB: // LD + controller.latch = controller.bus; + this.debug('Loading', toHex(controller.latch), 'from bus'); + break; + case 0xD: // SL1 + controller.latch = ((controller.latch << 1) | 0x01) & 0xff; + break; + default: + this.debug(`unknown command: ${toHex(command & 0xf)}`); + } + this.state = (command >> 4 & 0xF) as LssState; + + 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 (++drive.head >= track.length) { + drive.head = 0; + } + } + } + + if (++this.clock > 7) { + this.clock = 0; + } + } + } + + tick(): void { + this.moveHead(); + } + + onQ6High(_readMode: boolean): void { + // nothing? + } + + 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; + } + 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; + } +} diff --git a/js/cards/drivers/types.ts b/js/cards/drivers/types.ts new file mode 100644 index 0000000..caaa6e6 --- /dev/null +++ b/js/cards/drivers/types.ts @@ -0,0 +1,66 @@ +import { DriveNumber, SupportedSectors } from 'js/formats/types'; +import { byte, nibble } from 'js/types'; + +export type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; +export type LssState = nibble; + +export type Phase = 0 | 1 | 2 | 3; + +/** + * State of the controller. + */ +export interface ControllerState { + /** Sectors supported by the controller. */ + sectors: SupportedSectors; + + /** Is the active drive powered on? */ + on: boolean; + + /** The active drive. */ + driveNo: DriveNumber; + + /** The 8-cycle LSS clock. */ + clock: LssClockCycle; + + /** Current state of the Logic State Sequencer. */ + state: LssState; + + /** Q6 (Shift/Load) */ + q6: boolean; + /** Q7 (Read/Write) */ + q7: boolean; + + /** Last data from the disk drive. */ + latch: byte; + /** Last data written by the CPU to card softswitch 0x8D. */ + bus: byte; +} + +/** Common information for Nibble and WOZ disks. */ +export interface Drive { + /** Whether the drive write protect is on. */ + readOnly: boolean; + /** 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 has been written to since it was loaded. */ + dirty: boolean; +} + +/** Base interface for disk driver states. */ +export interface DriverState {} + +/** Interface for drivers for various disk types. */ +export interface DiskDriver { + tick(): void; + onQ6Low(): void; + onQ6High(readMode: boolean): void; + onDriveOn(): void; + onDriveOff(): void; + clampTrack(): void; + getState(): DriverState; + setState(state: DriverState): void; +}