Add load binary file (#140)

* Add load binary file

* Add some validity indicators

* cleanup
This commit is contained in:
Will Scullin 2022-07-04 13:10:47 -07:00 committed by GitHub
parent c5faad2f9f
commit c2b78951a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 11 deletions

View File

@ -1,10 +1,15 @@
import { h, JSX } from 'preact'; import { h, JSX } from 'preact';
import cs from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Apple2 as Apple2Impl } from '../apple2'; import { Apple2 as Apple2Impl } from '../apple2';
import { ControlButton } from './ControlButton';
import { FileChooser } from './FileChooser';
import { Inset } from './Inset'; import { Inset } from './Inset';
import { loadLocalBinaryFile } from './util/files';
import styles from './css/Debugger.module.css'; import styles from './css/Debugger.module.css';
import { ControlButton } from './ControlButton'; import { spawn } from './util/promises';
import { toHex } from 'js/util';
export interface DebuggerProps { export interface DebuggerProps {
apple2: Apple2Impl | undefined; apple2: Apple2Impl | undefined;
@ -20,10 +25,25 @@ interface DebugData {
zeroPage: string; 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) => { export const Debugger = ({ apple2 }: DebuggerProps) => {
const debug = apple2?.getDebugger(); const debug = apple2?.getDebugger();
const [data, setData] = useState<DebugData>(); const [data, setData] = useState<DebugData>();
const [memoryPage, setMemoryPage] = useState('08'); const [memoryPage, setMemoryPage] = useState('08');
const [loadAddress, setLoadAddress] = useState('0800');
const [run, setRun] = useState(true);
const animationRef = useRef<number>(0); const animationRef = useRef<number>(0);
const animate = useCallback(() => { const animate = useCallback(() => {
@ -57,10 +77,38 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
debug?.step(); debug?.step();
}, [debug]); }, [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); 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) { if (!data) {
return null; return null;
} }
@ -74,9 +122,12 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
zeroPage zeroPage
} = data; } = data;
const memoryPageValid = VALID_PAGE.test(memoryPage);
const loadAddressValid = VALID_ADDRESS.test(loadAddress);
return ( return (
<Inset className={styles.inset}> <Inset className={styles.inset}>
<div className={styles.debugger}> <div className={cs(styles.debugger, styles.column)}>
<div className={styles.heading}>Debugger</div> <div className={styles.heading}>Debugger</div>
<span className={styles.subHeading}>Controls</span> <span className={styles.subHeading}>Controls</span>
<div className={styles.controls}> <div className={styles.controls}>
@ -125,18 +176,36 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
</div> </div>
</div> </div>
<div> <div>
<span className={styles.subHeading}>Memory Page: $</span> <hr />
<span className={styles.subHeading}>Memory Page: $ </span>
<input <input
min={0x00}
max={0xff}
value={memoryPage} value={memoryPage}
onChange={doMemoryPage} onChange={doMemoryPage}
maxLength={2} maxLength={2}
className={cs({ [styles.invalid]: !memoryPageValid })}
/> />
{memoryPageValid ? null : ERROR_ICON}
<pre className={styles.zp}> <pre className={styles.zp}>
{memory} {memory}
</pre> </pre>
</div> </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> </div>
</Inset> </Inset>
); );

View File

@ -49,3 +49,29 @@ input[type="file"]::file-selector-button {
display: flex; display: flex;
flex-direction: column; 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;
}

View File

@ -7,19 +7,31 @@
border: 1px inset; border: 1px inset;
} }
.debugger input { .debugger {
border: 1px inset;
font-size: 12px; font-size: 12px;
} }
.debugger button,
.debugger input {
font-size: 12px;
}
.debugger input[type="text"] {
border: 1px inset;
}
.debugger hr {
color: #c4c1a0;
}
.inset { .inset {
margin: 5px 10px; margin: 5px 10px 0;
width: auto; width: auto;
} }
.heading { .heading {
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 18px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -54,3 +66,22 @@
.stack { .stack {
width: 10em; 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;
}

View File

@ -1,4 +1,4 @@
import { includes } from 'js/types'; import { includes, word } from 'js/types';
import { initGamepad } from 'js/ui/gamepad'; import { initGamepad } from 'js/ui/gamepad';
import { import {
BlockFormat, BlockFormat,
@ -12,6 +12,7 @@ import {
} from 'js/formats/types'; } from 'js/formats/types';
import Disk2 from 'js/cards/disk2'; import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport'; import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
type ProgressCallback = (current: number, total: number) => void; type ProgressCallback = (current: number, total: number) => void;
@ -255,3 +256,24 @@ export class SmartStorageBroker implements MassStorage<unknown> {
return null; 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);
});
};

View File

@ -72,6 +72,17 @@ export default class Debugger {
this.container.run(); 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 = () => isRunning = () =>
this.container.isRunning(); this.container.isRunning();
@ -212,6 +223,35 @@ export default class Debugger {
return result; 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) => { list = (pc: word) => {
const results = []; const results = [];
for (let idx = 0; idx < 20; idx++) { for (let idx = 0; idx < 20; idx++) {