2021-07-10 00:54:27 +00:00
|
|
|
import { bit, byte, memory } from '../types';
|
2019-10-02 02:56:10 +00:00
|
|
|
import { base64_decode, base64_encode } from '../base64';
|
|
|
|
import { bytify, debug, toHex } from '../util';
|
2023-11-24 14:45:55 +00:00
|
|
|
import {
|
|
|
|
NibbleDisk,
|
|
|
|
ENCODING_NIBBLE,
|
|
|
|
JSONDisk,
|
|
|
|
isNibbleDiskFormat,
|
|
|
|
SupportedSectors,
|
|
|
|
} from './types';
|
2020-11-24 16:48:14 +00:00
|
|
|
|
2021-01-03 22:59:42 +00:00
|
|
|
/**
|
|
|
|
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2020-12-29 14:40:40 +00:00
|
|
|
export const DO = [
|
2023-11-24 14:45:55 +00:00
|
|
|
0x0, 0x7, 0xe, 0x6, 0xd, 0x5, 0xc, 0x4,
|
|
|
|
0xb, 0x3, 0xa, 0x2, 0x9, 0x1, 0x8, 0xf,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2020-12-29 14:40:40 +00:00
|
|
|
|
2021-01-03 22:59:42 +00:00
|
|
|
/**
|
|
|
|
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2020-11-24 16:48:14 +00:00
|
|
|
export const _DO = [
|
2023-11-24 14:45:55 +00:00
|
|
|
0x0, 0xd, 0xb, 0x9, 0x7, 0x5, 0x3, 0x1,
|
|
|
|
0xe, 0xc, 0xa, 0x8, 0x6, 0x4, 0x2, 0xf,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2021-01-03 22:59:42 +00:00
|
|
|
/**
|
|
|
|
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2021-01-03 22:59:42 +00:00
|
|
|
export const PO = [
|
|
|
|
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
2023-11-24 14:45:55 +00:00
|
|
|
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2021-03-13 21:18:32 +00:00
|
|
|
|
2021-01-03 22:59:42 +00:00
|
|
|
/**
|
|
|
|
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2020-11-24 16:48:14 +00:00
|
|
|
export const _PO = [
|
|
|
|
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
2023-11-24 14:45:55 +00:00
|
|
|
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2021-01-03 23:01:30 +00:00
|
|
|
/**
|
|
|
|
* DOS 13-sector disk physical sector order (index is disk sector, value is
|
|
|
|
* physical sector).
|
|
|
|
*/
|
|
|
|
export const D13O = [
|
2023-11-24 14:45:55 +00:00
|
|
|
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2021-01-03 23:01:30 +00:00
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
export const _D13O = [
|
2023-11-24 14:45:55 +00:00
|
|
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc,
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2022-09-18 13:40:08 +00:00
|
|
|
const TRANS53 = [
|
2019-10-02 02:56:10 +00:00
|
|
|
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
|
|
|
|
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
|
|
|
|
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
|
|
|
|
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2022-09-18 13:40:08 +00:00
|
|
|
export const DETRANS53 = [
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // A0
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, // A8
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x06, // B0
|
|
|
|
0x00, 0x00, 0x07, 0x08, 0x00, 0x09, 0x0A, 0x0B, // B8
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C0
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C8
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0D, // D0
|
|
|
|
0x00, 0x00, 0x0E, 0x0F, 0x00, 0x10, 0x11, 0x12, // D8
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // E0
|
|
|
|
0x00, 0x00, 0x13, 0x14, 0x00, 0x15, 0x16, 0x17, // E8
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x19, 0x1A, // F0
|
|
|
|
0x00, 0x00, 0x1B, 0x1C, 0x00, 0x1D, 0x1E, 0x1F, // F8
|
|
|
|
] as const;
|
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2022-09-18 13:40:08 +00:00
|
|
|
const TRANS62 = [
|
2019-10-02 02:56:10 +00:00
|
|
|
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
|
|
|
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
|
|
|
|
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
|
|
|
|
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
|
|
|
|
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
|
|
|
|
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
|
|
|
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
|
|
|
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
// prettier-ignore
|
2022-09-18 13:40:08 +00:00
|
|
|
export const DETRANS62 = [
|
2019-10-02 02:56:10 +00:00
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
|
|
|
0x00, 0x00, 0x02, 0x03, 0x00, 0x04, 0x05, 0x06,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
|
|
|
|
0x00, 0x00, 0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
|
|
|
0x00, 0x00, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
|
|
|
|
0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E,
|
|
|
|
0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x21,
|
|
|
|
0x00, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x2A, 0x2B,
|
|
|
|
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
|
|
|
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
|
|
|
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
|
2021-07-07 00:04:02 +00:00
|
|
|
] as const;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
/**
|
2021-07-07 00:04:02 +00:00
|
|
|
* Converts a byte into its 4x4 encoded representation
|
|
|
|
*
|
|
|
|
* @param val byte to encode.
|
|
|
|
* @returns A two byte array of representing the 4x4 encoding.
|
2019-10-02 02:56:10 +00:00
|
|
|
*/
|
2020-11-24 16:48:14 +00:00
|
|
|
export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
2020-11-24 16:53:43 +00:00
|
|
|
let xx = val & 0xaa;
|
|
|
|
let yy = val & 0x55;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
xx >>= 1;
|
|
|
|
xx |= 0xaa;
|
|
|
|
yy |= 0xaa;
|
|
|
|
|
|
|
|
return [xx, yy];
|
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
|
|
|
* Converts 2 4x4 encoded bytes into a byte value
|
|
|
|
*
|
|
|
|
* @param xx First encoded byte.
|
|
|
|
* @param yy Second encoded byte.
|
|
|
|
* @returns The decoded value.
|
|
|
|
*/
|
2020-11-24 16:48:14 +00:00
|
|
|
export function defourXfour(xx: byte, yy: byte): byte {
|
2019-10-02 02:56:10 +00:00
|
|
|
return ((xx << 1) | 0x01) & yy;
|
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
|
|
|
* Converts a raw sector into a nibblized representation to be combined into a
|
|
|
|
* nibblized 16 sector track.
|
|
|
|
*
|
|
|
|
* @param volume volume number
|
|
|
|
* @param track track number
|
|
|
|
* @param sector sector number
|
|
|
|
* @param data sector data
|
|
|
|
* @returns a nibblized representation of the sector data
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
export function explodeSector16(
|
|
|
|
volume: byte,
|
|
|
|
track: byte,
|
|
|
|
sector: byte,
|
|
|
|
data: memory
|
|
|
|
): byte[] {
|
2020-11-24 16:48:14 +00:00
|
|
|
let buf = [];
|
|
|
|
let gap;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
if (sector === 0)
|
|
|
|
// Gap 1
|
2019-10-02 02:56:10 +00:00
|
|
|
gap = 0x80;
|
2023-11-24 14:45:55 +00:00
|
|
|
else {
|
|
|
|
// Gap 3
|
2019-10-02 02:56:10 +00:00
|
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
|
|
}
|
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
buf.push(0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Address Field
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:53:43 +00:00
|
|
|
const checksum = volume ^ track ^ sector;
|
2019-10-02 02:56:10 +00:00
|
|
|
buf = buf.concat([0xd5, 0xaa, 0x96]); // Address Prolog D5 AA 96
|
|
|
|
buf = buf.concat(fourXfour(volume));
|
|
|
|
buf = buf.concat(fourXfour(track));
|
|
|
|
buf = buf.concat(fourXfour(sector));
|
|
|
|
buf = buf.concat(fourXfour(checksum));
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 2 (5 bytes)
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
buf.push(0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Data Field
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
|
2020-11-24 16:53:43 +00:00
|
|
|
const nibbles: byte[] = [];
|
2020-11-24 16:48:14 +00:00
|
|
|
const ptr2 = 0;
|
|
|
|
const ptr6 = 0x56;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
nibbles[idx] = 0;
|
|
|
|
}
|
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
let idx2 = 0x55;
|
|
|
|
for (let idx6 = 0x101; idx6 >= 0; idx6--) {
|
|
|
|
let val6 = data[idx6 % 0x100];
|
|
|
|
let val2: byte = nibbles[ptr2 + idx2];
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
|
|
val6 >>= 1;
|
|
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
|
|
val6 >>= 1;
|
|
|
|
|
|
|
|
nibbles[ptr6 + idx6] = val6;
|
|
|
|
nibbles[ptr2 + idx2] = val2;
|
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
if (--idx2 < 0) idx2 = 0x55;
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
let last = 0;
|
|
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
|
|
|
const val = nibbles[idx];
|
2022-09-18 13:40:08 +00:00
|
|
|
buf.push(TRANS62[last ^ val]);
|
2019-10-02 02:56:10 +00:00
|
|
|
last = val;
|
|
|
|
}
|
2022-09-18 13:40:08 +00:00
|
|
|
buf.push(TRANS62[last]);
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-12-29 14:40:40 +00:00
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 3
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
buf.push(0xff);
|
|
|
|
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
|
|
|
* Converts a raw sector into a nibblized representation to be combined into
|
|
|
|
* a nibblized 13 sector track.
|
|
|
|
*
|
|
|
|
* @param volume volume number
|
|
|
|
* @param track track number
|
|
|
|
* @param sector sector number
|
|
|
|
* @param data sector data
|
|
|
|
* @returns a nibblized representation of the sector data
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
export function explodeSector13(
|
|
|
|
volume: byte,
|
|
|
|
track: byte,
|
|
|
|
sector: byte,
|
|
|
|
data: memory
|
|
|
|
): byte[] {
|
2020-11-24 16:48:14 +00:00
|
|
|
let buf = [];
|
|
|
|
let gap;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
if (sector === 0)
|
|
|
|
// Gap 1
|
2019-10-02 02:56:10 +00:00
|
|
|
gap = 0x80;
|
2023-11-24 14:45:55 +00:00
|
|
|
else {
|
|
|
|
// Gap 3
|
2019-10-02 02:56:10 +00:00
|
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
|
|
}
|
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
buf.push(0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Address Field
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:53:43 +00:00
|
|
|
const checksum = volume ^ track ^ sector;
|
2019-10-02 02:56:10 +00:00
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xb5]); // Address Prolog D5 AA B5
|
|
|
|
buf = buf.concat(fourXfour(volume));
|
|
|
|
buf = buf.concat(fourXfour(track));
|
|
|
|
buf = buf.concat(fourXfour(sector));
|
|
|
|
buf = buf.concat(fourXfour(checksum));
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 2 (5 bytes)
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
buf.push(0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Data Field
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
|
2020-11-24 16:53:43 +00:00
|
|
|
const nibbles = [];
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
let jdx = 0;
|
|
|
|
for (let idx = 0x32; idx >= 0; idx--) {
|
2020-11-24 16:53:43 +00:00
|
|
|
const a5 = data[jdx] >> 3;
|
|
|
|
const a3 = data[jdx] & 0x07;
|
2019-10-02 02:56:10 +00:00
|
|
|
jdx++;
|
2020-11-24 16:53:43 +00:00
|
|
|
const b5 = data[jdx] >> 3;
|
|
|
|
const b3 = data[jdx] & 0x07;
|
2019-10-02 02:56:10 +00:00
|
|
|
jdx++;
|
2020-11-24 16:53:43 +00:00
|
|
|
const c5 = data[jdx] >> 3;
|
|
|
|
const c3 = data[jdx] & 0x07;
|
2019-10-02 02:56:10 +00:00
|
|
|
jdx++;
|
2020-11-24 16:53:43 +00:00
|
|
|
const d5 = data[jdx] >> 3;
|
|
|
|
const d3 = data[jdx] & 0x07;
|
2019-10-02 02:56:10 +00:00
|
|
|
jdx++;
|
2020-11-24 16:53:43 +00:00
|
|
|
const e5 = data[jdx] >> 3;
|
|
|
|
const e3 = data[jdx] & 0x07;
|
2019-10-02 02:56:10 +00:00
|
|
|
jdx++;
|
|
|
|
nibbles[idx + 0x00] = a5;
|
|
|
|
nibbles[idx + 0x33] = b5;
|
|
|
|
nibbles[idx + 0x66] = c5;
|
|
|
|
nibbles[idx + 0x99] = d5;
|
|
|
|
nibbles[idx + 0xcc] = e5;
|
2023-11-24 14:45:55 +00:00
|
|
|
nibbles[idx + 0x100] =
|
|
|
|
(a3 << 2) | ((d3 & 0x4) >> 1) | ((e3 & 0x4) >> 2);
|
|
|
|
nibbles[idx + 0x133] = (b3 << 2) | (d3 & 0x2) | ((e3 & 0x2) >> 1);
|
|
|
|
nibbles[idx + 0x166] = (c3 << 2) | ((d3 & 0x1) << 1) | (e3 & 0x1);
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
nibbles[0xff] = data[jdx] >> 3;
|
|
|
|
nibbles[0x199] = data[jdx] & 0x07;
|
|
|
|
|
2020-11-24 16:48:14 +00:00
|
|
|
let last = 0;
|
|
|
|
for (let idx = 0x199; idx >= 0x100; idx--) {
|
|
|
|
const val = nibbles[idx];
|
2022-09-18 13:40:08 +00:00
|
|
|
buf.push(TRANS53[last ^ val]);
|
2019-10-02 02:56:10 +00:00
|
|
|
last = val;
|
|
|
|
}
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let idx = 0x0; idx < 0x100; idx++) {
|
|
|
|
const val = nibbles[idx];
|
2022-09-18 13:40:08 +00:00
|
|
|
buf.push(TRANS53[last ^ val]);
|
2019-10-02 02:56:10 +00:00
|
|
|
last = val;
|
|
|
|
}
|
2022-09-18 13:40:08 +00:00
|
|
|
buf.push(TRANS53[last]);
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
|
|
|
|
/*
|
2020-11-24 16:48:14 +00:00
|
|
|
* Gap 3
|
|
|
|
*/
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
buf.push(0xff);
|
|
|
|
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
|
2022-07-23 19:00:38 +00:00
|
|
|
export interface TrackNibble {
|
|
|
|
track: byte;
|
|
|
|
sector: byte;
|
|
|
|
nibble: byte;
|
2022-09-18 13:40:08 +00:00
|
|
|
sectors: SupportedSectors;
|
|
|
|
}
|
|
|
|
|
|
|
|
enum LookingFor {
|
|
|
|
START_OF_FIELD_MARKER_FIRST_NIBBLE,
|
|
|
|
START_OF_FIELD_MARKER_SECOND_NIBBLE,
|
|
|
|
FIELD_TYPE_MARKER,
|
|
|
|
ADDRESS_FIELD,
|
|
|
|
ADDRESS_FIELD_13,
|
|
|
|
DATA_FIELD_6AND2,
|
|
|
|
DATA_FIELD_5AND3,
|
|
|
|
}
|
|
|
|
|
|
|
|
export class FindSectorError extends Error {
|
2023-11-24 14:45:55 +00:00
|
|
|
constructor(track: byte, sector: byte, e: unknown) {
|
|
|
|
super(
|
|
|
|
`Error finding track ${track} (${toHex(
|
|
|
|
track
|
|
|
|
)}), sector ${sector} (${toHex(sector)}): ` +
|
|
|
|
(e instanceof Error ? `${e.message}` : `${String(e)}`)
|
|
|
|
);
|
2022-09-18 13:40:08 +00:00
|
|
|
}
|
2022-07-23 19:00:38 +00:00
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
2022-09-18 13:40:08 +00:00
|
|
|
* Finds a sector of data from a nibblized disk. The sector given should be the
|
|
|
|
* "physical" sector number, meaning the one that appears in the address field.
|
|
|
|
* The first sector with the right sector number and data whose checksum matches
|
|
|
|
* is returned. This means that for a dual-boot disk (DOS 3.2 and DOS 3.3),
|
|
|
|
* whichever sector is found first wins.
|
2021-07-07 00:04:02 +00:00
|
|
|
*
|
|
|
|
* @param disk Nibble disk
|
|
|
|
* @param track track number to read
|
|
|
|
* @param sector sector number to read
|
2022-09-18 13:40:08 +00:00
|
|
|
* @returns the track, sector, nibble offset, and detected sectors
|
2021-07-07 00:04:02 +00:00
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
export function findSector(
|
|
|
|
disk: NibbleDisk,
|
|
|
|
track: byte,
|
|
|
|
sector: byte
|
|
|
|
): TrackNibble {
|
2022-09-18 13:40:08 +00:00
|
|
|
const cur = disk.tracks[track];
|
|
|
|
let sectors: SupportedSectors = 16;
|
|
|
|
let state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
2020-11-24 16:48:14 +00:00
|
|
|
let idx = 0;
|
|
|
|
let retry = 0;
|
2019-10-02 02:56:10 +00:00
|
|
|
|
|
|
|
function _readNext() {
|
2020-11-24 16:53:43 +00:00
|
|
|
const result = cur[idx++];
|
2019-10-02 02:56:10 +00:00
|
|
|
if (idx >= cur.length) {
|
|
|
|
idx = 0;
|
|
|
|
retry++;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2020-11-24 16:48:14 +00:00
|
|
|
function _skipBytes(count: number) {
|
2019-10-02 02:56:10 +00:00
|
|
|
idx += count;
|
|
|
|
if (idx >= cur.length) {
|
|
|
|
idx %= cur.length;
|
|
|
|
retry++;
|
|
|
|
}
|
|
|
|
}
|
2023-11-24 14:45:55 +00:00
|
|
|
let t = 0,
|
|
|
|
s = 0,
|
|
|
|
v = 0,
|
|
|
|
checkSum;
|
2019-10-02 02:56:10 +00:00
|
|
|
while (retry < 4) {
|
2022-09-18 13:40:08 +00:00
|
|
|
let val: byte;
|
2019-10-02 02:56:10 +00:00
|
|
|
switch (state) {
|
2022-09-18 13:40:08 +00:00
|
|
|
case LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE:
|
2020-11-26 01:28:37 +00:00
|
|
|
val = _readNext();
|
2023-11-24 14:45:55 +00:00
|
|
|
state =
|
|
|
|
val === 0xd5
|
|
|
|
? LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE
|
|
|
|
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
2020-11-26 01:28:37 +00:00
|
|
|
break;
|
2022-09-18 13:40:08 +00:00
|
|
|
case LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE:
|
2020-11-26 01:28:37 +00:00
|
|
|
val = _readNext();
|
2023-11-24 14:45:55 +00:00
|
|
|
state =
|
|
|
|
val === 0xaa
|
|
|
|
? LookingFor.FIELD_TYPE_MARKER
|
|
|
|
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
2020-11-26 01:28:37 +00:00
|
|
|
break;
|
2022-09-18 13:40:08 +00:00
|
|
|
case LookingFor.FIELD_TYPE_MARKER:
|
2020-11-26 01:28:37 +00:00
|
|
|
val = _readNext();
|
2022-09-18 13:40:08 +00:00
|
|
|
switch (val) {
|
|
|
|
case 0x96:
|
|
|
|
state = LookingFor.ADDRESS_FIELD;
|
|
|
|
sectors = 16;
|
|
|
|
break;
|
2023-11-24 14:45:55 +00:00
|
|
|
case 0xb5:
|
2022-09-18 13:40:08 +00:00
|
|
|
state = LookingFor.ADDRESS_FIELD;
|
|
|
|
sectors = 13;
|
|
|
|
break;
|
2023-11-24 14:45:55 +00:00
|
|
|
case 0xad:
|
|
|
|
state =
|
|
|
|
sectors === 16
|
|
|
|
? LookingFor.DATA_FIELD_6AND2
|
|
|
|
: LookingFor.DATA_FIELD_5AND3;
|
2022-09-18 13:40:08 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
|
|
|
}
|
2020-11-26 01:28:37 +00:00
|
|
|
break;
|
2022-09-18 13:40:08 +00:00
|
|
|
case LookingFor.ADDRESS_FIELD:
|
2020-11-26 01:28:37 +00:00
|
|
|
v = defourXfour(_readNext(), _readNext()); // Volume
|
2022-09-18 13:40:08 +00:00
|
|
|
t = defourXfour(_readNext(), _readNext()); // Track
|
|
|
|
s = defourXfour(_readNext(), _readNext()); // Sector
|
2020-11-26 01:28:37 +00:00
|
|
|
checkSum = defourXfour(_readNext(), _readNext());
|
2022-05-18 15:19:45 +00:00
|
|
|
if (checkSum !== (v ^ t ^ s)) {
|
2023-11-24 14:45:55 +00:00
|
|
|
debug(
|
|
|
|
'Invalid header checksum:',
|
|
|
|
toHex(v),
|
|
|
|
toHex(t),
|
|
|
|
toHex(s),
|
|
|
|
toHex(checkSum)
|
|
|
|
);
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
2020-11-26 01:28:37 +00:00
|
|
|
_skipBytes(3); // Skip footer
|
|
|
|
state = 0;
|
|
|
|
break;
|
2022-09-18 13:40:08 +00:00
|
|
|
case LookingFor.DATA_FIELD_6AND2:
|
|
|
|
if (s === sector && t === track) {
|
2022-08-20 15:31:34 +00:00
|
|
|
// Save start of data
|
|
|
|
const nibble = idx;
|
|
|
|
|
|
|
|
// Do checksum on data
|
|
|
|
let last = 0;
|
|
|
|
for (let jdx = 0; jdx < 0x156; jdx++) {
|
2022-09-18 13:40:08 +00:00
|
|
|
last = DETRANS62[_readNext() - 0x80] ^ last;
|
2022-08-20 15:31:34 +00:00
|
|
|
}
|
2022-09-18 13:40:08 +00:00
|
|
|
const checkSum = DETRANS62[_readNext() - 0x80] ^ last;
|
2022-08-20 15:31:34 +00:00
|
|
|
// Validate checksum before returning
|
|
|
|
if (!checkSum) {
|
2022-09-18 13:40:08 +00:00
|
|
|
return { track, sector, nibble, sectors };
|
2022-08-20 15:31:34 +00:00
|
|
|
} else {
|
2023-11-24 14:45:55 +00:00
|
|
|
debug(
|
|
|
|
'Invalid data checksum:',
|
|
|
|
toHex(last),
|
|
|
|
toHex(track),
|
|
|
|
toHex(sector),
|
|
|
|
toHex(checkSum)
|
|
|
|
);
|
2022-08-20 15:31:34 +00:00
|
|
|
}
|
|
|
|
_skipBytes(3); // Skip footer
|
2023-11-24 14:45:55 +00:00
|
|
|
} else _skipBytes(0x159); // Skip data, checksum and footer
|
2022-09-18 13:40:08 +00:00
|
|
|
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
|
|
|
break;
|
|
|
|
case LookingFor.DATA_FIELD_5AND3:
|
|
|
|
if (s === sector && t === track) {
|
|
|
|
// Save start of data
|
|
|
|
const nibble = idx;
|
|
|
|
|
|
|
|
// Do checksum on data
|
|
|
|
let last = 0;
|
2023-11-24 14:45:55 +00:00
|
|
|
for (let jdx = 0; jdx < 0x19a; jdx++) {
|
|
|
|
last = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
}
|
2023-11-24 14:45:55 +00:00
|
|
|
const checkSum = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
// Validate checksum before returning
|
|
|
|
if (!checkSum) {
|
|
|
|
return { track, sector, nibble, sectors };
|
|
|
|
} else {
|
2023-11-24 14:45:55 +00:00
|
|
|
debug(
|
|
|
|
'Invalid data checksum:',
|
|
|
|
toHex(last),
|
|
|
|
toHex(track),
|
|
|
|
toHex(sector),
|
|
|
|
toHex(checkSum)
|
|
|
|
);
|
2022-09-18 13:40:08 +00:00
|
|
|
}
|
|
|
|
_skipBytes(3); // Skip footer
|
2023-11-24 14:45:55 +00:00
|
|
|
} else {
|
|
|
|
_skipBytes(0x19a); // Skip data, checksum and footer
|
2022-09-18 13:40:08 +00:00
|
|
|
}
|
|
|
|
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
2020-11-26 01:28:37 +00:00
|
|
|
break;
|
|
|
|
default:
|
2022-09-18 13:40:08 +00:00
|
|
|
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
|
2020-11-26 01:28:37 +00:00
|
|
|
break;
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
}
|
2022-09-18 13:40:08 +00:00
|
|
|
throw new FindSectorError(track, sector, `too many retries (${retry})`);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class InvalidChecksum extends Error {
|
|
|
|
constructor(expected: byte, received: byte) {
|
|
|
|
super(`Expected: ${toHex(expected)}, received: ${toHex(received)}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ReadSectorError extends Error {
|
2023-11-24 14:45:55 +00:00
|
|
|
constructor(track: byte, sector: byte, e: unknown) {
|
|
|
|
super(
|
|
|
|
`Error reading track ${track} (${toHex(
|
|
|
|
track
|
|
|
|
)}), sector ${sector} (${toHex(sector)}): ` +
|
|
|
|
(e instanceof Error ? `${e.message}` : `${String(e)}`)
|
|
|
|
);
|
2022-09-18 13:40:08 +00:00
|
|
|
}
|
2022-07-23 19:00:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-09-18 13:40:08 +00:00
|
|
|
* Reads a sector of data from a nibblized disk. The sector given should be the
|
2023-11-24 14:45:55 +00:00
|
|
|
* "physical" sector number, meaning the one that appears in the address field.
|
2022-09-18 13:40:08 +00:00
|
|
|
* Like `findSector`, the first sector with the right sector number and data
|
|
|
|
* whose checksum matches is returned. This means that for a dual-boot disk
|
|
|
|
* (DOS 3.2 and DOS 3.3), whichever sector is found first wins.
|
2022-07-23 19:00:38 +00:00
|
|
|
*
|
2022-09-18 13:40:08 +00:00
|
|
|
* This does not work for WOZ disks.
|
2023-11-24 14:45:55 +00:00
|
|
|
*
|
2022-09-18 13:40:08 +00:00
|
|
|
* If the given track and sector combination is not found, a `ReadSectorError`
|
|
|
|
* will be thrown.
|
2022-07-23 19:00:38 +00:00
|
|
|
*
|
|
|
|
* @param disk Nibble disk
|
|
|
|
* @param track track number to read
|
|
|
|
* @param sector sector number to read
|
|
|
|
* @returns An array of sector data bytes.
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
export function readSector(
|
|
|
|
disk: NibbleDisk,
|
|
|
|
track: byte,
|
|
|
|
sector: byte
|
|
|
|
): Uint8Array {
|
2022-07-23 19:00:38 +00:00
|
|
|
const trackNibble = findSector(disk, track, sector);
|
2022-09-18 13:40:08 +00:00
|
|
|
const { nibble, sectors } = trackNibble;
|
2022-07-23 19:00:38 +00:00
|
|
|
const cur = disk.tracks[track];
|
|
|
|
|
|
|
|
let idx = nibble;
|
2022-08-20 15:31:34 +00:00
|
|
|
const _readNext = () => {
|
2022-07-23 19:00:38 +00:00
|
|
|
const result = cur[idx++];
|
2022-08-20 15:31:34 +00:00
|
|
|
if (idx >= cur.length) {
|
2022-07-23 19:00:38 +00:00
|
|
|
idx = 0;
|
|
|
|
}
|
|
|
|
return result;
|
2022-08-20 15:31:34 +00:00
|
|
|
};
|
2022-07-23 19:00:38 +00:00
|
|
|
|
2022-09-18 13:40:08 +00:00
|
|
|
try {
|
2023-11-24 14:45:55 +00:00
|
|
|
return sectors === 13
|
|
|
|
? readSector13(_readNext)
|
|
|
|
: readSector16(_readNext);
|
2022-09-18 13:40:08 +00:00
|
|
|
} catch (e: unknown) {
|
|
|
|
throw new ReadSectorError(track, sector, e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function readSector16(_readNext: () => byte): Uint8Array {
|
2022-07-23 19:00:38 +00:00
|
|
|
const data = new Uint8Array(256);
|
|
|
|
const data2 = [];
|
2022-09-18 13:40:08 +00:00
|
|
|
let last: byte = 0;
|
2022-07-23 19:00:38 +00:00
|
|
|
let val;
|
|
|
|
|
|
|
|
for (let jdx = 0x55; jdx >= 0; jdx--) {
|
2022-09-18 13:40:08 +00:00
|
|
|
val = DETRANS62[_readNext() - 0x80] ^ last;
|
2022-07-23 19:00:38 +00:00
|
|
|
data2[jdx] = val;
|
|
|
|
last = val;
|
|
|
|
}
|
|
|
|
for (let jdx = 0; jdx < 0x100; jdx++) {
|
2022-09-18 13:40:08 +00:00
|
|
|
val = DETRANS62[_readNext() - 0x80] ^ last;
|
2022-07-23 19:00:38 +00:00
|
|
|
data[jdx] = val;
|
|
|
|
last = val;
|
|
|
|
}
|
2022-09-18 13:40:08 +00:00
|
|
|
const checkSum = DETRANS62[_readNext() - 0x80] ^ last;
|
2022-07-23 19:00:38 +00:00
|
|
|
if (checkSum) {
|
2022-09-18 13:40:08 +00:00
|
|
|
throw new InvalidChecksum(last, checkSum ^ last);
|
2022-07-23 19:00:38 +00:00
|
|
|
}
|
|
|
|
for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
|
|
|
|
data[kdx] <<= 1;
|
|
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
|
|
data[kdx] |= 0x01;
|
|
|
|
}
|
|
|
|
data2[jdx] >>= 1;
|
|
|
|
|
|
|
|
data[kdx] <<= 1;
|
|
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
|
|
data[kdx] |= 0x01;
|
|
|
|
}
|
|
|
|
data2[jdx] >>= 1;
|
|
|
|
|
|
|
|
if (--jdx < 0) jdx = 0x55;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2022-09-18 13:40:08 +00:00
|
|
|
function readSector13(_readNext: () => byte) {
|
|
|
|
const data = new Uint8Array(256);
|
|
|
|
let val: byte;
|
|
|
|
let last: byte = 0;
|
|
|
|
|
|
|
|
// special low 3-bits of 0xFF
|
2023-11-24 14:45:55 +00:00
|
|
|
val = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
last = val;
|
|
|
|
data[0xff] = val & 0b111;
|
|
|
|
|
|
|
|
// expect 0x99 nibbles of packed lower 3-bits in reverse order
|
|
|
|
for (let i = 0x98; i >= 0x00; i--) {
|
2023-11-24 14:45:55 +00:00
|
|
|
val = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
last = val;
|
|
|
|
const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
|
|
|
|
const dOff = 3 + 5 * (0x32 - (i % 0x33));
|
|
|
|
const eOff = 4 + 5 * (0x32 - (i % 0x33));
|
|
|
|
const bit = 2 - Math.floor(i / 0x33);
|
|
|
|
data[off] = (val & 0b11100) >> 2;
|
|
|
|
data[dOff] ^= ((val & 0b00010) >> 1) << bit;
|
|
|
|
data[eOff] ^= (val & 0b1) << bit;
|
|
|
|
}
|
|
|
|
|
|
|
|
// expect 0xFE nibbles of upper 5-bits
|
2023-11-24 14:45:55 +00:00
|
|
|
for (let i = 0; i < 0xff; i++) {
|
|
|
|
val = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
last = val;
|
|
|
|
const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
|
|
|
|
data[off] ^= val << 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
// and the last special nibble for 0xFF
|
2023-11-24 14:45:55 +00:00
|
|
|
val = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
last = val;
|
2023-11-24 14:45:55 +00:00
|
|
|
data[0xff] ^= val << 3;
|
2022-09-18 13:40:08 +00:00
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
const checkSum = DETRANS53[_readNext() - 0xa0] ^ last;
|
2022-09-18 13:40:08 +00:00
|
|
|
if (checkSum) {
|
|
|
|
throw new InvalidChecksum(last, checkSum ^ last);
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2022-07-23 19:00:38 +00:00
|
|
|
/**
|
|
|
|
* Reads a sector of data from a nibblized disk
|
|
|
|
*
|
|
|
|
* TODO(flan): Does not work on WOZ disks
|
|
|
|
*
|
|
|
|
* @param disk Nibble disk
|
|
|
|
* @param track track number to read
|
|
|
|
* @param sector sector number to read
|
|
|
|
* @returns An array of sector data bytes.
|
|
|
|
*/
|
2023-11-24 14:45:55 +00:00
|
|
|
export function writeSector(
|
|
|
|
disk: NibbleDisk,
|
|
|
|
track: byte,
|
|
|
|
sector: byte,
|
|
|
|
_data: Uint8Array
|
|
|
|
): boolean {
|
2022-07-23 19:00:38 +00:00
|
|
|
const trackNibble = findSector(disk, track, sector);
|
|
|
|
if (!trackNibble) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Todo
|
|
|
|
|
|
|
|
return true;
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
|
|
|
* Convert a nibblized disk into a JSON string for storage.
|
|
|
|
*
|
|
|
|
* @param disk Nibblized disk
|
|
|
|
* @param pretty Whether to format the output string
|
|
|
|
* @returns A JSON string representing the disk
|
|
|
|
*/
|
|
|
|
export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
|
2020-11-24 16:48:14 +00:00
|
|
|
// For 'nib', tracks are encoded as strings. For all other formats,
|
|
|
|
// tracks are arrays of sectors which are encoded as strings.
|
2020-11-24 16:53:43 +00:00
|
|
|
const data: string[] | string[][] = [];
|
2020-11-24 16:48:14 +00:00
|
|
|
let format = 'dsk';
|
2021-07-07 00:04:02 +00:00
|
|
|
for (let t = 0; t < disk.tracks.length; t++) {
|
2019-10-02 02:56:10 +00:00
|
|
|
data[t] = [];
|
2021-07-07 00:04:02 +00:00
|
|
|
if (disk.format === 'nib') {
|
2019-10-02 02:56:10 +00:00
|
|
|
format = 'nib';
|
2021-07-07 00:04:02 +00:00
|
|
|
data[t] = base64_encode(disk.tracks[t]);
|
2019-10-02 02:56:10 +00:00
|
|
|
} else {
|
2020-11-24 16:53:43 +00:00
|
|
|
for (let s = 0; s < 0x10; s++) {
|
2022-09-18 13:40:08 +00:00
|
|
|
const _sector = disk.format === 'po' ? _PO[s] : _DO[s];
|
2023-11-24 14:45:55 +00:00
|
|
|
(data[t] as string[])[s] = base64_encode(
|
|
|
|
readSector(disk, t, _sector)
|
|
|
|
);
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-24 14:45:55 +00:00
|
|
|
return JSON.stringify(
|
|
|
|
{
|
|
|
|
type: format,
|
|
|
|
encoding: 'base64',
|
|
|
|
volume: disk.volume,
|
|
|
|
data: data,
|
|
|
|
readOnly: disk.readOnly,
|
|
|
|
},
|
|
|
|
undefined,
|
|
|
|
pretty ? ' ' : undefined
|
|
|
|
);
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
/**
|
|
|
|
* Convert a JSON string into a nibblized disk.
|
|
|
|
*
|
|
|
|
* @param data JSON string representing a disk image, created by [jsonEncode].
|
|
|
|
* @returns A nibblized disk
|
|
|
|
*/
|
|
|
|
|
|
|
|
export function jsonDecode(data: string): NibbleDisk {
|
2020-11-24 16:53:43 +00:00
|
|
|
const tracks: memory[] = [];
|
2022-05-31 15:38:40 +00:00
|
|
|
const json = JSON.parse(data) as JSONDisk;
|
|
|
|
const v = json.volume || 254;
|
|
|
|
const readOnly = json.readOnly || false;
|
2020-11-24 16:48:14 +00:00
|
|
|
for (let t = 0; t < json.data.length; t++) {
|
|
|
|
let track: byte[] = [];
|
|
|
|
for (let s = 0; s < json.data[t].length; s++) {
|
2022-05-18 15:19:45 +00:00
|
|
|
const _s = json.type === 'po' ? PO[s] : DO[s];
|
2022-05-31 15:38:40 +00:00
|
|
|
const sector: string = json.data[t][_s] as string;
|
2020-11-24 16:48:14 +00:00
|
|
|
const d = base64_decode(sector);
|
2019-10-02 02:56:10 +00:00
|
|
|
track = track.concat(explodeSector16(v, t, s, d));
|
|
|
|
}
|
|
|
|
tracks[t] = bytify(track);
|
|
|
|
}
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
if (!isNibbleDiskFormat(json.type)) {
|
|
|
|
throw new Error(`JSON disks of type ${json.type} are not supported`);
|
|
|
|
}
|
2021-07-07 00:04:02 +00:00
|
|
|
const disk: NibbleDisk = {
|
2020-11-24 16:48:14 +00:00
|
|
|
volume: v,
|
|
|
|
format: json.type,
|
2021-07-07 00:04:02 +00:00
|
|
|
encoding: ENCODING_NIBBLE,
|
Floppy controller refactorings 1 (#155)
* Add `DiskMetada` to the `Disk` interface
Before, metadata about the image, such as name, side, etc. was mixed
in with actual disk image information. This change breaks that
information into a separate structure called `DiskMetadata`.
Currently, the only two fields are `name` and `side`, but the idea is
that more fields could be added as necessary, like a description, a
scan of the disk or label, etc. In a follow-on change, the default
write-protection status will come from the metadata as well.
The current implementation copies the metadata when saving/restoring
state, loading disk images, etc. In the future, the metadata should
passed around until the format is required to change (like saving one
disk image format as another). Likewise, in the future, in may be
desirable to be able to override the disk image metadata with
user-supplied metadata. This could be use, for example, to
temporarily add or remove write-protection from a disk image.
All existing tests pass and the emulator builds with no errors.
* Rename `writeMode` to `q7`
Before, nibble disk emulation used the `writeMode` field to keep track
of whether the drive should be read from or written to, but the WOZ
emulation used `q7` to keep track of the same state.
This change renames `writeMode` to `q7` because it more accurately
reflects the state of the Disk II controller as specified in the
manuals, DOS source, and, especially, _Understanding the Apple //e_ by
Jim Sather.
* Remove the coil state
Before, `q` captured the state of the coils. But it was never read.
This change just deletes it.
* Use the bootstrap and sequencer ROMs with indirection
Before, the contents of the bootstrap ROM and sequencer ROM were set
directly on fields of the controller. These were not saved or
restored with the state in `getState` and `setState`. (It would have
been very space inefficient if they had).
Now, these ROMs are used from constants indexed by the number of
sectors the card supports. This, in turn, means that if the number of
sectors is saved with the state, it can be easily restored.
* Split out the Disk II controller state
This change factors the emulated hardware state into a separate
structure in the Disk II controller. The idea is that this hardware
state will be able to be shared with the WOZ and nibble disk code
instead of sharing _all_ of the controller state (like callbacks and
so forth).
* Factor out disk insertion
Before, several places in the code essentially inserted a new disk
image into the drive, which similar—but not always exactly the
same—code. Now there is an `insertDisk` method that is responsible
for inserting a new `FloppyDisk`.
All tests pass, everything compiles, manually tested nibble disks and
WOZ disks.
2022-09-01 01:55:01 +00:00
|
|
|
metadata: { name: json.name },
|
2020-11-24 16:48:14 +00:00
|
|
|
tracks,
|
|
|
|
readOnly,
|
|
|
|
};
|
2019-10-02 02:56:10 +00:00
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
return disk;
|
|
|
|
}
|
|
|
|
|
2021-07-10 00:54:27 +00:00
|
|
|
/**
|
|
|
|
* Debugging method that displays the logical sector ordering of a nibblized disk
|
|
|
|
*
|
|
|
|
* @param disk
|
|
|
|
*/
|
|
|
|
|
2021-07-07 00:04:02 +00:00
|
|
|
export function analyseDisk(disk: NibbleDisk) {
|
2022-07-23 19:00:38 +00:00
|
|
|
for (let track = 0; track < disk.tracks.length; track++) {
|
2021-07-07 00:04:02 +00:00
|
|
|
let outStr = `${toHex(track)}: `;
|
2023-11-24 14:45:55 +00:00
|
|
|
let val,
|
|
|
|
state = 0;
|
2021-07-07 00:04:02 +00:00
|
|
|
let idx = 0;
|
|
|
|
const cur = disk.tracks[track];
|
|
|
|
|
|
|
|
const _readNext = () => {
|
|
|
|
const result = cur[idx++];
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _skipBytes = (count: number) => {
|
|
|
|
idx += count;
|
|
|
|
};
|
|
|
|
|
2023-11-24 14:45:55 +00:00
|
|
|
let t = 0,
|
|
|
|
s = 0,
|
|
|
|
v = 0,
|
|
|
|
checkSum;
|
2021-07-07 00:04:02 +00:00
|
|
|
while (idx < cur.length) {
|
|
|
|
switch (state) {
|
|
|
|
case 0:
|
|
|
|
val = _readNext();
|
2023-11-24 14:45:55 +00:00
|
|
|
state = val === 0xd5 ? 1 : 0;
|
2021-07-07 00:04:02 +00:00
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
val = _readNext();
|
2023-11-24 14:45:55 +00:00
|
|
|
state = val === 0xaa ? 2 : 0;
|
2021-07-07 00:04:02 +00:00
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
val = _readNext();
|
2023-11-24 14:45:55 +00:00
|
|
|
state = val === 0x96 ? 3 : val === 0xad ? 4 : 0;
|
2021-07-07 00:04:02 +00:00
|
|
|
break;
|
|
|
|
case 3: // Address
|
|
|
|
v = defourXfour(_readNext(), _readNext()); // Volume
|
|
|
|
t = defourXfour(_readNext(), _readNext());
|
|
|
|
s = defourXfour(_readNext(), _readNext());
|
|
|
|
checkSum = defourXfour(_readNext(), _readNext());
|
2022-05-18 15:19:45 +00:00
|
|
|
if (checkSum !== (v ^ t ^ s)) {
|
2023-11-24 14:45:55 +00:00
|
|
|
debug(
|
|
|
|
'Invalid header checksum:',
|
|
|
|
toHex(v),
|
|
|
|
toHex(t),
|
|
|
|
toHex(s),
|
|
|
|
toHex(checkSum)
|
|
|
|
);
|
2021-07-07 00:04:02 +00:00
|
|
|
} else {
|
|
|
|
outStr += toHex(s, 1);
|
|
|
|
}
|
|
|
|
_skipBytes(3); // Skip footer
|
|
|
|
state = 0;
|
|
|
|
break;
|
|
|
|
case 4: // Valid header
|
|
|
|
_skipBytes(0x159); // Skip data, checksum and footer
|
|
|
|
state = 0;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
debug(outStr);
|
|
|
|
}
|
2021-07-10 00:54:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-07-10 00:53:30 +00:00
|
|
|
* Debugging utility to convert a bitstream into a nibble. Does not wrap.
|
2021-07-10 00:54:27 +00:00
|
|
|
*
|
|
|
|
* @param bits Bitstream containing nibbles
|
|
|
|
* @param offset Offset into bitstream to start nibblizing
|
2021-07-10 00:53:30 +00:00
|
|
|
* @returns nibble, the next nibble in the bitstream,
|
|
|
|
* and offset, the end of that nibble in the bitstream
|
2021-07-10 00:54:27 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
export function grabNibble(bits: bit[], offset: number) {
|
|
|
|
let nibble = 0;
|
|
|
|
let waitForOne = true;
|
|
|
|
|
|
|
|
while (offset < bits.length) {
|
|
|
|
const bit = bits[offset];
|
|
|
|
if (bit) {
|
|
|
|
nibble = (nibble << 1) | 0x01;
|
|
|
|
waitForOne = false;
|
|
|
|
} else {
|
|
|
|
if (!waitForOne) {
|
|
|
|
nibble = nibble << 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nibble & 0x80) {
|
|
|
|
// nibble complete return it
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
offset += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
nibble: nibble,
|
2023-11-24 14:45:55 +00:00
|
|
|
offset: offset,
|
2021-07-10 00:54:27 +00:00
|
|
|
};
|
2019-10-02 02:56:10 +00:00
|
|
|
}
|