diff --git a/frontends/web/index.ts b/frontends/web/index.ts index 2bd2deabc2aa1a314df3ea0eddf83a9fad59bb04..bf4afbd1c5c7592b58c5b014907d69cabae2a583 100644 --- a/frontends/web/index.ts +++ b/frontends/web/index.ts @@ -50,9 +50,12 @@ const BACKGROUNDS = [ background: background, backgrounds: BACKGROUNDS }); - await emulator.main({ romUrl: romUrl }); // sets the emulator in the global scope this is useful // to be able to access the emulator from global functions window.emulator = emulator; + + // starts the emulator with the provided ROM URL, this is + // going to run the main emulator (infinite) loop + await emulator.main({ romUrl: romUrl }); })(); diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts index be8be017e4b68ee2c5ce88645424dcdb785770fb..6ec9010a9a7d8099e4c715093e38cbf130dfcf2b 100644 --- a/frontends/web/react/components/index.ts +++ b/frontends/web/react/components/index.ts @@ -2,4 +2,5 @@ export * from "./audio-gb/audio-gb"; export * from "./debug/debug"; export * from "./help/help"; export * from "./registers-gb/registers-gb"; +export * from "./serial-section/serial-section"; export * from "./tiles-gb/tiles-gb"; diff --git a/frontends/web/react/components/serial-section/serial-section.css b/frontends/web/react/components/serial-section/serial-section.css new file mode 100644 index 0000000000000000000000000000000000000000..ba1a95ad35c4da654c3324919935439d4c38e1ce --- /dev/null +++ b/frontends/web/react/components/serial-section/serial-section.css @@ -0,0 +1,11 @@ +.serial-section .printer > .printer-lines { + font-size: 0px; +} + +.serial-section .printer > .printer-lines > .printer-line { + display: block; +} + +.serial-section .printer > .printer-lines > .placeholder { + font-size: initial; +} diff --git a/frontends/web/react/components/serial-section/serial-section.tsx b/frontends/web/react/components/serial-section/serial-section.tsx new file mode 100644 index 0000000000000000000000000000000000000000..be0018447965bccb8ffe44babb077381c060b98b --- /dev/null +++ b/frontends/web/react/components/serial-section/serial-section.tsx @@ -0,0 +1,137 @@ +import React, { FC, useEffect, useRef, useState } from "react"; +import { ButtonSwitch, Info, Pair, PanelTab } from "emukit"; +import { GameboyEmulator, bufferToDataUrl } from "../../../ts"; + +import "./serial-section.css"; + +const DEVICE_ICON: { [key: string]: string } = { + Null: "🛑", + Logger: "📜", + Printer: "🖨ï¸" +}; + +export type LoggerCallback = (data: Uint8Array) => void; +export type PrinterCallback = (imageBuffer: Uint8Array) => void; + +type SerialSectionProps = { + emulator: GameboyEmulator; + style?: string[]; + onLogger?: (onLoggerData: LoggerCallback) => void; + onPrinter?: (onPrinterData: PrinterCallback) => void; +}; + +export const SerialSection: FC<SerialSectionProps> = ({ + emulator, + style = [], + onLogger, + onPrinter +}) => { + const classes = () => ["serial-section", ...style].join(" "); + const [loggerData, setLoggerData] = useState<string>(); + const [printerImageUrls, setPrinterImageUrls] = useState<string[]>(); + const loggerDataRef = useRef<string[]>([]); + const printerDataRef = useRef<string[]>([]); + const loggerRef = useRef<HTMLDivElement>(null); + const imagesRef = useRef<HTMLDivElement>(null); + + const onLoggerData = (data: Uint8Array) => { + const byte = data[0]; + const charByte = String.fromCharCode(byte); + loggerDataRef.current.push(charByte); + setLoggerData(loggerDataRef.current.join("")); + }; + const onPrinterData = (imageBuffer: Uint8Array) => { + const imageUrl = bufferToDataUrl(imageBuffer, 160); + printerDataRef.current.unshift(imageUrl); + setPrinterImageUrls([...printerDataRef.current]); + }; + + useEffect(() => { + if (loggerRef.current) { + onLogger && onLogger(onLoggerData); + } + }, [loggerRef, loggerRef.current]); + + useEffect(() => { + if (imagesRef.current) { + onPrinter && onPrinter(onPrinterData); + } + }, [imagesRef, imagesRef.current]); + + const onEngineChange = (option: string) => { + switch (option) { + case "Null": + emulator.loadNullDevice(); + break; + + case "Logger": + emulator.loadLoggerDevice(); + break; + + case "Printer": + emulator.loadPrinterDevice(); + break; + } + + const optionIcon = DEVICE_ICON[option] ?? ""; + emulator.handlers.showToast?.( + `${optionIcon} ${option} attached to the serial port & active` + ); + }; + + const getTabs = () => { + return [ + <Info> + <Pair + key="button-device" + name={"Device"} + valueNode={ + <ButtonSwitch + options={["Null", "Logger", "Printer"]} + size={"large"} + style={["simple"]} + onChange={onEngineChange} + /> + } + /> + <Pair key="baud-rate" name={"Baud Rate"} value={"1 KB/s"} /> + </Info>, + <div className="logger" ref={loggerRef}> + <div className="logger-data"> + {loggerData || "Logger contents are empty."} + </div> + </div>, + <div className="printer" ref={imagesRef}> + <div className="printer-lines"> + {printerImageUrls ? ( + printerImageUrls.map((url, index) => ( + <img + key={index} + className="printer-line" + src={url} + /> + )) + ) : ( + <span className="placeholder"> + Printer contents are empty. + </span> + )} + </div> + </div> + ]; + }; + const getTabNames = () => { + return ["Settings", "Logger", "Printer"]; + }; + return ( + <div className={classes()}> + <PanelTab + tabs={getTabs()} + tabNames={getTabNames()} + selectors={true} + /> + </div> + ); +}; + +export default SerialSection; diff --git a/frontends/web/res/serial.svg b/frontends/web/res/serial.svg new file mode 100644 index 0000000000000000000000000000000000000000..578d6cb83ce6ee6cb657df805dfbb43e22df1f01 --- /dev/null +++ b/frontends/web/res/serial.svg @@ -0,0 +1 @@ +<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="swapHorizontalIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="swapHorizontalIconTitle">Swap items (horizontally)</title> <path d="M16 4L19 7L16 10"/> <path d="M4 7L18 7"/> <path d="M7 20L4 17L7 14"/> <path d="M19 17L5 17"/> </svg> \ No newline at end of file diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts index 7023ecabbb95b34769d4f03824f50e6455d9a07b..c861d82554f6c9beb91332324ea5db5334eb766b 100644 --- a/frontends/web/ts/gb.ts +++ b/frontends/web/ts/gb.ts @@ -1,6 +1,7 @@ import { AudioSpecs, BenchmarkResult, + SectionInfo, Compilation, Compiler, DebugPanel, @@ -16,8 +17,16 @@ import { Size } from "emukit"; import { PALETTES, PALETTES_MAP } from "./palettes"; -import { base64ToBuffer, bufferToBase64, bufferToDataUrl } from "./util"; -import { DebugAudio, DebugVideo, HelpFaqs, HelpKeyboard } from "../react"; +import { base64ToBuffer, bufferToBase64 } from "./util"; +import { + DebugAudio, + DebugVideo, + HelpFaqs, + HelpKeyboard, + LoggerCallback, + PrinterCallback, + SerialSection, +} from "../react"; import { Cartridge, @@ -124,6 +133,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { */ private extraSettings: Record<string, string> = {}; + private onLoggerData: ((data: Uint8Array) => void) | null = null; + + private onPrinterData: ((imageBuffer: Uint8Array) => void) | null = null; + constructor(extraSettings = {}) { super(); this.extraSettings = extraSettings; @@ -388,7 +401,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // a valid state ready to be used this.gameBoy.reset(); this.gameBoy.load_boot_default(); - this.gameBoy.load_printer_ws(); + this.gameBoy.load_null_ws(); const cartridge = this.gameBoy.load_rom_ws(romData); // updates the name of the currently selected engine @@ -465,6 +478,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ]; } + get sections(): SectionInfo[] { + return [ + { + name: "Serial", + icon: require("../res/serial.svg"), + node: SerialSection({ + emulator: this, + onLogger: (onLoggerData: LoggerCallback) => + (this.onLoggerData = onLoggerData), + onPrinter: (onPrinterData: PrinterCallback) => + (this.onPrinterData = onPrinterData) + }) + } + ]; + } + get help(): HelpPanel[] { return [ { @@ -740,6 +769,26 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.storeSettings(); } + loadNullDevice() { + this.gameBoy?.load_null_ws(); + } + + loadLoggerDevice() { + this.gameBoy?.load_logger_ws(); + } + + loadPrinterDevice() { + this.gameBoy?.load_printer_ws(); + } + + onLoggerDevice(data: Uint8Array) { + this.onLoggerData?.(data); + } + + onPrinterDevice(imageBuffer: Uint8Array) { + this.onPrinterData?.(imageBuffer); + } + /** * Tries to load game RAM from the `localStorage` using the * current cartridge title as the name of the item and @@ -810,6 +859,7 @@ declare global { interface Window { emulator: GameboyEmulator; panic: (message: string) => void; + loggerCallback: (data: Uint8Array) => void; printerCallback: (imageBuffer: Uint8Array) => void; } @@ -822,9 +872,12 @@ window.panic = (message: string) => { console.error(message); }; +window.loggerCallback = (data: Uint8Array) => { + window.emulator.onLoggerDevice(data); +}; + window.printerCallback = (imageBuffer: Uint8Array) => { - const imageUrl = bufferToDataUrl(imageBuffer, 160); - console.image(imageUrl, imageBuffer.length / 160 / 3); + window.emulator.onPrinterDevice(imageBuffer); }; console.image = (url: string, size = 80) => {