Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions client/public/ATtiny85-overlay.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions client/public/ATtiny85.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions client/src/components/features/arduino-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { usePinPollingEngine } from "@/hooks/usePinPollingEngine";
import { onCustomEvent, offCustomEvent } from "@/utils/event-utils";
import { Logger } from "@shared/logger";
import { DEFAULT_BOARD_ID, getBoard, type BoardId } from "@shared/boards";
import { activePinLabel, analogChannelPin, isAnalogPin } from "@/lib/active-board";
import { activeAnalogPinTitle, activePinLabel, analogChannelPin, isAnalogPin } from "@/lib/active-board";
import { type BoardView, DEFAULT_LOGO_PLACEMENT, boardView } from "@/lib/board-view";
import type { RuntimeSimulationStatus } from "@shared/types/arduino.types";

Expand Down Expand Up @@ -731,7 +731,7 @@ function getAnalogDialogCoordinates(
left = Math.max(viewportMargin, Math.min(globalThis.innerWidth - dialogWidth - viewportMargin, left));
top = Math.max(viewportMargin, Math.min(globalThis.innerHeight - dialogHeight - viewportMargin, top));

const pinLabel = activePinLabel(dialog.pin);
const pinLabel = activeAnalogPinTitle(dialog.pin);

return { left, top, dialogWidth, dialogHeight, pinLabel };
}
Expand Down
7 changes: 6 additions & 1 deletion client/src/hooks/use-simulation-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useRef } from "react";
import { useMutation, type UseMutationResult } from "@tanstack/react-query";
import { Logger } from "@shared/logger";
import { normalizeSimulationTimeout } from "@shared/input-limits";
import { sketchSerialBaud } from "@shared/parser-patterns";
import type { SimulationStatus } from "@shared/types/arduino.types";
import type { IncomingArduinoMessage } from "@/types/websocket";
import { useSimulationControllerState } from "./use-simulation-controller-state";
Expand Down Expand Up @@ -190,7 +191,11 @@ export function useSimulationController(
if (!artifact) throw new Error("The latest build has no image for the browser emulation. Compile again.");
const runtime = await hub.load("browser");
// Start first: replacing a run reports the old one stopped, which must not end the new one.
runtime.start({ kind: "browser", artifact, ...(params.board ? { board: params.board } : {}), timeoutSec: timeout });
const serialBaud = sketchSerialBaud(compiledCodeRef.current ?? "");
runtime.start({
kind: "browser", artifact, ...(params.board ? { board: params.board } : {}), timeoutSec: timeout,
...(serialBaud === undefined ? {} : { serialBaud }),
});
hub.setActive("browser");
return { success: true };
}
Expand Down
11 changes: 6 additions & 5 deletions client/src/hooks/usePinPollingEngine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import type { PinState } from "@/components/features/arduino-board";
import type { BoardPin } from "@shared/board-pins";
import { activeAnalogPins, activePins, activePwmPins, isActivePwmPin } from "@/lib/active-board";
import { activeAnalogPins, activeLedBuiltin, activePins, activePwmPins, isActivePwmPin } from "@/lib/active-board";

// Constants
/** Height of the Uno drawing; the fallback where the SVG names no view box. */
Expand Down Expand Up @@ -362,7 +362,7 @@ export function usePinPollingEngine({
}
};

/** Update a single analog pin element: the SVG names it by its label (`A0`…`A15`). */
/** Update a single analog pin element: the SVG names it as the sketch does (`A0`…`A15`; on the ATtiny85 its number). */
const updateAnalogPin = (svgEl: SVGSVGElement, { pin: pinNumber, name }: BoardPin) => {
const frame = pinElement<SVGRectElement>(svgEl, `pin-${name}-frame`);
const state = pinElement<SVGCircleElement>(svgEl, `pin-${name}-state`);
Expand Down Expand Up @@ -397,9 +397,10 @@ export function usePinPollingEngine({
const ledL = svgEl.querySelector<SVGRectElement>("#led-l");
const ledTx = svgEl.querySelector<SVGRectElement>("#led-tx");
const ledRx = svgEl.querySelector<SVGRectElement>("#led-rx");
const pin13On = (pinStates.find((p) => p.pin === 13)?.value ?? 0) > 0;
const ledBuiltin = activeLedBuiltin();
const ledBuiltinOn = (pinStates.find((p) => p.pin === ledBuiltin)?.value ?? 0) > 0;
applyLedVisual(ledOn, isSimulationRunning, "var(--color-led-green)", "#glow-green");
applyLedVisual(ledL, pin13On, "var(--color-led-yellow)", "#glow-yellow");
applyLedVisual(ledL, ledBuiltinOn, "var(--color-led-yellow)", "#glow-yellow");
applyLedVisual(ledTx, txBlink, "var(--color-led-yellow)", "#glow-yellow");
applyLedVisual(ledRx, rxBlink, "var(--color-led-yellow)", "#glow-yellow");
};
Expand All @@ -422,7 +423,7 @@ export function usePinPollingEngine({

for (const analog of activeAnalogPins()) {
const valStr = getPinState(analog.pin)?.value?.toString() ?? "";
updateLabelForPin(svgEl, `pin-${analog.label}-state`, `pin-${analog.label}-frame`, `pin-${analog.label}-val`, valStr, showPWMValues);
updateLabelForPin(svgEl, `pin-${analog.name}-state`, `pin-${analog.name}-frame`, `pin-${analog.name}-val`, valStr, showPWMValues);
}
};

Expand Down
6 changes: 6 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,12 @@ button:not(.arduino-overlay):not(.parser-message-btn) > svg {
min-height: 0 !important;
}

/* The floating panel navigation (mobile-layout.tsx: up to five 40px buttons with gaps, padding
and margins, bottom right) must not cover the board's pins: the board is drawn above it. */
.workspace-layout[data-layout-mode="mobile"] .workspace-board-panel.workspace-mobile-full-panel {
padding-bottom: 20rem;
}

/* Recharts default tooltip — styled with Tailwind utilities via @apply */
.recharts-default-tooltip {
@apply text-ui-xs text-white rounded p-2;
Expand Down
11 changes: 10 additions & 1 deletion client/src/lib/active-board.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { boardAnalogPins, boardPins, boardPwmPins, boardPinLabel, findBoardPin, type BoardPin } from "@shared/board-pins";
import { boardAnalogPins, boardPins, boardPwmPins, boardPinLabel, findBoardPin, resolveBoardPin, type BoardPin } from "@shared/board-pins";
import { type BoardId, DEFAULT_BOARD_ID } from "@shared/boards";

/**
Expand All @@ -25,9 +25,18 @@ export const isAnalogPin = (pin: number): boolean => findBoardPin(hardwareBoard(
/** `13`, `A0`: how a sketch writes the pin on the active board. */
export const activePinLabel = (pin: number): string => boardPinLabel(hardwareBoard(), pin);

/** The name and, where it is a second name (ATtiny85), the analog name: `2 (A1)`; else as `activePinLabel`. */
export const activeAnalogPinTitle = (pin: number): string => {
const found = findBoardPin(hardwareBoard(), pin);
return found?.analogName ? `${found.name} (${found.analogName})` : activePinLabel(pin);
};

/** Pin number of analog input `A<channel>` on the active board. */
export const analogChannelPin = (channel: number): number | undefined => boardAnalogPins(hardwareBoard()).find((pin) => pin.adcChannel === channel)?.pin;

export const isActivePwmPin = (pin: number): boolean => boardPwmPins(hardwareBoard()).includes(pin);
export const activePwmPins = (): readonly number[] => boardPwmPins(hardwareBoard());
export const activeAnalogPins = (): readonly BoardPin[] => boardAnalogPins(hardwareBoard());

/** `LED_BUILTIN` of the active board: 13 on the ATmega boards, 1 on the ATtiny85. */
export const activeLedBuiltin = (): number => resolveBoardPin(hardwareBoard(), "LED_BUILTIN") ?? 13;
11 changes: 9 additions & 2 deletions client/src/lib/board-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ const MEGA_VIEW: BoardView = {
logo: { left: 74, top: 38, width: 20, height: 10 },
};

const VIEWS: Partial<Record<BoardId, BoardView>> = { uno: UNO_VIEW, nano: NANO_VIEW, mega2560: MEGA_VIEW };
// Generated by scripts/generate-attiny85-svg.mjs: a DIP-8 on a 32 x 24 mm carrier at 4 units per mm.
// The mark sits on the free area at the top, between the ON and L LEDs.
const ATTINY85_VIEW: BoardView = {
boardSvg: "/ATtiny85.svg", overlaySvg: "/ATtiny85-overlay.svg", width: 128, height: 96,
logo: { left: 42, top: 15, width: 24, height: 10 },
};

const VIEWS: Record<BoardId, BoardView> = { uno: UNO_VIEW, nano: NANO_VIEW, mega2560: MEGA_VIEW, attiny85: ATTINY85_VIEW };

/** The view of a board; a board without graphics yet (ATtiny85) is drawn as the Uno. */
/** The view of a board. */
export function boardView(board: BoardId = DEFAULT_BOARD_ID): BoardView {
return VIEWS[board] ?? UNO_VIEW;
}
11 changes: 8 additions & 3 deletions client/src/lib/runtime/browser-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class BrowserRuntime implements SimulationRuntime {
private readonly gate = new RunGate();
private worker: EmulatorWorker | undefined;
private phase: Phase = "idle";
private pendingStart: { readonly timeoutSec: number } | undefined;
private pendingStart: { readonly timeoutSec: number; readonly serialBaud?: number } | undefined;
private timeoutSec = 0;
private deadline: unknown;
private heartbeatTimer: unknown;
Expand Down Expand Up @@ -120,7 +120,7 @@ export class BrowserRuntime implements SimulationRuntime {
this.registry.clear();
this.board = artifact.board;
this.heldCarriageReturn.clear();
this.pendingStart = { timeoutSec: request.timeoutSec };
this.pendingStart = { timeoutSec: request.timeoutSec, ...(request.serialBaud === undefined ? {} : { serialBaud: request.serialBaud }) };
this.timeoutSec = request.timeoutSec;
this.phase = "loading";
this.lastHeard = this.timers.now();
Expand Down Expand Up @@ -264,7 +264,12 @@ export class BrowserRuntime implements SimulationRuntime {
if (this.phase !== "loading" || !start) return;
this.pendingStart = undefined;
this.phase = "starting";
this.post({ type: "start", runId: this.gate.runIdFor("start"), timeoutMs: start.timeoutSec * 1000 });
this.post({
type: "start",
runId: this.gate.runIdFor("start"),
timeoutMs: start.timeoutSec * 1000,
...(start.serialBaud === undefined ? {} : { serialBaud: start.serialBaud }),
});
this.expect(this.limits.confirmMs, "the emulator did not start");
}

Expand Down
2 changes: 2 additions & 0 deletions client/src/lib/runtime/simulation-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type StartRequest =
/** The board selected in the UI; the worker refuses an artifact built for another board. Defaults to the artifact's board. */
readonly board?: BoardId;
readonly timeoutSec: number;
/** `Serial.begin()` value of the sketch, for a board whose `Serial` is a software UART (ATtiny85). */
readonly serialBaud?: number;
}
| {
readonly kind: "host-mock";
Expand Down
Loading
Loading