apple2js/js/cards/disk2.ts
Ian Flanigan a7bf5d025d
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.)
2022-09-24 09:51:40 +02:00

807 lines
28 KiB
TypeScript

import { base64_encode } from '../base64';
import type {
byte,
Card,
ReadonlyUint8Array
} from '../types';
import {
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
FloppyFormat, FormatWorkerMessage,
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
} from '../formats/types';
import {
createDisk,
createDiskFromJsonDisk
} from '../formats/create_disk';
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
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
// See Understanding the Apple IIe, Table 9.1
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
PHASE0ON: 0x81, // Q0H: Phase 0 ON
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
PHASE1ON: 0x83, // Q1H: Phase 1 ON
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
PHASE2ON: 0x85, // Q2H: Phase 2 ON
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
PHASE3ON: 0x87, // Q3H: Phase 3 ON
DRIVEOFF: 0x88, // Q4L: Drives OFF
DRIVEON: 0x89, // Q4H: Selected drive ON
DRIVE1: 0x8A, // Q5L: Select drive 1
DRIVE2: 0x8B, // Q5H: Select drive 2
DRIVEREAD: 0x8C, // Q6L: Shift while writing; read data
DRIVEWRITE: 0x8D, // Q6H: Load while writing; read write protect
DRIVEREADMODE: 0x8E, // Q7L: Read
DRIVEWRITEMODE: 0x8F // Q7H: Write
} as const;
/** Logic state sequencer ROM */
// See Understanding the Apple IIe, Table 9.3 Logic State Sequencer Commands
// CODE OPERATION BEFORE AFTER
// 0 CLR XXXXXXXX 00000000
// 8 NOP ABCDEFGH ABCDEFGH
// 9 SL0 ABCDEFGH BCDEFGH0
// A SR (write protected) ABCDEFGH 11111111
// (not write protected) ABCDEFGH 0ABCDEFG
// B LOAD XXXXXXXX YYYYYYYY
// D SL1 ABCDEFGH BCDEFGH1
const SEQUENCER_ROM_13 = [
// See Understanding the Apple IIe, Figure 9.10 The DOS 3.2 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.10 for Q7 H (Write).
//
// Q7 L (Read) Q7 H (Write)
// Q6 L (Shift) Q6 H (Load) Q6 L (Shift) Q6 H (Load)
// QA L QA H QA L QA H QA L QA H QA L QA H
// 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
0xD8, 0x18, 0x18, 0x08, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 0
0xD8, 0x2D, 0x28, 0x28, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, // 1
0xD8, 0x38, 0x38, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B, // 2
0xD8, 0x48, 0xD8, 0x48, 0x0A, 0x0A, 0x0A, 0x0A, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, // 3
0xD8, 0x58, 0xD8, 0x58, 0x0A, 0x0A, 0x0A, 0x0A, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, // 4
0xD8, 0x68, 0xD8, 0x68, 0x0A, 0x0A, 0x0A, 0x0A, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, // 5
0xD8, 0x78, 0xD8, 0x78, 0x0A, 0x0A, 0x0A, 0x0A, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, // 6
0xD8, 0x88, 0xD8, 0x88, 0x0A, 0x0A, 0x0A, 0x0A, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, // 7
0xD8, 0x98, 0xD8, 0x98, 0x0A, 0x0A, 0x0A, 0x0A, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, // 8
0xD8, 0x09, 0xD8, 0xA8, 0x0A, 0x0A, 0x0A, 0x0A, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, // 9
0xCD, 0xBD, 0xD8, 0xB8, 0x0A, 0x0A, 0x0A, 0x0A, 0xB9, 0xB9, 0xB9, 0xB9, 0xBB, 0xBB, 0xBB, 0xBB, // A
0xD9, 0x39, 0xD8, 0xC8, 0x0A, 0x0A, 0x0A, 0x0A, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, // B
0xD9, 0xD9, 0xD8, 0xA0, 0x0A, 0x0A, 0x0A, 0x0A, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, // C
0x1D, 0x0D, 0xE8, 0xE8, 0x0A, 0x0A, 0x0A, 0x0A, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, // D
0xFD, 0xFD, 0xF8, 0xF8, 0x0A, 0x0A, 0x0A, 0x0A, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, // E
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
] as const;
const SEQUENCER_ROM_16 = [
// See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.11 for Q7 H (Write).
//
// Q7 L (Read) Q7 H (Write)
// Q6 L (Shift) Q6 H (Load) Q6 L (Shift) Q6 H (Load)
// QA L QA H QA L QA H QA L QA H QA L QA H
// 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
0x18, 0x18, 0x18, 0x18, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 0
0x2D, 0x2D, 0x38, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, // 1
0xD8, 0x38, 0x08, 0x28, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B, // 2
0xD8, 0x48, 0x48, 0x48, 0x0A, 0x0A, 0x0A, 0x0A, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, // 3
0xD8, 0x58, 0xD8, 0x58, 0x0A, 0x0A, 0x0A, 0x0A, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, // 4
0xD8, 0x68, 0xD8, 0x68, 0x0A, 0x0A, 0x0A, 0x0A, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, // 5
0xD8, 0x78, 0xD8, 0x78, 0x0A, 0x0A, 0x0A, 0x0A, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, // 6
0xD8, 0x88, 0xD8, 0x88, 0x0A, 0x0A, 0x0A, 0x0A, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, // 7
0xD8, 0x98, 0xD8, 0x98, 0x0A, 0x0A, 0x0A, 0x0A, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, // 8
0xD8, 0x29, 0xD8, 0xA8, 0x0A, 0x0A, 0x0A, 0x0A, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, // 9
0xCD, 0xBD, 0xD8, 0xB8, 0x0A, 0x0A, 0x0A, 0x0A, 0xB9, 0xB9, 0xB9, 0xB9, 0xBB, 0xBB, 0xBB, 0xBB, // A
0xD9, 0x59, 0xD8, 0xC8, 0x0A, 0x0A, 0x0A, 0x0A, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, // B
0xD9, 0xD9, 0xD8, 0xA0, 0x0A, 0x0A, 0x0A, 0x0A, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, // C
0xD8, 0x08, 0xE8, 0xE8, 0x0A, 0x0A, 0x0A, 0x0A, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, // D
0xFD, 0xFD, 0xF8, 0xF8, 0x0A, 0x0A, 0x0A, 0x0A, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, // E
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
] as const;
/** Contents of the P6 sequencer ROM. */
export const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
13: SEQUENCER_ROM_13,
16: SEQUENCER_ROM_16,
} as const;
/** Contents of the P5 ROM at 0xCnXX. */
const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
13: BOOTSTRAP_ROM_13,
16: BOOTSTRAP_ROM_16,
} as const;
/**
* How far the head moves, in quarter tracks, when in phase X and phase Y is
* activated. For example, if in phase 0 (top row), turning on phase 3 would
* step backwards a quarter track while turning on phase 2 would step forwards
* a half track.
*
* Note that this emulation is highly simplified as it only takes into account
* the order that coils are powered on and ignores when they are powered off.
* The actual hardware allows for multiple coils to be powered at the same time
* providing different levels of torque on the head arm. Along with that, the
* RWTS uses a complex delay system to drive the coils faster based on expected
* head momentum.
*
* Examining the https://computerhistory.org/blog/apple-ii-dos-source-code/,
* one finds the SEEK routine on line 4831 of `appdos31.lst`. It uses `ONTABLE`
* and `OFFTABLE` (each 12 bytes) to know exactly how many microseconds to
* power on/off each coil as the head accelerates. At the end, the final coil
* is left powered on 9.5 milliseconds to ensure the head has settled.
*
* https://embeddedmicro.weebly.com/apple-2iie.html shows traces of the boot
* seek (which is slightly different) and a regular seek.
*/
const PHASE_DELTA = [
[0, 1, 2, -1],
[-1, 0, 1, 2],
[-2, -1, 0, 1],
[1, -2, -1, 0]
] as const;
/** Callbacks triggered by events of the drive or controller. */
export interface Callbacks {
/** Called when a drive turns on or off. */
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: (driveNo: DriveNumber, dirty: boolean) => void;
/** Called when a disk is inserted or removed from the drive. */
label: (driveNo: DriveNumber, name?: string, side?: string) => void;
}
interface DriveState {
disk: FloppyDisk;
driver: DriverState;
readOnly: boolean;
track: byte;
head: byte;
phase: Phase;
dirty: boolean;
}
/** State of the controller for saving/restoring. */
interface State {
drives: DriveState[];
controllerState: ControllerState;
}
function getDiskState(disk: FloppyDisk): FloppyDisk {
if (isNoFloppyDisk(disk)) {
const { encoding, metadata, readOnly } = disk;
return {
encoding,
metadata: { ...metadata },
readOnly,
};
}
if (isNibbleDisk(disk)) {
const { format, encoding, metadata, readOnly, volume, tracks } = disk;
const result: NibbleDisk = {
format,
encoding,
volume,
tracks: [],
readOnly,
metadata: { ...metadata },
};
for (let idx = 0; idx < tracks.length; idx++) {
result.tracks.push(new Uint8Array(tracks[idx]));
}
return result;
}
if (isWozDisk(disk)) {
const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk;
const result: WozDisk = {
format,
encoding,
readOnly,
trackMap: [],
rawTracks: [],
metadata: { ...metadata },
info: disk.info,
};
result.trackMap = [...trackMap];
for (let idx = 0; idx < rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(rawTracks[idx]));
}
return result;
}
throw new Error('Unknown drive state');
}
/**
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
},
2: { // Drive 2
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
}
};
private disks: Record<DriveNumber, FloppyDisk> = {
1: {
encoding: NO_DISK,
readOnly: false,
metadata: { name: 'Disk 1' },
},
2: {
encoding: NO_DISK,
readOnly: false,
metadata: { name: 'Disk 2' },
}
};
private driver: Record<DriveNumber, DiskDriver> = {
1: new EmptyDriver(this.drives[1]),
2: new EmptyDriver(this.drives[2]),
};
private state: ControllerState;
/** Drive off timeout id or null. */
private offTimeout: number | null = null;
/** Current drive object. Must only be set by `updateActiveDrive()`. */
private curDrive: Drive;
/** Current driver object. Must only be set by `updateAcivetDrive()`. */
private curDriver: DiskDriver;
private worker: Worker;
/** Builds a new Disk ][ card. */
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
this.debug('Disk ][');
this.state = {
sectors,
bus: 0,
latch: 0,
driveNo: 1,
on: false,
q6: false,
q7: false,
clock: 0,
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
// essentially the start of the sequencer loop and produces
// correctly synced nibbles immediately. Starting at state 0
// would introduce a spurrious 1 in the latch at the beginning,
// which requires reading several more sync bytes to sync up.
state: 2,
};
this.updateActiveDrive();
this.initWorker();
}
/** Updates the active drive based on the controller state. */
private updateActiveDrive() {
this.curDrive = this.drives[this.state.driveNo];
this.curDriver = this.driver[this.state.driveNo];
}
private debug(..._args: unknown[]) {
// debug(..._args);
}
public head(): number {
return this.curDrive.head;
}
/**
* Sets whether the head positioning stepper motor coil for the given
* phase is on or off. Normally, the motor must be stepped two phases
* per track. Half tracks can be written by stepping only once; quarter
* tracks by activating two neighboring coils at once.
*/
private setPhase(phase: Phase, on: boolean) {
// According to Sather, UtA2e, p. 9-12, Drive On/Off and Drive
// Select:
// Turning a drive on ($C089,X) [...]:
// 1. [...]
// 5. [...] enables head positioning [...]
//
// Therefore do nothing if no drive is on.
if (!this.state.on) {
this.debug(`ignoring phase ${phase}${on ? ' on' : ' off'}`);
return;
}
this.debug(`phase ${phase}${on ? ' on' : ' off'}`);
if (on) {
this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2;
this.curDrive.phase = phase;
}
this.curDriver.clampTrack();
// debug(
// 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3),
// '(' + toHex(_cur.track) + ')',
// '[' + phase + ':' + (on ? 'on' : 'off') + ']');
}
private access(off: byte, val?: byte) {
const state = this.state;
let result = 0;
const readMode = val === undefined;
switch (off & 0x8f) {
case LOC.PHASE0OFF: // 0x00
this.setPhase(0, false);
break;
case LOC.PHASE0ON: // 0x01
this.setPhase(0, true);
break;
case LOC.PHASE1OFF: // 0x02
this.setPhase(1, false);
break;
case LOC.PHASE1ON: // 0x03
this.setPhase(1, true);
break;
case LOC.PHASE2OFF: // 0x04
this.setPhase(2, false);
break;
case LOC.PHASE2ON: // 0x05
this.setPhase(2, true);
break;
case LOC.PHASE3OFF: // 0x06
this.setPhase(3, false);
break;
case LOC.PHASE3ON: // 0x07
this.setPhase(3, true);
break;
case LOC.DRIVEOFF: // 0x08
if (!this.offTimeout) {
if (state.on) {
// TODO(flan): This is fragile because it relies on
// wall-clock time instead of emulator time.
this.offTimeout = window.setTimeout(() => {
this.debug('Drive Off');
state.on = false;
this.callbacks.driveLight(state.driveNo, false);
this.curDriver.onDriveOff();
}, 1000);
}
}
break;
case LOC.DRIVEON: // 0x09
if (this.offTimeout) {
// TODO(flan): Fragile—see above
window.clearTimeout(this.offTimeout);
this.offTimeout = null;
}
if (!state.on) {
this.debug('Drive On');
state.on = true;
this.callbacks.driveLight(state.driveNo, true);
this.curDriver.onDriveOn();
}
break;
case LOC.DRIVE1: // 0x0a
this.debug('Disk 1');
state.driveNo = 1;
this.updateActiveDrive();
if (state.on) {
this.callbacks.driveLight(2, false);
this.callbacks.driveLight(1, true);
}
break;
case LOC.DRIVE2: // 0x0b
this.debug('Disk 2');
state.driveNo = 2;
this.updateActiveDrive();
if (state.on) {
this.callbacks.driveLight(1, false);
this.callbacks.driveLight(2, true);
}
break;
case LOC.DRIVEREAD: // 0x0c (Q6L) Shift
state.q6 = false;
this.curDriver.onQ6Low();
break;
case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD
state.q6 = true;
this.curDriver.onQ6High(readMode);
break;
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
this.debug('Read Mode');
state.q7 = false;
break;
case LOC.DRIVEWRITEMODE: // 0x0f (Q7H)
this.debug('Write Mode');
state.q7 = true;
break;
default:
break;
}
this.tick();
if (readMode) {
// According to UtAIIe, p. 9-13 to 9-14, any even address can be
// used to read the data register onto the CPU bus, although some
// also cause conflicts with the disk controller commands.
if ((off & 0x01) === 0) {
result = state.latch;
} else {
result = 0;
}
} else {
// It's not explicitly stated, but writes to any address set the
// data register.
state.bus = val;
}
return result;
}
private updateDirty(driveNo: DriveNumber, dirty: boolean) {
this.drives[driveNo].dirty = dirty;
if (this.callbacks.dirty) {
this.callbacks.dirty(driveNo, dirty);
}
}
ioSwitch(off: byte, val?: byte) {
return this.access(off, val);
}
read(_page: byte, off: byte) {
return BOOTSTRAP_ROM[this.sectors][off];
}
write() {
// not writable
}
reset() {
const state = this.state;
if (state.on) {
this.callbacks.driveLight(state.driveNo, false);
state.q7 = false;
state.on = false;
state.driveNo = 1;
}
this.updateActiveDrive();
}
tick() {
this.curDriver.tick();
}
private getDriveState(driveNo: DriveNumber): DriveState {
const curDrive = this.drives[driveNo];
const curDisk = this.disks[driveNo];
const curDriver = this.driver[driveNo];
const { readOnly, track, head, phase, dirty } = curDrive;
return {
disk: getDiskState(curDisk),
driver: curDriver.getState(),
readOnly,
track,
head,
phase,
dirty,
};
}
getState(): State {
const result = {
drives: [] as DriveState[],
controllerState: { ...this.state },
};
result.drives[1] = this.getDriveState(1);
result.drives[2] = this.getDriveState(2);
return result;
}
private setDriveState(driveNo: DriveNumber, state: DriveState) {
const { track, head, phase, readOnly, dirty } = state;
this.drives[driveNo] = {
track,
head,
phase,
readOnly,
dirty,
};
const disk = getDiskState(state.disk);
this.setDiskInternal(driveNo, disk);
this.driver[driveNo].setState(state.driver);
}
setState(state: State) {
this.state = { ...state.controllerState };
for (const d of DRIVE_NUMBERS) {
this.setDriveState(d, state.drives[d]);
const { name, side } = state.drives[d].disk.metadata;
const { dirty } = state.drives[d];
this.callbacks.label(d, name, side);
this.callbacks.driveLight(d, this.state.on);
this.callbacks.dirty(d, dirty);
}
this.updateActiveDrive();
}
getMetadata(driveNo: DriveNumber) {
const { track, head, phase, readOnly, dirty } = this.drives[driveNo];
return {
track,
head,
phase,
readOnly,
dirty,
};
}
/** Reads the given track and physical sector. */
rwts(driveNo: DriveNumber, track: byte, sector: byte) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks');
}
return readSector(curDisk, track, sector);
}
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
// TODO(flan): This implementation is not very safe.
setDisk(driveNo: DriveNumber, jsonDisk: JSONDisk) {
if (this.worker) {
const message: FormatWorkerMessage = {
type: PROCESS_JSON_DISK,
payload: {
driveNo: driveNo,
jsonDisk
},
};
this.worker.postMessage(message);
return true;
} else {
const disk = createDiskFromJsonDisk(jsonDisk);
if (disk) {
this.insertDisk(driveNo, disk);
return true;
}
}
return false;
}
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(driveNo: DriveNumber, json: string) {
if (this.worker) {
const message: FormatWorkerMessage = {
type: PROCESS_JSON,
payload: {
driveNo: driveNo,
json
},
};
this.worker.postMessage(message);
} else {
const disk = jsonDecode(json);
this.insertDisk(driveNo, disk);
}
return true;
}
setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
const readOnly = false;
const volume = 254;
const options = {
name,
rawData,
readOnly,
volume,
};
if (this.worker) {
const message: FormatWorkerMessage = {
type: PROCESS_BINARY,
payload: {
driveNo: driveNo,
fmt,
options,
}
};
this.worker.postMessage(message);
return true;
} else {
const disk = createDisk(fmt, options);
if (disk) {
this.insertDisk(driveNo, disk);
return true;
}
}
return false;
}
initWorker() {
if (!window.Worker) {
return;
}
try {
this.worker = new Worker('dist/format_worker.bundle.js');
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DISK_PROCESSED:
{
const { driveNo: drive, disk } = data.payload;
if (disk) {
this.insertDisk(drive, disk);
}
}
break;
}
});
} catch (e: unknown) {
console.error(e);
}
}
private setDiskInternal(driveNo: DriveNumber, disk: FloppyDisk) {
this.disks[driveNo] = disk;
if (isNoFloppyDisk(disk)) {
this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
} else if (isNibbleDisk(disk)) {
this.driver[driveNo] =
new NibbleDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true));
} else if (isWozDisk(disk)) {
this.driver[driveNo] =
new WozDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true),
this.io);
} else {
throw new Error(`Unknown disk format ${disk.encoding}`);
}
this.updateActiveDrive();
}
private insertDisk(driveNo: DriveNumber, disk: FloppyDisk) {
this.setDiskInternal(driveNo, disk);
this.drives[driveNo].head = 0;
const { name, side } = disk.metadata;
this.updateDirty(driveNo, this.drives[driveNo].dirty);
this.callbacks.label(driveNo, name, side);
}
/**
* Returns the binary image of the non-WOZ disk in the given drive.
* For WOZ disks, this method returns `null`. If the `ext` parameter
* is supplied, the returned data will match that format or an error
* will be thrown. If the `ext` parameter is not supplied, the
* original image format for the disk in the drive will be used. If
* the current data on the disk is no longer readable in that format,
* an error will be thrown. Using `ext == 'nib'` will always return
* an image.
*/
getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
return null;
}
const { format, readOnly, tracks, volume } = curDisk;
const { name } = curDisk.metadata;
const len = format === 'nib' ?
tracks.reduce((acc, track) => acc + track.length, 0) :
this.sectors * tracks.length * 256;
const data = new Uint8Array(len);
ext = ext ?? format;
let idx = 0;
for (let t = 0; t < tracks.length; t++) {
if (ext === 'nib') {
data.set(tracks[t], idx);
idx += tracks[t].length;
} else {
let maxSector: SupportedSectors;
let sectorMap: typeof _PO | typeof _DO | typeof _D13O;
if (ext === 'd13') {
maxSector = 13;
sectorMap = _D13O;
} else {
maxSector = 16;
sectorMap = format === 'po' ? _PO : _DO;
}
for (let s = 0; s < maxSector; s++) {
const _s = sectorMap[s];
const sector = readSector({ ...curDisk, format: ext }, t, _s);
data.set(sector, idx);
idx += sector.length;
}
}
}
return {
ext,
metadata: { name },
data: data.buffer,
readOnly,
volume,
};
}
// TODO(flan): Does not work with WOZ or D13 disks
getBase64(driveNo: DriveNumber) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
return null;
}
const data: string[][] | string[] = [];
for (let t = 0; t < curDisk.tracks.length; t++) {
if (isNibbleDisk(curDisk)) {
data[t] = base64_encode(curDisk.tracks[t]);
} else {
const track: string[] = [];
for (let s = 0; s < 0x10; s++) {
track[s] = base64_encode(readSector(curDisk, t, s));
}
data[t] = track;
}
}
return data;
}
}