From 20acc252d8b2c87a6345268c74f19f9f24fcc251 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Mon, 9 Nov 2026 17:00:15 +0000 Subject: [PATCH] Acknowledge simulator serial output for flow control Opt in to the simulator's serial flow control via config and acknowledge serial output from xterm's write callback. The simulator then sends output as fast as the terminal can write it, rather than at the device's baud rate, and a busy program can't get ahead of the terminal. Output discarded while the terminal is unmounted is still acknowledged so the simulator doesn't wait. Temporarily points the simulator at the serial-output-flow-control review build for testing. Remove before merge. Refs #1242 --- src/device/simulator.ts | 18 ++++++++++++++++++ src/serial/XTerm.tsx | 14 +++++++++++++- src/simulator/Simulator.tsx | 16 ++++++++++++---- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/device/simulator.ts b/src/device/simulator.ts index 20d5cfa41..a7b9901e6 100644 --- a/src/device/simulator.ts +++ b/src/device/simulator.ts @@ -120,6 +120,11 @@ export type SensorStateKey = Extract< interface Config { language: string; translations: Record; + /** + * If true, the simulator waits for serial output to be acknowledged via + * acknowledgeSerialOutput rather than limiting it to the device's baud rate. + */ + serialFlowControl: boolean; } export interface DataLog { @@ -318,6 +323,19 @@ export class SimulatorDeviceConnection this.setStatus(ConnectionStatus.Disconnected); } + /** + * Acknowledge serial output once it's been processed, so the simulator can + * send more. + * + * @param length The total length of the serial data processed. + */ + acknowledgeSerialOutput(length: number): void { + // The iframe may have gone by the time the terminal finishes writing. + if (this.iframe()) { + this.postMessage("serial_output_ack", { length }); + } + } + async serialWrite(data: string): Promise { this.postMessage("serial_input", { data, diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index eab1df359..72f0e4def 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -14,6 +14,7 @@ import useIsUnmounted from "../common/use-is-unmounted"; import { backgroundColorTerm } from "../deployment/misc"; import { SerialData } from "@microbit/microbit-connection/usb"; import { parseTraceLine, useDevice } from "../device/device-hooks"; +import { SimulatorDeviceConnection } from "../device/simulator"; import { useSelection } from "../workbench/use-selection"; import { WebLinkProvider } from "./link-provider"; import { useCurrentTerminalRef } from "./serial-hooks"; @@ -104,9 +105,20 @@ const useManagedTermimal = ( customKeyEventHandler(e, tabOutRef) ); + // The simulator waits for us to acknowledge serial output so a busy + // program can't send it faster than we can write it. + const acknowledge = (length: number) => { + if (device instanceof SimulatorDeviceConnection) { + device.acknowledgeSerialOutput(length); + } + }; const serialListener = (event: SerialData) => { + const { length } = event.data; if (!isUnmounted()) { - terminal.write(event.data); + terminal.write(event.data, () => acknowledge(length)); + } else { + // Discarded, but still acknowledged so the simulator doesn't wait. + acknowledge(length); } }; const resetListener = () => { diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 7c9b5b7eb..b8bb1b27a 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -45,10 +45,16 @@ const Simulator = ({ const staging = "https://python-simulator.usermbit.org/staging/simulator.html?flag=sw"; - // For testing with sim branches: - //const branch = "upgrade-mpy"; - //const url = new URL(`https://review-python-simulator.usermbit.org/${branch}/simulator.html`); - const url = new URL(stage === "PRODUCTION" ? production : staging); + // For testing with sim branches. + // TODO: remove before merge, once the simulator is released. + const branch: string | undefined = "serial-output-flow-control"; + const url = new URL( + branch + ? `https://review-python-simulator.usermbit.org/${branch}/simulator.html` + : stage === "PRODUCTION" + ? production + : staging + ); url.searchParams.append("color", brand500); return url.toString(); }, [brand500]); @@ -163,6 +169,8 @@ const updateTranslations = ( intl.formatMessage({ id: "simulator-" + k }), ]) ), + // The terminal acknowledges serial output (see XTerm.tsx). + serialFlowControl: true, }; simulator.configure(config); };