Merge pull request #163 from iflan/split-drivers-into-files

Split drivers into files
This commit is contained in:
Will Scullin 2022-10-01 11:17:12 -07:00 committed by GitHub
commit 5bd51d64c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 752 additions and 745 deletions

View File

@ -2,33 +2,14 @@ import { base64_encode } from '../base64';
import type { import type {
byte, byte,
Card, Card,
nibble, ReadonlyUint8Array
ReadonlyUint8Array,
} from '../types'; } from '../types';
import { import {
FormatWorkerMessage, DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
FormatWorkerResponse, FloppyFormat, FormatWorkerMessage,
NibbleFormat, FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
DISK_PROCESSED, MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
DRIVE_NUMBERS,
DriveNumber,
JSONDisk,
PROCESS_BINARY,
PROCESS_JSON_DISK,
PROCESS_JSON,
MassStorage,
MassStorageData,
SupportedSectors,
FloppyDisk,
FloppyFormat,
WozDisk,
NibbleDisk,
isNibbleDisk,
isWozDisk,
NoFloppyDisk,
isNoFloppyDisk,
NO_DISK,
} from '../formats/types'; } from '../formats/types';
import { import {
@ -36,12 +17,17 @@ import {
createDiskFromJsonDisk createDiskFromJsonDisk
} from '../formats/create_disk'; } from '../formats/create_disk';
import { toHex } from '../util';
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils'; import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2';
import Apple2IO from '../apple2io'; 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 */ /** Softswitch locations */
const LOC = { const LOC = {
// Disk II Controller Commands // Disk II Controller Commands
@ -129,7 +115,7 @@ const SEQUENCER_ROM_16 = [
] as const; ] as const;
/** Contents of the P6 sequencer ROM. */ /** Contents of the P6 sequencer ROM. */
const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = { export const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
13: SEQUENCER_ROM_13, 13: SEQUENCER_ROM_13,
16: SEQUENCER_ROM_16, 16: SEQUENCER_ROM_16,
} as const; } as const;
@ -140,10 +126,6 @@ const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
16: BOOTSTRAP_ROM_16, 16: BOOTSTRAP_ROM_16,
} as const; } 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 * 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 * activated. For example, if in phase 0 (top row), turning on phase 3 would
@ -173,75 +155,18 @@ const PHASE_DELTA = [
[1, -2, -1, 0] [1, -2, -1, 0]
] as const; ] 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. */
drive: 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. */ /** Callbacks triggered by events of the drive or controller. */
export interface Callbacks { export interface Callbacks {
/** Called when a drive turns on or off. */ /** 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, * 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 * this is only called when the drive stops spinning or is removed from
* the drive. * 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. */ /** 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. */
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 { interface DriveState {
@ -255,17 +180,11 @@ interface DriveState {
} }
/** State of the controller for saving/restoring. */ /** State of the controller for saving/restoring. */
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
interface State { interface State {
drives: DriveState[]; drives: DriveState[];
driver: DriverState[];
controllerState: ControllerState; 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 { function getDiskState(disk: FloppyDisk): FloppyDisk {
if (isNoFloppyDisk(disk)) { if (isNoFloppyDisk(disk)) {
const { encoding, metadata, readOnly } = disk; const { encoding, metadata, readOnly } = disk;
@ -312,429 +231,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
throw new Error('Unknown drive state'); 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 driveNumber: 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.drive === this.driveNumber;
}
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(
driveNumber: DriveNumber,
drive: Drive,
readonly disk: NibbleDisk,
controller: ControllerState,
private readonly onDirty: () => void) {
super(driveNumber, 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(
driveNumber: DriveNumber,
drive: Drive,
readonly disk: WozDisk,
controller: ControllerState,
private readonly onDirty: () => void,
private readonly io: Apple2IO) {
super(driveNumber, 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. * Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/ */
@ -794,7 +290,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
sectors, sectors,
bus: 0, bus: 0,
latch: 0, latch: 0,
drive: 1, driveNo: 1,
on: false, on: false,
q6: false, q6: false,
q7: false, q7: false,
@ -814,8 +310,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
/** Updates the active drive based on the controller state. */ /** Updates the active drive based on the controller state. */
private updateActiveDrive() { private updateActiveDrive() {
this.curDrive = this.drives[this.state.drive]; this.curDrive = this.drives[this.state.driveNo];
this.curDriver = this.driver[this.state.drive]; this.curDriver = this.driver[this.state.driveNo];
} }
private debug(..._args: unknown[]) { private debug(..._args: unknown[]) {
@ -898,7 +394,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.offTimeout = window.setTimeout(() => { this.offTimeout = window.setTimeout(() => {
this.debug('Drive Off'); this.debug('Drive Off');
state.on = false; state.on = false;
this.callbacks.driveLight(state.drive, false); this.callbacks.driveLight(state.driveNo, false);
this.curDriver.onDriveOff(); this.curDriver.onDriveOff();
}, 1000); }, 1000);
} }
@ -913,14 +409,14 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!state.on) { if (!state.on) {
this.debug('Drive On'); this.debug('Drive On');
state.on = true; state.on = true;
this.callbacks.driveLight(state.drive, true); this.callbacks.driveLight(state.driveNo, true);
this.curDriver.onDriveOn(); this.curDriver.onDriveOn();
} }
break; break;
case LOC.DRIVE1: // 0x0a case LOC.DRIVE1: // 0x0a
this.debug('Disk 1'); this.debug('Disk 1');
state.drive = 1; state.driveNo = 1;
this.updateActiveDrive(); this.updateActiveDrive();
if (state.on) { if (state.on) {
this.callbacks.driveLight(2, false); this.callbacks.driveLight(2, false);
@ -929,7 +425,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
break; break;
case LOC.DRIVE2: // 0x0b case LOC.DRIVE2: // 0x0b
this.debug('Disk 2'); this.debug('Disk 2');
state.drive = 2; state.driveNo = 2;
this.updateActiveDrive(); this.updateActiveDrive();
if (state.on) { if (state.on) {
this.callbacks.driveLight(1, false); this.callbacks.driveLight(1, false);
@ -980,10 +476,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return result; return result;
} }
private updateDirty(drive: DriveNumber, dirty: boolean) { private updateDirty(driveNo: DriveNumber, dirty: boolean) {
this.drives[drive].dirty = dirty; this.drives[driveNo].dirty = dirty;
if (this.callbacks.dirty) { if (this.callbacks.dirty) {
this.callbacks.dirty(drive, dirty); this.callbacks.dirty(driveNo, dirty);
} }
} }
@ -1002,10 +498,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
reset() { reset() {
const state = this.state; const state = this.state;
if (state.on) { if (state.on) {
this.callbacks.driveLight(state.drive, false); this.callbacks.driveLight(state.driveNo, false);
state.q7 = false; state.q7 = false;
state.on = false; state.on = false;
state.drive = 1; state.driveNo = 1;
} }
this.updateActiveDrive(); this.updateActiveDrive();
} }
@ -1014,10 +510,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.curDriver.tick(); this.curDriver.tick();
} }
private getDriveState(drive: DriveNumber): DriveState { private getDriveState(driveNo: DriveNumber): DriveState {
const curDrive = this.drives[drive]; const curDrive = this.drives[driveNo];
const curDisk = this.disks[drive]; const curDisk = this.disks[driveNo];
const curDriver = this.driver[drive]; const curDriver = this.driver[driveNo];
const { readOnly, track, head, phase, dirty } = curDrive; const { readOnly, track, head, phase, dirty } = curDrive;
return { return {
disk: getDiskState(curDisk), disk: getDiskState(curDisk),
@ -1033,7 +529,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
getState(): State { getState(): State {
const result = { const result = {
drives: [] as DriveState[], drives: [] as DriveState[],
driver: [] as DriverState[],
controllerState: { ...this.state }, controllerState: { ...this.state },
}; };
result.drives[1] = this.getDriveState(1); result.drives[1] = this.getDriveState(1);
@ -1042,9 +537,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return result; return result;
} }
private setDriveState(drive: DriveNumber, state: DriveState) { private setDriveState(driveNo: DriveNumber, state: DriveState) {
const { track, head, phase, readOnly, dirty } = state; const { track, head, phase, readOnly, dirty } = state;
this.drives[drive] = { this.drives[driveNo] = {
track, track,
head, head,
phase, phase,
@ -1052,8 +547,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
dirty, dirty,
}; };
const disk = getDiskState(state.disk); const disk = getDiskState(state.disk);
this.setDiskInternal(drive, disk); this.setDiskInternal(driveNo, disk);
this.driver[drive].setState(state.driver); this.driver[driveNo].setState(state.driver);
} }
@ -1082,8 +577,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
/** Reads the given track and physical sector. */ /** Reads the given track and physical sector. */
rwts(disk: DriveNumber, track: byte, sector: byte) { rwts(driveNo: DriveNumber, track: byte, sector: byte) {
const curDisk = this.disks[disk]; const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) { if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks'); throw new Error('Can\'t read WOZ disks');
} }
@ -1092,12 +587,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */ /** Sets the data for `drive` from `disk`, which is expected to be JSON. */
// TODO(flan): This implementation is not very safe. // TODO(flan): This implementation is not very safe.
setDisk(drive: DriveNumber, jsonDisk: JSONDisk) { setDisk(driveNo: DriveNumber, jsonDisk: JSONDisk) {
if (this.worker) { if (this.worker) {
const message: FormatWorkerMessage = { const message: FormatWorkerMessage = {
type: PROCESS_JSON_DISK, type: PROCESS_JSON_DISK,
payload: { payload: {
drive, driveNo: driveNo,
jsonDisk jsonDisk
}, },
}; };
@ -1106,39 +601,39 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} else { } else {
const disk = createDiskFromJsonDisk(jsonDisk); const disk = createDiskFromJsonDisk(jsonDisk);
if (disk) { if (disk) {
this.insertDisk(drive, disk); this.insertDisk(driveNo, disk);
return true; return true;
} }
} }
return false; return false;
} }
getJSON(drive: DriveNumber, pretty: boolean = false) { getJSON(driveNo: DriveNumber, pretty: boolean = false) {
const curDisk = this.disks[drive]; const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) { if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t save WOZ disks to JSON'); throw new Error('Can\'t save WOZ disks to JSON');
} }
return jsonEncode(curDisk, pretty); return jsonEncode(curDisk, pretty);
} }
setJSON(drive: DriveNumber, json: string) { setJSON(driveNo: DriveNumber, json: string) {
if (this.worker) { if (this.worker) {
const message: FormatWorkerMessage = { const message: FormatWorkerMessage = {
type: PROCESS_JSON, type: PROCESS_JSON,
payload: { payload: {
drive, driveNo: driveNo,
json json
}, },
}; };
this.worker.postMessage(message); this.worker.postMessage(message);
} else { } else {
const disk = jsonDecode(json); const disk = jsonDecode(json);
this.insertDisk(drive, disk); this.insertDisk(driveNo, disk);
} }
return true; return true;
} }
setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) { setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
const readOnly = false; const readOnly = false;
const volume = 254; const volume = 254;
const options = { const options = {
@ -1152,7 +647,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
const message: FormatWorkerMessage = { const message: FormatWorkerMessage = {
type: PROCESS_BINARY, type: PROCESS_BINARY,
payload: { payload: {
drive, driveNo: driveNo,
fmt, fmt,
options, options,
} }
@ -1163,7 +658,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} else { } else {
const disk = createDisk(fmt, options); const disk = createDisk(fmt, options);
if (disk) { if (disk) {
this.insertDisk(drive, disk); this.insertDisk(driveNo, disk);
return true; return true;
} }
} }
@ -1183,7 +678,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
switch (data.type) { switch (data.type) {
case DISK_PROCESSED: case DISK_PROCESSED:
{ {
const { drive, disk } = data.payload; const { driveNo: drive, disk } = data.payload;
if (disk) { if (disk) {
this.insertDisk(drive, disk); this.insertDisk(drive, disk);
} }
@ -1196,26 +691,26 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
} }
private setDiskInternal(drive: DriveNumber, disk: FloppyDisk) { private setDiskInternal(driveNo: DriveNumber, disk: FloppyDisk) {
this.disks[drive] = disk; this.disks[driveNo] = disk;
if (isNoFloppyDisk(disk)) { if (isNoFloppyDisk(disk)) {
this.driver[drive] = new EmptyDriver(this.drives[drive]); this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
} else if (isNibbleDisk(disk)) { } else if (isNibbleDisk(disk)) {
this.driver[drive] = this.driver[driveNo] =
new NibbleDiskDriver( new NibbleDiskDriver(
drive, driveNo,
this.drives[drive], this.drives[driveNo],
disk, disk,
this.state, this.state,
() => this.updateDirty(drive, true)); () => this.updateDirty(driveNo, true));
} else if (isWozDisk(disk)) { } else if (isWozDisk(disk)) {
this.driver[drive] = this.driver[driveNo] =
new WozDiskDriver( new WozDiskDriver(
drive, driveNo,
this.drives[drive], this.drives[driveNo],
disk, disk,
this.state, this.state,
() => this.updateDirty(drive, true), () => this.updateDirty(driveNo, true),
this.io); this.io);
} else { } else {
throw new Error(`Unknown disk format ${disk.encoding}`); throw new Error(`Unknown disk format ${disk.encoding}`);
@ -1223,12 +718,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.updateActiveDrive(); this.updateActiveDrive();
} }
private insertDisk(drive: DriveNumber, disk: FloppyDisk) { private insertDisk(driveNo: DriveNumber, disk: FloppyDisk) {
this.setDiskInternal(drive, disk); this.setDiskInternal(driveNo, disk);
this.drives[drive].head = 0; this.drives[driveNo].head = 0;
const { name, side } = disk.metadata; const { name, side } = disk.metadata;
this.updateDirty(drive, this.drives[drive].dirty); this.updateDirty(driveNo, this.drives[driveNo].dirty);
this.callbacks.label(drive, name, side); this.callbacks.label(driveNo, name, side);
} }
/** /**
@ -1241,8 +736,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
* an error will be thrown. Using `ext == 'nib'` will always return * an error will be thrown. Using `ext == 'nib'` will always return
* an image. * an image.
*/ */
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null { getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
const curDisk = this.disks[drive]; const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) { if (!isNibbleDisk(curDisk)) {
return null; return null;
} }
@ -1289,8 +784,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
// TODO(flan): Does not work with WOZ or D13 disks // TODO(flan): Does not work with WOZ or D13 disks
getBase64(drive: DriveNumber) { getBase64(driveNo: DriveNumber) {
const curDisk = this.disks[drive]; const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) { if (!isNibbleDisk(curDisk)) {
return null; return null;
} }

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

66
js/cards/drivers/types.ts Normal file
View File

@ -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;
}

View File

@ -18,9 +18,9 @@ export interface SmartPortOptions {
} }
export interface Callbacks { export interface Callbacks {
driveLight: (drive: DriveNumber, on: boolean) => void; driveLight: (driveNo: DriveNumber, on: boolean) => void;
dirty: (drive: DriveNumber, dirty: boolean) => void; dirty: (driveNo: DriveNumber, dirty: boolean) => void;
label: (drive: DriveNumber, name?: string, side?: string) => void; label: (driveNo: DriveNumber, name?: string, side?: string) => void;
} }
class Address { class Address {
@ -152,15 +152,15 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
// debug.apply(this, arguments); // debug.apply(this, arguments);
} }
private driveLight(drive: DriveNumber) { private driveLight(driveNo: DriveNumber) {
if (!this.busy[drive]) { if (!this.busy[driveNo]) {
this.busy[drive] = true; this.busy[driveNo] = true;
this.callbacks?.driveLight(drive, true); this.callbacks?.driveLight(driveNo, true);
} }
clearTimeout(this.busyTimeout[drive]); clearTimeout(this.busyTimeout[driveNo]);
this.busyTimeout[drive] = setTimeout(() => { this.busyTimeout[driveNo] = setTimeout(() => {
this.busy[drive] = false; this.busy[driveNo] = false;
this.callbacks?.driveLight(drive, false); this.callbacks?.driveLight(driveNo, false);
}, 100); }, 100);
} }
@ -168,7 +168,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* dumpBlock * dumpBlock
*/ */
dumpBlock(drive: DriveNumber, block: number) { dumpBlock(driveNo: DriveNumber, block: number) {
let result = ''; let result = '';
let b; let b;
let jdx; let jdx;
@ -176,7 +176,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
for (let idx = 0; idx < 32; idx++) { for (let idx = 0; idx < 32; idx++) {
result += toHex(idx << 4, 4) + ': '; result += toHex(idx << 4, 4) + ': ';
for (jdx = 0; jdx < 16; jdx++) { 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) { if (jdx === 8) {
result += ' '; result += ' ';
} }
@ -184,7 +184,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
} }
result += ' '; result += ' ';
for (jdx = 0; jdx < 16; jdx++) { 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) { if (jdx === 8) {
result += ' '; result += ' ';
} }
@ -203,9 +203,9 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* getDeviceInfo * getDeviceInfo
*/ */
getDeviceInfo(state: CpuState, drive: DriveNumber) { getDeviceInfo(state: CpuState, driveNo: DriveNumber) {
if (this.disks[drive]) { if (this.disks[driveNo]) {
const blocks = this.disks[drive].blocks.length; const blocks = this.disks[driveNo].blocks.length;
state.x = blocks & 0xff; state.x = blocks & 0xff;
state.y = blocks >> 8; state.y = blocks >> 8;
@ -221,23 +221,23 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* readBlock * readBlock
*/ */
readBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) { readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
this.debug(`read drive=${drive}`); this.debug(`read drive=${driveNo}`);
this.debug(`read buffer=${buffer.toString()}`); this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`); this.debug(`read block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) { if (!this.disks[driveNo]?.blocks.length) {
debug('Drive', drive, 'is empty'); debug('Drive', driveNo, 'is empty');
state.a = DEVICE_OFFLINE; state.a = DEVICE_OFFLINE;
state.s |= flags.C; state.s |= flags.C;
return; return;
} }
// debug('read', '\n' + dumpBlock(drive, block)); // debug('read', '\n' + dumpBlock(drive, block));
this.driveLight(drive); this.driveLight(driveNo);
for (let idx = 0; idx < 512; idx++) { 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); buffer = buffer.inc(1);
} }
@ -249,30 +249,30 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* writeBlock * writeBlock
*/ */
writeBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) { writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
this.debug(`write drive=${drive}`); this.debug(`write drive=${driveNo}`);
this.debug(`write buffer=${buffer.toString()}`); this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`); this.debug(`write block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) { if (!this.disks[driveNo]?.blocks.length) {
debug('Drive', drive, 'is empty'); debug('Drive', driveNo, 'is empty');
state.a = DEVICE_OFFLINE; state.a = DEVICE_OFFLINE;
state.s |= flags.C; state.s |= flags.C;
return; return;
} }
if (this.disks[drive].readOnly) { if (this.disks[driveNo].readOnly) {
debug('Drive', drive, 'is write protected'); debug('Drive', driveNo, 'is write protected');
state.a = WRITE_PROTECTED; state.a = WRITE_PROTECTED;
state.s |= flags.C; state.s |= flags.C;
return; return;
} }
// debug('write', '\n' + dumpBlock(drive, block)); // debug('write', '\n' + dumpBlock(drive, block));
this.driveLight(drive); this.driveLight(driveNo);
for (let idx = 0; idx < 512; idx++) { 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); buffer = buffer.inc(1);
} }
state.a = 0; state.a = 0;
@ -283,25 +283,25 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* formatDevice * formatDevice
*/ */
formatDevice(state: CpuState, drive: DriveNumber) { formatDevice(state: CpuState, driveNo: DriveNumber) {
if (!this.disks[drive]?.blocks.length) { if (!this.disks[driveNo]?.blocks.length) {
debug('Drive', drive, 'is empty'); debug('Drive', driveNo, 'is empty');
state.a = DEVICE_OFFLINE; state.a = DEVICE_OFFLINE;
state.s |= flags.C; state.s |= flags.C;
return; return;
} }
if (this.disks[drive].readOnly) { if (this.disks[driveNo].readOnly) {
debug('Drive', drive, 'is write protected'); debug('Drive', driveNo, 'is write protected');
state.a = WRITE_PROTECTED; state.a = WRITE_PROTECTED;
state.s |= flags.C; state.s |= flags.C;
return; return;
} }
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) { for (let idx = 0; idx < this.disks[driveNo].blocks.length; idx++) {
this.disks[drive].blocks[idx] = new Uint8Array(); this.disks[driveNo].blocks[idx] = new Uint8Array();
for (let jdx = 0; jdx < 512; jdx++) { 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<BlockFormat>, Restor
); );
} }
setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) { setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
let volume = 254; let volume = 254;
let readOnly = false; let readOnly = false;
if (fmt === '2mg') { if (fmt === '2mg') {
const header = read2MGHeader(rawData); const header = read2MGHeader(rawData);
this.metadata[drive] = header; this.metadata[driveNo] = header;
const { bytes, offset } = header; const { bytes, offset } = header;
volume = header.volume; volume = header.volume;
readOnly = header.readOnly; readOnly = header.readOnly;
rawData = rawData.slice(offset, offset + bytes); rawData = rawData.slice(offset, offset + bytes);
} else { } else {
this.metadata[drive] = null; this.metadata[driveNo] = null;
} }
const options = { const options = {
rawData, rawData,
@ -569,9 +569,9 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
volume, volume,
}; };
this.ext[drive] = fmt; this.ext[driveNo] = fmt;
this.disks[drive] = createBlockDisk(fmt, options); this.disks[driveNo] = createBlockDisk(fmt, options);
this.callbacks?.label(drive, name); this.callbacks?.label(driveNo, name);
return true; return true;
} }

View File

@ -63,19 +63,19 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
<DiskDragTarget <DiskDragTarget
className={styles.disk} className={styles.disk}
storage={smartPort} storage={smartPort}
drive={number} driveNo={number}
formats={BLOCK_FORMATS} formats={BLOCK_FORMATS}
onError={setError} onError={setError}
> >
<ErrorModal error={error} setError={setError} /> <ErrorModal error={error} setError={setError} />
<BlockFileModal <BlockFileModal
smartPort={smartPort} smartPort={smartPort}
number={number} driveNo={number}
onClose={doClose} onClose={doClose}
isOpen={modalOpen} isOpen={modalOpen}
/> />
<DownloadModal <DownloadModal
number={number} driveNo={number}
massStorage={smartPort} massStorage={smartPort}
isOpen={downloadModalOpen} isOpen={downloadModalOpen}
onClose={doCloseDownload} onClose={doCloseDownload}

View File

@ -21,11 +21,11 @@ const DISK_TYPES: FilePickerAcceptType[] = [
interface BlockFileModalProps { interface BlockFileModalProps {
isOpen: boolean; isOpen: boolean;
smartPort: SmartPort; smartPort: SmartPort;
number: DriveNumber; driveNo: DriveNumber;
onClose: (closeBox?: boolean) => void; onClose: (closeBox?: boolean) => void;
} }
export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFileModalProps) => { export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen } : BlockFileModalProps) => {
const [handles, setHandles] = useState<FileSystemFileHandle[]>(); const [handles, setHandles] = useState<FileSystemFileHandle[]>();
const [busy, setBusy] = useState<boolean>(false); const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true); const [empty, setEmpty] = useState<boolean>(true);

View File

@ -6,7 +6,7 @@ import { spawn } from './util/promises';
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> { export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
storage: MassStorage<T> | undefined; storage: MassStorage<T> | undefined;
drive?: DriveNumber; driveNo?: DriveNumber;
formats: typeof FLOPPY_FORMATS formats: typeof FLOPPY_FORMATS
| typeof BLOCK_FORMATS | typeof BLOCK_FORMATS
| typeof DISK_FORMATS; | typeof DISK_FORMATS;
@ -16,7 +16,7 @@ export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElemen
export const DiskDragTarget = ({ export const DiskDragTarget = ({
storage, storage,
drive, driveNo,
dropRef, dropRef,
formats, formats,
onError, onError,
@ -54,7 +54,7 @@ export const DiskDragTarget = ({
const onDrop = (event: DragEvent) => { const onDrop = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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; const dt = event.dataTransfer;
if (dt?.files.length === 1 && storage) { if (dt?.files.length === 1 && storage) {
@ -87,7 +87,7 @@ export const DiskDragTarget = ({
div.removeEventListener('drop', onDrop); div.removeEventListener('drop', onDrop);
}; };
} }
}, [drive, dropRef, formats, onError, storage]); }, [driveNo, dropRef, formats, onError, storage]);
return ( return (
<div ref={ref} {...props}> <div ref={ref} {...props}>

View File

@ -65,13 +65,13 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
<DiskDragTarget <DiskDragTarget
className={styles.disk} className={styles.disk}
storage={disk2} storage={disk2}
drive={number} driveNo={number}
formats={FLOPPY_FORMATS} formats={FLOPPY_FORMATS}
onError={setError} onError={setError}
> >
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} /> <FileModal disk2={disk2} driveNo={number} onClose={doClose} isOpen={modalOpen} />
<DownloadModal <DownloadModal
number={number} driveNo={number}
massStorage={disk2} massStorage={disk2}
isOpen={downloadModalOpen} isOpen={downloadModalOpen}
onClose={doCloseDownload} onClose={doCloseDownload}

View File

@ -8,18 +8,18 @@ import styles from './css/DownloadModal.module.css';
interface DownloadModalProps { interface DownloadModalProps {
isOpen: boolean; isOpen: boolean;
massStorage: MassStorage<unknown>; massStorage: MassStorage<unknown>;
number: DriveNumber; driveNo: DriveNumber;
onClose: (closeBox?: boolean) => void; 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 [href, setHref] = useState('');
const [downloadName, setDownloadName] = useState(''); const [downloadName, setDownloadName] = useState('');
const doCancel = useCallback(() => onClose(true), [onClose]); const doCancel = useCallback(() => onClose(true), [onClose]);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
const storageData = massStorage.getBinary(number); const storageData = massStorage.getBinary(driveNo);
if (storageData) { if (storageData) {
const { ext, data } = storageData; const { ext, data } = storageData;
const { name } = storageData.metadata; const { name } = storageData.metadata;
@ -37,7 +37,7 @@ export const DownloadModal = ({ massStorage, number, onClose, isOpen } : Downloa
setHref(''); setHref('');
setDownloadName(''); setDownloadName('');
} }
}, [isOpen, number, massStorage]); }, [isOpen, driveNo, massStorage]);
return ( return (
<> <>

View File

@ -10,7 +10,7 @@ import { ErrorModal } from './ErrorModal';
import { ProgressModal } from './ProgressModal'; import { ProgressModal } from './ProgressModal';
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files'; import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
import { useHash } from './hooks/useHash'; 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 { spawn, Ready } from './util/promises';
import styles from './css/Drives.module.css'; 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 hashParts = getHashParts(hash);
const controllers: AbortController[] = []; const controllers: AbortController[] = [];
let loading = 0; let loading = 0;
for (const drive of [1, 2] as DriveNumber[]) { for (const driveNo of DRIVE_NUMBERS) {
if (hashParts && hashParts[drive]) { if (hashParts && hashParts[driveNo]) {
const hashPart = decodeURIComponent(hashParts[drive]); const hashPart = decodeURIComponent(hashParts[driveNo]);
const isHttp = hashPart.match(/^https?:/i); const isHttp = hashPart.match(/^https?:/i);
const isJson = hashPart.match(/\.json$/i); const isJson = hashPart.match(/\.json$/i);
if (isHttp && !isJson) { if (isHttp && !isJson) {
@ -101,7 +101,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
try { try {
await loadHttpUnknownFile( await loadHttpUnknownFile(
smartStorageBroker, smartStorageBroker,
drive, driveNo,
hashPart, hashPart,
signal, signal,
onProgress); onProgress);
@ -116,7 +116,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
})); }));
} else { } else {
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`; const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
loadJSON(disk2, drive, url).catch((e) => setError(e)); loadJSON(disk2, driveNo, url).catch((e) => setError(e));
} }
} }
} }

View File

@ -28,7 +28,7 @@ export type NibbleFileCallback = (
interface FileModalProps { interface FileModalProps {
isOpen: boolean; isOpen: boolean;
disk2: DiskII; disk2: DiskII;
number: DriveNumber; driveNo: DriveNumber;
onClose: (closeBox?: boolean) => void; onClose: (closeBox?: boolean) => void;
} }
@ -38,7 +38,7 @@ interface IndexEntry {
category: string; category: string;
} }
export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => { export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
const [busy, setBusy] = useState<boolean>(false); const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true); const [empty, setEmpty] = useState<boolean>(true);
const [category, setCategory] = useState<string>(); const [category, setCategory] = useState<string>();
@ -69,13 +69,13 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
try { try {
if (handles?.length === 1) { if (handles?.length === 1) {
hashParts[number] = ''; hashParts[driveNo] = '';
await loadLocalNibbleFile(disk2, number, await handles[0].getFile()); await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
} }
if (filename) { if (filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', '']; const name = filename.match(/\/([^/]+).json$/) || ['', ''];
hashParts[number] = name[1]; hashParts[driveNo] = name[1];
await loadJSON(disk2, number, filename); await loadJSON(disk2, driveNo, filename);
} }
} catch (e) { } catch (e) {
setError(e); setError(e);
@ -86,7 +86,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
} }
setHashParts(hashParts); setHashParts(hashParts);
}, [disk2, filename, number, onClose, handles, hash]); }, [disk2, filename, driveNo, onClose, handles, hash]);
const onChange = useCallback((handles: FileSystemFileHandle[]) => { const onChange = useCallback((handles: FileSystemFileHandle[]) => {
setEmpty(handles.length === 0); setEmpty(handles.length === 0);

View File

@ -235,7 +235,7 @@ const Catalog = ({ dos, setFileData }: CatalogProps) => {
*/ */
interface DiskInfoProps { interface DiskInfoProps {
massStorage: MassStorage<DiskFormat>; massStorage: MassStorage<DiskFormat>;
drive: DriveNumber; driveNo: DriveNumber;
setFileData: (fileData: FileData) => void; setFileData: (fileData: FileData) => void;
} }
@ -250,9 +250,9 @@ interface DiskInfoProps {
* @param drive The drive number * @param drive The drive number
* @returns DiskInfo component * @returns DiskInfo component
*/ */
const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
const disk = useMemo(() => { const disk = useMemo(() => {
const massStorageData = massStorage.getBinary(drive, 'po'); const massStorageData = massStorage.getBinary(driveNo, 'po');
if (massStorageData) { if (massStorageData) {
const { data, readOnly, ext } = massStorageData; const { data, readOnly, ext } = massStorageData;
const { name } = massStorageData.metadata; const { name } = massStorageData.metadata;
@ -265,7 +265,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
volume: 254, volume: 254,
}); });
} else if (data.byteLength < 800 * 1024) { } else if (data.byteLength < 800 * 1024) {
const doData = massStorage.getBinary(drive, 'do'); const doData = massStorage.getBinary(driveNo, 'do');
if (doData) { if (doData) {
if (isMaybeDOS33(doData)) { if (isMaybeDOS33(doData)) {
disk = createDiskFromDOS({ disk = createDiskFromDOS({
@ -288,7 +288,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
return disk; return disk;
} }
return null; return null;
}, [massStorage, drive]); }, [massStorage, driveNo]);
if (disk) { if (disk) {
try { try {
@ -409,11 +409,11 @@ export const Disks = ({ apple2 }: DisksProps) => {
<div className={debuggerStyles.subHeading}> <div className={debuggerStyles.subHeading}>
{card.constructor.name} - 1 {card.constructor.name} - 1
</div> </div>
<DiskInfo massStorage={card} drive={1} setFileData={setFileData} /> <DiskInfo massStorage={card} driveNo={1} setFileData={setFileData} />
<div className={debuggerStyles.subHeading}> <div className={debuggerStyles.subHeading}>
{card.constructor.name} - 2 {card.constructor.name} - 2
</div> </div>
<DiskInfo massStorage={card} drive={2} setFileData={setFileData} /> <DiskInfo massStorage={card} driveNo={2} setFileData={setFileData} />
</div> </div>
))} ))}
<FileViewer fileData={fileData} onClose={onClose} /> <FileViewer fileData={fileData} onClose={onClose} />

View File

@ -48,7 +48,7 @@ export const getNameAndExtension = (url: string) => {
export const loadLocalFile = ( export const loadLocalFile = (
storage: MassStorage<FloppyFormat|BlockFormat>, storage: MassStorage<FloppyFormat|BlockFormat>,
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
number: DriveNumber, driveNo: DriveNumber,
file: File, file: File,
) => { ) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -58,7 +58,7 @@ export const loadLocalFile = (
const { name, ext } = getNameAndExtension(file.name); const { name, ext } = getNameAndExtension(file.name);
if (includes(formats, ext)) { if (includes(formats, ext)) {
initGamepad(); initGamepad();
if (storage.setBinary(number, name, ext, result)) { if (storage.setBinary(driveNo, name, ext, result)) {
resolve(true); resolve(true);
} else { } else {
reject(`Unable to load ${name}`); reject(`Unable to load ${name}`);
@ -76,12 +76,12 @@ export const loadLocalFile = (
* selection form element to be loaded. * selection form element to be loaded.
* *
* @param smartPort SmartPort object * @param smartPort SmartPort object
* @param number Drive number * @param driveNo Drive number
* @param file Browser File object to load * @param file Browser File object to load
* @returns true if successful * @returns true if successful
*/ */
export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, file: File) => { export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, file: File) => {
return loadLocalFile(smartPort, BLOCK_FORMATS, number, 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. * selection form element to be loaded.
* *
* @param disk2 Disk2 object * @param disk2 Disk2 object
* @param number Drive number * @param driveNo Drive number
* @param file Browser File object to load * @param file Browser File object to load
* @returns true if successful * @returns true if successful
*/ */
export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: File) => {
return loadLocalFile(disk2, FLOPPY_FORMATS, number, file); return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file);
}; };
/** /**
@ -103,13 +103,13 @@ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: Fil
* as the emulator. * as the emulator.
* *
* @param disk2 Disk2 object * @param disk2 Disk2 object
* @param number Drive number * @param driveNo Drive number
* @param url URL, relative or absolute to JSON file * @param url URL, relative or absolute to JSON file
* @returns true if successful * @returns true if successful
*/ */
export const loadJSON = async ( export const loadJSON = async (
disk2: Disk2, disk2: Disk2,
number: DriveNumber, driveNo: DriveNumber,
url: string, url: string,
) => { ) => {
const response = await fetch(url); const response = await fetch(url);
@ -120,7 +120,7 @@ export const loadJSON = async (
if (!includes(FLOPPY_FORMATS, data.type)) { if (!includes(FLOPPY_FORMATS, data.type)) {
throw new Error(`Type "${data.type}" not recognized.`); throw new Error(`Type "${data.type}" not recognized.`);
} }
disk2.setDisk(number, data); disk2.setDisk(driveNo, data);
initGamepad(data.gamepad); initGamepad(data.gamepad);
}; };
@ -166,13 +166,13 @@ export const loadHttpFile = async (
* as the emulator. * as the emulator.
* *
* @param smartPort SmartPort object * @param smartPort SmartPort object
* @param number Drive number * @param driveNo Drive number
* @param url URL, relative or absolute to JSON file * @param url URL, relative or absolute to JSON file
* @returns true if successful * @returns true if successful
*/ */
export const loadHttpBlockFile = async ( export const loadHttpBlockFile = async (
smartPort: SmartPort, smartPort: SmartPort,
number: DriveNumber, driveNo: DriveNumber,
url: string, url: string,
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: ProgressCallback onProgress?: ProgressCallback
@ -182,7 +182,7 @@ export const loadHttpBlockFile = async (
throw new Error(`Extension "${ext}" not recognized.`); throw new Error(`Extension "${ext}" not recognized.`);
} }
const data = await loadHttpFile(url, signal, onProgress); const data = await loadHttpFile(url, signal, onProgress);
smartPort.setBinary(number, name, ext, data); smartPort.setBinary(driveNo, name, ext, data);
initGamepad(); initGamepad();
return true; return true;
@ -194,55 +194,55 @@ export const loadHttpBlockFile = async (
* as the emulator. * as the emulator.
* *
* @param disk2 Disk2 object * @param disk2 Disk2 object
* @param number Drive number * @param driveNo Drive number
* @param url URL, relative or absolute to JSON file * @param url URL, relative or absolute to JSON file
* @returns true if successful * @returns true if successful
*/ */
export const loadHttpNibbleFile = async ( export const loadHttpNibbleFile = async (
disk2: Disk2, disk2: Disk2,
number: DriveNumber, driveNo: DriveNumber,
url: string, url: string,
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: ProgressCallback onProgress?: ProgressCallback
) => { ) => {
if (url.endsWith('.json')) { if (url.endsWith('.json')) {
return loadJSON(disk2, number, url); return loadJSON(disk2, driveNo, url);
} }
const { name, ext } = getNameAndExtension(url); const { name, ext } = getNameAndExtension(url);
if (!includes(FLOPPY_FORMATS, ext)) { if (!includes(FLOPPY_FORMATS, ext)) {
throw new Error(`Extension "${ext}" not recognized.`); throw new Error(`Extension "${ext}" not recognized.`);
} }
const data = await loadHttpFile(url, signal, onProgress); const data = await loadHttpFile(url, signal, onProgress);
disk2.setBinary(number, name, ext, data); disk2.setBinary(driveNo, name, ext, data);
initGamepad(); initGamepad();
return loadHttpFile(url, signal, onProgress); return loadHttpFile(url, signal, onProgress);
}; };
export const loadHttpUnknownFile = async ( export const loadHttpUnknownFile = async (
smartStorageBroker: SmartStorageBroker, smartStorageBroker: SmartStorageBroker,
number: DriveNumber, driveNo: DriveNumber,
url: string, url: string,
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: ProgressCallback, onProgress?: ProgressCallback,
) => { ) => {
const data = await loadHttpFile(url, signal, onProgress); const data = await loadHttpFile(url, signal, onProgress);
const { name, ext } = getNameAndExtension(url); const { name, ext } = getNameAndExtension(url);
smartStorageBroker.setBinary(number, name, ext, data); smartStorageBroker.setBinary(driveNo, name, ext, data);
}; };
export class SmartStorageBroker implements MassStorage<unknown> { export class SmartStorageBroker implements MassStorage<unknown> {
constructor(private disk2: Disk2, private smartPort: SmartPort) {} 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 (includes(DISK_FORMATS, ext)) {
if (data.byteLength >= 800 * 1024) { if (data.byteLength >= 800 * 1024) {
if (includes(BLOCK_FORMATS, ext)) { if (includes(BLOCK_FORMATS, ext)) {
this.smartPort.setBinary(drive, name, ext, data); this.smartPort.setBinary(driveNo, name, ext, data);
} else { } else {
throw new Error(`Unable to load "${name}"`); throw new Error(`Unable to load "${name}"`);
} }
} else if (includes(FLOPPY_FORMATS, ext)) { } else if (includes(FLOPPY_FORMATS, ext)) {
this.disk2.setBinary(drive, name, ext, data); this.disk2.setBinary(driveNo, name, ext, data);
} else { } else {
throw new Error(`Unable to load "${name}"`); throw new Error(`Unable to load "${name}"`);
} }

View File

@ -234,7 +234,7 @@ export const PROCESS_JSON = 'PROCESS_JSON';
export interface ProcessBinaryMessage { export interface ProcessBinaryMessage {
type: typeof PROCESS_BINARY; type: typeof PROCESS_BINARY;
payload: { payload: {
drive: DriveNumber; driveNo: DriveNumber;
fmt: FloppyFormat; fmt: FloppyFormat;
options: DiskOptions; options: DiskOptions;
}; };
@ -244,7 +244,7 @@ export interface ProcessBinaryMessage {
export interface ProcessJsonDiskMessage { export interface ProcessJsonDiskMessage {
type: typeof PROCESS_JSON_DISK; type: typeof PROCESS_JSON_DISK;
payload: { payload: {
drive: DriveNumber; driveNo: DriveNumber;
jsonDisk: JSONDisk; jsonDisk: JSONDisk;
}; };
} }
@ -253,7 +253,7 @@ export interface ProcessJsonDiskMessage {
export interface ProcessJsonMessage { export interface ProcessJsonMessage {
type: typeof PROCESS_JSON; type: typeof PROCESS_JSON;
payload: { payload: {
drive: DriveNumber; driveNo: DriveNumber;
json: string; json: string;
}; };
} }
@ -272,7 +272,7 @@ export const DISK_PROCESSED = 'DISK_PROCESSED';
export interface DiskProcessedResponse { export interface DiskProcessedResponse {
type: typeof DISK_PROCESSED; type: typeof DISK_PROCESSED;
payload: { payload: {
drive: DriveNumber; driveNo: DriveNumber;
disk: FloppyDisk | null; disk: FloppyDisk | null;
}; };
} }

View File

@ -84,7 +84,7 @@ let joystick: JoyStick;
let system: System; let system: System;
let keyboard: KeyBoard; let keyboard: KeyBoard;
let io: Apple2IO; let io: Apple2IO;
let _currentDrive: DriveNumber = 1; let driveNo: DriveNumber = 1;
let _e: boolean; let _e: boolean;
let ready: Promise<[void, void]>; let ready: Promise<[void, void]>;
@ -103,14 +103,13 @@ export function compileApplesoftProgram(program: string) {
} }
export function openLoad(driveString: string, event: MouseEvent) { export function openLoad(driveString: string, event: MouseEvent) {
const drive = parseInt(driveString, 10) as DriveNumber; driveNo = parseInt(driveString, 10) as DriveNumber;
_currentDrive = drive; if (event.metaKey && includes(DRIVE_NUMBERS, driveNo)) {
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
openLoadHTTP(); openLoadHTTP();
} else { } else {
if (disk_cur_cat[drive]) { if (disk_cur_cat[driveNo]) {
const element = document.querySelector<HTMLSelectElement>('#category_select')!; const element = document.querySelector<HTMLSelectElement>('#category_select')!;
element.value = disk_cur_cat[drive]; element.value = disk_cur_cat[driveNo];
selectCategory(); selectCategory();
} }
MicroModal.show('load-modal'); MicroModal.show('load-modal');
@ -118,27 +117,27 @@ export function openLoad(driveString: string, event: MouseEvent) {
} }
export function openSave(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 mimeType = 'application/octet-stream';
const storageData = _disk2.getBinary(drive); const storageData = _disk2.getBinary(driveNo);
const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!; const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
if (!storageData) { if (!storageData) {
alert(`No data from drive ${drive}`); alert(`No data from drive ${driveNo}`);
return; return;
} }
const { data } = storageData; const { data } = storageData;
const blob = new Blob([data], { 'type': mimeType }); const blob = new Blob([data], { 'type': mimeType });
a.href = window.URL.createObjectURL(blob); a.href = window.URL.createObjectURL(blob);
a.download = driveLights.label(drive) + '.dsk'; a.download = driveLights.label(driveNo) + '.dsk';
if (event.metaKey) { if (event.metaKey) {
dumpDisk(drive); dumpDisk(driveNo);
} else { } else {
const saveName = document.querySelector<HTMLInputElement>('#save_name')!; const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
saveName.value = driveLights.label(drive); saveName.value = driveLights.label(driveNo);
MicroModal.show('save-modal'); MicroModal.show('save-modal');
} }
} }
@ -154,12 +153,12 @@ export function openAlert(msg: string) {
* Drag and Drop * Drag and Drop
*/ */
export function handleDragOver(_drive: number, event: DragEvent) { export function handleDragOver(_driveNo: number, event: DragEvent) {
event.preventDefault(); event.preventDefault();
event.dataTransfer!.dropEffect = 'copy'; event.dataTransfer!.dropEffect = 'copy';
} }
export function handleDragEnd(_drive: number, event: DragEvent) { export function handleDragEnd(_driveNo: number, event: DragEvent) {
const dt = event.dataTransfer!; const dt = event.dataTransfer!;
if (dt.items) { if (dt.items) {
for (let i = 0; i < dt.items.length; i++) { 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.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (drive < 1) { if (driveNo < 1) {
if (!_disk2.getMetadata(1)) { if (!_disk2.getMetadata(1)) {
drive = 1; driveNo = 1;
} else if (!_disk2.getMetadata(2)) { } else if (!_disk2.getMetadata(2)) {
drive = 2; driveNo = 2;
} else { } else {
drive = 1; driveNo = 1;
} }
} }
const dt = event.dataTransfer!; const dt = event.dataTransfer!;
if (dt.files.length === 1) { if (dt.files.length === 1) {
const runOnLoad = event.shiftKey; 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) { } else if (dt.files.length === 2) {
doLoadLocal(1, dt.files[0]); doLoadLocal(1, dt.files[0]);
doLoadLocal(2, dt.files[1]); doLoadLocal(2, dt.files[1]);
@ -195,7 +194,7 @@ export function handleDrop(drive: number, event: DragEvent) {
if (dt.items[idx].type === 'text/uri-list') { if (dt.items[idx].type === 'text/uri-list') {
dt.items[idx].getAsString(function (url) { dt.items[idx].getAsString(function (url) {
const parts = hup().split('|'); const parts = hup().split('|');
parts[drive - 1] = url; parts[driveNo - 1] = url;
document.location.hash = parts.join('|'); 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(); loadingStart();
fetch(url).then(function (response: Response) { fetch(url).then(function (response: Response) {
@ -241,7 +240,7 @@ export function loadAjax(drive: DriveNumber, url: string) {
if (data.type === 'binary') { if (data.type === 'binary') {
loadBinary(data ); loadBinary(data );
} else if (includes(DISK_FORMATS, data.type)) { } else if (includes(DISK_FORMATS, data.type)) {
loadDisk(drive, data); loadDisk(driveNo, data);
} }
initGamepad(data.gamepad); initGamepad(data.gamepad);
loadingStop(); loadingStop();
@ -269,7 +268,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
const files = localFile.files; const files = localFile.files;
if (files && files.length === 1) { if (files && files.length === 1) {
const runOnLoad = event.shiftKey; const runOnLoad = event.shiftKey;
doLoadLocal(_currentDrive, files[0], { runOnLoad }); doLoadLocal(driveNo, files[0], { runOnLoad });
} else if (url) { } else if (url) {
let filename; let filename;
MicroModal.close('load-modal'); MicroModal.close('load-modal');
@ -278,7 +277,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
if (filename === '__manage') { if (filename === '__manage') {
openManage(); openManage();
} else { } else {
loadLocalStorage(_currentDrive, filename); loadLocalStorage(driveNo, filename);
} }
} else { } else {
const r1 = /json\/disks\/(.*).json$/.exec(url); const r1 = /json\/disks\/(.*).json$/.exec(url);
@ -288,7 +287,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
filename = url; filename = url;
} }
const parts = hup().split('|'); const parts = hup().split('|');
parts[_currentDrive - 1] = filename; parts[driveNo - 1] = filename;
document.location.hash = parts.join('|'); document.location.hash = parts.join('|');
} }
} }
@ -297,7 +296,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
export function doSave() { export function doSave() {
const saveName = document.querySelector<HTMLInputElement>('#save_name')!; const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
const name = saveName.value; const name = saveName.value;
saveLocalStorage(_currentDrive, name); saveLocalStorage(driveNo, name);
MicroModal.close('save-modal'); MicroModal.close('save-modal');
window.setTimeout(() => openAlert('Saved'), 0); window.setTimeout(() => openAlert('Saved'), 0);
} }
@ -313,7 +312,7 @@ interface LoadOptions {
runOnLoad?: boolean; runOnLoad?: boolean;
} }
function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOptions> = {}) { function doLoadLocal(driveNo: DriveNumber, file: File, options: Partial<LoadOptions> = {}) {
const parts = file.name.split('.'); const parts = file.name.split('.');
const ext = parts[parts.length - 1].toLowerCase(); const ext = parts[parts.length - 1].toLowerCase();
const matches = file.name.match(CIDERPRESS_EXTENSION); const matches = file.name.match(CIDERPRESS_EXTENSION);
@ -322,7 +321,7 @@ function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOption
[, type, aux] = matches; [, type, aux] = matches;
} }
if (includes(DISK_FORMATS, ext)) { if (includes(DISK_FORMATS, ext)) {
doLoadLocalDisk(drive, file); doLoadLocalDisk(driveNo, file);
} else if (includes(TAPE_TYPES, ext)) { } else if (includes(TAPE_TYPES, ext)) {
tape.doLoadLocalTape(file); tape.doLoadLocalTape(file);
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) { } else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
@ -366,7 +365,7 @@ function doLoadBinary(file: File, options: LoadOptions) {
fileReader.readAsArrayBuffer(file); fileReader.readAsArrayBuffer(file);
} }
function doLoadLocalDisk(drive: DriveNumber, file: File) { function doLoadLocalDisk(driveNo: DriveNumber, file: File) {
loadingStart(); loadingStart();
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = function () { fileReader.onload = function () {
@ -377,14 +376,14 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
// Remove any json file reference // Remove any json file reference
const files = hup().split('|'); const files = hup().split('|');
files[drive - 1] = ''; files[driveNo - 1] = '';
document.location.hash = files.join('|'); document.location.hash = files.join('|');
if (includes(DISK_FORMATS, ext)) { if (includes(DISK_FORMATS, ext)) {
if (result.byteLength >= 800 * 1024) { if (result.byteLength >= 800 * 1024) {
if ( if (
includes(BLOCK_FORMATS, ext) && includes(BLOCK_FORMATS, ext) &&
_massStorage.setBinary(drive, name, ext, result) _massStorage.setBinary(driveNo, name, ext, result)
) { ) {
initGamepad(); initGamepad();
} else { } else {
@ -393,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
} else { } else {
if ( if (
includes(FLOPPY_FORMATS, ext) && includes(FLOPPY_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, result) _disk2.setBinary(driveNo, name, ext, result)
) { ) {
initGamepad(); initGamepad();
} else { } else {
@ -406,7 +405,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
fileReader.readAsArrayBuffer(file); fileReader.readAsArrayBuffer(file);
} }
export function doLoadHTTP(drive: DriveNumber, url?: string) { export function doLoadHTTP(driveNo: DriveNumber, url?: string) {
if (!url) { if (!url) {
MicroModal.close('http-modal'); MicroModal.close('http-modal');
} }
@ -454,13 +453,13 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
if (includes(DISK_FORMATS, ext)) { if (includes(DISK_FORMATS, ext)) {
if (data.byteLength >= 800 * 1024) { if (data.byteLength >= 800 * 1024) {
if (includes(BLOCK_FORMATS, ext)) { if (includes(BLOCK_FORMATS, ext)) {
_massStorage.setBinary(drive, name, ext, data); _massStorage.setBinary(driveNo, name, ext, data);
initGamepad(); initGamepad();
} }
} else { } else {
if ( if (
includes(FLOPPY_FORMATS, ext) && includes(FLOPPY_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, data) _disk2.setBinary(driveNo, name, ext, data)
) { ) {
initGamepad(); initGamepad();
} }
@ -547,11 +546,11 @@ function updateSoundButton(on: boolean) {
} }
} }
function dumpDisk(drive: DriveNumber) { function dumpDisk(driveNo: DriveNumber) {
const wind = window.open('', '_blank')!; const wind = window.open('', '_blank')!;
wind.document.title = driveLights.label(drive); wind.document.title = driveLights.label(driveNo);
wind.document.write('<pre>'); wind.document.write('<pre>');
wind.document.write(_disk2.getJSON(drive, true)); wind.document.write(_disk2.getJSON(driveNo, true));
wind.document.write('</pre>'); wind.document.write('</pre>');
wind.document.close(); wind.document.close();
} }
@ -586,7 +585,7 @@ export function selectCategory() {
option.value = file.filename; option.value = file.filename;
option.innerText = name; option.innerText = name;
diskSelect.append(option); diskSelect.append(option);
if (disk_cur_name[_currentDrive] === name) { if (disk_cur_name[driveNo] === name) {
option.selected = true; option.selected = true;
} }
} }
@ -603,7 +602,7 @@ export function clickDisk(event: MouseEvent|KeyboardEvent) {
} }
/** Called to load disks from the local catalog. */ /** Called to load disks from the local catalog. */
function loadDisk(drive: DriveNumber, disk: JSONDisk) { function loadDisk(driveNo: DriveNumber, disk: JSONDisk) {
let name = disk.name; let name = disk.name;
const category = disk.category!; // all disks in the local catalog have a category 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; name += ' - ' + disk.disk;
} }
disk_cur_cat[drive] = category; disk_cur_cat[driveNo] = category;
disk_cur_name[drive] = name; disk_cur_name[driveNo] = name;
_disk2.setDisk(drive, disk); _disk2.setDisk(driveNo, disk);
initGamepad(disk.gamepad); initGamepad(disk.gamepad);
} }
@ -654,16 +653,16 @@ type LocalDiskIndex = {
[name: string]: string; [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 diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
const json = _disk2.getJSON(drive); const json = _disk2.getJSON(driveNo);
diskIndex[name] = json; diskIndex[name] = json;
window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex)); window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex));
driveLights.label(drive, name); driveLights.label(driveNo, name);
driveLights.dirty(drive, false); driveLights.dirty(driveNo, false);
updateLocalStorage(); updateLocalStorage();
} }
@ -677,12 +676,12 @@ function deleteLocalStorage(name: string) {
updateLocalStorage(); updateLocalStorage();
} }
function loadLocalStorage(drive: DriveNumber, name: string) { function loadLocalStorage(driveNo: DriveNumber, name: string) {
const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
if (diskIndex[name]) { if (diskIndex[name]) {
_disk2.setJSON(drive, diskIndex[name]); _disk2.setJSON(driveNo, diskIndex[name]);
driveLights.label(drive, name); driveLights.label(driveNo, name);
driveLights.dirty(drive, false); driveLights.dirty(driveNo, false);
} }
} }

View File

@ -2,8 +2,8 @@ import { Callbacks } from '../cards/disk2';
import type { DriveNumber } from '../formats/types'; import type { DriveNumber } from '../formats/types';
export default class DriveLights implements Callbacks { export default class DriveLights implements Callbacks {
public driveLight(drive: DriveNumber, on: boolean) { public driveLight(driveNo: DriveNumber, on: boolean) {
const disk = document.querySelector<HTMLElement>(`#disk${drive}`); const disk = document.querySelector<HTMLElement>(`#disk${driveNo}`);
if (disk) { if (disk) {
disk.style.backgroundImage = disk.style.backgroundImage =
on ? 'url(css/red-on-16.png)' : 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; // document.querySelector('#disksave' + drive).disabled = !dirty;
} }
public label(drive: DriveNumber, label?: string, side?: string) { public label(driveNo: DriveNumber, label?: string, side?: string) {
const labelElement = document.querySelector<HTMLElement>(`#disk-label${drive}`); const labelElement = document.querySelector<HTMLElement>(`#disk-label${driveNo}`);
let labelText = ''; let labelText = '';
if (labelElement) { if (labelElement) {
labelText = labelElement.innerText; labelText = labelElement.innerText;

View File

@ -67,7 +67,7 @@ describe('DiskII', () => {
const state = diskII.getState(); const state = diskII.getState();
// These are just arbitrary changes, not an exhaustive list of fields. // These are just arbitrary changes, not an exhaustive list of fields.
(state.drives[1].driver as {skip:number}).skip = 1; (state.drives[1].driver as {skip:number}).skip = 1;
state.controllerState.drive = 2; state.controllerState.driveNo = 2;
state.controllerState.latch = 0x42; state.controllerState.latch = 0x42;
state.controllerState.on = true; state.controllerState.on = true;
state.controllerState.q7 = true; state.controllerState.q7 = true;
@ -760,11 +760,11 @@ class TestDiskReader {
nibbles = 0; nibbles = 0;
diskII: DiskII; 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); mocked(apple2IO).cycles.mockImplementation(() => this.cycles);
this.diskII = new DiskII(apple2IO, callbacks); this.diskII = new DiskII(apple2IO, callbacks);
this.diskII.setBinary(drive, label, 'woz', image); this.diskII.setBinary(driveNo, label, 'woz', image);
} }
readNibble(): byte { readNibble(): byte {

View File

@ -19,7 +19,7 @@ debug('Worker loaded');
addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => { addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
debug('Worker started', message.type); debug('Worker started', message.type);
const data = message.data; const data = message.data;
const { drive } = data.payload; const { driveNo } = data.payload;
let disk: FloppyDisk | null = null; let disk: FloppyDisk | null = null;
switch (data.type) { switch (data.type) {
@ -45,7 +45,7 @@ addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
const response: DiskProcessedResponse = { const response: DiskProcessedResponse = {
type: DISK_PROCESSED, type: DISK_PROCESSED,
payload: { payload: {
drive, driveNo,
disk disk
} }
}; };