apple2js/test/js/cards/disk2.spec.ts

836 lines
33 KiB
TypeScript
Raw Normal View History

/** @jest-environment jsdom */
import fs from 'fs';
import Apple2IO from 'js/apple2io';
import DiskII, { Callbacks } from 'js/cards/disk2';
import CPU6502 from 'js/cpu6502';
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.)
2022-09-17 13:41:35 +00:00
import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types';
import { byte } from 'js/types';
import { toHex } from 'js/util';
import { VideoModes } from 'js/videomodes';
import { mocked } from 'ts-jest/utils';
import { BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK_IMAGE } from '../formats/testdata/16sector';
jest.mock('js/apple2io');
jest.mock('js/videomodes');
type Phase = 0 | 1 | 2 | 3; // not exported from DiskII
const STEPS_PER_TRACK = 4;
const PHASES_PER_TRACK = 2;
function setTrack(diskII: DiskII, track: number) {
const initialState = diskII.getState();
initialState.drives[1].track = track * STEPS_PER_TRACK;
initialState.drives[1].phase = (track * PHASES_PER_TRACK) % 4 as Phase;
diskII.setState(initialState);
}
function setWriteProtected(diskII: DiskII, isWriteProtected: boolean) {
const initialState = diskII.getState();
initialState.drives[1].readOnly = isWriteProtected;
diskII.setState(initialState);
}
describe('DiskII', () => {
const mockApple2IO = new Apple2IO({} as unknown as CPU6502, {} as unknown as VideoModes);
const callbacks: Callbacks = {
driveLight: jest.fn(),
dirty: jest.fn(),
label: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
});
it('is constructable', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
expect(diskII).not.toBeNull();
});
it('round-trips the state when there are no changes', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
const state = diskII.getState();
diskII.setState(state);
expect(diskII.getState()).toEqual(state);
});
it('round-trips the state when there are changes', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.setBinary(2, 'BYTES_BY_SECTOR', 'po', BYTES_BY_SECTOR_IMAGE);
const state = diskII.getState();
// These are just arbitrary changes, not an exhaustive list of fields.
state.skip = 1;
state.controllerState.drive = 2;
Floppy controller refactorings 1 (#155) * Add `DiskMetada` to the `Disk` interface Before, metadata about the image, such as name, side, etc. was mixed in with actual disk image information. This change breaks that information into a separate structure called `DiskMetadata`. Currently, the only two fields are `name` and `side`, but the idea is that more fields could be added as necessary, like a description, a scan of the disk or label, etc. In a follow-on change, the default write-protection status will come from the metadata as well. The current implementation copies the metadata when saving/restoring state, loading disk images, etc. In the future, the metadata should passed around until the format is required to change (like saving one disk image format as another). Likewise, in the future, in may be desirable to be able to override the disk image metadata with user-supplied metadata. This could be use, for example, to temporarily add or remove write-protection from a disk image. All existing tests pass and the emulator builds with no errors. * Rename `writeMode` to `q7` Before, nibble disk emulation used the `writeMode` field to keep track of whether the drive should be read from or written to, but the WOZ emulation used `q7` to keep track of the same state. This change renames `writeMode` to `q7` because it more accurately reflects the state of the Disk II controller as specified in the manuals, DOS source, and, especially, _Understanding the Apple //e_ by Jim Sather. * Remove the coil state Before, `q` captured the state of the coils. But it was never read. This change just deletes it. * Use the bootstrap and sequencer ROMs with indirection Before, the contents of the bootstrap ROM and sequencer ROM were set directly on fields of the controller. These were not saved or restored with the state in `getState` and `setState`. (It would have been very space inefficient if they had). Now, these ROMs are used from constants indexed by the number of sectors the card supports. This, in turn, means that if the number of sectors is saved with the state, it can be easily restored. * Split out the Disk II controller state This change factors the emulated hardware state into a separate structure in the Disk II controller. The idea is that this hardware state will be able to be shared with the WOZ and nibble disk code instead of sharing _all_ of the controller state (like callbacks and so forth). * Factor out disk insertion Before, several places in the code essentially inserted a new disk image into the drive, which similar—but not always exactly the same—code. Now there is an `insertDisk` method that is responsible for inserting a new `FloppyDisk`. All tests pass, everything compiles, manually tested nibble disks and WOZ disks.
2022-09-01 01:55:01 +00:00
state.controllerState.latch = 0x42;
state.controllerState.on = true;
state.controllerState.q7 = true;
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.)
2022-09-17 13:41:35 +00:00
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);
expect(diskII.getState()).toEqual(state);
});
it('calls all of the callbacks when state is restored', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
jest.resetAllMocks();
const state = diskII.getState();
diskII.setState(state);
expect(callbacks.driveLight).toHaveBeenCalledTimes(2);
expect(callbacks.driveLight).toHaveBeenCalledWith(1, false);
expect(callbacks.driveLight).toHaveBeenCalledWith(2, false);
expect(callbacks.label).toHaveBeenCalledTimes(2);
expect(callbacks.label).toHaveBeenCalledWith(1, 'BYTES_BY_TRACK', undefined);
expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined);
expect(callbacks.dirty).toHaveBeenCalledTimes(2);
expect(callbacks.dirty).toHaveBeenCalledWith(1, true);
expect(callbacks.dirty).toHaveBeenCalledWith(2, false);
});
describe('drive lights', () => {
it('turns on drive light 1 when the motor is turned on', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.ioSwitch(0x89); // turn on the motor
expect(callbacks.driveLight).toBeCalledTimes(1);
expect(callbacks.driveLight).toBeCalledWith(1, true);
});
it('turns off drive light 1 when the motor is turned off', () => {
jest.useFakeTimers();
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.ioSwitch(0x89); // turn on the motor
mocked(callbacks.driveLight).mockReset();
diskII.ioSwitch(0x88); // turn off the motor
jest.runAllTimers();
expect(callbacks.driveLight).toBeCalledTimes(1);
expect(callbacks.driveLight).toBeCalledWith(1, false);
jest.useRealTimers();
});
it('turns on drive light 2 when drive 2 is selected and the motor is turned on', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.ioSwitch(0x8B); // select drive 2
diskII.ioSwitch(0x89); // turn on the motor
expect(callbacks.driveLight).toBeCalledTimes(1);
expect(callbacks.driveLight).toBeCalledWith(2, true);
});
it('turns off drive light 2 when drive 2 is selected and the motor is turned off', () => {
jest.useFakeTimers();
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.ioSwitch(0x8B); // select drive 2
diskII.ioSwitch(0x89); // turn on the motor
mocked(callbacks.driveLight).mockReset();
diskII.ioSwitch(0x88); // turn off the motor
jest.runAllTimers();
expect(callbacks.driveLight).toBeCalledTimes(1);
expect(callbacks.driveLight).toBeCalledWith(2, false);
jest.useRealTimers();
});
it('turns off drive light 1 and turns on drive light two when drive 2 is selected', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8B); // select drive 2
expect(callbacks.driveLight).toBeCalledTimes(3);
expect(callbacks.driveLight).toHaveBeenNthCalledWith(1, 1, true);
expect(callbacks.driveLight).toHaveBeenNthCalledWith(2, 1, false);
expect(callbacks.driveLight).toHaveBeenNthCalledWith(3, 2, true);
});
});
describe('head positioning', () => {
it('does not allow head positioning when the drive is off', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x84); // coil 2 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(0);
expect(state.drives[1].track).toBe(0);
});
it('allows head positioning when the drive is on', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x84); // coil 2 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(2);
expect(state.drives[1].track).toBe(4);
});
it('moves the head to track 2 from track 0 when all phases are cycled', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x80); // coil 0 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(0);
expect(state.drives[1].track).toBe(2 * STEPS_PER_TRACK);
});
it('moves the head to track 10 from track 8 when all phases are cycled', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 8);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x80); // coil 0 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(0);
expect(state.drives[1].track).toBe(10 * STEPS_PER_TRACK);
});
it('stops the head at track 34', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 33);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x80); // coil 0 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(0);
// The emulated Disk II puts data for track n on the
// 4 quarter-tracks starting with n * STEPS_PER_TRACK.
// On a real Disk II, the data would likely be on 3
// quarter-tracks starting with n * STEPS_PER_TRACK - 1,
// leaving 1 essentially blank quarter track at the
// half-track.
expect(state.drives[1].track).toBe(35 * STEPS_PER_TRACK - 1);
});
it('moves a half track when only one phase is activated', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x86); // coil 3 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(3);
expect(state.drives[1].track).toBe(15 * STEPS_PER_TRACK + 2);
});
it('moves backward one track when phases are cycled in reverse', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x80); // coil 0 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(0);
expect(state.drives[1].track).toBe(14 * STEPS_PER_TRACK);
});
it('moves backward two tracks when all phases are cycled in reverse', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x84); // coil 2 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(2);
expect(state.drives[1].track).toBe(13 * STEPS_PER_TRACK);
});
it('does not move backwards past track 0', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 1);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x84); // coil 2 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(2);
expect(state.drives[1].track).toBe(0);
});
it('moves backward one half track', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x82); // coil 1 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(1);
expect(state.drives[1].track).toBe(14.5 * STEPS_PER_TRACK);
});
// The emulated Disk II is not able to step quarter tracks because
// it does not track when phases are turned off.
// eslint-disable-next-line jest/no-disabled-tests
it.skip('moves a quarter track when two neighboring phases are activated and held', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x87); // coil 3 on
const state = diskII.getState();
expect(state.drives[1].phase).toBe(3);
expect(state.drives[1].track).toBe(15 * STEPS_PER_TRACK + 1);
});
// The emulated Disk II is not able to step quarter tracks because
// it does not track when phases are turned off.
// eslint-disable-next-line jest/no-disabled-tests
it.skip('moves backward one quarter track', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setTrack(diskII, 15);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x83); // coil 1 on
const state = diskII.getState();
expect(state.drives[1].phase).toBe(1);
expect(state.drives[1].track).toBe(14.25 * STEPS_PER_TRACK);
});
});
describe('reading nibble-based disks', () => {
it('spins the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8e); // read mode
// Just check for changing nibbles
let spinning = false;
const firstNibble = diskII.ioSwitch(0x8c); // read data
for (let i = 0; i < 512; i++) {
const thisNibble = diskII.ioSwitch(0x8c); // read data
if (thisNibble >= 0x80 && firstNibble !== thisNibble) {
spinning = true;
}
}
expect(spinning).toBeTruthy();
});
it('after reading the data, the data register is set to zero', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8e); // read mode
// Find address field prolog
let nibble = diskII.ioSwitch(0x8c); // read data
for (let i = 0; i < 512 && nibble !== 0xD5; i++) {
nibble = diskII.ioSwitch(0x8c); // read data
}
expect(nibble).toBe(0xD5);
nibble = diskII.ioSwitch(0x8c); // read data
// expect next read to be a zero because the sequencer is waiting
// for data
expect(nibble).toBe(0x00);
});
it('after reading the data, then zero, there is new data', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8e); // read mode
// Find address field prolog
let nibble = diskII.ioSwitch(0x8c); // read data
for (let i = 0; i < 512 && nibble !== 0xD5; i++) {
nibble = diskII.ioSwitch(0x8c); // read data
}
expect(nibble).toBe(0xD5);
nibble = diskII.ioSwitch(0x8c); // read data
// expect next read to be a zero
expect(nibble).toBe(0x00);
// expect next read to be new data
nibble = diskII.ioSwitch(0x8c); // read data
expect(nibble).toBe(0xAA);
});
it('read write protect status', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
setWriteProtected(diskII, true);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8E); // read mode
diskII.ioSwitch(0x8D); // read write protect if read
const isWriteProtected = diskII.ioSwitch(0x8E); // read data
expect(isWriteProtected).toBe(0xff);
});
});
describe('writing nibble-based disks', () => {
it('writes a nibble to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
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.)
2022-09-17 13:41:35 +00:00
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
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.)
2022-09-17 13:41:35 +00:00
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);
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.)
2022-09-17 13:41:35 +00:00
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
diskII.ioSwitch(0x8F, 0x81); // write
diskII.ioSwitch(0x8C); // shift
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.)
2022-09-17 13:41:35 +00:00
disk1 = diskII.getState().drives[1].disk as NibbleDisk;
track0 = disk1.tracks[0];
expect(track0[0]).toBe(0x80);
expect(track0[1]).toBe(0x81);
});
it('sets disk state to dirty and calls the dirty callback when written', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
let state = diskII.getState();
state.drives[1].dirty = false;
diskII.setState(state);
jest.resetAllMocks();
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8F, 0x80); // write
diskII.ioSwitch(0x8C); // shift
expect(callbacks.dirty).toHaveBeenCalledTimes(1);
expect(callbacks.dirty).toHaveBeenCalledWith(1, true);
state = diskII.getState();
expect(state.drives[1].dirty).toBeTruthy();
});
});
describe('reading WOZ-based disks', () => {
const DOS33_SYSTEM_MASTER_IMAGE =
fs.readFileSync('test/js/cards/data/DOS 3.3 System Master.woz').buffer;
it('accepts WOZ-based disks', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
expect(true).toBeTruthy();
});
it('stops the head at the end of the image', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
setTrack(diskII, 33);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x85); // coil 2 on
for (let i = 0; i < 5; i++) {
diskII.ioSwitch(0x87); // coil 3 on
diskII.ioSwitch(0x84); // coil 2 off
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x86); // coil 3 off
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x85); // coil 2 on
diskII.ioSwitch(0x82); // coil 1 off
}
diskII.ioSwitch(0x84); // coil 2 off
const state = diskII.getState();
expect(state.drives[1].phase).toBe(2);
// For WOZ images, the number of tracks is the number in the image.
// The DOS3.3 System Master was imaged on a 40 track drive, so it
// has data for all 40 tracks, even though the last few are garbage.
expect(state.drives[1].track).toBe(40 * STEPS_PER_TRACK - 1);
});
it('spins the disk when motor is on', () => {
let cycles: number = 0;
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
let state = diskII.getState();
expect(state.drives[1].head).toBe(0);
diskII.ioSwitch(0x89); // turn on the motor
cycles += 10;
diskII.tick();
state = diskII.getState();
expect(state.drives[1].head).toBeGreaterThan(0);
});
it('does not spin the disk when motor is off', () => {
let cycles: number = 0;
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
let state = diskII.getState();
expect(state.drives[1].head).toBe(0);
cycles += 10;
diskII.tick();
state = diskII.getState();
expect(state.drives[1].head).toBe(0);
});
it('reads an FF sync byte from the beginning of the image', () => {
let cycles: number = 0;
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8e); // read mode
// The initial bytes in the image are: FF 3F CF F3
// making the bit stream:
//
// 1111 1111 0011 1111 1100 1111 1111 0011
//
// That's three FF sync bytes in a row. Assuming
// the sequencer is in state 2, each sync byte takes
// 32 clock cycles to read, is held for 8 clock
// cycles while the extra zeros are shifted in, then
// is held 8 more clock cycles while the sequencer
// reads the next two bits.
cycles += 40; // shift 10 bits
const nibble = diskII.ioSwitch(0x8c); // read data
expect(nibble).toBe(0xFF);
});
it('reads several FF sync bytes', () => {
let cycles: number = 0;
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8e); // read mode
// The initial bytes in the image are: FF 3F CF F3
// making the bit stream:
//
// 1111 1111 0011 1111 1100 1111 1111 0011
//
// That's three FF sync bytes in a row. Assuming
// the sequencer is in state 2, each sync byte takes
// 32 clock cycles to read, is held for 8 clock
// cycles while the extra zeros are shifted in, then
// is held 8 more clock cycles while the sequencer
// reads the next two bits. This means that 3 sync
// bytes will be available for 3 * 40 + 8 cycles.
for (let i = 0; i < 3 * 40 + 8; i++) {
cycles++;
const nibble = diskII.ioSwitch(0x8c); // read data
if (nibble & 0x80) {
// Nibbles are only valid when the high bit is set.
// eslint-disable-next-line jest/no-conditional-expect
expect(nibble).toBe(0xFF);
}
}
});
it('reads random garbage on uninitialized tracks', () => {
let cycles: number = 0;
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
// Step to track 0.5
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x81); // coil 0 on
diskII.ioSwitch(0x83); // coil 1 on
diskII.ioSwitch(0x80); // coil 0 off
diskII.ioSwitch(0x82); // coil 1 off
diskII.ioSwitch(0x8e); // read mode
// Read 5 nibbles
const nibbles: byte[] = [];
let read = false;
while (nibbles.length < 5) {
cycles++;
const nibble = diskII.ioSwitch(0x8c); // read data
const qa = nibble & 0x80;
if (qa && !read) {
nibbles.push(nibble);
read = true;
}
if (!qa && read) {
read = false;
}
}
// Test that the first doesn't equal any of the others.
// (Yes, this test could fail with some bad luck.)
let equal = false;
for (let i = 1; i < 5; i++) {
equal ||= nibbles[0] === nibbles[i];
}
expect(equal).not.toBeTruthy();
});
it('disk spins at a consistent speed', () => {
const reader = new TestDiskReader(1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, mockApple2IO, callbacks);
reader.diskII.ioSwitch(0x89); // turn on the motor
reader.diskII.ioSwitch(0x8e); // read mode
// Find track 0, sector 0
reader.findSector(0);
// Save the start cycles
let lastCycles = mockApple2IO.cycles();
// Find track 0, sector 0 again
reader.findSector(0);
let currentCycles = reader.cycles;
expect(currentCycles - lastCycles).toBe(201216);
lastCycles = currentCycles;
// Find track 0, sector 0 once again
reader.findSector(0);
currentCycles = reader.cycles;
expect(currentCycles - lastCycles).toBe(201216);
});
});
describe('writing WOZ-based disks', () => {
const DOS33_SYSTEM_MASTER_IMAGE =
fs.readFileSync('test/js/cards/data/DOS 3.3 System Master.woz').buffer;
it('can write something', () => {
const reader = new TestDiskReader(1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, mockApple2IO, callbacks);
const diskII = reader.diskII;
const before = reader.rawTracks();
diskII.ioSwitch(0x89); // turn on the motor
// emulate STA $C08F,X (5 CPU cycles)
reader.cycles += 4; // op + load address + work
diskII.tick();
reader.cycles += 1;
diskII.ioSwitch(0x8F, 0x80); // write
// read $C08C,X
reader.cycles += 4; // op + load address + work
diskII.tick();
reader.cycles += 1;
diskII.ioSwitch(0x8C); // shift
reader.cycles += 29; // wait
diskII.tick(); // nop (make sure the change is applied)
const after = reader.rawTracks();
expect(before).not.toEqual(after);
});
});
});
class TestDiskReader {
cycles: number = 0;
nibbles = 0;
diskII: DiskII;
constructor(drive: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) {
mocked(apple2IO).cycles.mockImplementation(() => this.cycles);
this.diskII = new DiskII(apple2IO, callbacks);
this.diskII.setBinary(drive, label, 'woz', image);
}
readNibble(): byte {
let result: number = 0;
for (let i = 0; i < 100; i++) {
this.cycles++;
const nibble = this.diskII.ioSwitch(0x8c); // read data
if (nibble & 0x80) {
result = nibble;
} else if (result & 0x80) {
this.nibbles++;
return result;
}
}
throw new Error('Did not find a nibble in 100 clock cycles');
}
findAddressField() {
let s = '';
for (let i = 0; i < 600; i++) {
let nibble = this.readNibble();
if (nibble !== 0xD5) {
s += ` ${toHex(nibble)}`;
continue;
}
nibble = this.readNibble();
if (nibble !== 0xAA) {
continue;
}
nibble = this.readNibble();
if (nibble !== 0x96) {
continue;
}
return;
}
throw new Error(`Did not find an address field in 500 nibbles: ${s}`);
}
nextSector() {
this.findAddressField();
const volume = (this.readNibble() << 1 | 1) & this.readNibble();
const track = (this.readNibble() << 1 | 1) & this.readNibble();
const sector = (this.readNibble() << 1 | 1) & this.readNibble();
// console.log(`vol: ${volume} trk: ${track} sec: ${thisSector} ${this.diskII.head()} ${this.nibbles}`);
return { volume, track, sector };
}
findSector(sector: byte) {
for (let i = 0; i < 32; i++) {
const { sector: thisSector } = this.nextSector();
if (sector === thisSector) {
return;
}
}
throw new Error(`Did not find sector ${sector} in 32 sectors`);
}
rawTracks() {
// NOTE(flan): Hack to access private properties.
const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk;
const result: Uint8Array[] = [];
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.)
2022-09-17 13:41:35 +00:00
for (let i = 0; i < disk.rawTracks.length; i++) {
result[i] = disk.rawTracks[i].slice(0);
}
return result;
}
}