Split disk data out into its own record (#158)

* Harmonize drive and disk type hierarchies

Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.

However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type.  This
replaces `NIBBLE_FORMATS` in most places.  A couple of new type guards
for disk formats and disks have been added as well.

All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.

* Move disk data to a `disk` field in the drive

Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.

Now, the disk data is in a `disk` field whose type depends on the
drive type.  This makes responisbility a bit easier.

One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata.  When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.

All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).

* Squash the `Drive` type hierarchy

Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`.  With the extraction of the disk data to a
single field, this type hierarchy makes no sense.  Instead, it
suffices to check the type of the disk.

This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.

All tests pass, everything compiles, and both WOZ and nibble disks
work locally.

* Use more destructuring assignment

Now, more places use constructs like:

```TypeScript
    const { metadata, readOnly, track, head, phase, dirty } = drive;
    return {
        disk: getDiskState(drive.disk),
        metadata: {...metadata},
        readOnly,
        track,
        head,
        phase,
        dirty,
    };
```

* Remove the `Disk` object from the `Drive` object

This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.

This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.

Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
This commit is contained in:
Ian Flanigan 2022-09-17 15:41:35 +02:00 committed by GitHub
parent 41e0609f55
commit 2793c25c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 315 additions and 262 deletions

View File

@ -389,6 +389,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
@ -472,7 +473,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
volume,
readOnly
};
const disk = createBlockDisk(options);
const disk = createBlockDisk(ext, options);
return this.setBlockVolume(drive, disk);
}
@ -485,7 +486,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
}
const { blocks, readOnly } = blockDisk;
const { name } = blockDisk.metadata;
let ext;
let ext: '2mg' | 'po';
let data: ArrayBuffer;
if (this._metadata[drive]) {
ext = '2mg';

View File

@ -2,7 +2,6 @@ import { base64_encode } from '../base64';
import type {
byte,
Card,
memory,
nibble,
ReadonlyUint8Array,
} from '../types';
@ -15,16 +14,21 @@ import {
DRIVE_NUMBERS,
DriveNumber,
JSONDisk,
ENCODING_NIBBLE,
PROCESS_BINARY,
PROCESS_JSON_DISK,
PROCESS_JSON,
ENCODING_BITSTREAM,
MassStorage,
MassStorageData,
DiskMetadata,
SupportedSectors,
FloppyDisk,
FloppyFormat,
WozDisk,
NibbleDisk,
isNibbleDisk,
isWozDisk,
NoFloppyDisk,
isNoFloppyDisk,
NO_DISK,
} from '../formats/types';
import {
@ -212,66 +216,26 @@ export interface Callbacks {
}
/** Common information for Nibble and WOZ disks. */
interface BaseDrive {
/** Current disk format. */
format: NibbleFormat;
/** Current disk volume number. */
volume: byte;
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 write protect is on. */
readOnly: boolean;
/** Whether the drive has been written to since it was loaded. */
dirty: boolean;
/** Metadata about the disk image */
metadata: DiskMetadata;
}
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
interface WozDrive extends BaseDrive {
/** Woz encoding */
encoding: typeof ENCODING_BITSTREAM;
/** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */
trackMap: byte[];
/** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */
rawTracks: Uint8Array[];
}
/** Nibble format track data. */
interface NibbleDrive extends BaseDrive {
/** Nibble encoding */
encoding: typeof ENCODING_NIBBLE;
/** Nibble data. The index is the track number. */
tracks: memory[];
}
type Drive = WozDrive | NibbleDrive;
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
return drive.encoding === ENCODING_NIBBLE;
}
function isWozDrive(drive: Drive): drive is WozDrive {
return drive.encoding === ENCODING_BITSTREAM;
}
interface DriveState {
format: NibbleFormat;
encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE;
volume: byte;
tracks: memory[];
disk: FloppyDisk;
readOnly: boolean;
track: byte;
head: byte;
phase: Phase;
readOnly: boolean;
dirty: boolean;
trackMap: number[];
rawTracks: Uint8Array[];
metadata: DiskMetadata;
}
/** State of the controller for saving/restoring. */
@ -282,73 +246,54 @@ interface State {
controllerState: ControllerState;
}
function getDriveState(drive: Drive): DriveState {
const result: DriveState = {
format: drive.format,
encoding: drive.encoding,
volume: drive.volume,
tracks: [],
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty,
trackMap: [],
rawTracks: [],
metadata: { ...drive.metadata },
};
if (isNibbleDrive(drive)) {
for (let idx = 0; idx < drive.tracks.length; idx++) {
result.tracks.push(new Uint8Array(drive.tracks[idx]));
}
function getDiskState(disk: NoFloppyDisk): NoFloppyDisk;
function getDiskState(disk: NibbleDisk): NibbleDisk;
function getDiskState(disk: WozDisk): WozDisk;
function getDiskState(disk: FloppyDisk): FloppyDisk;
function getDiskState(disk: FloppyDisk): FloppyDisk {
if (isNoFloppyDisk(disk)) {
const { encoding, metadata, readOnly } = disk;
return {
encoding,
metadata: {...metadata},
readOnly,
};
}
if (isWozDrive(drive)) {
result.trackMap = [...drive.trackMap];
for (let idx = 0; idx < drive.rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(drive.rawTracks[idx]));
}
}
return result;
}
function setDriveState(state: DriveState) {
let result: Drive;
if (state.encoding === ENCODING_NIBBLE) {
result = {
format: state.format,
encoding: ENCODING_NIBBLE,
volume: state.volume,
if (isNibbleDisk(disk)) {
const { format, encoding, metadata, readOnly, volume, tracks } = disk;
const result: NibbleDisk = {
format,
encoding,
volume,
tracks: [],
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
dirty: state.dirty,
metadata: { ...state.metadata },
readOnly,
metadata: { ...metadata },
};
for (let idx = 0; idx < state.tracks.length; idx++) {
result.tracks.push(new Uint8Array(state.tracks[idx]));
}
} else {
result = {
format: state.format,
encoding: ENCODING_BITSTREAM,
volume: state.volume,
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
dirty: state.dirty,
trackMap: [...state.trackMap],
rawTracks: [],
metadata: { ...state.metadata },
};
for (let idx = 0; idx < state.rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(state.rawTracks[idx]));
for (let idx = 0; idx < tracks.length; idx++) {
result.tracks.push(new Uint8Array(tracks[idx]));
}
return result;
}
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');
}
/**
@ -358,27 +303,30 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
metadata: { name: 'Disk 1' },
},
2: { // Drive 2
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
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' },
}
};
@ -393,8 +341,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private skip = 0;
/** Drive off timeout id or null. */
private offTimeout: number | null = null;
/** Current drive object. */
private cur: Drive;
/** Current drive object. Must only be set by `updateActiveDrive()`. */
private curDrive: Drive;
/** Current disk object. Must only be set by `updateActiveDrive()`. */
private curDisk: FloppyDisk;
/** Nibbles read this on cycle */
private nibbleCount = 0;
@ -432,17 +382,23 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
state: 2,
};
this.cur = this.drives[this.state.drive];
this.updateActiveDrive();
this.initWorker();
}
/** Updates the active drive based on the controller state. */
private updateActiveDrive() {
this.curDrive = this.drives[this.state.drive];
this.curDisk = this.disks[this.state.drive];
}
private debug(..._args: unknown[]) {
// debug(..._args);
}
public head(): number {
return this.cur.head;
return this.curDrive.head;
}
/**
@ -487,18 +443,18 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
let workCycles = (cycles - this.lastCycles) * 2;
this.lastCycles = cycles;
if (!isWozDrive(this.cur)) {
if (!isWozDisk(this.curDisk)) {
return;
}
const track =
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0];
const state = this.state;
while (workCycles-- > 0) {
let pulse: number = 0;
if (state.clock === 4) {
pulse = track[this.cur.head];
pulse = track[this.curDrive.head];
if (!pulse) {
// More than 2 zeros can not be read reliably.
if (++this.zeros > 2) {
@ -531,7 +487,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
break;
case 0xA: // SR
state.latch >>= 1;
if (this.cur.readOnly) {
if (this.curDrive.readOnly) {
state.latch |= 0x80;
}
break;
@ -550,12 +506,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.clock === 4) {
if (state.on) {
if (state.q7) {
track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00;
track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00;
this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00);
}
if (++this.cur.head >= track.length) {
this.cur.head = 0;
if (++this.curDrive.head >= track.length) {
this.curDrive.head = 0;
}
}
}
@ -568,29 +524,29 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
// Only called for non-WOZ disks
private readWriteNext() {
if (!isNibbleDrive(this.cur)) {
if (!isNibbleDisk(this.curDisk)) {
return;
}
const state = this.state;
if (state.on && (this.skip || state.q7)) {
const track = this.cur.tracks[this.cur.track >> 2];
const track = this.curDisk.tracks[this.curDrive.track >> 2];
if (track && track.length) {
if (this.cur.head >= track.length) {
this.cur.head = 0;
if (this.curDrive.head >= track.length) {
this.curDrive.head = 0;
}
if (state.q7) {
if (!this.cur.readOnly) {
track[this.cur.head] = state.bus;
if (!this.cur.dirty) {
if (!this.curDrive.readOnly) {
track[this.curDrive.head] = state.bus;
if (!this.curDrive.dirty) {
this.updateDirty(state.drive, true);
}
}
} else {
state.latch = track[this.cur.head];
state.latch = track[this.curDrive.head];
}
++this.cur.head;
++this.curDrive.head;
}
} else {
state.latch = 0;
@ -619,22 +575,24 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.debug(`phase ${phase}${on ? ' on' : ' off'}`);
if (on) {
this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2;
this.cur.phase = phase;
this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2;
this.curDrive.phase = phase;
}
// The emulator clamps the track to the valid track range available
// in the image, but real Disk II drives can seek past track 34 by
// at least a half track, usually a full track. Some 3rd party
// drives can seek to track 39.
const maxTrack = isNibbleDrive(this.cur)
? this.cur.tracks.length * 4 - 1
: this.cur.trackMap.length - 1;
if (this.cur.track > maxTrack) {
this.cur.track = maxTrack;
const maxTrack = isNibbleDisk(this.curDisk)
? this.curDisk.tracks.length * 4 - 1
: (isWozDisk(this.curDisk)
? this.curDisk.trackMap.length - 1
: 0);
if (this.curDrive.track > maxTrack) {
this.curDrive.track = maxTrack;
}
if (this.cur.track < 0x0) {
this.cur.track = 0x0;
if (this.curDrive.track < 0x0) {
this.curDrive.track = 0x0;
}
// debug(
@ -706,7 +664,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.DRIVE1: // 0x0a
this.debug('Disk 1');
state.drive = 1;
this.cur = this.drives[state.drive];
this.updateActiveDrive();
if (state.on) {
this.callbacks.driveLight(2, false);
this.callbacks.driveLight(1, true);
@ -715,7 +673,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.DRIVE2: // 0x0b
this.debug('Disk 2');
state.drive = 2;
this.cur = this.drives[state.drive];
this.updateActiveDrive();
if (state.on) {
this.callbacks.driveLight(1, false);
this.callbacks.driveLight(2, true);
@ -727,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.q7) {
this.debug('clearing _q6/SHIFT');
}
if (isNibbleDrive(this.cur)) {
if (isNibbleDisk(this.curDisk)) {
this.readWriteNext();
}
break;
@ -737,9 +695,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.q7) {
this.debug('setting _q6/LOAD');
}
if (isNibbleDrive(this.cur)) {
if (isNibbleDisk(this.curDisk)) {
if (readMode && !state.q7) {
if (this.cur.readOnly) {
if (this.curDrive.readOnly) {
state.latch = 0xff;
this.debug('Setting readOnly');
} else {
@ -812,60 +770,85 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
state.q7 = false;
state.on = false;
state.drive = 1;
this.cur = this.drives[state.drive];
}
this.updateActiveDrive();
}
tick() {
this.moveHead();
}
private getDriveState(drive: DriveNumber): DriveState {
const curDrive = this.drives[drive];
const curDisk = this.disks[drive];
const { readOnly, track, head, phase, dirty } = curDrive;
return {
disk: getDiskState(curDisk),
readOnly,
track,
head,
phase,
dirty,
};
}
getState(): State {
const result = {
drives: [] as DriveState[],
skip: this.skip,
controllerState: { ...this.state },
};
result.drives[1] = getDriveState(this.drives[1]);
result.drives[2] = getDriveState(this.drives[2]);
result.drives[1] = this.getDriveState(1);
result.drives[2] = this.getDriveState(2);
return result;
}
private setDriveState(drive: DriveNumber, state: DriveState) {
const { track, head, phase, readOnly, dirty } = state;
this.drives[drive] = {
track,
head,
phase,
readOnly,
dirty,
};
this.disks[drive] = getDiskState(state.disk);
}
setState(state: State) {
this.skip = state.skip;
this.state = { ...state.controllerState };
for (const d of DRIVE_NUMBERS) {
this.drives[d] = setDriveState(state.drives[d]);
const { name, side } = state.drives[d].metadata;
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.cur = this.drives[this.state.drive];
this.updateActiveDrive();
}
getMetadata(driveNo: DriveNumber) {
const drive = this.drives[driveNo];
const { track, head, phase, readOnly, dirty } = this.drives[driveNo];
return {
format: drive.format,
volume: drive.volume,
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty
track,
head,
phase,
readOnly,
dirty,
};
}
// TODO(flan): Does not work on WOZ disks
rwts(disk: DriveNumber, track: byte, sector: byte) {
const cur = this.drives[disk];
if (!isNibbleDrive(cur)) {
rwts(drive: DriveNumber, track: byte, sector: byte) {
const curDisk = this.disks[drive];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks');
}
return readSector(cur, track, sector);
return readSector(curDisk, track, sector);
}
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
@ -892,11 +875,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
getJSON(drive: DriveNumber, pretty: boolean = false) {
const cur = this.drives[drive];
if (!isNibbleDrive(cur)) {
const curDisk = this.disks[drive];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t save WOZ disks to JSON');
}
return jsonEncode(cur, pretty);
return jsonEncode(curDisk, pretty);
}
setJSON(drive: DriveNumber, json: string) {
@ -916,7 +899,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return true;
}
setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) {
setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
const readOnly = false;
const volume = 254;
const options = {
@ -975,27 +958,28 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
const cur = this.drives[drive];
Object.assign(cur, disk);
const { name, side } = cur.metadata;
this.disks[drive] = disk;
this.drives[drive].head = 0;
this.updateActiveDrive();
const { name, side } = disk.metadata;
this.updateDirty(drive, true);
this.callbacks.label(drive, name, side);
}
// TODO(flan): Does not work with WOZ or D13 disks
getBinary(drive: DriveNumber, ext?: NibbleFormat): MassStorageData | null {
const cur = this.drives[drive];
if (!isNibbleDrive(cur)) {
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz' | 'd13'>): MassStorageData | null {
const curDisk = this.disks[drive];
if (!isNibbleDisk(curDisk)) {
return null;
}
const { format, readOnly, tracks, volume } = cur;
const { name } = cur.metadata;
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;
const extension = ext ?? format;
let idx = 0;
for (let t = 0; t < tracks.length; t++) {
if (ext === 'nib') {
@ -1003,7 +987,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
idx += tracks[t].length;
} else {
for (let s = 0; s < 0x10; s++) {
const sector = readSector({ ...cur, format: ext }, t, s);
const sector = readSector({ ...curDisk, format: extension }, t, s);
data.set(sector, idx);
idx += sector.length;
}
@ -1011,7 +995,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
return {
ext,
ext: extension,
metadata: { name },
data: data.buffer,
readOnly,
@ -1021,18 +1005,18 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
// TODO(flan): Does not work with WOZ or D13 disks
getBase64(drive: DriveNumber) {
const cur = this.drives[drive];
if (!isNibbleDrive(cur)) {
const curDisk = this.disks[drive];
if (!isNibbleDisk(curDisk)) {
return null;
}
const data: string[][] | string[] = [];
for (let t = 0; t < cur.tracks.length; t++) {
if (cur.format === 'nib') {
data[t] = base64_encode(cur.tracks[t]);
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(cur, t, s));
track[s] = base64_encode(readSector(curDisk, t, s));
}
data[t] = track;
}

View File

@ -1,7 +1,7 @@
import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData } from '../formats/types';
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types';
import CPU6502, { CpuState, flags } from '../cpu6502';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import createBlockDisk from '../formats/block';
@ -129,7 +129,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
private disks: BlockDisk[] = [];
private busy: boolean[] = [];
private busyTimeout: ReturnType<typeof setTimeout>[] = [];
private ext: string[] = [];
private ext: DiskFormat[] = [];
private metadata: Array<HeaderData | null> = [];
constructor(
@ -522,6 +522,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
@ -539,6 +540,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
@ -547,7 +549,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
);
}
setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) {
setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
let volume = 254;
let readOnly = false;
if (fmt === '2mg') {
@ -568,7 +570,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
};
this.ext[drive] = fmt;
this.disks[drive] = createBlockDisk(options);
this.disks[drive] = createBlockDisk(fmt, options);
this.callbacks?.label(drive, name);
return true;

View File

@ -1,4 +1,4 @@
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, MassStorage, NIBBLE_FORMATS } from 'js/formats/types';
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types';
import { h, JSX, RefObject } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import { loadLocalFile } from './util/files';
@ -7,7 +7,7 @@ import { spawn } from './util/promises';
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
storage: MassStorage<T> | undefined;
drive?: DriveNumber;
formats: typeof NIBBLE_FORMATS
formats: typeof FLOPPY_FORMATS
| typeof BLOCK_FORMATS
| typeof DISK_FORMATS;
dropRef?: RefObject<HTMLElement>;

View File

@ -7,7 +7,7 @@ import { FileModal } from './FileModal';
import styles from './css/DiskII.module.css';
import { DiskDragTarget } from './DiskDragTarget';
import { NIBBLE_FORMATS } from 'js/formats/types';
import { FLOPPY_FORMATS } from 'js/formats/types';
import { DownloadModal } from './DownloadModal';
/**
@ -66,7 +66,7 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
className={styles.disk}
storage={disk2}
drive={number}
formats={NIBBLE_FORMATS}
formats={FLOPPY_FORMATS}
onError={setError}
>
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} />

View File

@ -1,6 +1,6 @@
import { h, Fragment, JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { DiskDescriptor, DriveNumber, NibbleFormat, NIBBLE_FORMATS } from '../formats/types';
import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
import DiskII from '../cards/disk2';
@ -15,7 +15,7 @@ import styles from './css/FileModal.module.css';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
accept: { 'application/octet-stream': NIBBLE_FORMATS.map(x => '.' + x) },
accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) },
}
];

View File

@ -2,7 +2,7 @@ import { h, Fragment } from 'preact';
import { useMemo } from 'preact/hooks';
import cs from 'classnames';
import { Apple2 as Apple2Impl } from 'js/apple2';
import { BlockDisk, DiskFormat, DriveNumber, MassStorage, NibbleDisk } from 'js/formats/types';
import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types';
import { slot } from 'js/apple2io';
import DiskII from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
@ -38,7 +38,7 @@ const formatDate = (date: Date) => {
* @param disk NibbleDisk or BlockDisk
* @returns true if is BlockDisk
*/
function isBlockDisk(disk: NibbleDisk | BlockDisk): disk is BlockDisk {
function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk {
return !!((disk as BlockDisk).blocks);
}
@ -256,7 +256,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
if (massStorageData) {
const { data, readOnly, ext } = massStorageData;
const { name } = massStorageData.metadata;
let disk: BlockDisk | NibbleDisk | null = null;
let disk: BlockDisk | FloppyDisk | null = null;
if (ext === '2mg') {
disk = createDiskFrom2MG({
name,
@ -277,8 +277,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
}
}
}
if (!disk) {
disk = createBlockDisk({
if (!disk && isBlockDiskFormat(ext)) {
disk = createBlockDisk(ext, {
name,
rawData: data,
readOnly,
@ -330,7 +330,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
</div>
);
}
} else {
} else if (isNibbleDisk(disk)) {
const dos = new DOS33(disk);
return (
<div className={styles.volume}>

View File

@ -1,18 +1,18 @@
import { includes, word } from 'js/types';
import { initGamepad } from 'js/ui/gamepad';
import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
import {
BlockFormat,
BLOCK_FORMATS,
DISK_FORMATS,
DriveNumber,
FloppyFormat,
FLOPPY_FORMATS,
JSONDisk,
MassStorage,
NibbleFormat,
NIBBLE_FORMATS,
} from 'js/formats/types';
import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
import { includes, word } from 'js/types';
import { initGamepad } from 'js/ui/gamepad';
type ProgressCallback = (current: number, total: number) => void;
@ -46,8 +46,8 @@ export const getNameAndExtension = (url: string) => {
};
export const loadLocalFile = (
storage: MassStorage<NibbleFormat|BlockFormat>,
formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
storage: MassStorage<FloppyFormat|BlockFormat>,
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
number: DriveNumber,
file: File,
) => {
@ -94,7 +94,7 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi
* @returns true if successful
*/
export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => {
return loadLocalFile(disk2, NIBBLE_FORMATS, number, file);
return loadLocalFile(disk2, FLOPPY_FORMATS, number, file);
};
/**
@ -117,7 +117,7 @@ export const loadJSON = async (
throw new Error(`Error loading: ${response.statusText}`);
}
const data = await response.json() as JSONDisk;
if (!includes(NIBBLE_FORMATS, data.type)) {
if (!includes(FLOPPY_FORMATS, data.type)) {
throw new Error(`Type "${data.type}" not recognized.`);
}
disk2.setDisk(number, data);
@ -209,7 +209,7 @@ export const loadHttpNibbleFile = async (
return loadJSON(disk2, number, url);
}
const { name, ext } = getNameAndExtension(url);
if (!includes(NIBBLE_FORMATS, ext)) {
if (!includes(FLOPPY_FORMATS, ext)) {
throw new Error(`Extension "${ext}" not recognized.`);
}
const data = await loadHttpFile(url, signal, onProgress);
@ -241,7 +241,7 @@ export class SmartStorageBroker implements MassStorage<unknown> {
} else {
throw new Error(`Unable to load "${name}"`);
}
} else if (includes(NIBBLE_FORMATS, ext)) {
} else if (includes(FLOPPY_FORMATS, ext)) {
this.disk2.setBinary(drive, name, ext, data);
} else {
throw new Error(`Unable to load "${name}"`);

View File

@ -1,10 +1,10 @@
import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types';
import { DiskOptions, BlockDisk, ENCODING_BLOCK, BlockFormat } from './types';
/**
* Returns a `Disk` object for a block volume with block-ordered data.
* @param options the disk image and options
*/
export default function createBlockDisk(options: DiskOptions): BlockDisk {
export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions): BlockDisk {
const { rawData, readOnly, name } = options;
if (!rawData) {
@ -20,6 +20,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk {
const disk: BlockDisk = {
encoding: ENCODING_BLOCK,
format: fmt,
blocks,
metadata: { name },
readOnly,

View File

@ -1,6 +1,6 @@
import { includes, memory } from '../types';
import { base64_decode } from '../base64';
import { DiskOptions, FloppyDisk, JSONDisk, NibbleFormat, NIBBLE_FORMATS } from './types';
import { BitstreamFormat, DiskOptions, FloppyDisk, FloppyFormat, JSONDisk, NibbleDisk, NibbleFormat, NIBBLE_FORMATS, WozDisk } from './types';
import createDiskFrom2MG from './2mg';
import createDiskFromD13 from './d13';
import createDiskFromDOS from './do';
@ -8,13 +8,13 @@ import createDiskFromProDOS from './po';
import createDiskFromWoz from './woz';
import createDiskFromNibble from './nib';
/**
*
* @param fmt Type of
* @param options
* @returns A nibblized disk
*/
export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk | null {
/** Creates a `NibbleDisk` from the given format and options. */
export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null;
/** Creates a `WozDisk` from the given format and options. */
export function createDisk(fmt: BitstreamFormat, options: DiskOptions): WozDisk | null;
/** Creates a `FloppyDisk` (either a `NibbleDisk` or a `WozDisk`) from the given format and options. */
export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null;
export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null {
let disk: FloppyDisk | null = null;
switch (fmt) {
@ -42,7 +42,8 @@ export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk
return disk;
}
export function createDiskFromJsonDisk(disk: JSONDisk): FloppyDisk | null {
/** Creates a NibbleDisk from JSON */
export function createDiskFromJsonDisk(disk: JSONDisk): NibbleDisk | null {
const fmt = disk.type;
const readOnly = disk.readOnly;
const name = disk.name;

View File

@ -1,7 +1,7 @@
import { bit, byte, memory } from '../types';
import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util';
import { NibbleDisk, ENCODING_NIBBLE, JSONDisk } from './types';
import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat } from './types';
/**
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
@ -550,6 +550,9 @@ export function jsonDecode(data: string): NibbleDisk {
}
tracks[t] = bytify(track);
}
if (!isNibbleDiskFormat(json.type)) {
throw new Error(`JSON disks of type ${json.type} are not supported`);
}
const disk: NibbleDisk = {
volume: v,
format: json.type,

View File

@ -64,22 +64,29 @@ export interface Disk {
readOnly: boolean;
}
export const NO_DISK = 'empty';
export const ENCODING_NIBBLE = 'nibble';
export const ENCODING_BITSTREAM = 'bitstream';
export const ENCODING_BLOCK = 'block';
export interface FloppyDisk extends Disk {
tracks: memory[];
encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM | typeof NO_DISK;
}
export interface NoFloppyDisk extends FloppyDisk {
encoding: typeof NO_DISK;
}
export interface NibbleDisk extends FloppyDisk {
encoding: typeof ENCODING_NIBBLE;
format: DiskFormat;
format: Exclude<NibbleFormat, 'woz'>;
volume: byte;
tracks: memory[];
}
export interface WozDisk extends FloppyDisk {
encoding: typeof ENCODING_BITSTREAM;
format: 'woz';
trackMap: number[];
rawTracks: Uint8Array[];
info: InfoChunk | undefined;
@ -87,14 +94,13 @@ export interface WozDisk extends FloppyDisk {
export interface BlockDisk extends Disk {
encoding: typeof ENCODING_BLOCK;
format: BlockFormat;
blocks: Uint8Array[];
}
/**
* File types supported by the disk format processors and
* block devices.
* File types supported by floppy devices in nibble mode.
*/
export const NIBBLE_FORMATS = [
'2mg',
'd13',
@ -102,21 +108,70 @@ export const NIBBLE_FORMATS = [
'dsk',
'po',
'nib',
'woz'
] as const;
/**
* File types supported by floppy devices in bitstream mode.
*/
export const BITSTREAM_FORMATS = [
'woz',
] as const;
/**
* All file types supported by floppy devices.
*/
export const FLOPPY_FORMATS = [
...NIBBLE_FORMATS,
...BITSTREAM_FORMATS,
] as const;
/**
* File types supported by block devices.
*/
export const BLOCK_FORMATS = [
'2mg',
'hdv',
'po',
] as const;
export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS] as const;
/**
* All supported disk formats.
*/
export const DISK_FORMATS = [
...FLOPPY_FORMATS,
...BLOCK_FORMATS,
] as const;
export type FloppyFormat = MemberOf<typeof FLOPPY_FORMATS>;
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
export type BitstreamFormat = 'woz';
export type BlockFormat = MemberOf<typeof BLOCK_FORMATS>;
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
/** Type guard for nibble disk formats. */
export function isNibbleDiskFormat(f: DiskFormat): f is NibbleFormat {
return f in NIBBLE_FORMATS;
}
/** Type guard for block disk formats. */
export function isBlockDiskFormat(f: DiskFormat): f is BlockFormat {
return f in BLOCK_FORMATS;
}
export function isNoFloppyDisk(disk: Disk): disk is NoFloppyDisk {
return (disk as NoFloppyDisk)?.encoding === NO_DISK;
}
/** Type guard for NibbleDisks */
export function isNibbleDisk(disk: Disk): disk is NibbleDisk {
return (disk as NibbleDisk)?.encoding === ENCODING_NIBBLE;
}
/** Type guard for NibbleDisks */
export function isWozDisk(disk: Disk): disk is WozDisk {
return (disk as WozDisk)?.encoding === ENCODING_BITSTREAM;
}
/**
* Base format for JSON defined disks
*/
@ -180,7 +235,7 @@ export interface ProcessBinaryMessage {
type: typeof PROCESS_BINARY;
payload: {
drive: DriveNumber;
fmt: NibbleFormat;
fmt: FloppyFormat;
options: DiskOptions;
};
}
@ -227,7 +282,7 @@ export type FormatWorkerResponse =
export interface MassStorageData {
metadata: DiskMetadata;
ext: string;
ext: DiskFormat;
readOnly: boolean;
volume?: byte;
data: ArrayBuffer;

View File

@ -293,8 +293,8 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk {
const disk: WozDisk = {
encoding: ENCODING_BITSTREAM,
format: 'woz',
trackMap: tmap?.trackMap || [],
tracks: trks?.tracks || [],
rawTracks: trks?.rawTracks || [],
readOnly: true, //chunks.info.writeProtected === 1;
metadata: {

View File

@ -11,10 +11,10 @@ import {
DriveNumber,
DRIVE_NUMBERS,
MassStorage,
NIBBLE_FORMATS,
JSONBinaryImage,
JSONDisk,
BlockFormat
BlockFormat,
FLOPPY_FORMATS
} from '../formats/types';
import { initGamepad } from './gamepad';
import KeyBoard from './keyboard';
@ -392,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
}
} else {
if (
includes(NIBBLE_FORMATS, ext) &&
includes(FLOPPY_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, result)
) {
initGamepad();
@ -459,7 +459,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
}
} else {
if (
includes(NIBBLE_FORMATS, ext) &&
includes(FLOPPY_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, data)
) {
initGamepad();

View File

@ -4,7 +4,7 @@ import fs from 'fs';
import Apple2IO from 'js/apple2io';
import DiskII, { Callbacks } from 'js/cards/disk2';
import CPU6502 from 'js/cpu6502';
import { DriveNumber } from 'js/formats/types';
import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types';
import { byte } from 'js/types';
import { toHex } from 'js/util';
import { VideoModes } from 'js/videomodes';
@ -71,7 +71,8 @@ describe('DiskII', () => {
state.controllerState.latch = 0x42;
state.controllerState.on = true;
state.controllerState.q7 = true;
state.drives[2].tracks[14][12] = 0x80;
const disk2 = state.drives[2].disk as NibbleDisk;
disk2.tracks[14][12] = 0x80;
state.drives[2].head = 1000;
state.drives[2].phase = 3;
diskII.setState(state);
@ -478,21 +479,24 @@ describe('DiskII', () => {
it('writes a nibble to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
let track0 = diskII.getState().drives[1].tracks[0];
let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
let track0 = disk1.tracks[0];
expect(track0[0]).toBe(0xFF);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8F, 0x80); // write
diskII.ioSwitch(0x8C); // shift
track0 = diskII.getState().drives[1].tracks[0];
disk1 = diskII.getState().drives[1].disk as NibbleDisk;
track0 = disk1.tracks[0];
expect(track0[0]).toBe(0x80);
});
it('writes two nibbles to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
let track0 = diskII.getState().drives[1].tracks[0];
let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
let track0 = disk1.tracks[0];
expect(track0[0]).toBe(0xFF);
diskII.ioSwitch(0x89); // turn on the motor
@ -501,7 +505,8 @@ describe('DiskII', () => {
diskII.ioSwitch(0x8F, 0x81); // write
diskII.ioSwitch(0x8C); // shift
track0 = diskII.getState().drives[1].tracks[0];
disk1 = diskII.getState().drives[1].disk as NibbleDisk;
track0 = disk1.tracks[0];
expect(track0[0]).toBe(0x80);
expect(track0[1]).toBe(0x81);
});
@ -819,10 +824,10 @@ class TestDiskReader {
rawTracks() {
// NOTE(flan): Hack to access private properties.
const disk = this.diskII as unknown as { cur: { rawTracks: Uint8Array[] } };
const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk;
const result: Uint8Array[] = [];
for (let i = 0; i < disk.cur.rawTracks.length; i++) {
result[i] = disk.cur.rawTracks[i].slice(0);
for (let i = 0; i < disk.rawTracks.length; i++) {
result[i] = disk.rawTracks[i].slice(0);
}
return result;

View File

@ -193,6 +193,7 @@ describe('2mg format', () => {
metadata: { name: 'Good disk' },
readOnly: false,
encoding: ENCODING_BLOCK,
format: 'hdv',
};
const image = create2MGFromBlockDisk(header, disk);
expect(VALID_PRODOS_IMAGE.buffer).toEqual(image);

View File

@ -21,16 +21,16 @@ describe('woz', () => {
const disk = createDiskFromWoz(options);
expect(disk).toEqual({
metadata: { name: 'Mock Woz 1' },
metadata: { name: 'Mock Woz 1', side: undefined },
readOnly: true,
encoding: ENCODING_BITSTREAM,
format: 'woz',
trackMap: mockTMAP,
rawTracks: [new Uint8Array([
1, 1, 0, 1, 0, 1, 0, 1,
1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0,
])],
tracks: [new Uint8Array([0xD5, 0xAA, 0x96])],
info: {
bitTiming: 0,
bootSector: 0,
@ -64,13 +64,13 @@ describe('woz', () => {
},
readOnly: true,
encoding: ENCODING_BITSTREAM,
format: 'woz',
trackMap: mockTMAP,
rawTracks: [new Uint8Array([
1, 1, 0, 1, 0, 1, 0, 1,
1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0,
])],
tracks: [new Uint8Array([0xD5, 0xAA, 0x96])],
info: {
bitTiming: 0,
bootSector: 0,