mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Add load binary file (#140)
* Add load binary file * Add some validity indicators * cleanup
This commit is contained in:
parent
c5faad2f9f
commit
c2b78951a7
@ -1,10 +1,15 @@
|
||||
import { h, JSX } from 'preact';
|
||||
import cs from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Apple2 as Apple2Impl } from '../apple2';
|
||||
import { ControlButton } from './ControlButton';
|
||||
import { FileChooser } from './FileChooser';
|
||||
import { Inset } from './Inset';
|
||||
import { loadLocalBinaryFile } from './util/files';
|
||||
|
||||
import styles from './css/Debugger.module.css';
|
||||
import { ControlButton } from './ControlButton';
|
||||
import { spawn } from './util/promises';
|
||||
import { toHex } from 'js/util';
|
||||
|
||||
export interface DebuggerProps {
|
||||
apple2: Apple2Impl | undefined;
|
||||
@ -20,10 +25,25 @@ interface DebugData {
|
||||
zeroPage: string;
|
||||
}
|
||||
|
||||
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
|
||||
const VALID_PAGE = /^[0-9A-F]{1,2}$/i;
|
||||
const VALID_ADDRESS = /^[0-9A-F]{1,4}$/i;
|
||||
|
||||
const ERROR_ICON = (
|
||||
<div className={styles.invalid}>
|
||||
<i
|
||||
className="fa-solid fa-triangle-exclamation"
|
||||
title="Invalid hex address"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Debugger = ({ apple2 }: DebuggerProps) => {
|
||||
const debug = apple2?.getDebugger();
|
||||
const [data, setData] = useState<DebugData>();
|
||||
const [memoryPage, setMemoryPage] = useState('08');
|
||||
const [loadAddress, setLoadAddress] = useState('0800');
|
||||
const [run, setRun] = useState(true);
|
||||
const animationRef = useRef<number>(0);
|
||||
|
||||
const animate = useCallback(() => {
|
||||
@ -57,10 +77,38 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
|
||||
debug?.step();
|
||||
}, [debug]);
|
||||
|
||||
const doMemoryPage = useCallback((event: JSX.TargetedMouseEvent<HTMLInputElement>) => {
|
||||
const doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setLoadAddress(event.currentTarget.value);
|
||||
}, []);
|
||||
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setRun(event.currentTarget.checked);
|
||||
}, []);
|
||||
|
||||
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setMemoryPage(event.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => {
|
||||
if (debug && handles.length === 1) {
|
||||
spawn(async () => {
|
||||
const file = await handles[0].getFile();
|
||||
let atAddress = parseInt(loadAddress, 16) || 0x800;
|
||||
|
||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||
if (matches && matches.length === 3) {
|
||||
const [, , aux] = matches;
|
||||
atAddress = parseInt(aux, 16);
|
||||
}
|
||||
|
||||
await loadLocalBinaryFile(file, atAddress, debug);
|
||||
setLoadAddress(toHex(atAddress, 4));
|
||||
if (run) {
|
||||
debug?.runAt(atAddress);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [debug, loadAddress, run]);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
@ -74,9 +122,12 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
|
||||
zeroPage
|
||||
} = data;
|
||||
|
||||
const memoryPageValid = VALID_PAGE.test(memoryPage);
|
||||
const loadAddressValid = VALID_ADDRESS.test(loadAddress);
|
||||
|
||||
return (
|
||||
<Inset className={styles.inset}>
|
||||
<div className={styles.debugger}>
|
||||
<div className={cs(styles.debugger, styles.column)}>
|
||||
<div className={styles.heading}>Debugger</div>
|
||||
<span className={styles.subHeading}>Controls</span>
|
||||
<div className={styles.controls}>
|
||||
@ -125,18 +176,36 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeading}>Memory Page: $</span>
|
||||
<hr />
|
||||
<span className={styles.subHeading}>Memory Page: $ </span>
|
||||
<input
|
||||
min={0x00}
|
||||
max={0xff}
|
||||
value={memoryPage}
|
||||
onChange={doMemoryPage}
|
||||
maxLength={2}
|
||||
className={cs({ [styles.invalid]: !memoryPageValid })}
|
||||
/>
|
||||
{memoryPageValid ? null : ERROR_ICON}
|
||||
<pre className={styles.zp}>
|
||||
{memory}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<hr />
|
||||
<span className={styles.subHeading}>Load File: $ </span>
|
||||
<input
|
||||
type="text"
|
||||
value={loadAddress}
|
||||
maxLength={4}
|
||||
onChange={doLoadAddress}
|
||||
className={cs({ [styles.invalid]: !loadAddressValid })}
|
||||
/>
|
||||
{loadAddressValid ? null : ERROR_ICON}
|
||||
{' '}
|
||||
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
|
||||
<div className={styles.fileChooser}>
|
||||
<FileChooser onChange={doChooseFile} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Inset>
|
||||
);
|
||||
|
@ -49,3 +49,29 @@ input[type="file"]::file-selector-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
background-color: #65594d;
|
||||
border: 1px inset #65594d;
|
||||
padding: 7px;
|
||||
top: 7px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: #65594d;
|
||||
border: 1px inset #65594d;
|
||||
color: #0d0;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::after {
|
||||
content: "\2716";
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 2px;
|
||||
color: #0d0;
|
||||
}
|
||||
|
@ -7,19 +7,31 @@
|
||||
border: 1px inset;
|
||||
}
|
||||
|
||||
.debugger input {
|
||||
border: 1px inset;
|
||||
.debugger {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debugger button,
|
||||
.debugger input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debugger input[type="text"] {
|
||||
border: 1px inset;
|
||||
}
|
||||
|
||||
.debugger hr {
|
||||
color: #c4c1a0;
|
||||
}
|
||||
|
||||
.inset {
|
||||
margin: 5px 10px;
|
||||
margin: 5px 10px 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@ -54,3 +66,22 @@
|
||||
.stack {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.fileChooser {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
div.invalid {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.invalid i {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: -16px;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { includes } from 'js/types';
|
||||
import { includes, word } from 'js/types';
|
||||
import { initGamepad } from 'js/ui/gamepad';
|
||||
import {
|
||||
BlockFormat,
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from 'js/formats/types';
|
||||
import Disk2 from 'js/cards/disk2';
|
||||
import SmartPort from 'js/cards/smartport';
|
||||
import Debugger from 'js/debugger';
|
||||
|
||||
type ProgressCallback = (current: number, total: number) => void;
|
||||
|
||||
@ -255,3 +256,24 @@ export class SmartStorageBroker implements MassStorage<unknown> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load binary file into memory.
|
||||
*
|
||||
* @param file File object to read into memory
|
||||
* @param address Address at which to start load
|
||||
* @param debug Debugger object
|
||||
* @returns resolves to true if successful
|
||||
*/
|
||||
export const loadLocalBinaryFile = (file: File, address: word, debug: Debugger) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function () {
|
||||
const result = this.result as ArrayBuffer;
|
||||
const bytes = new Uint8Array(result);
|
||||
debug.setMemory(address, bytes);
|
||||
resolve(true);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
@ -72,6 +72,17 @@ export default class Debugger {
|
||||
this.container.run();
|
||||
};
|
||||
|
||||
/**
|
||||
* Restart at a given memory address.
|
||||
*
|
||||
* @param address Address to start execution
|
||||
*/
|
||||
|
||||
runAt = (address: word) => {
|
||||
this.cpu.reset();
|
||||
this.cpu.setPC(address);
|
||||
};
|
||||
|
||||
isRunning = () =>
|
||||
this.container.isRunning();
|
||||
|
||||
@ -212,6 +223,35 @@ export default class Debugger {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a range of memory. Will wrap at memory limit.
|
||||
*
|
||||
* @param address Starting address to read memory
|
||||
* @param length Length of memory to read.
|
||||
* @returns Byte array containing memory
|
||||
*/
|
||||
getMemory(address: word, length: word) {
|
||||
const bytes = new Uint8Array(length);
|
||||
for (let idx = 0; idx < length; idx++) {
|
||||
address &= 0xffff;
|
||||
bytes[idx] = this.cpu.read(address++);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a range of memory. Will wrap at memory limit.
|
||||
*
|
||||
* @param address Starting address to write memory
|
||||
* @param bytes Data to write
|
||||
*/
|
||||
setMemory(address: word, bytes: Uint8Array) {
|
||||
for (const byte of bytes) {
|
||||
address &= 0xffff;
|
||||
this.cpu.write(address++, byte);
|
||||
}
|
||||
}
|
||||
|
||||
list = (pc: word) => {
|
||||
const results = [];
|
||||
for (let idx = 0; idx < 20; idx++) {
|
||||
|
Loading…
Reference in New Issue
Block a user