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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { h, Fragment, JSX } from 'preact'; import { h, Fragment, JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks'; 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 { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files'; import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
import DiskII from '../cards/disk2'; import DiskII from '../cards/disk2';
@ -15,7 +15,7 @@ import styles from './css/FileModal.module.css';
const DISK_TYPES: FilePickerAcceptType[] = [ const DISK_TYPES: FilePickerAcceptType[] = [
{ {
description: 'Disk Images', 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 { useMemo } from 'preact/hooks';
import cs from 'classnames'; import cs from 'classnames';
import { Apple2 as Apple2Impl } from 'js/apple2'; 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 { slot } from 'js/apple2io';
import DiskII from 'js/cards/disk2'; import DiskII from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport'; import SmartPort from 'js/cards/smartport';
@ -38,7 +38,7 @@ const formatDate = (date: Date) => {
* @param disk NibbleDisk or BlockDisk * @param disk NibbleDisk or BlockDisk
* @returns true if is 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); return !!((disk as BlockDisk).blocks);
} }
@ -256,7 +256,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
if (massStorageData) { if (massStorageData) {
const { data, readOnly, ext } = massStorageData; const { data, readOnly, ext } = massStorageData;
const { name } = massStorageData.metadata; const { name } = massStorageData.metadata;
let disk: BlockDisk | NibbleDisk | null = null; let disk: BlockDisk | FloppyDisk | null = null;
if (ext === '2mg') { if (ext === '2mg') {
disk = createDiskFrom2MG({ disk = createDiskFrom2MG({
name, name,
@ -277,8 +277,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
} }
} }
} }
if (!disk) { if (!disk && isBlockDiskFormat(ext)) {
disk = createBlockDisk({ disk = createBlockDisk(ext, {
name, name,
rawData: data, rawData: data,
readOnly, readOnly,
@ -330,7 +330,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
</div> </div>
); );
} }
} else { } else if (isNibbleDisk(disk)) {
const dos = new DOS33(disk); const dos = new DOS33(disk);
return ( return (
<div className={styles.volume}> <div className={styles.volume}>

View File

@ -1,18 +1,18 @@
import { includes, word } from 'js/types'; import Disk2 from 'js/cards/disk2';
import { initGamepad } from 'js/ui/gamepad'; import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
import { import {
BlockFormat, BlockFormat,
BLOCK_FORMATS, BLOCK_FORMATS,
DISK_FORMATS, DISK_FORMATS,
DriveNumber, DriveNumber,
FloppyFormat,
FLOPPY_FORMATS,
JSONDisk, JSONDisk,
MassStorage, MassStorage,
NibbleFormat,
NIBBLE_FORMATS,
} from 'js/formats/types'; } from 'js/formats/types';
import Disk2 from 'js/cards/disk2'; import { includes, word } from 'js/types';
import SmartPort from 'js/cards/smartport'; import { initGamepad } from 'js/ui/gamepad';
import Debugger from 'js/debugger';
type ProgressCallback = (current: number, total: number) => void; type ProgressCallback = (current: number, total: number) => void;
@ -46,8 +46,8 @@ export const getNameAndExtension = (url: string) => {
}; };
export const loadLocalFile = ( export const loadLocalFile = (
storage: MassStorage<NibbleFormat|BlockFormat>, storage: MassStorage<FloppyFormat|BlockFormat>,
formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
number: DriveNumber, number: DriveNumber,
file: File, file: File,
) => { ) => {
@ -94,7 +94,7 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi
* @returns true if successful * @returns true if successful
*/ */
export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { 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}`); throw new Error(`Error loading: ${response.statusText}`);
} }
const data = await response.json() as JSONDisk; 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.`); throw new Error(`Type "${data.type}" not recognized.`);
} }
disk2.setDisk(number, data); disk2.setDisk(number, data);
@ -209,7 +209,7 @@ export const loadHttpNibbleFile = async (
return loadJSON(disk2, number, url); return loadJSON(disk2, number, url);
} }
const { name, ext } = getNameAndExtension(url); const { name, ext } = getNameAndExtension(url);
if (!includes(NIBBLE_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);
@ -241,7 +241,7 @@ export class SmartStorageBroker implements MassStorage<unknown> {
} else { } else {
throw new Error(`Unable to load "${name}"`); 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); this.disk2.setBinary(drive, name, ext, data);
} else { } else {
throw new Error(`Unable to load "${name}"`); 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. * Returns a `Disk` object for a block volume with block-ordered data.
* @param options the disk image and options * @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; const { rawData, readOnly, name } = options;
if (!rawData) { if (!rawData) {
@ -20,6 +20,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk {
const disk: BlockDisk = { const disk: BlockDisk = {
encoding: ENCODING_BLOCK, encoding: ENCODING_BLOCK,
format: fmt,
blocks, blocks,
metadata: { name }, metadata: { name },
readOnly, readOnly,

View File

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

View File

@ -1,7 +1,7 @@
import { bit, byte, memory } from '../types'; import { bit, byte, memory } from '../types';
import { base64_decode, base64_encode } from '../base64'; import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util'; 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). * 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); tracks[t] = bytify(track);
} }
if (!isNibbleDiskFormat(json.type)) {
throw new Error(`JSON disks of type ${json.type} are not supported`);
}
const disk: NibbleDisk = { const disk: NibbleDisk = {
volume: v, volume: v,
format: json.type, format: json.type,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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