mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Merge pull request #163 from iflan/split-drivers-into-files
Split drivers into files
This commit is contained in:
commit
5bd51d64c5
@ -2,33 +2,14 @@ import { base64_encode } from '../base64';
|
|||||||
import type {
|
import type {
|
||||||
byte,
|
byte,
|
||||||
Card,
|
Card,
|
||||||
nibble,
|
ReadonlyUint8Array
|
||||||
ReadonlyUint8Array,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormatWorkerMessage,
|
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
|
||||||
FormatWorkerResponse,
|
FloppyFormat, FormatWorkerMessage,
|
||||||
NibbleFormat,
|
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
|
||||||
DISK_PROCESSED,
|
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
|
||||||
DRIVE_NUMBERS,
|
|
||||||
DriveNumber,
|
|
||||||
JSONDisk,
|
|
||||||
PROCESS_BINARY,
|
|
||||||
PROCESS_JSON_DISK,
|
|
||||||
PROCESS_JSON,
|
|
||||||
MassStorage,
|
|
||||||
MassStorageData,
|
|
||||||
SupportedSectors,
|
|
||||||
FloppyDisk,
|
|
||||||
FloppyFormat,
|
|
||||||
WozDisk,
|
|
||||||
NibbleDisk,
|
|
||||||
isNibbleDisk,
|
|
||||||
isWozDisk,
|
|
||||||
NoFloppyDisk,
|
|
||||||
isNoFloppyDisk,
|
|
||||||
NO_DISK,
|
|
||||||
} from '../formats/types';
|
} from '../formats/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -36,12 +17,17 @@ import {
|
|||||||
createDiskFromJsonDisk
|
createDiskFromJsonDisk
|
||||||
} from '../formats/create_disk';
|
} from '../formats/create_disk';
|
||||||
|
|
||||||
import { toHex } from '../util';
|
|
||||||
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
|
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
|
||||||
|
|
||||||
import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2';
|
|
||||||
import Apple2IO from '../apple2io';
|
import Apple2IO from '../apple2io';
|
||||||
|
|
||||||
|
import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
|
||||||
|
|
||||||
|
import { EmptyDriver } from './drivers/EmptyDriver';
|
||||||
|
import { NibbleDiskDriver } from './drivers/NibbleDiskDriver';
|
||||||
|
import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types';
|
||||||
|
import { WozDiskDriver } from './drivers/WozDiskDriver';
|
||||||
|
|
||||||
/** Softswitch locations */
|
/** Softswitch locations */
|
||||||
const LOC = {
|
const LOC = {
|
||||||
// Disk II Controller Commands
|
// Disk II Controller Commands
|
||||||
@ -129,7 +115,7 @@ const SEQUENCER_ROM_16 = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Contents of the P6 sequencer ROM. */
|
/** Contents of the P6 sequencer ROM. */
|
||||||
const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
|
export const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
|
||||||
13: SEQUENCER_ROM_13,
|
13: SEQUENCER_ROM_13,
|
||||||
16: SEQUENCER_ROM_16,
|
16: SEQUENCER_ROM_16,
|
||||||
} as const;
|
} as const;
|
||||||
@ -140,10 +126,6 @@ const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
|
|||||||
16: BOOTSTRAP_ROM_16,
|
16: BOOTSTRAP_ROM_16,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
||||||
type LssState = nibble;
|
|
||||||
type Phase = 0 | 1 | 2 | 3;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far the head moves, in quarter tracks, when in phase X and phase Y is
|
* How far the head moves, in quarter tracks, when in phase X and phase Y is
|
||||||
* activated. For example, if in phase 0 (top row), turning on phase 3 would
|
* activated. For example, if in phase 0 (top row), turning on phase 3 would
|
||||||
@ -173,75 +155,18 @@ const PHASE_DELTA = [
|
|||||||
[1, -2, -1, 0]
|
[1, -2, -1, 0]
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* State of the controller.
|
|
||||||
*/
|
|
||||||
interface ControllerState {
|
|
||||||
/** Sectors supported by the controller. */
|
|
||||||
sectors: SupportedSectors;
|
|
||||||
|
|
||||||
/** Is the active drive powered on? */
|
|
||||||
on: boolean;
|
|
||||||
|
|
||||||
/** The active drive. */
|
|
||||||
drive: DriveNumber;
|
|
||||||
|
|
||||||
/** The 8-cycle LSS clock. */
|
|
||||||
clock: LssClockCycle;
|
|
||||||
/** Current state of the Logic State Sequencer. */
|
|
||||||
state: nibble;
|
|
||||||
|
|
||||||
/** Q6 (Shift/Load) */
|
|
||||||
q6: boolean;
|
|
||||||
/** Q7 (Read/Write) */
|
|
||||||
q7: boolean;
|
|
||||||
|
|
||||||
/** Last data from the disk drive. */
|
|
||||||
latch: byte;
|
|
||||||
/** Last data written by the CPU to card softswitch 0x8D. */
|
|
||||||
bus: byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interface for drivers for various disk types. */
|
|
||||||
interface DiskDriver {
|
|
||||||
tick(): void;
|
|
||||||
onQ6Low(): void;
|
|
||||||
onQ6High(readMode: boolean): void;
|
|
||||||
onDriveOn(): void;
|
|
||||||
onDriveOff(): void;
|
|
||||||
clampTrack(): void;
|
|
||||||
getState(): DriverState;
|
|
||||||
setState(state: DriverState): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DriverState = EmptyDriverState | NibbleDiskDriverState | WozDiskDriverState;
|
|
||||||
|
|
||||||
/** Callbacks triggered by events of the drive or controller. */
|
/** Callbacks triggered by events of the drive or controller. */
|
||||||
export interface Callbacks {
|
export interface Callbacks {
|
||||||
/** Called when a drive turns on or off. */
|
/** Called when a drive turns on or off. */
|
||||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
driveLight: (driveNo: DriveNumber, on: boolean) => void;
|
||||||
/**
|
/**
|
||||||
* Called when a disk has been written to. For performance and integrity,
|
* Called when a disk has been written to. For performance and integrity,
|
||||||
* this is only called when the drive stops spinning or is removed from
|
* this is only called when the drive stops spinning or is removed from
|
||||||
* the drive.
|
* the drive.
|
||||||
*/
|
*/
|
||||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
dirty: (driveNo: DriveNumber, dirty: boolean) => void;
|
||||||
/** Called when a disk is inserted or removed from the drive. */
|
/** Called when a disk is inserted or removed from the drive. */
|
||||||
label: (drive: DriveNumber, name?: string, side?: string) => void;
|
label: (driveNo: DriveNumber, name?: string, side?: string) => void;
|
||||||
}
|
|
||||||
|
|
||||||
/** Common information for Nibble and WOZ disks. */
|
|
||||||
interface Drive {
|
|
||||||
/** Whether the drive write protect is on. */
|
|
||||||
readOnly: boolean;
|
|
||||||
/** Quarter track position of read/write head. */
|
|
||||||
track: byte;
|
|
||||||
/** Position of the head on the track. */
|
|
||||||
head: byte;
|
|
||||||
/** Current active coil in the head stepper motor. */
|
|
||||||
phase: Phase;
|
|
||||||
/** Whether the drive has been written to since it was loaded. */
|
|
||||||
dirty: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DriveState {
|
interface DriveState {
|
||||||
@ -255,17 +180,11 @@ interface DriveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** State of the controller for saving/restoring. */
|
/** State of the controller for saving/restoring. */
|
||||||
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
|
|
||||||
interface State {
|
interface State {
|
||||||
drives: DriveState[];
|
drives: DriveState[];
|
||||||
driver: DriverState[];
|
|
||||||
controllerState: ControllerState;
|
controllerState: ControllerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDiskState(disk: NoFloppyDisk): NoFloppyDisk;
|
|
||||||
function getDiskState(disk: NibbleDisk): NibbleDisk;
|
|
||||||
function getDiskState(disk: WozDisk): WozDisk;
|
|
||||||
function getDiskState(disk: FloppyDisk): FloppyDisk;
|
|
||||||
function getDiskState(disk: FloppyDisk): FloppyDisk {
|
function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||||
if (isNoFloppyDisk(disk)) {
|
if (isNoFloppyDisk(disk)) {
|
||||||
const { encoding, metadata, readOnly } = disk;
|
const { encoding, metadata, readOnly } = disk;
|
||||||
@ -312,429 +231,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
|||||||
throw new Error('Unknown drive state');
|
throw new Error('Unknown drive state');
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmptyDriverState { }
|
|
||||||
|
|
||||||
class EmptyDriver implements DiskDriver {
|
|
||||||
constructor(private readonly drive: Drive) { }
|
|
||||||
|
|
||||||
tick(): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6Low(): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6High(_readMode: boolean): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOn(): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOff(): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
clampTrack(): void {
|
|
||||||
// For empty drives, the emulator clamps the track to 0 to 34,
|
|
||||||
// but real Disk II drives can seek past track 34 by at least a
|
|
||||||
// half track, usually a full track. Some 3rd party drives can
|
|
||||||
// seek to track 39.
|
|
||||||
if (this.drive.track < 0) {
|
|
||||||
this.drive.track = 0;
|
|
||||||
}
|
|
||||||
if (this.drive.track > 34) {
|
|
||||||
this.drive.track = 34;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(_state: EmptyDriverState): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseDiskDriver implements DiskDriver {
|
|
||||||
constructor(
|
|
||||||
protected readonly driveNumber: DriveNumber,
|
|
||||||
protected readonly drive: Drive,
|
|
||||||
protected readonly disk: NibbleDisk | WozDisk,
|
|
||||||
protected readonly controller: ControllerState) { }
|
|
||||||
|
|
||||||
/** Called frequently to ensure the disk is spinning. */
|
|
||||||
abstract tick(): void;
|
|
||||||
|
|
||||||
/** Called when Q6 is set LOW. */
|
|
||||||
abstract onQ6Low(): void;
|
|
||||||
|
|
||||||
/** Called when Q6 is set HIGH. */
|
|
||||||
abstract onQ6High(readMode: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when drive is turned on. This is guaranteed to be called
|
|
||||||
* only when the associated drive is toggled from off to on. This
|
|
||||||
* is also guaranteed to be called when a new disk is inserted when
|
|
||||||
* the drive is already on.
|
|
||||||
*/
|
|
||||||
abstract onDriveOn(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when drive is turned off. This is guaranteed to be called
|
|
||||||
* only when the associated drive is toggled from on to off.
|
|
||||||
*/
|
|
||||||
abstract onDriveOff(): void;
|
|
||||||
|
|
||||||
debug(..._args: unknown[]) {
|
|
||||||
// debug(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called every time the head moves to clamp the track to a valid
|
|
||||||
* range.
|
|
||||||
*/
|
|
||||||
abstract clampTrack(): void;
|
|
||||||
|
|
||||||
isOn(): boolean {
|
|
||||||
return this.controller.on && this.controller.drive === this.driveNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
isWriteProtected(): boolean {
|
|
||||||
return this.drive.readOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract getState(): DriverState;
|
|
||||||
|
|
||||||
abstract setState(state: DriverState): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NibbleDiskDriverState {
|
|
||||||
skip: number;
|
|
||||||
nibbleCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NibbleDiskDriver extends BaseDiskDriver {
|
|
||||||
/**
|
|
||||||
* When `1`, the next nibble will be available for read; when `0`,
|
|
||||||
* the card is pretending to wait for data to be shifted in by the
|
|
||||||
* sequencer.
|
|
||||||
*/
|
|
||||||
private skip: number = 0;
|
|
||||||
/** Number of nibbles reads since the drive was turned on. */
|
|
||||||
private nibbleCount: number = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
driveNumber: DriveNumber,
|
|
||||||
drive: Drive,
|
|
||||||
readonly disk: NibbleDisk,
|
|
||||||
controller: ControllerState,
|
|
||||||
private readonly onDirty: () => void) {
|
|
||||||
super(driveNumber, drive, disk, controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6Low(): void {
|
|
||||||
const drive = this.drive;
|
|
||||||
const disk = this.disk;
|
|
||||||
if (this.isOn() && (this.skip || this.controller.q7)) {
|
|
||||||
const track = disk.tracks[drive.track >> 2];
|
|
||||||
if (track && track.length) {
|
|
||||||
if (drive.head >= track.length) {
|
|
||||||
drive.head = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.controller.q7) {
|
|
||||||
const writeProtected = disk.readOnly;
|
|
||||||
if (!writeProtected) {
|
|
||||||
track[drive.head] = this.controller.bus;
|
|
||||||
drive.dirty = true;
|
|
||||||
this.onDirty();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.controller.latch = track[drive.head];
|
|
||||||
this.nibbleCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
++drive.head;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.controller.latch = 0;
|
|
||||||
}
|
|
||||||
this.skip = (++this.skip % 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6High(readMode: boolean): void {
|
|
||||||
const drive = this.drive;
|
|
||||||
if (readMode && !this.controller.q7) {
|
|
||||||
const writeProtected = drive.readOnly;
|
|
||||||
if (writeProtected) {
|
|
||||||
this.controller.latch = 0xff;
|
|
||||||
this.debug('Setting readOnly');
|
|
||||||
} else {
|
|
||||||
this.controller.latch >>= 1;
|
|
||||||
this.debug('Clearing readOnly');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOn(): void {
|
|
||||||
this.nibbleCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOff(): void {
|
|
||||||
this.debug('nibbles read', this.nibbleCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
clampTrack(): void {
|
|
||||||
// For NibbleDisks, the emulator clamps the track to the available
|
|
||||||
// range.
|
|
||||||
if (this.drive.track < 0) {
|
|
||||||
this.drive.track = 0;
|
|
||||||
}
|
|
||||||
const lastTrack = 35 * 4 - 1;
|
|
||||||
if (this.drive.track > lastTrack) {
|
|
||||||
this.drive.track = lastTrack;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(): NibbleDiskDriverState {
|
|
||||||
const { skip, nibbleCount } = this;
|
|
||||||
return { skip, nibbleCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(state: NibbleDiskDriverState) {
|
|
||||||
this.skip = state.skip;
|
|
||||||
this.nibbleCount = state.nibbleCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WozDiskDriverState {
|
|
||||||
clock: LssClockCycle;
|
|
||||||
state: LssState;
|
|
||||||
lastCycles: number;
|
|
||||||
zeros: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WozDiskDriver extends BaseDiskDriver {
|
|
||||||
/** Logic state sequencer clock cycle. */
|
|
||||||
private clock: LssClockCycle;
|
|
||||||
/** Logic state sequencer state. */
|
|
||||||
private state: LssState;
|
|
||||||
/** Current CPU cycle count. */
|
|
||||||
private lastCycles: number = 0;
|
|
||||||
/**
|
|
||||||
* Number of zeros read in a row. The Disk ][ can only read two zeros in a
|
|
||||||
* row reliably; above that and the drive starts reporting garbage. See
|
|
||||||
* "Freaking Out Like a MC3470" in the WOZ spec.
|
|
||||||
*/
|
|
||||||
private zeros = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
driveNumber: DriveNumber,
|
|
||||||
drive: Drive,
|
|
||||||
readonly disk: WozDisk,
|
|
||||||
controller: ControllerState,
|
|
||||||
private readonly onDirty: () => void,
|
|
||||||
private readonly io: Apple2IO) {
|
|
||||||
super(driveNumber, drive, disk, controller);
|
|
||||||
|
|
||||||
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
|
||||||
// essentially the start of the sequencer loop and produces
|
|
||||||
// correctly synced nibbles immediately. Starting at state 0
|
|
||||||
// would introduce a spurrious 1 in the latch at the beginning,
|
|
||||||
// which requires reading several more sync bytes to sync up.
|
|
||||||
this.state = 2;
|
|
||||||
this.clock = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOn(): void {
|
|
||||||
this.lastCycles = this.io.cycles();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOff(): void {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spin the disk under the read/write head for WOZ images.
|
|
||||||
*
|
|
||||||
* This implementation emulates every clock cycle of the 2 MHz
|
|
||||||
* sequencer since the last time it was called in order to
|
|
||||||
* determine the current state. Because this is called on
|
|
||||||
* every access to the softswitches, the data in the latch
|
|
||||||
* will be correct on every read.
|
|
||||||
*
|
|
||||||
* The emulation of the disk makes a few simplifying assumptions:
|
|
||||||
*
|
|
||||||
* * The motor turns on instantly.
|
|
||||||
* * The head moves tracks instantly.
|
|
||||||
* * The length (in bits) of each track of the WOZ image
|
|
||||||
* represents one full rotation of the disk and that each
|
|
||||||
* bit is evenly spaced.
|
|
||||||
* * Writing will not change the track length. This means
|
|
||||||
* that short tracks stay short.
|
|
||||||
* * The read head picks up the next bit when the sequencer
|
|
||||||
* clock === 4.
|
|
||||||
* * Head position X on track T is equivalent to head position
|
|
||||||
* X on track T′. (This is not the recommendation in the WOZ
|
|
||||||
* spec.)
|
|
||||||
* * Unspecified tracks contain a single zero bit. (A very
|
|
||||||
* short track, indeed!)
|
|
||||||
* * Two zero bits are sufficient to cause the MC3470 to freak
|
|
||||||
* out. When freaking out, it returns 0 and 1 with equal
|
|
||||||
* probability.
|
|
||||||
* * Any softswitch changes happen before `moveHead`. This is
|
|
||||||
* important because it means that if the clock is ever
|
|
||||||
* advanced more than one cycle between calls, the
|
|
||||||
* softswitch changes will appear to happen at the very
|
|
||||||
* beginning, not just before the last cycle.
|
|
||||||
*/
|
|
||||||
private moveHead() {
|
|
||||||
// TODO(flan): Short-circuit if the drive is not on.
|
|
||||||
const cycles = this.io.cycles();
|
|
||||||
|
|
||||||
// Spin the disk the number of elapsed cycles since last call
|
|
||||||
let workCycles = (cycles - this.lastCycles) * 2;
|
|
||||||
this.lastCycles = cycles;
|
|
||||||
|
|
||||||
const drive = this.drive;
|
|
||||||
const disk = this.disk;
|
|
||||||
const controller = this.controller;
|
|
||||||
|
|
||||||
// TODO(flan): Improve unformatted track behavior. The WOZ
|
|
||||||
// documentation suggests using an empty track of 6400 bytes
|
|
||||||
// (51,200 bits).
|
|
||||||
const track =
|
|
||||||
disk.rawTracks[disk.trackMap[drive.track]] || [0];
|
|
||||||
|
|
||||||
while (workCycles-- > 0) {
|
|
||||||
let pulse: number = 0;
|
|
||||||
if (this.clock === 4) {
|
|
||||||
pulse = track[drive.head];
|
|
||||||
if (!pulse) {
|
|
||||||
// More than 2 zeros can not be read reliably.
|
|
||||||
// TODO(flan): Revisit with the new MC3470
|
|
||||||
// suggested 4-bit window behavior.
|
|
||||||
if (++this.zeros > 2) {
|
|
||||||
const r = Math.random();
|
|
||||||
pulse = r >= 0.5 ? 1 : 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.zeros = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
idx |= pulse ? 0x00 : 0x01;
|
|
||||||
idx |= controller.latch & 0x80 ? 0x02 : 0x00;
|
|
||||||
idx |= controller.q6 ? 0x04 : 0x00;
|
|
||||||
idx |= controller.q7 ? 0x08 : 0x00;
|
|
||||||
idx |= this.state << 4;
|
|
||||||
|
|
||||||
const command = SEQUENCER_ROM[controller.sectors][idx];
|
|
||||||
|
|
||||||
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
|
|
||||||
|
|
||||||
switch (command & 0xf) {
|
|
||||||
case 0x0: // CLR
|
|
||||||
controller.latch = 0;
|
|
||||||
break;
|
|
||||||
case 0x8: // NOP
|
|
||||||
break;
|
|
||||||
case 0x9: // SL0
|
|
||||||
controller.latch = (controller.latch << 1) & 0xff;
|
|
||||||
break;
|
|
||||||
case 0xA: // SR
|
|
||||||
controller.latch >>= 1;
|
|
||||||
if (this.isWriteProtected()) {
|
|
||||||
controller.latch |= 0x80;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 0xB: // LD
|
|
||||||
controller.latch = controller.bus;
|
|
||||||
this.debug('Loading', toHex(controller.latch), 'from bus');
|
|
||||||
break;
|
|
||||||
case 0xD: // SL1
|
|
||||||
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
|
||||||
}
|
|
||||||
this.state = (command >> 4 & 0xF) as LssState;
|
|
||||||
|
|
||||||
if (this.clock === 4) {
|
|
||||||
if (this.isOn()) {
|
|
||||||
if (controller.q7) {
|
|
||||||
// TODO(flan): This assumes that writes are happening in
|
|
||||||
// a "friendly" way, namely where the track was originally
|
|
||||||
// written. To do this correctly, the virtual head should
|
|
||||||
// actually keep track of the current quarter track plus
|
|
||||||
// the one on each side. Then, when writing, it should
|
|
||||||
// check that all three are actually the same rawTrack. If
|
|
||||||
// they aren't, then the trackMap has to be updated as
|
|
||||||
// well.
|
|
||||||
track[drive.head] = this.state & 0x8 ? 0x01 : 0x00;
|
|
||||||
this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00);
|
|
||||||
drive.dirty = true;
|
|
||||||
this.onDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++drive.head >= track.length) {
|
|
||||||
drive.head = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++this.clock > 7) {
|
|
||||||
this.clock = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(): void {
|
|
||||||
this.moveHead();
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6High(_readMode: boolean): void {
|
|
||||||
// nothing?
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6Low(): void {
|
|
||||||
// nothing?
|
|
||||||
}
|
|
||||||
|
|
||||||
clampTrack(): void {
|
|
||||||
// For NibbleDisks, the emulator clamps the track to the available
|
|
||||||
// range.
|
|
||||||
if (this.drive.track < 0) {
|
|
||||||
this.drive.track = 0;
|
|
||||||
}
|
|
||||||
const lastTrack = this.disk.trackMap.length - 1;
|
|
||||||
if (this.drive.track > lastTrack) {
|
|
||||||
this.drive.track = lastTrack;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(): WozDiskDriverState {
|
|
||||||
const { clock, state, lastCycles, zeros } = this;
|
|
||||||
return { clock, state, lastCycles, zeros };
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(state: WozDiskDriverState) {
|
|
||||||
this.clock = state.clock;
|
|
||||||
this.state = state.state;
|
|
||||||
this.lastCycles = state.lastCycles;
|
|
||||||
this.zeros = state.zeros;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||||
*/
|
*/
|
||||||
@ -794,7 +290,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
sectors,
|
sectors,
|
||||||
bus: 0,
|
bus: 0,
|
||||||
latch: 0,
|
latch: 0,
|
||||||
drive: 1,
|
driveNo: 1,
|
||||||
on: false,
|
on: false,
|
||||||
q6: false,
|
q6: false,
|
||||||
q7: false,
|
q7: false,
|
||||||
@ -814,8 +310,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
|
|
||||||
/** Updates the active drive based on the controller state. */
|
/** Updates the active drive based on the controller state. */
|
||||||
private updateActiveDrive() {
|
private updateActiveDrive() {
|
||||||
this.curDrive = this.drives[this.state.drive];
|
this.curDrive = this.drives[this.state.driveNo];
|
||||||
this.curDriver = this.driver[this.state.drive];
|
this.curDriver = this.driver[this.state.driveNo];
|
||||||
}
|
}
|
||||||
|
|
||||||
private debug(..._args: unknown[]) {
|
private debug(..._args: unknown[]) {
|
||||||
@ -898,7 +394,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
this.offTimeout = window.setTimeout(() => {
|
this.offTimeout = window.setTimeout(() => {
|
||||||
this.debug('Drive Off');
|
this.debug('Drive Off');
|
||||||
state.on = false;
|
state.on = false;
|
||||||
this.callbacks.driveLight(state.drive, false);
|
this.callbacks.driveLight(state.driveNo, false);
|
||||||
this.curDriver.onDriveOff();
|
this.curDriver.onDriveOff();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@ -913,14 +409,14 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
if (!state.on) {
|
if (!state.on) {
|
||||||
this.debug('Drive On');
|
this.debug('Drive On');
|
||||||
state.on = true;
|
state.on = true;
|
||||||
this.callbacks.driveLight(state.drive, true);
|
this.callbacks.driveLight(state.driveNo, true);
|
||||||
this.curDriver.onDriveOn();
|
this.curDriver.onDriveOn();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LOC.DRIVE1: // 0x0a
|
case LOC.DRIVE1: // 0x0a
|
||||||
this.debug('Disk 1');
|
this.debug('Disk 1');
|
||||||
state.drive = 1;
|
state.driveNo = 1;
|
||||||
this.updateActiveDrive();
|
this.updateActiveDrive();
|
||||||
if (state.on) {
|
if (state.on) {
|
||||||
this.callbacks.driveLight(2, false);
|
this.callbacks.driveLight(2, false);
|
||||||
@ -929,7 +425,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
break;
|
break;
|
||||||
case LOC.DRIVE2: // 0x0b
|
case LOC.DRIVE2: // 0x0b
|
||||||
this.debug('Disk 2');
|
this.debug('Disk 2');
|
||||||
state.drive = 2;
|
state.driveNo = 2;
|
||||||
this.updateActiveDrive();
|
this.updateActiveDrive();
|
||||||
if (state.on) {
|
if (state.on) {
|
||||||
this.callbacks.driveLight(1, false);
|
this.callbacks.driveLight(1, false);
|
||||||
@ -980,10 +476,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDirty(drive: DriveNumber, dirty: boolean) {
|
private updateDirty(driveNo: DriveNumber, dirty: boolean) {
|
||||||
this.drives[drive].dirty = dirty;
|
this.drives[driveNo].dirty = dirty;
|
||||||
if (this.callbacks.dirty) {
|
if (this.callbacks.dirty) {
|
||||||
this.callbacks.dirty(drive, dirty);
|
this.callbacks.dirty(driveNo, dirty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1002,10 +498,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
reset() {
|
reset() {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
if (state.on) {
|
if (state.on) {
|
||||||
this.callbacks.driveLight(state.drive, false);
|
this.callbacks.driveLight(state.driveNo, false);
|
||||||
state.q7 = false;
|
state.q7 = false;
|
||||||
state.on = false;
|
state.on = false;
|
||||||
state.drive = 1;
|
state.driveNo = 1;
|
||||||
}
|
}
|
||||||
this.updateActiveDrive();
|
this.updateActiveDrive();
|
||||||
}
|
}
|
||||||
@ -1014,10 +510,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
this.curDriver.tick();
|
this.curDriver.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDriveState(drive: DriveNumber): DriveState {
|
private getDriveState(driveNo: DriveNumber): DriveState {
|
||||||
const curDrive = this.drives[drive];
|
const curDrive = this.drives[driveNo];
|
||||||
const curDisk = this.disks[drive];
|
const curDisk = this.disks[driveNo];
|
||||||
const curDriver = this.driver[drive];
|
const curDriver = this.driver[driveNo];
|
||||||
const { readOnly, track, head, phase, dirty } = curDrive;
|
const { readOnly, track, head, phase, dirty } = curDrive;
|
||||||
return {
|
return {
|
||||||
disk: getDiskState(curDisk),
|
disk: getDiskState(curDisk),
|
||||||
@ -1033,7 +529,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
getState(): State {
|
getState(): State {
|
||||||
const result = {
|
const result = {
|
||||||
drives: [] as DriveState[],
|
drives: [] as DriveState[],
|
||||||
driver: [] as DriverState[],
|
|
||||||
controllerState: { ...this.state },
|
controllerState: { ...this.state },
|
||||||
};
|
};
|
||||||
result.drives[1] = this.getDriveState(1);
|
result.drives[1] = this.getDriveState(1);
|
||||||
@ -1042,9 +537,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDriveState(drive: DriveNumber, state: DriveState) {
|
private setDriveState(driveNo: DriveNumber, state: DriveState) {
|
||||||
const { track, head, phase, readOnly, dirty } = state;
|
const { track, head, phase, readOnly, dirty } = state;
|
||||||
this.drives[drive] = {
|
this.drives[driveNo] = {
|
||||||
track,
|
track,
|
||||||
head,
|
head,
|
||||||
phase,
|
phase,
|
||||||
@ -1052,8 +547,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
dirty,
|
dirty,
|
||||||
};
|
};
|
||||||
const disk = getDiskState(state.disk);
|
const disk = getDiskState(state.disk);
|
||||||
this.setDiskInternal(drive, disk);
|
this.setDiskInternal(driveNo, disk);
|
||||||
this.driver[drive].setState(state.driver);
|
this.driver[driveNo].setState(state.driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1082,8 +577,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Reads the given track and physical sector. */
|
/** Reads the given track and physical sector. */
|
||||||
rwts(disk: DriveNumber, track: byte, sector: byte) {
|
rwts(driveNo: DriveNumber, track: byte, sector: byte) {
|
||||||
const curDisk = this.disks[disk];
|
const curDisk = this.disks[driveNo];
|
||||||
if (!isNibbleDisk(curDisk)) {
|
if (!isNibbleDisk(curDisk)) {
|
||||||
throw new Error('Can\'t read WOZ disks');
|
throw new Error('Can\'t read WOZ disks');
|
||||||
}
|
}
|
||||||
@ -1092,12 +587,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
|
|
||||||
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
||||||
// TODO(flan): This implementation is not very safe.
|
// TODO(flan): This implementation is not very safe.
|
||||||
setDisk(drive: DriveNumber, jsonDisk: JSONDisk) {
|
setDisk(driveNo: DriveNumber, jsonDisk: JSONDisk) {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
const message: FormatWorkerMessage = {
|
const message: FormatWorkerMessage = {
|
||||||
type: PROCESS_JSON_DISK,
|
type: PROCESS_JSON_DISK,
|
||||||
payload: {
|
payload: {
|
||||||
drive,
|
driveNo: driveNo,
|
||||||
jsonDisk
|
jsonDisk
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -1106,39 +601,39 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
} else {
|
} else {
|
||||||
const disk = createDiskFromJsonDisk(jsonDisk);
|
const disk = createDiskFromJsonDisk(jsonDisk);
|
||||||
if (disk) {
|
if (disk) {
|
||||||
this.insertDisk(drive, disk);
|
this.insertDisk(driveNo, disk);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
getJSON(driveNo: DriveNumber, pretty: boolean = false) {
|
||||||
const curDisk = this.disks[drive];
|
const curDisk = this.disks[driveNo];
|
||||||
if (!isNibbleDisk(curDisk)) {
|
if (!isNibbleDisk(curDisk)) {
|
||||||
throw new Error('Can\'t save WOZ disks to JSON');
|
throw new Error('Can\'t save WOZ disks to JSON');
|
||||||
}
|
}
|
||||||
return jsonEncode(curDisk, pretty);
|
return jsonEncode(curDisk, pretty);
|
||||||
}
|
}
|
||||||
|
|
||||||
setJSON(drive: DriveNumber, json: string) {
|
setJSON(driveNo: DriveNumber, json: string) {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
const message: FormatWorkerMessage = {
|
const message: FormatWorkerMessage = {
|
||||||
type: PROCESS_JSON,
|
type: PROCESS_JSON,
|
||||||
payload: {
|
payload: {
|
||||||
drive,
|
driveNo: driveNo,
|
||||||
json
|
json
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.worker.postMessage(message);
|
this.worker.postMessage(message);
|
||||||
} else {
|
} else {
|
||||||
const disk = jsonDecode(json);
|
const disk = jsonDecode(json);
|
||||||
this.insertDisk(drive, disk);
|
this.insertDisk(driveNo, disk);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
|
setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
|
||||||
const readOnly = false;
|
const readOnly = false;
|
||||||
const volume = 254;
|
const volume = 254;
|
||||||
const options = {
|
const options = {
|
||||||
@ -1152,7 +647,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
const message: FormatWorkerMessage = {
|
const message: FormatWorkerMessage = {
|
||||||
type: PROCESS_BINARY,
|
type: PROCESS_BINARY,
|
||||||
payload: {
|
payload: {
|
||||||
drive,
|
driveNo: driveNo,
|
||||||
fmt,
|
fmt,
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
@ -1163,7 +658,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
} else {
|
} else {
|
||||||
const disk = createDisk(fmt, options);
|
const disk = createDisk(fmt, options);
|
||||||
if (disk) {
|
if (disk) {
|
||||||
this.insertDisk(drive, disk);
|
this.insertDisk(driveNo, disk);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1183,7 +678,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case DISK_PROCESSED:
|
case DISK_PROCESSED:
|
||||||
{
|
{
|
||||||
const { drive, disk } = data.payload;
|
const { driveNo: drive, disk } = data.payload;
|
||||||
if (disk) {
|
if (disk) {
|
||||||
this.insertDisk(drive, disk);
|
this.insertDisk(drive, disk);
|
||||||
}
|
}
|
||||||
@ -1196,26 +691,26 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDiskInternal(drive: DriveNumber, disk: FloppyDisk) {
|
private setDiskInternal(driveNo: DriveNumber, disk: FloppyDisk) {
|
||||||
this.disks[drive] = disk;
|
this.disks[driveNo] = disk;
|
||||||
if (isNoFloppyDisk(disk)) {
|
if (isNoFloppyDisk(disk)) {
|
||||||
this.driver[drive] = new EmptyDriver(this.drives[drive]);
|
this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
|
||||||
} else if (isNibbleDisk(disk)) {
|
} else if (isNibbleDisk(disk)) {
|
||||||
this.driver[drive] =
|
this.driver[driveNo] =
|
||||||
new NibbleDiskDriver(
|
new NibbleDiskDriver(
|
||||||
drive,
|
driveNo,
|
||||||
this.drives[drive],
|
this.drives[driveNo],
|
||||||
disk,
|
disk,
|
||||||
this.state,
|
this.state,
|
||||||
() => this.updateDirty(drive, true));
|
() => this.updateDirty(driveNo, true));
|
||||||
} else if (isWozDisk(disk)) {
|
} else if (isWozDisk(disk)) {
|
||||||
this.driver[drive] =
|
this.driver[driveNo] =
|
||||||
new WozDiskDriver(
|
new WozDiskDriver(
|
||||||
drive,
|
driveNo,
|
||||||
this.drives[drive],
|
this.drives[driveNo],
|
||||||
disk,
|
disk,
|
||||||
this.state,
|
this.state,
|
||||||
() => this.updateDirty(drive, true),
|
() => this.updateDirty(driveNo, true),
|
||||||
this.io);
|
this.io);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown disk format ${disk.encoding}`);
|
throw new Error(`Unknown disk format ${disk.encoding}`);
|
||||||
@ -1223,12 +718,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
this.updateActiveDrive();
|
this.updateActiveDrive();
|
||||||
}
|
}
|
||||||
|
|
||||||
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
|
private insertDisk(driveNo: DriveNumber, disk: FloppyDisk) {
|
||||||
this.setDiskInternal(drive, disk);
|
this.setDiskInternal(driveNo, disk);
|
||||||
this.drives[drive].head = 0;
|
this.drives[driveNo].head = 0;
|
||||||
const { name, side } = disk.metadata;
|
const { name, side } = disk.metadata;
|
||||||
this.updateDirty(drive, this.drives[drive].dirty);
|
this.updateDirty(driveNo, this.drives[driveNo].dirty);
|
||||||
this.callbacks.label(drive, name, side);
|
this.callbacks.label(driveNo, name, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1241,8 +736,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
* an error will be thrown. Using `ext == 'nib'` will always return
|
* an error will be thrown. Using `ext == 'nib'` will always return
|
||||||
* an image.
|
* an image.
|
||||||
*/
|
*/
|
||||||
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
|
getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
|
||||||
const curDisk = this.disks[drive];
|
const curDisk = this.disks[driveNo];
|
||||||
if (!isNibbleDisk(curDisk)) {
|
if (!isNibbleDisk(curDisk)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1289,8 +784,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work with WOZ or D13 disks
|
// TODO(flan): Does not work with WOZ or D13 disks
|
||||||
getBase64(drive: DriveNumber) {
|
getBase64(driveNo: DriveNumber) {
|
||||||
const curDisk = this.disks[drive];
|
const curDisk = this.disks[driveNo];
|
||||||
if (!isNibbleDisk(curDisk)) {
|
if (!isNibbleDisk(curDisk)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
63
js/cards/drivers/BaseDiskDriver.ts
Normal file
63
js/cards/drivers/BaseDiskDriver.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
|
||||||
|
import { ControllerState, DiskDriver, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
|
||||||
|
*/
|
||||||
|
export abstract class BaseDiskDriver implements DiskDriver {
|
||||||
|
constructor(
|
||||||
|
protected readonly driveNo: DriveNumber,
|
||||||
|
protected readonly drive: Drive,
|
||||||
|
protected readonly disk: NibbleDisk | WozDisk,
|
||||||
|
protected readonly controller: ControllerState) { }
|
||||||
|
|
||||||
|
/** Called frequently to ensure the disk is spinning. */
|
||||||
|
abstract tick(): void;
|
||||||
|
|
||||||
|
/** Called when Q6 is set LOW. */
|
||||||
|
abstract onQ6Low(): void;
|
||||||
|
|
||||||
|
/** Called when Q6 is set HIGH. */
|
||||||
|
abstract onQ6High(readMode: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when drive is turned on. This is guaranteed to be called
|
||||||
|
* only when the associated drive is toggled from off to on. This
|
||||||
|
* is also guaranteed to be called when a new disk is inserted when
|
||||||
|
* the drive is already on.
|
||||||
|
*/
|
||||||
|
abstract onDriveOn(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when drive is turned off. This is guaranteed to be called
|
||||||
|
* only when the associated drive is toggled from on to off.
|
||||||
|
*/
|
||||||
|
abstract onDriveOff(): void;
|
||||||
|
|
||||||
|
debug(..._args: unknown[]) {
|
||||||
|
// debug(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called every time the head moves to clamp the track to a valid
|
||||||
|
* range.
|
||||||
|
*/
|
||||||
|
abstract clampTrack(): void;
|
||||||
|
|
||||||
|
/** Returns `true` if the controller is on and this drive is selected. */
|
||||||
|
isOn(): boolean {
|
||||||
|
return this.controller.on && this.controller.driveNo === this.driveNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if the drive's write protect switch is enabled. */
|
||||||
|
isWriteProtected(): boolean {
|
||||||
|
return this.drive.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current state of the driver as a serializable object. */
|
||||||
|
abstract getState(): DriverState;
|
||||||
|
|
||||||
|
/** Sets the state of the driver from the given `state`. */
|
||||||
|
abstract setState(state: DriverState): void;
|
||||||
|
}
|
53
js/cards/drivers/EmptyDriver.ts
Normal file
53
js/cards/drivers/EmptyDriver.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { DiskDriver, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
/** Returned state for an empty drive. */
|
||||||
|
export interface EmptyDriverState extends DriverState { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Driver for empty drives. This implementation does nothing except keep
|
||||||
|
* the head clamped between tracks 0 and 34.
|
||||||
|
*/
|
||||||
|
export class EmptyDriver implements DiskDriver {
|
||||||
|
constructor(private readonly drive: Drive) { }
|
||||||
|
|
||||||
|
tick(): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6Low(): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6High(_readMode: boolean): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOn(): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOff(): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
clampTrack(): void {
|
||||||
|
// For empty drives, the emulator clamps the track to 0 to 34,
|
||||||
|
// but real Disk II drives can seek past track 34 by at least a
|
||||||
|
// half track, usually a full track. Some 3rd party drives can
|
||||||
|
// seek to track 39.
|
||||||
|
if (this.drive.track < 0) {
|
||||||
|
this.drive.track = 0;
|
||||||
|
}
|
||||||
|
if (this.drive.track > 34) {
|
||||||
|
this.drive.track = 34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(_state: EmptyDriverState): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
106
js/cards/drivers/NibbleDiskDriver.ts
Normal file
106
js/cards/drivers/NibbleDiskDriver.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { DriveNumber, NibbleDisk } from '../../formats/types';
|
||||||
|
import { BaseDiskDriver } from './BaseDiskDriver';
|
||||||
|
import { ControllerState, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
interface NibbleDiskDriverState extends DriverState {
|
||||||
|
skip: number;
|
||||||
|
nibbleCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NibbleDiskDriver extends BaseDiskDriver {
|
||||||
|
/**
|
||||||
|
* When `1`, the next nibble will be available for read; when `0`,
|
||||||
|
* the card is pretending to wait for data to be shifted in by the
|
||||||
|
* sequencer.
|
||||||
|
*/
|
||||||
|
private skip: number = 0;
|
||||||
|
/** Number of nibbles reads since the drive was turned on. */
|
||||||
|
private nibbleCount: number = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
driveNo: DriveNumber,
|
||||||
|
drive: Drive,
|
||||||
|
readonly disk: NibbleDisk,
|
||||||
|
controller: ControllerState,
|
||||||
|
private readonly onDirty: () => void) {
|
||||||
|
super(driveNo, drive, disk, controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6Low(): void {
|
||||||
|
const drive = this.drive;
|
||||||
|
const disk = this.disk;
|
||||||
|
if (this.isOn() && (this.skip || this.controller.q7)) {
|
||||||
|
const track = disk.tracks[drive.track >> 2];
|
||||||
|
if (track && track.length) {
|
||||||
|
if (drive.head >= track.length) {
|
||||||
|
drive.head = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.controller.q7) {
|
||||||
|
const writeProtected = disk.readOnly;
|
||||||
|
if (!writeProtected) {
|
||||||
|
track[drive.head] = this.controller.bus;
|
||||||
|
drive.dirty = true;
|
||||||
|
this.onDirty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.controller.latch = track[drive.head];
|
||||||
|
this.nibbleCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
++drive.head;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.controller.latch = 0;
|
||||||
|
}
|
||||||
|
this.skip = (++this.skip % 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6High(readMode: boolean): void {
|
||||||
|
const drive = this.drive;
|
||||||
|
if (readMode && !this.controller.q7) {
|
||||||
|
const writeProtected = drive.readOnly;
|
||||||
|
if (writeProtected) {
|
||||||
|
this.controller.latch = 0xff;
|
||||||
|
this.debug('Setting readOnly');
|
||||||
|
} else {
|
||||||
|
this.controller.latch >>= 1;
|
||||||
|
this.debug('Clearing readOnly');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOn(): void {
|
||||||
|
this.nibbleCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOff(): void {
|
||||||
|
this.debug('nibbles read', this.nibbleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
clampTrack(): void {
|
||||||
|
// For NibbleDisks, the emulator clamps the track to the available
|
||||||
|
// range.
|
||||||
|
if (this.drive.track < 0) {
|
||||||
|
this.drive.track = 0;
|
||||||
|
}
|
||||||
|
const lastTrack = 35 * 4 - 1;
|
||||||
|
if (this.drive.track > lastTrack) {
|
||||||
|
this.drive.track = lastTrack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): NibbleDiskDriverState {
|
||||||
|
const { skip, nibbleCount } = this;
|
||||||
|
return { skip, nibbleCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: NibbleDiskDriverState) {
|
||||||
|
this.skip = state.skip;
|
||||||
|
this.nibbleCount = state.nibbleCount;
|
||||||
|
}
|
||||||
|
}
|
225
js/cards/drivers/WozDiskDriver.ts
Normal file
225
js/cards/drivers/WozDiskDriver.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import Apple2IO from '../../apple2io';
|
||||||
|
import { DriveNumber, WozDisk } from '../../formats/types';
|
||||||
|
import { toHex } from '../../util';
|
||||||
|
import { SEQUENCER_ROM } from '../disk2';
|
||||||
|
import { BaseDiskDriver } from './BaseDiskDriver';
|
||||||
|
import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types';
|
||||||
|
|
||||||
|
interface WozDiskDriverState extends DriverState {
|
||||||
|
clock: LssClockCycle;
|
||||||
|
state: LssState;
|
||||||
|
lastCycles: number;
|
||||||
|
zeros: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WozDiskDriver extends BaseDiskDriver {
|
||||||
|
/** Logic state sequencer clock cycle. */
|
||||||
|
private clock: LssClockCycle;
|
||||||
|
/** Logic state sequencer state. */
|
||||||
|
private state: LssState;
|
||||||
|
/** Current CPU cycle count. */
|
||||||
|
private lastCycles: number = 0;
|
||||||
|
/**
|
||||||
|
* Number of zeros read in a row. The Disk ][ can only read two zeros in a
|
||||||
|
* row reliably; above that and the drive starts reporting garbage. See
|
||||||
|
* "Freaking Out Like a MC3470" in the WOZ spec.
|
||||||
|
*/
|
||||||
|
private zeros = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
driveNo: DriveNumber,
|
||||||
|
drive: Drive,
|
||||||
|
readonly disk: WozDisk,
|
||||||
|
controller: ControllerState,
|
||||||
|
private readonly onDirty: () => void,
|
||||||
|
private readonly io: Apple2IO) {
|
||||||
|
super(driveNo, drive, disk, controller);
|
||||||
|
|
||||||
|
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
||||||
|
// essentially the start of the sequencer loop and produces
|
||||||
|
// correctly synced nibbles immediately. Starting at state 0
|
||||||
|
// would introduce a spurrious 1 in the latch at the beginning,
|
||||||
|
// which requires reading several more sync bytes to sync up.
|
||||||
|
this.state = 2;
|
||||||
|
this.clock = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOn(): void {
|
||||||
|
this.lastCycles = this.io.cycles();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOff(): void {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spin the disk under the read/write head for WOZ images.
|
||||||
|
*
|
||||||
|
* This implementation emulates every clock cycle of the 2 MHz
|
||||||
|
* sequencer since the last time it was called in order to
|
||||||
|
* determine the current state. Because this is called on
|
||||||
|
* every access to the softswitches, the data in the latch
|
||||||
|
* will be correct on every read.
|
||||||
|
*
|
||||||
|
* The emulation of the disk makes a few simplifying assumptions:
|
||||||
|
*
|
||||||
|
* * The motor turns on instantly.
|
||||||
|
* * The head moves tracks instantly.
|
||||||
|
* * The length (in bits) of each track of the WOZ image
|
||||||
|
* represents one full rotation of the disk and that each
|
||||||
|
* bit is evenly spaced.
|
||||||
|
* * Writing will not change the track length. This means
|
||||||
|
* that short tracks stay short.
|
||||||
|
* * The read head picks up the next bit when the sequencer
|
||||||
|
* clock === 4.
|
||||||
|
* * Head position X on track T is equivalent to head position
|
||||||
|
* X on track T′. (This is not the recommendation in the WOZ
|
||||||
|
* spec.)
|
||||||
|
* * Unspecified tracks contain a single zero bit. (A very
|
||||||
|
* short track, indeed!)
|
||||||
|
* * Two zero bits are sufficient to cause the MC3470 to freak
|
||||||
|
* out. When freaking out, it returns 0 and 1 with equal
|
||||||
|
* probability.
|
||||||
|
* * Any softswitch changes happen before `moveHead`. This is
|
||||||
|
* important because it means that if the clock is ever
|
||||||
|
* advanced more than one cycle between calls, the
|
||||||
|
* softswitch changes will appear to happen at the very
|
||||||
|
* beginning, not just before the last cycle.
|
||||||
|
*/
|
||||||
|
private moveHead() {
|
||||||
|
// TODO(flan): Short-circuit if the drive is not on.
|
||||||
|
const cycles = this.io.cycles();
|
||||||
|
|
||||||
|
// Spin the disk the number of elapsed cycles since last call
|
||||||
|
let workCycles = (cycles - this.lastCycles) * 2;
|
||||||
|
this.lastCycles = cycles;
|
||||||
|
|
||||||
|
const drive = this.drive;
|
||||||
|
const disk = this.disk;
|
||||||
|
const controller = this.controller;
|
||||||
|
|
||||||
|
// TODO(flan): Improve unformatted track behavior. The WOZ
|
||||||
|
// documentation suggests using an empty track of 6400 bytes
|
||||||
|
// (51,200 bits).
|
||||||
|
const track = disk.rawTracks[disk.trackMap[drive.track]] || [0];
|
||||||
|
|
||||||
|
while (workCycles-- > 0) {
|
||||||
|
let pulse: number = 0;
|
||||||
|
if (this.clock === 4) {
|
||||||
|
pulse = track[drive.head];
|
||||||
|
if (!pulse) {
|
||||||
|
// More than 2 zeros can not be read reliably.
|
||||||
|
// TODO(flan): Revisit with the new MC3470
|
||||||
|
// suggested 4-bit window behavior.
|
||||||
|
if (++this.zeros > 2) {
|
||||||
|
const r = Math.random();
|
||||||
|
pulse = r >= 0.5 ? 1 : 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.zeros = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
idx |= pulse ? 0x00 : 0x01;
|
||||||
|
idx |= controller.latch & 0x80 ? 0x02 : 0x00;
|
||||||
|
idx |= controller.q6 ? 0x04 : 0x00;
|
||||||
|
idx |= controller.q7 ? 0x08 : 0x00;
|
||||||
|
idx |= this.state << 4;
|
||||||
|
|
||||||
|
const command = SEQUENCER_ROM[controller.sectors][idx];
|
||||||
|
|
||||||
|
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
|
||||||
|
|
||||||
|
switch (command & 0xf) {
|
||||||
|
case 0x0: // CLR
|
||||||
|
controller.latch = 0;
|
||||||
|
break;
|
||||||
|
case 0x8: // NOP
|
||||||
|
break;
|
||||||
|
case 0x9: // SL0
|
||||||
|
controller.latch = (controller.latch << 1) & 0xff;
|
||||||
|
break;
|
||||||
|
case 0xA: // SR
|
||||||
|
controller.latch >>= 1;
|
||||||
|
if (this.isWriteProtected()) {
|
||||||
|
controller.latch |= 0x80;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0xB: // LD
|
||||||
|
controller.latch = controller.bus;
|
||||||
|
this.debug('Loading', toHex(controller.latch), 'from bus');
|
||||||
|
break;
|
||||||
|
case 0xD: // SL1
|
||||||
|
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
||||||
|
}
|
||||||
|
this.state = (command >> 4 & 0xF) as LssState;
|
||||||
|
|
||||||
|
if (this.clock === 4) {
|
||||||
|
if (this.isOn()) {
|
||||||
|
if (controller.q7) {
|
||||||
|
// TODO(flan): This assumes that writes are happening in
|
||||||
|
// a "friendly" way, namely where the track was originally
|
||||||
|
// written. To do this correctly, the virtual head should
|
||||||
|
// actually keep track of the current quarter track plus
|
||||||
|
// the one on each side. Then, when writing, it should
|
||||||
|
// check that all three are actually the same rawTrack. If
|
||||||
|
// they aren't, then the trackMap has to be updated as
|
||||||
|
// well.
|
||||||
|
track[drive.head] = this.state & 0x8 ? 0x01 : 0x00;
|
||||||
|
this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00);
|
||||||
|
drive.dirty = true;
|
||||||
|
this.onDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++drive.head >= track.length) {
|
||||||
|
drive.head = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++this.clock > 7) {
|
||||||
|
this.clock = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(): void {
|
||||||
|
this.moveHead();
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6High(_readMode: boolean): void {
|
||||||
|
// nothing?
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6Low(): void {
|
||||||
|
// nothing?
|
||||||
|
}
|
||||||
|
|
||||||
|
clampTrack(): void {
|
||||||
|
// For NibbleDisks, the emulator clamps the track to the available
|
||||||
|
// range.
|
||||||
|
if (this.drive.track < 0) {
|
||||||
|
this.drive.track = 0;
|
||||||
|
}
|
||||||
|
const lastTrack = this.disk.trackMap.length - 1;
|
||||||
|
if (this.drive.track > lastTrack) {
|
||||||
|
this.drive.track = lastTrack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): WozDiskDriverState {
|
||||||
|
const { clock, state, lastCycles, zeros } = this;
|
||||||
|
return { clock, state, lastCycles, zeros };
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: WozDiskDriverState) {
|
||||||
|
this.clock = state.clock;
|
||||||
|
this.state = state.state;
|
||||||
|
this.lastCycles = state.lastCycles;
|
||||||
|
this.zeros = state.zeros;
|
||||||
|
}
|
||||||
|
}
|
66
js/cards/drivers/types.ts
Normal file
66
js/cards/drivers/types.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { DriveNumber, SupportedSectors } from 'js/formats/types';
|
||||||
|
import { byte, nibble } from 'js/types';
|
||||||
|
|
||||||
|
export type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||||
|
export type LssState = nibble;
|
||||||
|
|
||||||
|
export type Phase = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the controller.
|
||||||
|
*/
|
||||||
|
export interface ControllerState {
|
||||||
|
/** Sectors supported by the controller. */
|
||||||
|
sectors: SupportedSectors;
|
||||||
|
|
||||||
|
/** Is the active drive powered on? */
|
||||||
|
on: boolean;
|
||||||
|
|
||||||
|
/** The active drive. */
|
||||||
|
driveNo: DriveNumber;
|
||||||
|
|
||||||
|
/** The 8-cycle LSS clock. */
|
||||||
|
clock: LssClockCycle;
|
||||||
|
|
||||||
|
/** Current state of the Logic State Sequencer. */
|
||||||
|
state: LssState;
|
||||||
|
|
||||||
|
/** Q6 (Shift/Load) */
|
||||||
|
q6: boolean;
|
||||||
|
/** Q7 (Read/Write) */
|
||||||
|
q7: boolean;
|
||||||
|
|
||||||
|
/** Last data from the disk drive. */
|
||||||
|
latch: byte;
|
||||||
|
/** Last data written by the CPU to card softswitch 0x8D. */
|
||||||
|
bus: byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Common information for Nibble and WOZ disks. */
|
||||||
|
export interface Drive {
|
||||||
|
/** Whether the drive write protect is on. */
|
||||||
|
readOnly: boolean;
|
||||||
|
/** Quarter track position of read/write head. */
|
||||||
|
track: byte;
|
||||||
|
/** Position of the head on the track. */
|
||||||
|
head: byte;
|
||||||
|
/** Current active coil in the head stepper motor. */
|
||||||
|
phase: Phase;
|
||||||
|
/** Whether the drive has been written to since it was loaded. */
|
||||||
|
dirty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base interface for disk driver states. */
|
||||||
|
export interface DriverState {}
|
||||||
|
|
||||||
|
/** Interface for drivers for various disk types. */
|
||||||
|
export interface DiskDriver {
|
||||||
|
tick(): void;
|
||||||
|
onQ6Low(): void;
|
||||||
|
onQ6High(readMode: boolean): void;
|
||||||
|
onDriveOn(): void;
|
||||||
|
onDriveOff(): void;
|
||||||
|
clampTrack(): void;
|
||||||
|
getState(): DriverState;
|
||||||
|
setState(state: DriverState): void;
|
||||||
|
}
|
@ -18,9 +18,9 @@ export interface SmartPortOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Callbacks {
|
export interface Callbacks {
|
||||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
driveLight: (driveNo: DriveNumber, on: boolean) => void;
|
||||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
dirty: (driveNo: DriveNumber, dirty: boolean) => void;
|
||||||
label: (drive: DriveNumber, name?: string, side?: string) => void;
|
label: (driveNo: DriveNumber, name?: string, side?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Address {
|
class Address {
|
||||||
@ -152,15 +152,15 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
// debug.apply(this, arguments);
|
// debug.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private driveLight(drive: DriveNumber) {
|
private driveLight(driveNo: DriveNumber) {
|
||||||
if (!this.busy[drive]) {
|
if (!this.busy[driveNo]) {
|
||||||
this.busy[drive] = true;
|
this.busy[driveNo] = true;
|
||||||
this.callbacks?.driveLight(drive, true);
|
this.callbacks?.driveLight(driveNo, true);
|
||||||
}
|
}
|
||||||
clearTimeout(this.busyTimeout[drive]);
|
clearTimeout(this.busyTimeout[driveNo]);
|
||||||
this.busyTimeout[drive] = setTimeout(() => {
|
this.busyTimeout[driveNo] = setTimeout(() => {
|
||||||
this.busy[drive] = false;
|
this.busy[driveNo] = false;
|
||||||
this.callbacks?.driveLight(drive, false);
|
this.callbacks?.driveLight(driveNo, false);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
* dumpBlock
|
* dumpBlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
dumpBlock(drive: DriveNumber, block: number) {
|
dumpBlock(driveNo: DriveNumber, block: number) {
|
||||||
let result = '';
|
let result = '';
|
||||||
let b;
|
let b;
|
||||||
let jdx;
|
let jdx;
|
||||||
@ -176,7 +176,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
for (let idx = 0; idx < 32; idx++) {
|
for (let idx = 0; idx < 32; idx++) {
|
||||||
result += toHex(idx << 4, 4) + ': ';
|
result += toHex(idx << 4, 4) + ': ';
|
||||||
for (jdx = 0; jdx < 16; jdx++) {
|
for (jdx = 0; jdx < 16; jdx++) {
|
||||||
b = this.disks[drive].blocks[block][idx * 16 + jdx];
|
b = this.disks[driveNo].blocks[block][idx * 16 + jdx];
|
||||||
if (jdx === 8) {
|
if (jdx === 8) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
}
|
}
|
||||||
result += ' ';
|
result += ' ';
|
||||||
for (jdx = 0; jdx < 16; jdx++) {
|
for (jdx = 0; jdx < 16; jdx++) {
|
||||||
b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f;
|
b = this.disks[driveNo].blocks[block][idx * 16 + jdx] & 0x7f;
|
||||||
if (jdx === 8) {
|
if (jdx === 8) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
@ -203,9 +203,9 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
* getDeviceInfo
|
* getDeviceInfo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDeviceInfo(state: CpuState, drive: DriveNumber) {
|
getDeviceInfo(state: CpuState, driveNo: DriveNumber) {
|
||||||
if (this.disks[drive]) {
|
if (this.disks[driveNo]) {
|
||||||
const blocks = this.disks[drive].blocks.length;
|
const blocks = this.disks[driveNo].blocks.length;
|
||||||
state.x = blocks & 0xff;
|
state.x = blocks & 0xff;
|
||||||
state.y = blocks >> 8;
|
state.y = blocks >> 8;
|
||||||
|
|
||||||
@ -221,23 +221,23 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
* readBlock
|
* readBlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
readBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
|
readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
|
||||||
this.debug(`read drive=${drive}`);
|
this.debug(`read drive=${driveNo}`);
|
||||||
this.debug(`read buffer=${buffer.toString()}`);
|
this.debug(`read buffer=${buffer.toString()}`);
|
||||||
this.debug(`read block=$${toHex(block)}`);
|
this.debug(`read block=$${toHex(block)}`);
|
||||||
|
|
||||||
if (!this.disks[drive]?.blocks.length) {
|
if (!this.disks[driveNo]?.blocks.length) {
|
||||||
debug('Drive', drive, 'is empty');
|
debug('Drive', driveNo, 'is empty');
|
||||||
state.a = DEVICE_OFFLINE;
|
state.a = DEVICE_OFFLINE;
|
||||||
state.s |= flags.C;
|
state.s |= flags.C;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug('read', '\n' + dumpBlock(drive, block));
|
// debug('read', '\n' + dumpBlock(drive, block));
|
||||||
this.driveLight(drive);
|
this.driveLight(driveNo);
|
||||||
|
|
||||||
for (let idx = 0; idx < 512; idx++) {
|
for (let idx = 0; idx < 512; idx++) {
|
||||||
buffer.writeByte(this.disks[drive].blocks[block][idx]);
|
buffer.writeByte(this.disks[driveNo].blocks[block][idx]);
|
||||||
buffer = buffer.inc(1);
|
buffer = buffer.inc(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,30 +249,30 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
* writeBlock
|
* writeBlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
writeBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
|
writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
|
||||||
this.debug(`write drive=${drive}`);
|
this.debug(`write drive=${driveNo}`);
|
||||||
this.debug(`write buffer=${buffer.toString()}`);
|
this.debug(`write buffer=${buffer.toString()}`);
|
||||||
this.debug(`write block=$${toHex(block)}`);
|
this.debug(`write block=$${toHex(block)}`);
|
||||||
|
|
||||||
if (!this.disks[drive]?.blocks.length) {
|
if (!this.disks[driveNo]?.blocks.length) {
|
||||||
debug('Drive', drive, 'is empty');
|
debug('Drive', driveNo, 'is empty');
|
||||||
state.a = DEVICE_OFFLINE;
|
state.a = DEVICE_OFFLINE;
|
||||||
state.s |= flags.C;
|
state.s |= flags.C;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.disks[drive].readOnly) {
|
if (this.disks[driveNo].readOnly) {
|
||||||
debug('Drive', drive, 'is write protected');
|
debug('Drive', driveNo, 'is write protected');
|
||||||
state.a = WRITE_PROTECTED;
|
state.a = WRITE_PROTECTED;
|
||||||
state.s |= flags.C;
|
state.s |= flags.C;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug('write', '\n' + dumpBlock(drive, block));
|
// debug('write', '\n' + dumpBlock(drive, block));
|
||||||
this.driveLight(drive);
|
this.driveLight(driveNo);
|
||||||
|
|
||||||
for (let idx = 0; idx < 512; idx++) {
|
for (let idx = 0; idx < 512; idx++) {
|
||||||
this.disks[drive].blocks[block][idx] = buffer.readByte();
|
this.disks[driveNo].blocks[block][idx] = buffer.readByte();
|
||||||
buffer = buffer.inc(1);
|
buffer = buffer.inc(1);
|
||||||
}
|
}
|
||||||
state.a = 0;
|
state.a = 0;
|
||||||
@ -283,25 +283,25 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
* formatDevice
|
* formatDevice
|
||||||
*/
|
*/
|
||||||
|
|
||||||
formatDevice(state: CpuState, drive: DriveNumber) {
|
formatDevice(state: CpuState, driveNo: DriveNumber) {
|
||||||
if (!this.disks[drive]?.blocks.length) {
|
if (!this.disks[driveNo]?.blocks.length) {
|
||||||
debug('Drive', drive, 'is empty');
|
debug('Drive', driveNo, 'is empty');
|
||||||
state.a = DEVICE_OFFLINE;
|
state.a = DEVICE_OFFLINE;
|
||||||
state.s |= flags.C;
|
state.s |= flags.C;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.disks[drive].readOnly) {
|
if (this.disks[driveNo].readOnly) {
|
||||||
debug('Drive', drive, 'is write protected');
|
debug('Drive', driveNo, 'is write protected');
|
||||||
state.a = WRITE_PROTECTED;
|
state.a = WRITE_PROTECTED;
|
||||||
state.s |= flags.C;
|
state.s |= flags.C;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) {
|
for (let idx = 0; idx < this.disks[driveNo].blocks.length; idx++) {
|
||||||
this.disks[drive].blocks[idx] = new Uint8Array();
|
this.disks[driveNo].blocks[idx] = new Uint8Array();
|
||||||
for (let jdx = 0; jdx < 512; jdx++) {
|
for (let jdx = 0; jdx < 512; jdx++) {
|
||||||
this.disks[drive].blocks[idx][jdx] = 0;
|
this.disks[driveNo].blocks[idx][jdx] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,18 +549,18 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
|
setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
|
||||||
let volume = 254;
|
let volume = 254;
|
||||||
let readOnly = false;
|
let readOnly = false;
|
||||||
if (fmt === '2mg') {
|
if (fmt === '2mg') {
|
||||||
const header = read2MGHeader(rawData);
|
const header = read2MGHeader(rawData);
|
||||||
this.metadata[drive] = header;
|
this.metadata[driveNo] = header;
|
||||||
const { bytes, offset } = header;
|
const { bytes, offset } = header;
|
||||||
volume = header.volume;
|
volume = header.volume;
|
||||||
readOnly = header.readOnly;
|
readOnly = header.readOnly;
|
||||||
rawData = rawData.slice(offset, offset + bytes);
|
rawData = rawData.slice(offset, offset + bytes);
|
||||||
} else {
|
} else {
|
||||||
this.metadata[drive] = null;
|
this.metadata[driveNo] = null;
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
rawData,
|
rawData,
|
||||||
@ -569,9 +569,9 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||||||
volume,
|
volume,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ext[drive] = fmt;
|
this.ext[driveNo] = fmt;
|
||||||
this.disks[drive] = createBlockDisk(fmt, options);
|
this.disks[driveNo] = createBlockDisk(fmt, options);
|
||||||
this.callbacks?.label(drive, name);
|
this.callbacks?.label(driveNo, name);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -63,19 +63,19 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
|||||||
<DiskDragTarget
|
<DiskDragTarget
|
||||||
className={styles.disk}
|
className={styles.disk}
|
||||||
storage={smartPort}
|
storage={smartPort}
|
||||||
drive={number}
|
driveNo={number}
|
||||||
formats={BLOCK_FORMATS}
|
formats={BLOCK_FORMATS}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
>
|
>
|
||||||
<ErrorModal error={error} setError={setError} />
|
<ErrorModal error={error} setError={setError} />
|
||||||
<BlockFileModal
|
<BlockFileModal
|
||||||
smartPort={smartPort}
|
smartPort={smartPort}
|
||||||
number={number}
|
driveNo={number}
|
||||||
onClose={doClose}
|
onClose={doClose}
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
/>
|
/>
|
||||||
<DownloadModal
|
<DownloadModal
|
||||||
number={number}
|
driveNo={number}
|
||||||
massStorage={smartPort}
|
massStorage={smartPort}
|
||||||
isOpen={downloadModalOpen}
|
isOpen={downloadModalOpen}
|
||||||
onClose={doCloseDownload}
|
onClose={doCloseDownload}
|
||||||
|
@ -21,11 +21,11 @@ const DISK_TYPES: FilePickerAcceptType[] = [
|
|||||||
interface BlockFileModalProps {
|
interface BlockFileModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
smartPort: SmartPort;
|
smartPort: SmartPort;
|
||||||
number: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
onClose: (closeBox?: boolean) => void;
|
onClose: (closeBox?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFileModalProps) => {
|
export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen } : BlockFileModalProps) => {
|
||||||
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
|
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
|
||||||
const [busy, setBusy] = useState<boolean>(false);
|
const [busy, setBusy] = useState<boolean>(false);
|
||||||
const [empty, setEmpty] = useState<boolean>(true);
|
const [empty, setEmpty] = useState<boolean>(true);
|
||||||
|
@ -6,7 +6,7 @@ import { spawn } from './util/promises';
|
|||||||
|
|
||||||
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
|
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||||
storage: MassStorage<T> | undefined;
|
storage: MassStorage<T> | undefined;
|
||||||
drive?: DriveNumber;
|
driveNo?: DriveNumber;
|
||||||
formats: typeof FLOPPY_FORMATS
|
formats: typeof FLOPPY_FORMATS
|
||||||
| typeof BLOCK_FORMATS
|
| typeof BLOCK_FORMATS
|
||||||
| typeof DISK_FORMATS;
|
| typeof DISK_FORMATS;
|
||||||
@ -16,7 +16,7 @@ export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElemen
|
|||||||
|
|
||||||
export const DiskDragTarget = ({
|
export const DiskDragTarget = ({
|
||||||
storage,
|
storage,
|
||||||
drive,
|
driveNo,
|
||||||
dropRef,
|
dropRef,
|
||||||
formats,
|
formats,
|
||||||
onError,
|
onError,
|
||||||
@ -54,7 +54,7 @@ export const DiskDragTarget = ({
|
|||||||
const onDrop = (event: DragEvent) => {
|
const onDrop = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const targetDrive = drive ?? 1; //TODO(whscullin) Maybe pick available drive
|
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
|
||||||
|
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
if (dt?.files.length === 1 && storage) {
|
if (dt?.files.length === 1 && storage) {
|
||||||
@ -87,7 +87,7 @@ export const DiskDragTarget = ({
|
|||||||
div.removeEventListener('drop', onDrop);
|
div.removeEventListener('drop', onDrop);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [drive, dropRef, formats, onError, storage]);
|
}, [driveNo, dropRef, formats, onError, storage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} {...props}>
|
<div ref={ref} {...props}>
|
||||||
|
@ -65,13 +65,13 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
|||||||
<DiskDragTarget
|
<DiskDragTarget
|
||||||
className={styles.disk}
|
className={styles.disk}
|
||||||
storage={disk2}
|
storage={disk2}
|
||||||
drive={number}
|
driveNo={number}
|
||||||
formats={FLOPPY_FORMATS}
|
formats={FLOPPY_FORMATS}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
>
|
>
|
||||||
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} />
|
<FileModal disk2={disk2} driveNo={number} onClose={doClose} isOpen={modalOpen} />
|
||||||
<DownloadModal
|
<DownloadModal
|
||||||
number={number}
|
driveNo={number}
|
||||||
massStorage={disk2}
|
massStorage={disk2}
|
||||||
isOpen={downloadModalOpen}
|
isOpen={downloadModalOpen}
|
||||||
onClose={doCloseDownload}
|
onClose={doCloseDownload}
|
||||||
|
@ -8,18 +8,18 @@ import styles from './css/DownloadModal.module.css';
|
|||||||
interface DownloadModalProps {
|
interface DownloadModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
massStorage: MassStorage<unknown>;
|
massStorage: MassStorage<unknown>;
|
||||||
number: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
onClose: (closeBox?: boolean) => void;
|
onClose: (closeBox?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadModal = ({ massStorage, number, onClose, isOpen } : DownloadModalProps) => {
|
export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen } : DownloadModalProps) => {
|
||||||
const [href, setHref] = useState('');
|
const [href, setHref] = useState('');
|
||||||
const [downloadName, setDownloadName] = useState('');
|
const [downloadName, setDownloadName] = useState('');
|
||||||
const doCancel = useCallback(() => onClose(true), [onClose]);
|
const doCancel = useCallback(() => onClose(true), [onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const storageData = massStorage.getBinary(number);
|
const storageData = massStorage.getBinary(driveNo);
|
||||||
if (storageData) {
|
if (storageData) {
|
||||||
const { ext, data } = storageData;
|
const { ext, data } = storageData;
|
||||||
const { name } = storageData.metadata;
|
const { name } = storageData.metadata;
|
||||||
@ -37,7 +37,7 @@ export const DownloadModal = ({ massStorage, number, onClose, isOpen } : Downloa
|
|||||||
setHref('');
|
setHref('');
|
||||||
setDownloadName('');
|
setDownloadName('');
|
||||||
}
|
}
|
||||||
}, [isOpen, number, massStorage]);
|
}, [isOpen, driveNo, massStorage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -10,7 +10,7 @@ import { ErrorModal } from './ErrorModal';
|
|||||||
import { ProgressModal } from './ProgressModal';
|
import { ProgressModal } from './ProgressModal';
|
||||||
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
|
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
|
||||||
import { useHash } from './hooks/useHash';
|
import { useHash } from './hooks/useHash';
|
||||||
import { DISK_FORMATS, DriveNumber, SupportedSectors } from 'js/formats/types';
|
import { DISK_FORMATS, DRIVE_NUMBERS, SupportedSectors } from 'js/formats/types';
|
||||||
import { spawn, Ready } from './util/promises';
|
import { spawn, Ready } from './util/promises';
|
||||||
|
|
||||||
import styles from './css/Drives.module.css';
|
import styles from './css/Drives.module.css';
|
||||||
@ -90,9 +90,9 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
|||||||
const hashParts = getHashParts(hash);
|
const hashParts = getHashParts(hash);
|
||||||
const controllers: AbortController[] = [];
|
const controllers: AbortController[] = [];
|
||||||
let loading = 0;
|
let loading = 0;
|
||||||
for (const drive of [1, 2] as DriveNumber[]) {
|
for (const driveNo of DRIVE_NUMBERS) {
|
||||||
if (hashParts && hashParts[drive]) {
|
if (hashParts && hashParts[driveNo]) {
|
||||||
const hashPart = decodeURIComponent(hashParts[drive]);
|
const hashPart = decodeURIComponent(hashParts[driveNo]);
|
||||||
const isHttp = hashPart.match(/^https?:/i);
|
const isHttp = hashPart.match(/^https?:/i);
|
||||||
const isJson = hashPart.match(/\.json$/i);
|
const isJson = hashPart.match(/\.json$/i);
|
||||||
if (isHttp && !isJson) {
|
if (isHttp && !isJson) {
|
||||||
@ -101,7 +101,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
|||||||
try {
|
try {
|
||||||
await loadHttpUnknownFile(
|
await loadHttpUnknownFile(
|
||||||
smartStorageBroker,
|
smartStorageBroker,
|
||||||
drive,
|
driveNo,
|
||||||
hashPart,
|
hashPart,
|
||||||
signal,
|
signal,
|
||||||
onProgress);
|
onProgress);
|
||||||
@ -116,7 +116,7 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
|
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
|
||||||
loadJSON(disk2, drive, url).catch((e) => setError(e));
|
loadJSON(disk2, driveNo, url).catch((e) => setError(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export type NibbleFileCallback = (
|
|||||||
interface FileModalProps {
|
interface FileModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
disk2: DiskII;
|
disk2: DiskII;
|
||||||
number: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
onClose: (closeBox?: boolean) => void;
|
onClose: (closeBox?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ interface IndexEntry {
|
|||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => {
|
export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
|
||||||
const [busy, setBusy] = useState<boolean>(false);
|
const [busy, setBusy] = useState<boolean>(false);
|
||||||
const [empty, setEmpty] = useState<boolean>(true);
|
const [empty, setEmpty] = useState<boolean>(true);
|
||||||
const [category, setCategory] = useState<string>();
|
const [category, setCategory] = useState<string>();
|
||||||
@ -69,13 +69,13 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (handles?.length === 1) {
|
if (handles?.length === 1) {
|
||||||
hashParts[number] = '';
|
hashParts[driveNo] = '';
|
||||||
await loadLocalNibbleFile(disk2, number, await handles[0].getFile());
|
await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
|
||||||
}
|
}
|
||||||
if (filename) {
|
if (filename) {
|
||||||
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
|
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
|
||||||
hashParts[number] = name[1];
|
hashParts[driveNo] = name[1];
|
||||||
await loadJSON(disk2, number, filename);
|
await loadJSON(disk2, driveNo, filename);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e);
|
setError(e);
|
||||||
@ -86,7 +86,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHashParts(hashParts);
|
setHashParts(hashParts);
|
||||||
}, [disk2, filename, number, onClose, handles, hash]);
|
}, [disk2, filename, driveNo, onClose, handles, hash]);
|
||||||
|
|
||||||
const onChange = useCallback((handles: FileSystemFileHandle[]) => {
|
const onChange = useCallback((handles: FileSystemFileHandle[]) => {
|
||||||
setEmpty(handles.length === 0);
|
setEmpty(handles.length === 0);
|
||||||
|
@ -235,7 +235,7 @@ const Catalog = ({ dos, setFileData }: CatalogProps) => {
|
|||||||
*/
|
*/
|
||||||
interface DiskInfoProps {
|
interface DiskInfoProps {
|
||||||
massStorage: MassStorage<DiskFormat>;
|
massStorage: MassStorage<DiskFormat>;
|
||||||
drive: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
setFileData: (fileData: FileData) => void;
|
setFileData: (fileData: FileData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,9 +250,9 @@ interface DiskInfoProps {
|
|||||||
* @param drive The drive number
|
* @param drive The drive number
|
||||||
* @returns DiskInfo component
|
* @returns DiskInfo component
|
||||||
*/
|
*/
|
||||||
const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
|
const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
|
||||||
const disk = useMemo(() => {
|
const disk = useMemo(() => {
|
||||||
const massStorageData = massStorage.getBinary(drive, 'po');
|
const massStorageData = massStorage.getBinary(driveNo, 'po');
|
||||||
if (massStorageData) {
|
if (massStorageData) {
|
||||||
const { data, readOnly, ext } = massStorageData;
|
const { data, readOnly, ext } = massStorageData;
|
||||||
const { name } = massStorageData.metadata;
|
const { name } = massStorageData.metadata;
|
||||||
@ -265,7 +265,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
|
|||||||
volume: 254,
|
volume: 254,
|
||||||
});
|
});
|
||||||
} else if (data.byteLength < 800 * 1024) {
|
} else if (data.byteLength < 800 * 1024) {
|
||||||
const doData = massStorage.getBinary(drive, 'do');
|
const doData = massStorage.getBinary(driveNo, 'do');
|
||||||
if (doData) {
|
if (doData) {
|
||||||
if (isMaybeDOS33(doData)) {
|
if (isMaybeDOS33(doData)) {
|
||||||
disk = createDiskFromDOS({
|
disk = createDiskFromDOS({
|
||||||
@ -288,7 +288,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
|
|||||||
return disk;
|
return disk;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [massStorage, drive]);
|
}, [massStorage, driveNo]);
|
||||||
|
|
||||||
if (disk) {
|
if (disk) {
|
||||||
try {
|
try {
|
||||||
@ -409,11 +409,11 @@ export const Disks = ({ apple2 }: DisksProps) => {
|
|||||||
<div className={debuggerStyles.subHeading}>
|
<div className={debuggerStyles.subHeading}>
|
||||||
{card.constructor.name} - 1
|
{card.constructor.name} - 1
|
||||||
</div>
|
</div>
|
||||||
<DiskInfo massStorage={card} drive={1} setFileData={setFileData} />
|
<DiskInfo massStorage={card} driveNo={1} setFileData={setFileData} />
|
||||||
<div className={debuggerStyles.subHeading}>
|
<div className={debuggerStyles.subHeading}>
|
||||||
{card.constructor.name} - 2
|
{card.constructor.name} - 2
|
||||||
</div>
|
</div>
|
||||||
<DiskInfo massStorage={card} drive={2} setFileData={setFileData} />
|
<DiskInfo massStorage={card} driveNo={2} setFileData={setFileData} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<FileViewer fileData={fileData} onClose={onClose} />
|
<FileViewer fileData={fileData} onClose={onClose} />
|
||||||
|
@ -48,7 +48,7 @@ export const getNameAndExtension = (url: string) => {
|
|||||||
export const loadLocalFile = (
|
export const loadLocalFile = (
|
||||||
storage: MassStorage<FloppyFormat|BlockFormat>,
|
storage: MassStorage<FloppyFormat|BlockFormat>,
|
||||||
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
|
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
|
||||||
number: DriveNumber,
|
driveNo: DriveNumber,
|
||||||
file: File,
|
file: File,
|
||||||
) => {
|
) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -58,7 +58,7 @@ export const loadLocalFile = (
|
|||||||
const { name, ext } = getNameAndExtension(file.name);
|
const { name, ext } = getNameAndExtension(file.name);
|
||||||
if (includes(formats, ext)) {
|
if (includes(formats, ext)) {
|
||||||
initGamepad();
|
initGamepad();
|
||||||
if (storage.setBinary(number, name, ext, result)) {
|
if (storage.setBinary(driveNo, name, ext, result)) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
reject(`Unable to load ${name}`);
|
reject(`Unable to load ${name}`);
|
||||||
@ -76,12 +76,12 @@ export const loadLocalFile = (
|
|||||||
* selection form element to be loaded.
|
* selection form element to be loaded.
|
||||||
*
|
*
|
||||||
* @param smartPort SmartPort object
|
* @param smartPort SmartPort object
|
||||||
* @param number Drive number
|
* @param driveNo Drive number
|
||||||
* @param file Browser File object to load
|
* @param file Browser File object to load
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, file: File) => {
|
export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, file: File) => {
|
||||||
return loadLocalFile(smartPort, BLOCK_FORMATS, number, file);
|
return loadLocalFile(smartPort, BLOCK_FORMATS, driveNo, file);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,12 +89,12 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi
|
|||||||
* selection form element to be loaded.
|
* selection form element to be loaded.
|
||||||
*
|
*
|
||||||
* @param disk2 Disk2 object
|
* @param disk2 Disk2 object
|
||||||
* @param number Drive number
|
* @param driveNo Drive number
|
||||||
* @param file Browser File object to load
|
* @param file Browser File object to load
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => {
|
export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: File) => {
|
||||||
return loadLocalFile(disk2, FLOPPY_FORMATS, number, file);
|
return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,13 +103,13 @@ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: Fil
|
|||||||
* as the emulator.
|
* as the emulator.
|
||||||
*
|
*
|
||||||
* @param disk2 Disk2 object
|
* @param disk2 Disk2 object
|
||||||
* @param number Drive number
|
* @param driveNo Drive number
|
||||||
* @param url URL, relative or absolute to JSON file
|
* @param url URL, relative or absolute to JSON file
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
export const loadJSON = async (
|
export const loadJSON = async (
|
||||||
disk2: Disk2,
|
disk2: Disk2,
|
||||||
number: DriveNumber,
|
driveNo: DriveNumber,
|
||||||
url: string,
|
url: string,
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
@ -120,7 +120,7 @@ export const loadJSON = async (
|
|||||||
if (!includes(FLOPPY_FORMATS, data.type)) {
|
if (!includes(FLOPPY_FORMATS, data.type)) {
|
||||||
throw new Error(`Type "${data.type}" not recognized.`);
|
throw new Error(`Type "${data.type}" not recognized.`);
|
||||||
}
|
}
|
||||||
disk2.setDisk(number, data);
|
disk2.setDisk(driveNo, data);
|
||||||
initGamepad(data.gamepad);
|
initGamepad(data.gamepad);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,13 +166,13 @@ export const loadHttpFile = async (
|
|||||||
* as the emulator.
|
* as the emulator.
|
||||||
*
|
*
|
||||||
* @param smartPort SmartPort object
|
* @param smartPort SmartPort object
|
||||||
* @param number Drive number
|
* @param driveNo Drive number
|
||||||
* @param url URL, relative or absolute to JSON file
|
* @param url URL, relative or absolute to JSON file
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
export const loadHttpBlockFile = async (
|
export const loadHttpBlockFile = async (
|
||||||
smartPort: SmartPort,
|
smartPort: SmartPort,
|
||||||
number: DriveNumber,
|
driveNo: DriveNumber,
|
||||||
url: string,
|
url: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
@ -182,7 +182,7 @@ export const loadHttpBlockFile = async (
|
|||||||
throw new Error(`Extension "${ext}" not recognized.`);
|
throw new Error(`Extension "${ext}" not recognized.`);
|
||||||
}
|
}
|
||||||
const data = await loadHttpFile(url, signal, onProgress);
|
const data = await loadHttpFile(url, signal, onProgress);
|
||||||
smartPort.setBinary(number, name, ext, data);
|
smartPort.setBinary(driveNo, name, ext, data);
|
||||||
initGamepad();
|
initGamepad();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -194,55 +194,55 @@ export const loadHttpBlockFile = async (
|
|||||||
* as the emulator.
|
* as the emulator.
|
||||||
*
|
*
|
||||||
* @param disk2 Disk2 object
|
* @param disk2 Disk2 object
|
||||||
* @param number Drive number
|
* @param driveNo Drive number
|
||||||
* @param url URL, relative or absolute to JSON file
|
* @param url URL, relative or absolute to JSON file
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
export const loadHttpNibbleFile = async (
|
export const loadHttpNibbleFile = async (
|
||||||
disk2: Disk2,
|
disk2: Disk2,
|
||||||
number: DriveNumber,
|
driveNo: DriveNumber,
|
||||||
url: string,
|
url: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
) => {
|
) => {
|
||||||
if (url.endsWith('.json')) {
|
if (url.endsWith('.json')) {
|
||||||
return loadJSON(disk2, number, url);
|
return loadJSON(disk2, driveNo, url);
|
||||||
}
|
}
|
||||||
const { name, ext } = getNameAndExtension(url);
|
const { name, ext } = getNameAndExtension(url);
|
||||||
if (!includes(FLOPPY_FORMATS, ext)) {
|
if (!includes(FLOPPY_FORMATS, ext)) {
|
||||||
throw new Error(`Extension "${ext}" not recognized.`);
|
throw new Error(`Extension "${ext}" not recognized.`);
|
||||||
}
|
}
|
||||||
const data = await loadHttpFile(url, signal, onProgress);
|
const data = await loadHttpFile(url, signal, onProgress);
|
||||||
disk2.setBinary(number, name, ext, data);
|
disk2.setBinary(driveNo, name, ext, data);
|
||||||
initGamepad();
|
initGamepad();
|
||||||
return loadHttpFile(url, signal, onProgress);
|
return loadHttpFile(url, signal, onProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadHttpUnknownFile = async (
|
export const loadHttpUnknownFile = async (
|
||||||
smartStorageBroker: SmartStorageBroker,
|
smartStorageBroker: SmartStorageBroker,
|
||||||
number: DriveNumber,
|
driveNo: DriveNumber,
|
||||||
url: string,
|
url: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
onProgress?: ProgressCallback,
|
onProgress?: ProgressCallback,
|
||||||
) => {
|
) => {
|
||||||
const data = await loadHttpFile(url, signal, onProgress);
|
const data = await loadHttpFile(url, signal, onProgress);
|
||||||
const { name, ext } = getNameAndExtension(url);
|
const { name, ext } = getNameAndExtension(url);
|
||||||
smartStorageBroker.setBinary(number, name, ext, data);
|
smartStorageBroker.setBinary(driveNo, name, ext, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SmartStorageBroker implements MassStorage<unknown> {
|
export class SmartStorageBroker implements MassStorage<unknown> {
|
||||||
constructor(private disk2: Disk2, private smartPort: SmartPort) {}
|
constructor(private disk2: Disk2, private smartPort: SmartPort) {}
|
||||||
|
|
||||||
setBinary(drive: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean {
|
setBinary(driveNo: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean {
|
||||||
if (includes(DISK_FORMATS, ext)) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
if (data.byteLength >= 800 * 1024) {
|
if (data.byteLength >= 800 * 1024) {
|
||||||
if (includes(BLOCK_FORMATS, ext)) {
|
if (includes(BLOCK_FORMATS, ext)) {
|
||||||
this.smartPort.setBinary(drive, name, ext, data);
|
this.smartPort.setBinary(driveNo, name, ext, data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unable to load "${name}"`);
|
throw new Error(`Unable to load "${name}"`);
|
||||||
}
|
}
|
||||||
} else if (includes(FLOPPY_FORMATS, ext)) {
|
} else if (includes(FLOPPY_FORMATS, ext)) {
|
||||||
this.disk2.setBinary(drive, name, ext, data);
|
this.disk2.setBinary(driveNo, name, ext, data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unable to load "${name}"`);
|
throw new Error(`Unable to load "${name}"`);
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@ export const PROCESS_JSON = 'PROCESS_JSON';
|
|||||||
export interface ProcessBinaryMessage {
|
export interface ProcessBinaryMessage {
|
||||||
type: typeof PROCESS_BINARY;
|
type: typeof PROCESS_BINARY;
|
||||||
payload: {
|
payload: {
|
||||||
drive: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
fmt: FloppyFormat;
|
fmt: FloppyFormat;
|
||||||
options: DiskOptions;
|
options: DiskOptions;
|
||||||
};
|
};
|
||||||
@ -244,7 +244,7 @@ export interface ProcessBinaryMessage {
|
|||||||
export interface ProcessJsonDiskMessage {
|
export interface ProcessJsonDiskMessage {
|
||||||
type: typeof PROCESS_JSON_DISK;
|
type: typeof PROCESS_JSON_DISK;
|
||||||
payload: {
|
payload: {
|
||||||
drive: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
jsonDisk: JSONDisk;
|
jsonDisk: JSONDisk;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -253,7 +253,7 @@ export interface ProcessJsonDiskMessage {
|
|||||||
export interface ProcessJsonMessage {
|
export interface ProcessJsonMessage {
|
||||||
type: typeof PROCESS_JSON;
|
type: typeof PROCESS_JSON;
|
||||||
payload: {
|
payload: {
|
||||||
drive: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
json: string;
|
json: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -272,7 +272,7 @@ export const DISK_PROCESSED = 'DISK_PROCESSED';
|
|||||||
export interface DiskProcessedResponse {
|
export interface DiskProcessedResponse {
|
||||||
type: typeof DISK_PROCESSED;
|
type: typeof DISK_PROCESSED;
|
||||||
payload: {
|
payload: {
|
||||||
drive: DriveNumber;
|
driveNo: DriveNumber;
|
||||||
disk: FloppyDisk | null;
|
disk: FloppyDisk | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
103
js/ui/apple2.ts
103
js/ui/apple2.ts
@ -84,7 +84,7 @@ let joystick: JoyStick;
|
|||||||
let system: System;
|
let system: System;
|
||||||
let keyboard: KeyBoard;
|
let keyboard: KeyBoard;
|
||||||
let io: Apple2IO;
|
let io: Apple2IO;
|
||||||
let _currentDrive: DriveNumber = 1;
|
let driveNo: DriveNumber = 1;
|
||||||
let _e: boolean;
|
let _e: boolean;
|
||||||
|
|
||||||
let ready: Promise<[void, void]>;
|
let ready: Promise<[void, void]>;
|
||||||
@ -103,14 +103,13 @@ export function compileApplesoftProgram(program: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function openLoad(driveString: string, event: MouseEvent) {
|
export function openLoad(driveString: string, event: MouseEvent) {
|
||||||
const drive = parseInt(driveString, 10) as DriveNumber;
|
driveNo = parseInt(driveString, 10) as DriveNumber;
|
||||||
_currentDrive = drive;
|
if (event.metaKey && includes(DRIVE_NUMBERS, driveNo)) {
|
||||||
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
|
|
||||||
openLoadHTTP();
|
openLoadHTTP();
|
||||||
} else {
|
} else {
|
||||||
if (disk_cur_cat[drive]) {
|
if (disk_cur_cat[driveNo]) {
|
||||||
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||||
element.value = disk_cur_cat[drive];
|
element.value = disk_cur_cat[driveNo];
|
||||||
selectCategory();
|
selectCategory();
|
||||||
}
|
}
|
||||||
MicroModal.show('load-modal');
|
MicroModal.show('load-modal');
|
||||||
@ -118,27 +117,27 @@ export function openLoad(driveString: string, event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function openSave(driveString: string, event: MouseEvent) {
|
export function openSave(driveString: string, event: MouseEvent) {
|
||||||
const drive = parseInt(driveString, 10) as DriveNumber;
|
const driveNo = parseInt(driveString, 10) as DriveNumber;
|
||||||
|
|
||||||
const mimeType = 'application/octet-stream';
|
const mimeType = 'application/octet-stream';
|
||||||
const storageData = _disk2.getBinary(drive);
|
const storageData = _disk2.getBinary(driveNo);
|
||||||
const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
|
const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
|
||||||
|
|
||||||
if (!storageData) {
|
if (!storageData) {
|
||||||
alert(`No data from drive ${drive}`);
|
alert(`No data from drive ${driveNo}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = storageData;
|
const { data } = storageData;
|
||||||
const blob = new Blob([data], { 'type': mimeType });
|
const blob = new Blob([data], { 'type': mimeType });
|
||||||
a.href = window.URL.createObjectURL(blob);
|
a.href = window.URL.createObjectURL(blob);
|
||||||
a.download = driveLights.label(drive) + '.dsk';
|
a.download = driveLights.label(driveNo) + '.dsk';
|
||||||
|
|
||||||
if (event.metaKey) {
|
if (event.metaKey) {
|
||||||
dumpDisk(drive);
|
dumpDisk(driveNo);
|
||||||
} else {
|
} else {
|
||||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||||
saveName.value = driveLights.label(drive);
|
saveName.value = driveLights.label(driveNo);
|
||||||
MicroModal.show('save-modal');
|
MicroModal.show('save-modal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,12 +153,12 @@ export function openAlert(msg: string) {
|
|||||||
* Drag and Drop
|
* Drag and Drop
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function handleDragOver(_drive: number, event: DragEvent) {
|
export function handleDragOver(_driveNo: number, event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer!.dropEffect = 'copy';
|
event.dataTransfer!.dropEffect = 'copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDragEnd(_drive: number, event: DragEvent) {
|
export function handleDragEnd(_driveNo: number, event: DragEvent) {
|
||||||
const dt = event.dataTransfer!;
|
const dt = event.dataTransfer!;
|
||||||
if (dt.items) {
|
if (dt.items) {
|
||||||
for (let i = 0; i < dt.items.length; i++) {
|
for (let i = 0; i < dt.items.length; i++) {
|
||||||
@ -170,23 +169,23 @@ export function handleDragEnd(_drive: number, event: DragEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDrop(drive: number, event: DragEvent) {
|
export function handleDrop(driveNo: number, event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (drive < 1) {
|
if (driveNo < 1) {
|
||||||
if (!_disk2.getMetadata(1)) {
|
if (!_disk2.getMetadata(1)) {
|
||||||
drive = 1;
|
driveNo = 1;
|
||||||
} else if (!_disk2.getMetadata(2)) {
|
} else if (!_disk2.getMetadata(2)) {
|
||||||
drive = 2;
|
driveNo = 2;
|
||||||
} else {
|
} else {
|
||||||
drive = 1;
|
driveNo = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dt = event.dataTransfer!;
|
const dt = event.dataTransfer!;
|
||||||
if (dt.files.length === 1) {
|
if (dt.files.length === 1) {
|
||||||
const runOnLoad = event.shiftKey;
|
const runOnLoad = event.shiftKey;
|
||||||
doLoadLocal(drive as DriveNumber, dt.files[0], { runOnLoad });
|
doLoadLocal(driveNo as DriveNumber, dt.files[0], { runOnLoad });
|
||||||
} else if (dt.files.length === 2) {
|
} else if (dt.files.length === 2) {
|
||||||
doLoadLocal(1, dt.files[0]);
|
doLoadLocal(1, dt.files[0]);
|
||||||
doLoadLocal(2, dt.files[1]);
|
doLoadLocal(2, dt.files[1]);
|
||||||
@ -195,7 +194,7 @@ export function handleDrop(drive: number, event: DragEvent) {
|
|||||||
if (dt.items[idx].type === 'text/uri-list') {
|
if (dt.items[idx].type === 'text/uri-list') {
|
||||||
dt.items[idx].getAsString(function (url) {
|
dt.items[idx].getAsString(function (url) {
|
||||||
const parts = hup().split('|');
|
const parts = hup().split('|');
|
||||||
parts[drive - 1] = url;
|
parts[driveNo - 1] = url;
|
||||||
document.location.hash = parts.join('|');
|
document.location.hash = parts.join('|');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -228,7 +227,7 @@ function loadingStop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadAjax(drive: DriveNumber, url: string) {
|
export function loadAjax(driveNo: DriveNumber, url: string) {
|
||||||
loadingStart();
|
loadingStart();
|
||||||
|
|
||||||
fetch(url).then(function (response: Response) {
|
fetch(url).then(function (response: Response) {
|
||||||
@ -241,7 +240,7 @@ export function loadAjax(drive: DriveNumber, url: string) {
|
|||||||
if (data.type === 'binary') {
|
if (data.type === 'binary') {
|
||||||
loadBinary(data );
|
loadBinary(data );
|
||||||
} else if (includes(DISK_FORMATS, data.type)) {
|
} else if (includes(DISK_FORMATS, data.type)) {
|
||||||
loadDisk(drive, data);
|
loadDisk(driveNo, data);
|
||||||
}
|
}
|
||||||
initGamepad(data.gamepad);
|
initGamepad(data.gamepad);
|
||||||
loadingStop();
|
loadingStop();
|
||||||
@ -269,7 +268,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
|
|||||||
const files = localFile.files;
|
const files = localFile.files;
|
||||||
if (files && files.length === 1) {
|
if (files && files.length === 1) {
|
||||||
const runOnLoad = event.shiftKey;
|
const runOnLoad = event.shiftKey;
|
||||||
doLoadLocal(_currentDrive, files[0], { runOnLoad });
|
doLoadLocal(driveNo, files[0], { runOnLoad });
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
let filename;
|
let filename;
|
||||||
MicroModal.close('load-modal');
|
MicroModal.close('load-modal');
|
||||||
@ -278,7 +277,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
|
|||||||
if (filename === '__manage') {
|
if (filename === '__manage') {
|
||||||
openManage();
|
openManage();
|
||||||
} else {
|
} else {
|
||||||
loadLocalStorage(_currentDrive, filename);
|
loadLocalStorage(driveNo, filename);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const r1 = /json\/disks\/(.*).json$/.exec(url);
|
const r1 = /json\/disks\/(.*).json$/.exec(url);
|
||||||
@ -288,7 +287,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
|
|||||||
filename = url;
|
filename = url;
|
||||||
}
|
}
|
||||||
const parts = hup().split('|');
|
const parts = hup().split('|');
|
||||||
parts[_currentDrive - 1] = filename;
|
parts[driveNo - 1] = filename;
|
||||||
document.location.hash = parts.join('|');
|
document.location.hash = parts.join('|');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,7 +296,7 @@ export function doLoad(event: MouseEvent|KeyboardEvent) {
|
|||||||
export function doSave() {
|
export function doSave() {
|
||||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||||
const name = saveName.value;
|
const name = saveName.value;
|
||||||
saveLocalStorage(_currentDrive, name);
|
saveLocalStorage(driveNo, name);
|
||||||
MicroModal.close('save-modal');
|
MicroModal.close('save-modal');
|
||||||
window.setTimeout(() => openAlert('Saved'), 0);
|
window.setTimeout(() => openAlert('Saved'), 0);
|
||||||
}
|
}
|
||||||
@ -313,7 +312,7 @@ interface LoadOptions {
|
|||||||
runOnLoad?: boolean;
|
runOnLoad?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOptions> = {}) {
|
function doLoadLocal(driveNo: DriveNumber, file: File, options: Partial<LoadOptions> = {}) {
|
||||||
const parts = file.name.split('.');
|
const parts = file.name.split('.');
|
||||||
const ext = parts[parts.length - 1].toLowerCase();
|
const ext = parts[parts.length - 1].toLowerCase();
|
||||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||||
@ -322,7 +321,7 @@ function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOption
|
|||||||
[, type, aux] = matches;
|
[, type, aux] = matches;
|
||||||
}
|
}
|
||||||
if (includes(DISK_FORMATS, ext)) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
doLoadLocalDisk(drive, file);
|
doLoadLocalDisk(driveNo, file);
|
||||||
} else if (includes(TAPE_TYPES, ext)) {
|
} else if (includes(TAPE_TYPES, ext)) {
|
||||||
tape.doLoadLocalTape(file);
|
tape.doLoadLocalTape(file);
|
||||||
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
|
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
|
||||||
@ -366,7 +365,7 @@ function doLoadBinary(file: File, options: LoadOptions) {
|
|||||||
fileReader.readAsArrayBuffer(file);
|
fileReader.readAsArrayBuffer(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
function doLoadLocalDisk(driveNo: DriveNumber, file: File) {
|
||||||
loadingStart();
|
loadingStart();
|
||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
fileReader.onload = function () {
|
fileReader.onload = function () {
|
||||||
@ -377,14 +376,14 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
|||||||
|
|
||||||
// Remove any json file reference
|
// Remove any json file reference
|
||||||
const files = hup().split('|');
|
const files = hup().split('|');
|
||||||
files[drive - 1] = '';
|
files[driveNo - 1] = '';
|
||||||
document.location.hash = files.join('|');
|
document.location.hash = files.join('|');
|
||||||
|
|
||||||
if (includes(DISK_FORMATS, ext)) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
if (result.byteLength >= 800 * 1024) {
|
if (result.byteLength >= 800 * 1024) {
|
||||||
if (
|
if (
|
||||||
includes(BLOCK_FORMATS, ext) &&
|
includes(BLOCK_FORMATS, ext) &&
|
||||||
_massStorage.setBinary(drive, name, ext, result)
|
_massStorage.setBinary(driveNo, name, ext, result)
|
||||||
) {
|
) {
|
||||||
initGamepad();
|
initGamepad();
|
||||||
} else {
|
} else {
|
||||||
@ -393,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
|||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
includes(FLOPPY_FORMATS, ext) &&
|
includes(FLOPPY_FORMATS, ext) &&
|
||||||
_disk2.setBinary(drive, name, ext, result)
|
_disk2.setBinary(driveNo, name, ext, result)
|
||||||
) {
|
) {
|
||||||
initGamepad();
|
initGamepad();
|
||||||
} else {
|
} else {
|
||||||
@ -406,7 +405,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
|||||||
fileReader.readAsArrayBuffer(file);
|
fileReader.readAsArrayBuffer(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
export function doLoadHTTP(driveNo: DriveNumber, url?: string) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
MicroModal.close('http-modal');
|
MicroModal.close('http-modal');
|
||||||
}
|
}
|
||||||
@ -454,13 +453,13 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
|||||||
if (includes(DISK_FORMATS, ext)) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
if (data.byteLength >= 800 * 1024) {
|
if (data.byteLength >= 800 * 1024) {
|
||||||
if (includes(BLOCK_FORMATS, ext)) {
|
if (includes(BLOCK_FORMATS, ext)) {
|
||||||
_massStorage.setBinary(drive, name, ext, data);
|
_massStorage.setBinary(driveNo, name, ext, data);
|
||||||
initGamepad();
|
initGamepad();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
includes(FLOPPY_FORMATS, ext) &&
|
includes(FLOPPY_FORMATS, ext) &&
|
||||||
_disk2.setBinary(drive, name, ext, data)
|
_disk2.setBinary(driveNo, name, ext, data)
|
||||||
) {
|
) {
|
||||||
initGamepad();
|
initGamepad();
|
||||||
}
|
}
|
||||||
@ -547,11 +546,11 @@ function updateSoundButton(on: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dumpDisk(drive: DriveNumber) {
|
function dumpDisk(driveNo: DriveNumber) {
|
||||||
const wind = window.open('', '_blank')!;
|
const wind = window.open('', '_blank')!;
|
||||||
wind.document.title = driveLights.label(drive);
|
wind.document.title = driveLights.label(driveNo);
|
||||||
wind.document.write('<pre>');
|
wind.document.write('<pre>');
|
||||||
wind.document.write(_disk2.getJSON(drive, true));
|
wind.document.write(_disk2.getJSON(driveNo, true));
|
||||||
wind.document.write('</pre>');
|
wind.document.write('</pre>');
|
||||||
wind.document.close();
|
wind.document.close();
|
||||||
}
|
}
|
||||||
@ -586,7 +585,7 @@ export function selectCategory() {
|
|||||||
option.value = file.filename;
|
option.value = file.filename;
|
||||||
option.innerText = name;
|
option.innerText = name;
|
||||||
diskSelect.append(option);
|
diskSelect.append(option);
|
||||||
if (disk_cur_name[_currentDrive] === name) {
|
if (disk_cur_name[driveNo] === name) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -603,7 +602,7 @@ export function clickDisk(event: MouseEvent|KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Called to load disks from the local catalog. */
|
/** Called to load disks from the local catalog. */
|
||||||
function loadDisk(drive: DriveNumber, disk: JSONDisk) {
|
function loadDisk(driveNo: DriveNumber, disk: JSONDisk) {
|
||||||
let name = disk.name;
|
let name = disk.name;
|
||||||
const category = disk.category!; // all disks in the local catalog have a category
|
const category = disk.category!; // all disks in the local catalog have a category
|
||||||
|
|
||||||
@ -611,10 +610,10 @@ function loadDisk(drive: DriveNumber, disk: JSONDisk) {
|
|||||||
name += ' - ' + disk.disk;
|
name += ' - ' + disk.disk;
|
||||||
}
|
}
|
||||||
|
|
||||||
disk_cur_cat[drive] = category;
|
disk_cur_cat[driveNo] = category;
|
||||||
disk_cur_name[drive] = name;
|
disk_cur_name[driveNo] = name;
|
||||||
|
|
||||||
_disk2.setDisk(drive, disk);
|
_disk2.setDisk(driveNo, disk);
|
||||||
initGamepad(disk.gamepad);
|
initGamepad(disk.gamepad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,16 +653,16 @@ type LocalDiskIndex = {
|
|||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function saveLocalStorage(drive: DriveNumber, name: string) {
|
function saveLocalStorage(driveNo: DriveNumber, name: string) {
|
||||||
const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
|
const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
|
||||||
|
|
||||||
const json = _disk2.getJSON(drive);
|
const json = _disk2.getJSON(driveNo);
|
||||||
diskIndex[name] = json;
|
diskIndex[name] = json;
|
||||||
|
|
||||||
window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex));
|
window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex));
|
||||||
|
|
||||||
driveLights.label(drive, name);
|
driveLights.label(driveNo, name);
|
||||||
driveLights.dirty(drive, false);
|
driveLights.dirty(driveNo, false);
|
||||||
updateLocalStorage();
|
updateLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,12 +676,12 @@ function deleteLocalStorage(name: string) {
|
|||||||
updateLocalStorage();
|
updateLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLocalStorage(drive: DriveNumber, name: string) {
|
function loadLocalStorage(driveNo: DriveNumber, name: string) {
|
||||||
const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
|
const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex;
|
||||||
if (diskIndex[name]) {
|
if (diskIndex[name]) {
|
||||||
_disk2.setJSON(drive, diskIndex[name]);
|
_disk2.setJSON(driveNo, diskIndex[name]);
|
||||||
driveLights.label(drive, name);
|
driveLights.label(driveNo, name);
|
||||||
driveLights.dirty(drive, false);
|
driveLights.dirty(driveNo, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ import { Callbacks } from '../cards/disk2';
|
|||||||
import type { DriveNumber } from '../formats/types';
|
import type { DriveNumber } from '../formats/types';
|
||||||
|
|
||||||
export default class DriveLights implements Callbacks {
|
export default class DriveLights implements Callbacks {
|
||||||
public driveLight(drive: DriveNumber, on: boolean) {
|
public driveLight(driveNo: DriveNumber, on: boolean) {
|
||||||
const disk = document.querySelector<HTMLElement>(`#disk${drive}`);
|
const disk = document.querySelector<HTMLElement>(`#disk${driveNo}`);
|
||||||
if (disk) {
|
if (disk) {
|
||||||
disk.style.backgroundImage =
|
disk.style.backgroundImage =
|
||||||
on ? 'url(css/red-on-16.png)' :
|
on ? 'url(css/red-on-16.png)' :
|
||||||
@ -11,12 +11,12 @@ export default class DriveLights implements Callbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dirty(_drive: DriveNumber, _dirty: boolean) {
|
public dirty(_driveNo: DriveNumber, _dirty: boolean) {
|
||||||
// document.querySelector('#disksave' + drive).disabled = !dirty;
|
// document.querySelector('#disksave' + drive).disabled = !dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public label(drive: DriveNumber, label?: string, side?: string) {
|
public label(driveNo: DriveNumber, label?: string, side?: string) {
|
||||||
const labelElement = document.querySelector<HTMLElement>(`#disk-label${drive}`);
|
const labelElement = document.querySelector<HTMLElement>(`#disk-label${driveNo}`);
|
||||||
let labelText = '';
|
let labelText = '';
|
||||||
if (labelElement) {
|
if (labelElement) {
|
||||||
labelText = labelElement.innerText;
|
labelText = labelElement.innerText;
|
||||||
|
@ -67,7 +67,7 @@ describe('DiskII', () => {
|
|||||||
const state = diskII.getState();
|
const state = diskII.getState();
|
||||||
// These are just arbitrary changes, not an exhaustive list of fields.
|
// These are just arbitrary changes, not an exhaustive list of fields.
|
||||||
(state.drives[1].driver as {skip:number}).skip = 1;
|
(state.drives[1].driver as {skip:number}).skip = 1;
|
||||||
state.controllerState.drive = 2;
|
state.controllerState.driveNo = 2;
|
||||||
state.controllerState.latch = 0x42;
|
state.controllerState.latch = 0x42;
|
||||||
state.controllerState.on = true;
|
state.controllerState.on = true;
|
||||||
state.controllerState.q7 = true;
|
state.controllerState.q7 = true;
|
||||||
@ -760,11 +760,11 @@ class TestDiskReader {
|
|||||||
nibbles = 0;
|
nibbles = 0;
|
||||||
diskII: DiskII;
|
diskII: DiskII;
|
||||||
|
|
||||||
constructor(drive: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) {
|
constructor(driveNo: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) {
|
||||||
mocked(apple2IO).cycles.mockImplementation(() => this.cycles);
|
mocked(apple2IO).cycles.mockImplementation(() => this.cycles);
|
||||||
|
|
||||||
this.diskII = new DiskII(apple2IO, callbacks);
|
this.diskII = new DiskII(apple2IO, callbacks);
|
||||||
this.diskII.setBinary(drive, label, 'woz', image);
|
this.diskII.setBinary(driveNo, label, 'woz', image);
|
||||||
}
|
}
|
||||||
|
|
||||||
readNibble(): byte {
|
readNibble(): byte {
|
||||||
|
@ -19,7 +19,7 @@ debug('Worker loaded');
|
|||||||
addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
|
addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
|
||||||
debug('Worker started', message.type);
|
debug('Worker started', message.type);
|
||||||
const data = message.data;
|
const data = message.data;
|
||||||
const { drive } = data.payload;
|
const { driveNo } = data.payload;
|
||||||
let disk: FloppyDisk | null = null;
|
let disk: FloppyDisk | null = null;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
@ -45,7 +45,7 @@ addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
|
|||||||
const response: DiskProcessedResponse = {
|
const response: DiskProcessedResponse = {
|
||||||
type: DISK_PROCESSED,
|
type: DISK_PROCESSED,
|
||||||
payload: {
|
payload: {
|
||||||
drive,
|
driveNo,
|
||||||
disk
|
disk
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user