diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index aee0677..b3df6f6 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -390,7 +390,7 @@ export default class CFFA implements Card, MassStorage, Restorable< ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; } return result; @@ -441,7 +441,7 @@ export default class CFFA implements Card, MassStorage, Restorable< const prodos = new ProDOSVolume(disk); - this._name[drive] = disk.name; + this._name[drive] = disk.metadata.name; this._partitions[drive] = prodos; if (drive) { @@ -483,7 +483,8 @@ export default class CFFA implements Card, MassStorage, Restorable< if (!blockDisk) { return null; } - const { name, blocks, readOnly } = blockDisk; + const { blocks, readOnly } = blockDisk; + const { name } = blockDisk.metadata; let ext; let data: ArrayBuffer; if (this._metadata[drive]) { @@ -498,7 +499,7 @@ export default class CFFA implements Card, MassStorage, Restorable< data = dataArray.buffer; } return { - name, + metadata: { name }, ext, data, readOnly, diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index e21188c..e4faa9e 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -4,7 +4,7 @@ import type { Card, memory, nibble, - rom, + ReadonlyUint8Array, } from '../types'; import { @@ -22,6 +22,9 @@ import { ENCODING_BITSTREAM, MassStorage, MassStorageData, + DiskMetadata, + SupportedSectors, + FloppyDisk, } from '../formats/types'; import { @@ -121,6 +124,18 @@ const SEQUENCER_ROM_16 = [ 0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F ] as const; +/** Contents of the P6 sequencer ROM. */ +const SEQUENCER_ROM: Record> = { + 13: SEQUENCER_ROM_13, + 16: SEQUENCER_ROM_16, +}; + +/** Contents of the P5 ROM at 0xCnXX. */ +const BOOTSTRAP_ROM: Record = { + 13: BOOTSTRAP_ROM_13, + 16: BOOTSTRAP_ROM_16, +}; + type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type Phase = 0 | 1 | 2 | 3; @@ -138,11 +153,10 @@ type Phase = 0 | 1 | 2 | 3; * head momentum. * * Examining the https://computerhistory.org/blog/apple-ii-dos-source-code/, - * one finds that the SEEK routine on line 4831 of `appdos31.lst`. It uses - * `ONTABLE` and `OFFTABLE` (each 12 bytes) to know exactly how many - * microseconds to power on/off each coil as the head accelerates. At the end, - * the final coil is left powered on 9.5 milliseconds to ensure the head has - * settled. + * one finds the SEEK routine on line 4831 of `appdos31.lst`. It uses `ONTABLE` + * and `OFFTABLE` (each 12 bytes) to know exactly how many microseconds to + * power on/off each coil as the head accelerates. At the end, the final coil + * is left powered on 9.5 milliseconds to ensure the head has settled. * * https://embeddedmicro.weebly.com/apple-2iie.html shows traces of the boot * seek (which is slightly different) and a regular seek. @@ -153,23 +167,56 @@ const PHASE_DELTA = [ [-2, -1, 0, 1], [1, -2, -1, 0] ] 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; +} + +/** Callbacks triggered by events of the drive or controller. */ export interface Callbacks { + /** Called when a drive turns on or off. */ driveLight: (drive: DriveNumber, on: boolean) => void; + /** + * Called when a disk has been written to. For performance and integrity, + * this is only called when the drive stops spinning or is removed from + * the drive. + */ dirty: (drive: DriveNumber, dirty: boolean) => void; + /** Called when a disk is inserted or removed from the drive. */ label: (drive: DriveNumber, name?: string, side?: string) => void; } /** Common information for Nibble and WOZ disks. */ - interface BaseDrive { /** Current disk format. */ format: NibbleFormat; /** Current disk volume number. */ volume: byte; - /** Displayed disk name */ - name: string; - /** (Optional) Disk side (Front/Back, A/B) */ - side?: string | undefined; /** Quarter track position of read/write head. */ track: byte; /** Position of the head on the track. */ @@ -180,6 +227,8 @@ interface BaseDrive { readOnly: boolean; /** Whether the drive has been written to since it was loaded. */ dirty: boolean; + /** Metadata about the disk image */ + metadata: DiskMetadata; } /** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ @@ -214,8 +263,6 @@ interface DriveState { format: NibbleFormat; encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE; volume: byte; - name: string; - side?: string | undefined; tracks: memory[]; track: byte; head: byte; @@ -224,15 +271,15 @@ interface DriveState { dirty: boolean; trackMap: number[]; rawTracks: Uint8Array[]; + metadata: DiskMetadata; } +/** State of the controller for saving/restoring. */ +// TODO(flan): It's unclear whether reusing ControllerState here is a good idea. interface State { drives: DriveState[]; skip: number; - latch: number; - writeMode: boolean; - on: boolean; - drive: DriveNumber; + controllerState: ControllerState; } function getDriveState(drive: Drive): DriveState { @@ -240,8 +287,6 @@ function getDriveState(drive: Drive): DriveState { format: drive.format, encoding: drive.encoding, volume: drive.volume, - name: drive.name, - side: drive.side, tracks: [], track: drive.track, head: drive.head, @@ -250,6 +295,7 @@ function getDriveState(drive: Drive): DriveState { dirty: drive.dirty, trackMap: [], rawTracks: [], + metadata: { ...drive.metadata }, }; if (isNibbleDrive(drive)) { @@ -273,14 +319,13 @@ function setDriveState(state: DriveState) { format: state.format, encoding: ENCODING_NIBBLE, volume: state.volume, - name: state.name, - side: state.side, tracks: [], track: state.track, head: state.head, phase: state.phase, readOnly: state.readOnly, dirty: state.dirty, + metadata: { ...state.metadata }, }; for (let idx = 0; idx < state.tracks.length; idx++) { result.tracks.push(new Uint8Array(state.tracks[idx])); @@ -290,8 +335,6 @@ function setDriveState(state: DriveState) { format: state.format, encoding: ENCODING_BITSTREAM, volume: state.volume, - name: state.name, - side: state.side, track: state.track, head: state.head, phase: state.phase, @@ -299,6 +342,7 @@ function setDriveState(state: DriveState) { dirty: state.dirty, trackMap: [...state.trackMap], rawTracks: [], + metadata: { ...state.metadata }, }; for (let idx = 0; idx < state.rawTracks.length; idx++) { result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); @@ -317,65 +361,46 @@ export default class DiskII implements Card, MassStorage { format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, - name: 'Disk 1', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + metadata: { name: 'Disk 1' }, }, 2: { // Drive 2 format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, - name: 'Disk 2', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + metadata: { name: 'Disk 2' }, } }; + private state: ControllerState; + /** * 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 = 0; - /** Last data written by the CPU to card softswitch 0x8D. */ - private bus = 0; - /** Drive data register. */ - private latch = 0; /** Drive off timeout id or null. */ private offTimeout: number | null = null; - /** Q6 (Shift/Load): Used by WOZ disks. */ - private q6 = 0; - /** Q7 (Read/Write): Used by WOZ disks. */ - private q7: boolean = false; - /** Q7 (Read/Write): Used by Nibble disks. */ - private writeMode = false; - /** Whether the selected drive is on. */ - private on = false; - /** Current drive number (1, 2). */ - private drive: DriveNumber = 1; /** Current drive object. */ - private cur = this.drives[this.drive]; + private cur: Drive; /** Nibbles read this on cycle */ private nibbleCount = 0; - /** Q0-Q3: Coil states. */ - private q = [false, false, false, false]; - - /** The 8-cycle LSS clock. */ - private clock: LssClockCycle = 0; /** Current CPU cycle count. */ private lastCycles = 0; - /** Current state of the Logic State Sequencer. */ - private state: nibble = 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 @@ -383,26 +408,31 @@ export default class DiskII implements Card, MassStorage { */ private zeros = 0; - /** Contents of the P5 ROM at 0xCnXX. */ - private bootstrapRom: rom; - /** Contents of the P6 ROM. */ - private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13; - private worker: Worker; /** Builds a new Disk ][ card. */ - constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) { + constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) { this.debug('Disk ]['); this.lastCycles = this.io.cycles(); - this.bootstrapRom = this.sectors === 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13; - this.sequencerRom = this.sectors === 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13; - // 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.state = { + sectors, + bus: 0, + latch: 0, + drive: 1, + on: false, + q6: false, + q7: false, + clock: 0, + // From the example in UtA2e, p. 9-29, col. 1, para. 1., this is + // essentially the start of the sequencer loop and produces + // correctly synced nibbles immediately. Starting at state 0 + // would introduce a spurrious 1 in the latch at the beginning, + // which requires reading several more sync bytes to sync up. + state: 2, + }; + + this.cur = this.drives[this.state.drive]; this.initWorker(); } @@ -462,10 +492,12 @@ export default class DiskII implements Card, MassStorage { } const track = this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; + + const state = this.state; while (workCycles-- > 0) { let pulse: number = 0; - if (this.clock === 4) { + if (state.clock === 4) { pulse = track[this.cur.head]; if (!pulse) { // More than 2 zeros can not be read reliably. @@ -479,47 +511,47 @@ export default class DiskII implements Card, MassStorage { let idx = 0; idx |= pulse ? 0x00 : 0x01; - idx |= this.latch & 0x80 ? 0x02 : 0x00; - idx |= this.q6 ? 0x04 : 0x00; - idx |= this.q7 ? 0x08 : 0x00; - idx |= this.state << 4; + idx |= state.latch & 0x80 ? 0x02 : 0x00; + idx |= state.q6 ? 0x04 : 0x00; + idx |= state.q7 ? 0x08 : 0x00; + idx |= state.state << 4; - const command = this.sequencerRom[idx]; + const command = SEQUENCER_ROM[this.sectors][idx]; - this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${this.q6} latch: ${toHex(this.latch)}`); + this.debug(`clock: ${state.clock} state: ${toHex(state.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${state.q6} latch: ${toHex(state.latch)}`); switch (command & 0xf) { case 0x0: // CLR - this.latch = 0; + state.latch = 0; break; case 0x8: // NOP break; case 0x9: // SL0 - this.latch = (this.latch << 1) & 0xff; + state.latch = (state.latch << 1) & 0xff; break; case 0xA: // SR - this.latch >>= 1; + state.latch >>= 1; if (this.cur.readOnly) { - this.latch |= 0x80; + state.latch |= 0x80; } break; case 0xB: // LD - this.latch = this.bus; - this.debug('Loading', toHex(this.latch), 'from bus'); + state.latch = state.bus; + this.debug('Loading', toHex(state.latch), 'from bus'); break; case 0xD: // SL1 - this.latch = ((this.latch << 1) | 0x01) & 0xff; + state.latch = ((state.latch << 1) | 0x01) & 0xff; break; default: this.debug(`unknown command: ${toHex(command & 0xf)}`); } - this.state = (command >> 4 & 0xF) as nibble; + state.state = (command >> 4 & 0xF) as nibble; - if (this.clock === 4) { - if (this.on) { - if (this.q7) { - track[this.cur.head] = this.state & 0x8 ? 0x01 : 0x00; - this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00); + if (state.clock === 4) { + if (state.on) { + if (state.q7) { + track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00; + this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); } if (++this.cur.head >= track.length) { @@ -528,8 +560,8 @@ export default class DiskII implements Card, MassStorage { } } - if (++this.clock > 7) { - this.clock = 0; + if (++state.clock > 7) { + state.clock = 0; } } } @@ -539,28 +571,29 @@ export default class DiskII implements Card, MassStorage { if (!isNibbleDrive(this.cur)) { return; } - if (this.on && (this.skip || this.writeMode)) { + const state = this.state; + if (state.on && (this.skip || state.q7)) { const track = this.cur.tracks[this.cur.track >> 2]; if (track && track.length) { if (this.cur.head >= track.length) { this.cur.head = 0; } - if (this.writeMode) { + if (state.q7) { if (!this.cur.readOnly) { - track[this.cur.head] = this.bus; + track[this.cur.head] = state.bus; if (!this.cur.dirty) { - this.updateDirty(this.drive, true); + this.updateDirty(state.drive, true); } } } else { - this.latch = track[this.cur.head]; + state.latch = track[this.cur.head]; } ++this.cur.head; } } else { - this.latch = 0; + state.latch = 0; } this.skip = (++this.skip % 2); } @@ -579,7 +612,7 @@ export default class DiskII implements Card, MassStorage { // 5. [...] enables head positioning [...] // // Therefore do nothing if no drive is on. - if (!this.on) { + if (!this.state.on) { this.debug(`ignoring phase ${phase}${on ? ' on' : ' off'}`); return; } @@ -608,11 +641,10 @@ export default class DiskII implements Card, MassStorage { // 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3), // '(' + toHex(_cur.track) + ')', // '[' + phase + ':' + (on ? 'on' : 'off') + ']'); - - this.q[phase] = on; } private access(off: byte, val?: byte) { + const state = this.state; let result = 0; const readMode = val === undefined; @@ -644,13 +676,13 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVEOFF: // 0x08 if (!this.offTimeout) { - if (this.on) { + if (state.on) { // TODO(flan): This is fragile because it relies on // wall-clock time instead of emulator time. this.offTimeout = window.setTimeout(() => { this.debug('Drive Off'); - this.on = false; - this.callbacks.driveLight(this.drive, false); + state.on = false; + this.callbacks.driveLight(state.drive, false); this.debug('nibbles read', this.nibbleCount); }, 1000); } @@ -662,37 +694,37 @@ export default class DiskII implements Card, MassStorage { window.clearTimeout(this.offTimeout); this.offTimeout = null; } - if (!this.on) { + if (!state.on) { this.debug('Drive On'); this.nibbleCount = 0; - this.on = true; + state.on = true; this.lastCycles = this.io.cycles(); - this.callbacks.driveLight(this.drive, true); + this.callbacks.driveLight(state.drive, true); } break; case LOC.DRIVE1: // 0x0a this.debug('Disk 1'); - this.drive = 1; - this.cur = this.drives[this.drive]; - if (this.on) { + state.drive = 1; + this.cur = this.drives[state.drive]; + if (state.on) { this.callbacks.driveLight(2, false); this.callbacks.driveLight(1, true); } break; case LOC.DRIVE2: // 0x0b this.debug('Disk 2'); - this.drive = 2; - this.cur = this.drives[this.drive]; - if (this.on) { + state.drive = 2; + this.cur = this.drives[state.drive]; + if (state.on) { this.callbacks.driveLight(1, false); this.callbacks.driveLight(2, true); } break; case LOC.DRIVEREAD: // 0x0c (Q6L) Shift - this.q6 = 0; - if (this.writeMode) { + state.q6 = false; + if (state.q7) { this.debug('clearing _q6/SHIFT'); } if (isNibbleDrive(this.cur)) { @@ -701,17 +733,17 @@ export default class DiskII implements Card, MassStorage { break; case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD - this.q6 = 1; - if (this.writeMode) { + state.q6 = true; + if (state.q7) { this.debug('setting _q6/LOAD'); } if (isNibbleDrive(this.cur)) { - if (readMode && !this.writeMode) { + if (readMode && !state.q7) { if (this.cur.readOnly) { - this.latch = 0xff; + state.latch = 0xff; this.debug('Setting readOnly'); } else { - this.latch = this.latch >> 1; + state.latch = state.latch >> 1; this.debug('Clearing readOnly'); } } @@ -720,13 +752,11 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVEREADMODE: // 0x0e (Q7L) this.debug('Read Mode'); - this.q7 = false; - this.writeMode = false; + state.q7 = false; break; case LOC.DRIVEWRITEMODE: // 0x0f (Q7H) this.debug('Write Mode'); - this.q7 = true; - this.writeMode = true; + state.q7 = true; break; default: @@ -740,7 +770,7 @@ export default class DiskII implements Card, MassStorage { // used to read the data register onto the CPU bus, although some // also cause conflicts with the disk controller commands. if ((off & 0x01) === 0) { - result = this.latch; + result = state.latch; if (result & 0x80) { this.nibbleCount++; } @@ -750,7 +780,7 @@ export default class DiskII implements Card, MassStorage { } else { // It's not explicitly stated, but writes to any address set the // data register. - this.bus = val; + state.bus = val; } return result; @@ -768,7 +798,7 @@ export default class DiskII implements Card, MassStorage { } read(_page: byte, off: byte) { - return this.bootstrapRom[off]; + return BOOTSTRAP_ROM[this.sectors][off]; } write() { @@ -776,15 +806,13 @@ export default class DiskII implements Card, MassStorage { } reset() { - if (this.on) { - this.callbacks.driveLight(this.drive, false); - this.writeMode = false; - this.on = false; - this.drive = 1; - this.cur = this.drives[this.drive]; - } - for (let idx = 0; idx < 4; idx++) { - this.q[idx] = false; + const state = this.state; + if (state.on) { + this.callbacks.driveLight(state.drive, false); + state.q7 = false; + state.on = false; + state.drive = 1; + this.cur = this.drives[state.drive]; } } @@ -793,16 +821,10 @@ export default class DiskII implements Card, MassStorage { } getState(): State { - // TODO(flan): This does not accurately save state. It's missing - // all of the state for WOZ disks and the current status of the - // bus. const result = { drives: [] as DriveState[], skip: this.skip, - latch: this.latch, - writeMode: this.writeMode, - on: this.on, - drive: this.drive + controllerState: { ...this.state }, }; result.drives[1] = getDriveState(this.drives[1]); result.drives[2] = getDriveState(this.drives[2]); @@ -812,18 +834,16 @@ export default class DiskII implements Card, MassStorage { setState(state: State) { this.skip = state.skip; - this.latch = state.latch; - this.writeMode = state.writeMode; - this.on = state.on; - this.drive = state.drive; + this.state = { ...state.controllerState }; for (const d of DRIVE_NUMBERS) { this.drives[d] = setDriveState(state.drives[d]); - const { name, side, dirty } = state.drives[d]; + const { name, side } = state.drives[d].metadata; + const { dirty } = state.drives[d]; this.callbacks.label(d, name, side); - this.callbacks.driveLight(d, this.on); + this.callbacks.driveLight(d, this.state.on); this.callbacks.dirty(d, dirty); } - this.cur = this.drives[this.drive]; + this.cur = this.drives[this.state.drive]; } getMetadata(driveNo: DriveNumber) { @@ -864,10 +884,7 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDiskFromJsonDisk(jsonDisk); if (disk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - this.updateDirty(drive, false); - this.callbacks.label(drive, disk.name, disk.side); + this.insertDisk(drive, disk); return true; } } @@ -893,8 +910,8 @@ export default class DiskII implements Card, MassStorage { }; this.worker.postMessage(message); } else { - const cur = this.drives[drive]; - Object.assign(cur, jsonDecode(json)); + const disk = jsonDecode(json); + this.insertDisk(drive, disk); } return true; } @@ -924,12 +941,7 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDisk(fmt, options); if (disk) { - const cur = this.drives[drive]; - const { name, side } = cur; - Object.assign(cur, disk); - this.updateDirty(drive, true); - this.callbacks.label(drive, name, side); - + this.insertDisk(drive, disk); return true; } } @@ -951,11 +963,7 @@ export default class DiskII implements Card, MassStorage { { const { drive, disk } = data.payload; if (disk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - const { name, side } = cur; - this.updateDirty(drive, true); - this.callbacks.label(drive, name, side); + this.insertDisk(drive, disk); } } break; @@ -966,13 +974,22 @@ export default class DiskII implements Card, MassStorage { } } + private insertDisk(drive: DriveNumber, disk: FloppyDisk) { + const cur = this.drives[drive]; + Object.assign(cur, disk); + const { name, side } = cur.metadata; + this.updateDirty(drive, true); + this.callbacks.label(drive, name, side); + } + // TODO(flan): Does not work with WOZ or D13 disks getBinary(drive: DriveNumber, ext?: NibbleFormat): MassStorageData | null { const cur = this.drives[drive]; if (!isNibbleDrive(cur)) { return null; } - const { format, name, readOnly, tracks, volume } = cur; + const { format, readOnly, tracks, volume } = cur; + const { name } = cur.metadata; const len = format === 'nib' ? tracks.reduce((acc, track) => acc + track.length, 0) : this.sectors * tracks.length * 256; @@ -995,7 +1012,7 @@ export default class DiskII implements Card, MassStorage { return { ext, - name, + metadata: { name }, data: data.buffer, readOnly, volume, diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 6cea143..5dccb1c 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -523,7 +523,7 @@ export default class SmartPort implements Card, MassStorage, Restor ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; return result; } @@ -540,7 +540,7 @@ export default class SmartPort implements Card, MassStorage, Restor ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; return result; } @@ -580,7 +580,8 @@ export default class SmartPort implements Card, MassStorage, Restor } const disk = this.disks[drive]; const ext = this.ext[drive]; - const { name, readOnly } = disk; + const { readOnly } = disk; + const { name } = disk.metadata; let data: ArrayBuffer; if (ext === '2mg') { data = create2MGFromBlockDisk(this.metadata[drive], disk); @@ -593,7 +594,7 @@ export default class SmartPort implements Card, MassStorage, Restor data = byteArray.buffer; } return { - name, + metadata: { name }, ext, data, readOnly, diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index 58ab4f0..29fcaf2 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -17,6 +17,7 @@ import { Videoterm } from './Videoterm'; import { spawn, Ready } from './util/promises'; import styles from './css/Apple2.module.css'; +import { SupportedSectors } from 'js/formats/types'; declare global { interface Window { @@ -33,7 +34,7 @@ export interface Apple2Props { e: boolean; gl: boolean; rom: string; - sectors: number; + sectors: SupportedSectors; } /** diff --git a/js/components/DownloadModal.tsx b/js/components/DownloadModal.tsx index f628c62..fd337bb 100644 --- a/js/components/DownloadModal.tsx +++ b/js/components/DownloadModal.tsx @@ -21,7 +21,8 @@ export const DownloadModal = ({ massStorage, number, onClose, isOpen } : Downloa if (isOpen) { const storageData = massStorage.getBinary(number); if (storageData) { - const { name, ext, data } = storageData; + const { ext, data } = storageData; + const { name } = storageData.metadata; if (data.byteLength) { const blob = new Blob( [data], diff --git a/js/components/Drives.tsx b/js/components/Drives.tsx index 6297079..c44d6e2 100644 --- a/js/components/Drives.tsx +++ b/js/components/Drives.tsx @@ -10,7 +10,7 @@ import { ErrorModal } from './ErrorModal'; import { ProgressModal } from './ProgressModal'; import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files'; import { useHash } from './hooks/useHash'; -import { DISK_FORMATS, DriveNumber } from 'js/formats/types'; +import { DISK_FORMATS, DriveNumber, SupportedSectors } from 'js/formats/types'; import { spawn, Ready } from './util/promises'; import styles from './css/Drives.module.css'; @@ -32,7 +32,7 @@ export interface DrivesProps { cpu: CPU6502 | undefined; io: Apple2IO | undefined; enhanced: boolean; - sectors: number; + sectors: SupportedSectors; ready: Ready; } diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index e01c273..11d5e84 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -254,7 +254,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { const disk = useMemo(() => { const massStorageData = massStorage.getBinary(drive, 'po'); if (massStorageData) { - const { name, data, readOnly, ext } = massStorageData; + const { data, readOnly, ext } = massStorageData; + const { name } = massStorageData.metadata; let disk: BlockDisk | NibbleDisk | null = null; if (ext === '2mg') { disk = createDiskFrom2MG({ diff --git a/js/components/util/systems.ts b/js/components/util/systems.ts index 6d5f34d..4653c4d 100644 --- a/js/components/util/systems.ts +++ b/js/components/util/systems.ts @@ -14,7 +14,7 @@ export const defaultSystem = { e: true, enhanced: true, sectors: 16, -}; +} as const; export const systemTypes: Record> = { // Apple //e @@ -68,4 +68,4 @@ export const systemTypes: Record> = { characterRom: 'apple2_char', e: false, } -}; +} as const; diff --git a/js/formats/block.ts b/js/formats/block.ts index f20caa0..1750ea8 100644 --- a/js/formats/block.ts +++ b/js/formats/block.ts @@ -21,7 +21,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk { const disk: BlockDisk = { encoding: ENCODING_BLOCK, blocks, - name, + metadata: { name }, readOnly, }; diff --git a/js/formats/d13.ts b/js/formats/d13.ts index ac5f9cc..20adac2 100644 --- a/js/formats/d13.ts +++ b/js/formats/d13.ts @@ -11,8 +11,7 @@ export default function createDiskFromDOS13(options: DiskOptions) { const disk: NibbleDisk = { format: 'd13', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume, readOnly, tracks: [] diff --git a/js/formats/do.ts b/js/formats/do.ts index 1a4f2b4..c0315d7 100644 --- a/js/formats/do.ts +++ b/js/formats/do.ts @@ -13,8 +13,7 @@ export default function createDiskFromDOS(options: DiskOptions): NibbleDisk { const disk: NibbleDisk = { format: 'dsk', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume, readOnly, tracks: [], diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 0d70c02..0bc409a 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -554,7 +554,7 @@ export function jsonDecode(data: string): NibbleDisk { volume: v, format: json.type, encoding: ENCODING_NIBBLE, - name: json.name, + metadata: { name: json.name }, tracks, readOnly, }; diff --git a/js/formats/nib.ts b/js/formats/nib.ts index 2758b35..51d34e8 100644 --- a/js/formats/nib.ts +++ b/js/formats/nib.ts @@ -11,8 +11,7 @@ export default function createDiskFromNibble(options: DiskOptions): NibbleDisk { const disk: NibbleDisk = { format: 'nib', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume: volume || 254, readOnly: readOnly || false, tracks: [] diff --git a/js/formats/po.ts b/js/formats/po.ts index 23de4c2..41150b0 100644 --- a/js/formats/po.ts +++ b/js/formats/po.ts @@ -13,8 +13,7 @@ export default function createDiskFromProDOS(options: DiskOptions) { const disk: NibbleDisk = { format: 'po', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume: volume || 254, tracks: [], readOnly: readOnly || false, diff --git a/js/formats/types.ts b/js/formats/types.ts index 7ca8692..272bc9e 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -2,13 +2,15 @@ import type { byte, memory, MemberOf, word } from '../types'; import type { GamepadConfiguration } from '../ui/types'; import { InfoChunk } from './woz'; +export const SUPPORTED_SECTORS = [13, 16] as const; +export type SupportedSectors = MemberOf; + export const DRIVE_NUMBERS = [1, 2] as const; export type DriveNumber = MemberOf; /** * Arguments for the disk format processors. */ - export interface DiskOptions { name: string; side?: string | undefined; @@ -33,7 +35,6 @@ export interface DiskDescriptor { /** * JSON binary image (not used?) */ - export interface JSONBinaryImage { type: 'binary'; start: word; @@ -42,14 +43,24 @@ export interface JSONBinaryImage { gamepad?: GamepadConfiguration; } +/** + * Information about a disk image not directly related to the + * disk contents. For example, the name or even a scan of the + * disk label are "metadata", but the volume number is not. + */ +export interface DiskMetadata { + /** Displayed disk name */ + name: string; + /** (Optional) Disk side (Front/Back, A/B) */ + side?: string | undefined; +} + /** * Return value from disk format processors. Describes raw disk * data which the DiskII card can process. */ - export interface Disk { - name: string; - side?: string | undefined; + metadata: DiskMetadata; readOnly: boolean; } @@ -207,7 +218,7 @@ export interface DiskProcessedResponse { type: typeof DISK_PROCESSED; payload: { drive: DriveNumber; - disk: Disk | null; + disk: FloppyDisk | null; }; } @@ -215,7 +226,7 @@ export type FormatWorkerResponse = DiskProcessedResponse; export interface MassStorageData { - name: string; + metadata: DiskMetadata; ext: string; readOnly: boolean; volume?: byte; diff --git a/js/formats/woz.ts b/js/formats/woz.ts index dd3b4a1..c632407 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -297,8 +297,10 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk { tracks: trks?.tracks || [], rawTracks: trks?.rawTracks || [], readOnly: true, //chunks.info.writeProtected === 1; - name: meta?.values['title'] || options.name, - side: meta?.values['side_name'] || meta?.values['side'], + metadata: { + name: meta?.values['title'] || options.name, + side: meta?.values['side_name'] || meta?.values['side'] + }, info }; diff --git a/js/main2.ts b/js/main2.ts index 9eaba1f..ef4a2ac 100644 --- a/js/main2.ts +++ b/js/main2.ts @@ -12,12 +12,13 @@ import Thunderclock from './cards/thunderclock'; import VideoTerm from './cards/videoterm'; import { Apple2 } from './apple2'; +import { SupportedSectors } from './formats/types'; const prefs = new Prefs(); const romVersion = prefs.readPref('computer_type2'); let rom: string; let characterRom: string; -let sectors = 16; +let sectors: SupportedSectors = 16; switch (romVersion) { case 'apple2': diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 663454e..38f6b5e 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -66,11 +66,11 @@ describe('DiskII', () => { const state = diskII.getState(); // These are just arbitrary changes, not an exhaustive list of fields. - state.drive = 2; state.skip = 1; - state.latch = 0x42; - state.on = true; - state.writeMode = true; + state.controllerState.drive = 2; + state.controllerState.latch = 0x42; + state.controllerState.on = true; + state.controllerState.q7 = true; state.drives[2].tracks[14][12] = 0x80; state.drives[2].head = 1000; state.drives[2].phase = 3; diff --git a/test/js/formats/2mg.spec.ts b/test/js/formats/2mg.spec.ts index 90dbe02..653acaa 100644 --- a/test/js/formats/2mg.spec.ts +++ b/test/js/formats/2mg.spec.ts @@ -190,7 +190,7 @@ describe('2mg format', () => { } const disk: BlockDisk = { blocks, - name: 'Good disk', + metadata: { name: 'Good disk' }, readOnly: false, encoding: ENCODING_BLOCK, }; diff --git a/test/js/formats/create_disk.spec.ts b/test/js/formats/create_disk.spec.ts index 32f64ac..a1d0081 100644 --- a/test/js/formats/create_disk.spec.ts +++ b/test/js/formats/create_disk.spec.ts @@ -7,9 +7,11 @@ describe('createDiskFromJsonDisk', () => { expect(disk).toEqual({ encoding: 'nibble', format: 'dsk', - name: 'Test Disk', + metadata: { + name: 'Test Disk', + side: 'Front', + }, readOnly: undefined, - side: 'Front', volume: 254, tracks: expect.any(Array) as number[][] }); diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts index 673fd6b..9855351 100644 --- a/test/js/formats/woz.spec.ts +++ b/test/js/formats/woz.spec.ts @@ -21,7 +21,7 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - name: 'Mock Woz 1', + metadata: { name: 'Mock Woz 1' }, readOnly: true, encoding: ENCODING_BITSTREAM, trackMap: mockTMAP, @@ -58,8 +58,10 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - name: 'Mock Woz 2', - side: 'B', + metadata: { + name: 'Mock Woz 2', + side: 'B', + }, readOnly: true, encoding: ENCODING_BITSTREAM, trackMap: mockTMAP, diff --git a/workers/format.worker.ts b/workers/format.worker.ts index b970a72..21df0e0 100644 --- a/workers/format.worker.ts +++ b/workers/format.worker.ts @@ -6,12 +6,12 @@ import { } from '../js/formats/create_disk'; import { FormatWorkerMessage, - Disk, DiskProcessedResponse, DISK_PROCESSED, PROCESS_BINARY, PROCESS_JSON_DISK, PROCESS_JSON, + FloppyDisk, } from '../js/formats/types'; debug('Worker loaded'); @@ -20,7 +20,7 @@ addEventListener('message', (message: MessageEvent) => { debug('Worker started', message.type); const data = message.data; const { drive } = data.payload; - let disk: Disk | null = null; + let disk: FloppyDisk | null = null; switch (data.type) { case PROCESS_BINARY: {