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 { 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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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++) {
|
||||||
|
Loading…
Reference in New Issue
Block a user