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
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
},
"/bin/ls": true,
"/usr/bin/git": true,
"test": true
"test": true,
"/^node scripts/wait-for-backend\\.mjs$/": {
"approve": true,
"matchCommandLine": true
}
},
}
340 changes: 182 additions & 158 deletions client/src/components/features/parser-output.tsx

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions client/src/pages/arduino-simulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import type {
ParserMessage,
IOPinRecord,
} from "@shared/schema";
import { parseStaticIORegistry } from "@shared/io-registry-parser";
import type { DebugMessageParams } from "@/hooks/use-compile-and-run";
import { isMac } from "@/lib/platform";

Expand Down Expand Up @@ -709,6 +710,17 @@ export default function ArduinoSimulator() {
}
}, [simulationStatus]);

// ── Static IO-Registry: update from code whenever simulation is not running ─
// Runs 300 ms after the user stops typing to avoid parsing every keystroke.
// When the simulation starts, the WS `io_registry` messages take over.
useEffect(() => {
if (simulationStatus !== "stopped") return;
const timer = setTimeout(() => {
setIoRegistry(parseStaticIORegistry(code));
}, 300);
return () => clearTimeout(timer);
}, [code, simulationStatus]);

// Tab management handlers
const handleTabClick = (tabId: string) => {
const tab = tabs.find((t) => t.id === tabId);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"scripts": {
"dev": "DISABLE_RATE_LIMIT=true tsx server/index.ts --host",
"dev:full": "concurrently -n \"BACKEND,CLIENT\" -c \"bgBlue,bgMagenta\" \"npm run dev\" \"npm run dev:client\"",
"dev:full": "concurrently -n \"BACKEND,CLIENT\" -c \"bgBlue,bgMagenta\" \"npm run dev\" \"node scripts/wait-for-backend.mjs && npm run dev:client\"",
"dev:client": "vite",
"preview": "vite preview",
"build": "npm run build:client && npm run build:server && npm run build:copy-public",
Expand Down
30 changes: 30 additions & 0 deletions scripts/wait-for-backend.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const target = process.env.BACKEND_HEALTH_URL || "http://127.0.0.1:3000/api/health";
const timeoutMs = Number(process.env.BACKEND_WAIT_TIMEOUT_MS || 30000);
const intervalMs = Number(process.env.BACKEND_WAIT_INTERVAL_MS || 200);

const start = Date.now();

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function isReady() {
try {
const res = await fetch(target, { method: "GET", cache: "no-store" });
return res.ok;
} catch {
return false;
}
}

while (true) {
if (await isReady()) {
console.log(`[wait-for-backend] Backend ready at ${target}`);
process.exit(0);
}

if (Date.now() - start >= timeoutMs) {
console.error(`[wait-for-backend] Timeout after ${timeoutMs}ms waiting for ${target}`);
process.exit(1);
}

await sleep(intervalMs);
}
70 changes: 64 additions & 6 deletions server/services/registry-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ function cleanupPinRecord(pin: IOPinRecord): IOPinRecord {
return cleaned;
}

function mergeUsedAtEntries(
existing: IOPinRecord["usedAt"],
incoming: IOPinRecord["usedAt"],
): IOPinRecord["usedAt"] {
const merged = [...(existing ?? []), ...(incoming ?? [])];
if (merged.length === 0) return undefined;

const unique = new Map<string, (typeof merged)[number]>();
for (const entry of merged) {
const key = `${entry.operation}@${entry.line}`;
if (!unique.has(key)) {
unique.set(key, entry);
}
}
return Array.from(unique.values());
}

/**
* RegistryManager handles the collection and management of Arduino pin states.
* It provides debouncing to minimize WebSocket traffic and change detection
Expand All @@ -76,6 +93,13 @@ export class RegistryManager {
private baudrate: number | undefined = undefined; // undefined = Serial.begin() not found in code
private destroyed = false; // Prevent logging after destruction
private debugStream: WriteStream | null = null; // Non-blocking telemetry stream
/**
* Anti-spam: tracks (pin, mode) pairs already sent via updatePinMode so that
* repeated calls (e.g. from loop()) never trigger a redundant WS message.
* Keyed as "<pinId>:<mode>" (e.g. "13:1").
* Reset on reset() / next program start.
*/
private runtimeSentFingerprints = new Set<string>();
private readonly logger = new Logger("RegistryManager");
private readonly onUpdateCallback?: RegistryUpdateCallback;
private readonly onTelemetryCallback?: TelemetryUpdateCallback;
Expand Down Expand Up @@ -317,7 +341,7 @@ export class RegistryManager {

// ROBUSTNESS: Flush current registry state before clearing
// This ensures any pins added via updatePinMode before IO_REGISTRY_START marker are sent
if (this.registry.length > 0 && this.isDirty) {
if (!this.waitingForRegistry && this.registry.length > 0 && this.isDirty) {
const hasDefinedPins = this.registry.some((p) => p.defined);
if (hasDefinedPins) {
this.logger.info(
Expand Down Expand Up @@ -355,7 +379,18 @@ export class RegistryManager {
return;
}
// Individual pin additions are not logged (20 per start is too noisy).
this.registry.push(pinRecord);
const existingIndex = this.registry.findIndex((p) => p.pin === pinRecord.pin);
if (existingIndex >= 0) {
const existing = this.registry[existingIndex];
this.registry[existingIndex] = {
...existing,
...pinRecord,
defined: existing.defined || pinRecord.defined,
usedAt: mergeUsedAtEntries(existing.usedAt, pinRecord.usedAt),
};
} else {
this.registry.push(pinRecord);
}
this.isDirty = true;
this.telemetry.incomingEvents++;
}
Expand Down Expand Up @@ -428,6 +463,12 @@ export class RegistryManager {
*/
updatePinMode(pin: number, mode: number): void {
if (this.destroyed) return;
// ── Anti-spam: skip if this (pin, mode) was already sent ─────────────────
const fingerprint = `${pin}:${mode}`;
if (this.runtimeSentFingerprints.has(fingerprint)) {
this.telemetry.incomingEvents++;
return; // No new information – don't update registry or trigger WS send
}
const pinStr = pin >= 14 && pin <= 19 ? `A${pin - 14}` : String(pin);
const existing = this.registry.find((p) => p.pin === pinStr);

Expand Down Expand Up @@ -461,8 +502,15 @@ export class RegistryManager {
this.logger.info(
`Registry send trigger: first-time pin use ${pinStr} (pinMode:${mode})`,
);
const nextHash = this.computeRegistryHash();
this.sendNow(nextHash, "pin-defined-changed");
this.runtimeSentFingerprints.add(fingerprint);
this.isDirty = true;
if (!this.isCollecting && !this.waitingForRegistry) {
const nextHash = this.computeRegistryHash();
this.sendNow(nextHash, "pin-defined-changed");
}
} else {
// Already defined but new mode (mode change) – mark as sent
this.runtimeSentFingerprints.add(fingerprint);
}
} else {
// Create new pin record if not yet in registry
Expand All @@ -477,8 +525,11 @@ export class RegistryManager {
this.logger.debug(
`New pin record created: ${pinStr} with mode=${mode}, sending immediately`,
);
const nextHash = this.computeRegistryHash();
this.sendNow(nextHash, "pin-new-record");
this.runtimeSentFingerprints.add(fingerprint);
if (!this.isCollecting && !this.waitingForRegistry) {
const nextHash = this.computeRegistryHash();
this.sendNow(nextHash, "pin-new-record");
}
}
}

Expand All @@ -505,6 +556,12 @@ export class RegistryManager {
this.waitTimer = setTimeout(() => {
if (this.waitingForRegistry) {
this.logger.warn("Registry wait timeout - releasing queue");
if (this.isDirty) {
const nextHash = this.computeRegistryHash();
if (nextHash !== this.registryHash) {
this.sendNow(nextHash, "wait-timeout-flush");
}
}
this.waitingForRegistry = false;
}
}, timeoutMs);
Expand All @@ -526,6 +583,7 @@ export class RegistryManager {
this.registryHash = "";
this.waitingForRegistry = false;
this.isDirty = false;
this.runtimeSentFingerprints.clear(); // reset anti-spam state for new sketch run

this.stopTelemetry();

Expand Down
2 changes: 1 addition & 1 deletion server/services/sandbox-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ export class SandboxRunner {
this.totalPausedTime = 0;
this.registryManager.reset();
this.registryManager.setBaudrate(this.baudrate);
this.registryManager.enableWaitMode(300); // Reduced from 1500ms to 300ms - faster serial output
this.registryManager.enableWaitMode(5000); // 5s timeout: wait for IO_REGISTRY_START; if never comes, flush once
this.messageQueue = [];
this.outputBuffer = "";
this.outputBufferIndex = 0;
Expand Down
Loading
Loading