From c64f2c3faca4333c3e93ad5112ffefac72c308b3 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Sun, 18 Oct 2026 10:06:46 +0200 Subject: [PATCH 1/3] fix(compiler): place sketch prototypes like the Arduino IDE The local build put every generated prototype at the top of the file. A prototype whose signature uses a typedef or a constant (Vektor add(Vektor, Vektor), void f(double m[M][N])) then failed, and a function with default arguments failed with "redefinition of default argument", although the Arduino compiler accepted the sketch. Prototypes now go directly before the first function the sketch itself defines (after inlined headers) and leave out default arguments. Comments are masked with the existing offset-preserving helper. The two builder tests that required the prototypes above the user-code marker now require them before the first function definition (approved by the user). Co-Authored-By: Claude Opus 5.5 --- server/services/sketch-file-builder.ts | 45 ++---- server/services/sketch-prototypes.ts | 146 ++++++++++++++++++ shared/source-project.ts | 3 +- .../services/sketch-file-builder.test.ts | 61 +++++++- .../server/services/sketch-prototypes.test.ts | 34 ++++ 5 files changed, 247 insertions(+), 42 deletions(-) create mode 100644 server/services/sketch-prototypes.ts create mode 100644 tests/server/services/sketch-prototypes.test.ts diff --git a/server/services/sketch-file-builder.ts b/server/services/sketch-file-builder.ts index f90260a60..67cd96a7a 100644 --- a/server/services/sketch-file-builder.ts +++ b/server/services/sketch-file-builder.ts @@ -13,6 +13,7 @@ import { detectSketchEntrypoints } from "@shared/utils/sketch-validation"; import { isSafeHeaderName } from "@shared/input-limits"; import { resolvePathWithinRoot } from "../security/safe-paths"; import { normalizeSourcePath, resolveSourceProject } from "@shared/source-project"; +import { insertForwardDeclarations, planForwardDeclarations } from "./sketch-prototypes"; interface SketchBuildResult { sketchDir: string; @@ -21,18 +22,7 @@ interface SketchBuildResult { } export function extractForwardDeclarations(code: string): string { - const stripped = code.replaceAll(/\/\/[^\n]*/g, "").replaceAll(/\/\*[\s\S]*?\*\//g, ""); - const skipped = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef", "setup", "loop", "main"]); - const definition = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm; - const seen = new Set(); - const declarations: string[] = []; - for (const match of stripped.matchAll(definition)) { - const signature = `${match[2]}${match[3]}`; - if (skipped.has(match[2]) || seen.has(signature)) continue; - seen.add(signature); - declarations.push(`${match[1].trim()} ${match[2]}${match[3]};`); - } - return declarations.join("\n"); + return planForwardDeclarations(code).declarations; } export class SketchFileBuilder { @@ -76,7 +66,7 @@ export class SketchFileBuilder { } const footer = this.buildFooter(hasSetup, hasLoop); - const projectSource = entryFile + const project = entryFile ? resolveSourceProject({ entryFile, files: { @@ -86,16 +76,17 @@ export class SketchFileBuilder { return files; }, {}), }, - }).source - : code; + }) + : undefined; + const projectSource = project ? project.source : code; + // Removing the include keeps every line in place, so lineOrigins still match. const cleanedCode = projectSource.replaceAll(/#include\s*[<"]Arduino\.h[>"]/g, ""); + const isSketchLine = project + ? (lineIndex: number) => project.lineOrigins[lineIndex]?.file === project.entryFile + : undefined; + const userCode = insertForwardDeclarations(cleanedCode, isSketchLine); - const forwardDecls = this.extractForwardDeclarations(cleanedCode); - const forwardSection = forwardDecls - ? `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${forwardDecls}\n\n` - : ""; - - const combined = `#include \n\n${forwardSection}// --- User code follows ---\n${cleanedCode}\n\n// --- Footer ---\n${footer}`; + const combined = `#include \n\n// --- User code follows ---\n${userCode}\n\n// --- Footer ---\n${footer}`; await writeFile(join(sketchDir, "Arduino.h"), ARDUINO_MOCK_CODE); await writeFile(sketchFile, combined); @@ -121,18 +112,6 @@ export class SketchFileBuilder { this.createdSketchDirs.delete(dir); } - /** - * Extracts forward declarations for all user-defined functions. - * - * The Arduino IDE automatically generates prototypes for every function in a - * sketch so that helper functions can be called before they are defined (just - * like in a real Arduino sketch). We replicate that behaviour here so that - * the local g++ compiler accepts the same code the Arduino IDE would accept. - */ - private extractForwardDeclarations(code: string): string { - return extractForwardDeclarations(code); - } - /** * Generates the main() wrapper based on presence of setup() and loop() */ diff --git a/server/services/sketch-prototypes.ts b/server/services/sketch-prototypes.ts new file mode 100644 index 000000000..bc044f833 --- /dev/null +++ b/server/services/sketch-prototypes.ts @@ -0,0 +1,146 @@ +/** + * Arduino-IDE-compatible function prototypes for the local g++ build. + * + * The Arduino IDE declares every sketch function before the first function + * definition, so a helper may be called before it is defined. The prototypes + * go there, not at the top of the file, because their signatures may use + * types and constants that the sketch defines first (a `typedef`, or `M` in + * `double mat[M][N]`). Default arguments are left out: C++ allows them only + * once, and the definition already carries them. + */ + +import { maskCommentsPreservingLines } from "@shared/source-project"; + +const FUNCTION_DEFINITION = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm; +const NOT_A_FUNCTION = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef"]); +const ENTRYPOINTS = new Set(["setup", "loop", "main"]); +const OPENING_BRACKETS = "([{"; +const CLOSING_BRACKETS = ")]}"; + +export interface ForwardDeclarationPlan { + /** Newline-separated prototypes, empty when the sketch needs none. */ + declarations: string; + /** Offset in the code where the prototypes belong; -1 when there is no function definition. */ + insertAt: number; +} + +interface ParameterScan { + parts: string[]; + current: string; + depth: number; + quote: string | null; + escaped: boolean; +} + +function scanQuoted(scan: ParameterScan, character: string): void { + scan.current += character; + if (scan.escaped) scan.escaped = false; + else if (character === "\\") scan.escaped = true; + else if (character === scan.quote) scan.quote = null; +} + +function scanParameterCharacter(scan: ParameterScan, character: string): void { + if (scan.quote) { + scanQuoted(scan, character); + return; + } + if (character === "," && scan.depth === 0) { + scan.parts.push(scan.current); + scan.current = ""; + return; + } + if (character === '"' || character === "'") scan.quote = character; + else if (OPENING_BRACKETS.includes(character)) scan.depth++; + else if (CLOSING_BRACKETS.includes(character)) scan.depth--; + scan.current += character; +} + +/** Index of the `=` that starts a default argument, ignoring `=` inside quotes or brackets. */ +function defaultArgumentStart(parameter: string): number { + let depth = 0; + let quote: string | null = null; + for (let index = 0; index < parameter.length; index++) { + const character = parameter[index]; + if (quote) { + if (character === "\\") index++; + else if (character === quote) quote = null; + } else if (character === '"' || character === "'") { + quote = character; + } else if (OPENING_BRACKETS.includes(character)) { + depth++; + } else if (CLOSING_BRACKETS.includes(character)) { + depth--; + } else if (character === "=" && depth === 0) { + return index; + } + } + return -1; +} + +/** Removes default arguments from a parenthesized parameter list such as `(int a, byte b = 2)`. */ +export function stripDefaultArguments(parameterList: string): string { + const scan: ParameterScan = { parts: [], current: "", depth: 0, quote: null, escaped: false }; + for (const character of parameterList.slice(1, -1)) scanParameterCharacter(scan, character); + scan.parts.push(scan.current); + if (!scan.parts.some((part) => defaultArgumentStart(part) >= 0)) return parameterList; + const parameters = scan.parts.map((part) => { + const start = defaultArgumentStart(part); + return (start >= 0 ? part.slice(0, start) : part).trim(); + }); + return `(${parameters.join(", ")})`; +} + +function lineIndexAt(code: string, offset: number): number { + let line = 0; + for (let index = code.indexOf("\n"); index >= 0 && index < offset; index = code.indexOf("\n", index + 1)) line++; + return line; +} + +/** + * Plans the prototypes for every function defined in `code`. + * + * `isSketchLine` tells which lines belong to the sketch itself rather than to + * an inlined header; the prototypes go before the first function the sketch + * defines, like in the Arduino IDE. Without a sketch function they go before + * the first function at all. + */ +export function planForwardDeclarations( + code: string, + isSketchLine: (lineIndex: number) => boolean = () => true, +): ForwardDeclarationPlan { + const masked = maskCommentsPreservingLines(code); + const seen = new Set(); + const declarations: string[] = []; + let firstDefinition = -1; + let firstSketchDefinition = -1; + + for (const match of masked.matchAll(FUNCTION_DEFINITION)) { + const name = match[2]; + if (NOT_A_FUNCTION.has(name)) continue; + const offset = match.index ?? 0; + if (firstDefinition < 0) firstDefinition = offset; + if (firstSketchDefinition < 0 && isSketchLine(lineIndexAt(masked, offset))) firstSketchDefinition = offset; + if (ENTRYPOINTS.has(name)) continue; + const parameters = stripDefaultArguments(match[3]); + const signature = `${name}${parameters}`; + if (seen.has(signature)) continue; + seen.add(signature); + declarations.push(`${match[1].trim()} ${signature};`); + } + + return { + declarations: declarations.join("\n"), + insertAt: firstSketchDefinition >= 0 ? firstSketchDefinition : firstDefinition, + }; +} + +/** Inserts the planned prototypes into `code`; returns `code` unchanged when none are needed. */ +export function insertForwardDeclarations( + code: string, + isSketchLine?: (lineIndex: number) => boolean, +): string { + const plan = planForwardDeclarations(code, isSketchLine); + if (!plan.declarations || plan.insertAt < 0) return code; + const section = `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${plan.declarations}\n\n`; + return `${code.slice(0, plan.insertAt)}${section}${code.slice(plan.insertAt)}`; +} diff --git a/shared/source-project.ts b/shared/source-project.ts index bd6eba28c..b66522a3b 100644 --- a/shared/source-project.ts +++ b/shared/source-project.ts @@ -143,7 +143,8 @@ function advanceStringMask(state: StringMaskState, current: string): StringMaskS return state; } -function maskCommentsPreservingLines(source: string): string { +/** Replaces comment characters with spaces, keeping every offset and line break of `source`. */ +export function maskCommentsPreservingLines(source: string): string { const result: string[] = []; let inBlockComment = false; let stringState: StringMaskState = { quote: null, escaped: false }; diff --git a/tests/server/services/sketch-file-builder.test.ts b/tests/server/services/sketch-file-builder.test.ts index bc6b22a0c..e2f1bba0a 100644 --- a/tests/server/services/sketch-file-builder.test.ts +++ b/tests/server/services/sketch-file-builder.test.ts @@ -95,11 +95,10 @@ void loop() {} void helper() {} `.trim(); const content = await buildSketch(code); - // Forward decl must appear BEFORE the user-code section + // Like the Arduino IDE: directly before the first function definition const fwdIdx = content.indexOf("void helper();"); - const userIdx = content.indexOf("// --- User code follows ---"); - expect(fwdIdx).toBeGreaterThan(-1); - expect(fwdIdx).toBeLessThan(userIdx); + expect(fwdIdx).toBeGreaterThan(content.indexOf("// --- User code follows ---")); + expect(fwdIdx).toBeLessThan(content.indexOf("void setup() {")); }); it("does not add forward declarations for setup() and loop()", async () => { @@ -136,11 +135,57 @@ void loop() {} int add(int left, int right) { return left + right; } `.trim(); const content = await buildSketch(code); - const declaration = "int add(int left, int right);"; - const declarationIndex = content.indexOf(declaration); - const userCodeIndex = content.indexOf("// --- User code follows ---"); + const declarationIndex = content.indexOf("int add(int left, int right);"); expect(declarationIndex).toBeGreaterThan(-1); - expect(declarationIndex).toBeLessThan(userCodeIndex); + expect(declarationIndex).toBeLessThan(content.indexOf("void setup() {")); + }); + + it("places prototypes after a typedef that their signatures use", async () => { + const code = ` +typedef struct vektor { float x; float y; } Vektor; +void setup() { Vektor v = addieren(v, v); } +void loop() {} +Vektor addieren(Vektor v, Vektor s) { return v; } +`.trim(); + const content = await buildSketch(code); + const declarationIndex = content.indexOf("Vektor addieren(Vektor v, Vektor s);"); + expect(declarationIndex).toBeGreaterThan(content.indexOf("} Vektor;")); + expect(declarationIndex).toBeLessThan(content.indexOf("void setup() {")); + }); + + it("places prototypes after constants used as array sizes", async () => { + const code = ` +const int M = 3; +void setup() {} +void loop() {} +void clear(double mat[M][M]) {} +`.trim(); + const content = await buildSketch(code); + const declarationIndex = content.indexOf("void clear(double mat[M][M]);"); + expect(declarationIndex).toBeGreaterThan(content.indexOf("const int M = 3;")); + }); + + it("leaves default arguments out of prototypes", async () => { + const code = ` +void setup() { show(); } +void loop() {} +void show(const char *name = "M", byte precision = 2) {} +`.trim(); + const content = await buildSketch(code); + expect(content).toContain("void show(const char *name, byte precision);"); + expect(content).toContain('void show(const char *name = "M", byte precision = 2) {}'); + }); + + it("places prototypes before the first sketch function, after an included header", async () => { + const content = await buildProjectSketch( + '#include "mat.h"\nvoid setup() { scale(2); }\nvoid loop() {}\nvoid scale(double factor) {}', + "main.ino", + [{ name: "mat.h", content: "const int M = 3;\nvoid show(int precision = 2) {}" }], + ); + const forwardIndex = content.indexOf("// --- Forward declarations"); + expect(forwardIndex).toBeGreaterThan(content.indexOf("void show(int precision = 2) {}")); + expect(forwardIndex).toBeLessThan(content.indexOf("void setup() {")); + expect(content).toContain("void scale(double factor);"); }); it("does not duplicate declarations for overloaded or repeated function names", async () => { diff --git a/tests/server/services/sketch-prototypes.test.ts b/tests/server/services/sketch-prototypes.test.ts new file mode 100644 index 000000000..e7eedbbb0 --- /dev/null +++ b/tests/server/services/sketch-prototypes.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { + insertForwardDeclarations, + planForwardDeclarations, + stripDefaultArguments, +} from "../../../server/services/sketch-prototypes"; + +describe("sketch prototypes", () => { + it("keeps a parameter list without default arguments unchanged", () => { + expect(stripDefaultArguments("(int left,int right)")).toBe("(int left,int right)"); + }); + + it("removes default arguments, including strings that contain commas or equals signs", () => { + expect(stripDefaultArguments('(const char *sep = ", =", byte n = 2, int last)')).toBe( + "(const char *sep, byte n, int last)", + ); + }); + + it("ignores functions that only appear in comments", () => { + const plan = planForwardDeclarations("/* int fake() { } */\nvoid setup() {}\nint real() { return 1; }"); + expect(plan.declarations).toBe("int real();"); + }); + + it("plans the insertion before the first function the sketch itself defines", () => { + const code = "void header() {}\nconst int M = 2;\nvoid setup() {}\nvoid helper(int a[M]) {}"; + const plan = planForwardDeclarations(code, (lineIndex) => lineIndex >= 1); + expect(plan.insertAt).toBe(code.indexOf("void setup()")); + expect(plan.declarations).toBe("void header();\nvoid helper(int a[M]);"); + }); + + it("returns code without function definitions unchanged", () => { + expect(insertForwardDeclarations("const int M = 2;\n")).toBe("const int M = 2;\n"); + }); +}); From 20754ff0d7d38dfa0f99a508e10e89dce347754a Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Sun, 18 Oct 2026 10:06:46 +0200 Subject: [PATCH 2/3] fix(mock): read an open INPUT_PULLUP pin as HIGH pinMode(pin, INPUT_PULLUP) now drives the simulated pin HIGH, as the internal pull-up resistor of the Uno does, and reports the value to the board view. Before, an open input read LOW as if a button were pressed. A toolchain integration test runs real sketches through the builder and g++ and covers the pull-up, a typedef signature, and a header with constant array sizes and default arguments. Co-Authored-By: Claude Opus 5.5 --- .../arduino-mock/arduino-registry-logic.ts | 9 +- .../sketch-runtime-fidelity.test.ts | 113 ++++++++++++++++++ vitest.config.ts | 2 + 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/integration/sketch-runtime-fidelity.test.ts diff --git a/server/mocks/arduino-mock/arduino-registry-logic.ts b/server/mocks/arduino-mock/arduino-registry-logic.ts index df6209bb1..821bfc144 100644 --- a/server/mocks/arduino-mock/arduino-registry-logic.ts +++ b/server/mocks/arduino-mock/arduino-registry-logic.ts @@ -65,11 +65,18 @@ void outputIORegistry() { // GPIO Functions with state tracking void pinMode(int pin, int mode) { if (pin >= 0 && pin < 20) { + bool enablesPullup = mode == INPUT_PULLUP && pinModes[pin] != INPUT_PULLUP; pinModes[pin] = mode; // Send pin state update via stderr (special protocol) { std::lock_guard lock(cerrMutex); std::cerr << "[[PIN_MODE:" << pin << ":" << mode << "]]" << std::endl; } - + + // The internal pull-up resistor makes an open input read HIGH until it is pulled LOW externally. + if (enablesPullup && pinValues[pin].exchange(HIGH, std::memory_order_seq_cst) != HIGH) { + std::lock_guard lock(cerrMutex); + std::cerr << "[[PIN_VALUE:" << pin << ":" << HIGH << "]]" << std::endl; + } + // Track in I/O Registry - add pinMode as an operation with mode info if (ioRegistry.find(pin) != ioRegistry.end()) { ioRegistry[pin].defined = true; diff --git a/tests/integration/sketch-runtime-fidelity.test.ts b/tests/integration/sketch-runtime-fidelity.test.ts new file mode 100644 index 000000000..a25336a61 --- /dev/null +++ b/tests/integration/sketch-runtime-fidelity.test.ts @@ -0,0 +1,113 @@ +import { execFile, spawn } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { SketchFileBuilder } from "../../server/services/sketch-file-builder"; + +const execFileAsync = promisify(execFile); +const RUN_TIMEOUT_MS = 10_000; + +function decodeSerial(stderr: string): string { + return [...stderr.matchAll(/\[\[SERIAL_EVENT:\d+:([A-Za-z0-9+/=]*)\]\]/g)] + .map((match) => Buffer.from(match[1], "base64").toString("utf8")) + .join(""); +} + +/** Builds a sketch like the local runner, runs it, and returns its serial output once it prints DONE. */ +async function runUntilDone( + root: string, + id: string, + code: string, + headers: Array<{ name: string; content: string }> = [], +): Promise { + const builder = new SketchFileBuilder(root); + const { sketchDir, sketchFile, exeFile } = await builder.build(code, id, headers, headers.length > 0 ? "sketch.ino" : undefined); + await execFileAsync("g++", ["-I", sketchDir, sketchFile, "-o", exeFile, "-pthread"]); + + return new Promise((resolve, reject) => { + const child = spawn(exeFile, [], { stdio: ["pipe", "ignore", "pipe"] }); + let stderr = ""; + const timer = setTimeout(() => { + child.kill("SIGKILL"); + reject(new Error(`Sketch ${id} did not print DONE; serial output: ${decodeSerial(stderr)}`)); + }, RUN_TIMEOUT_MS); + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString("utf8"); + const serial = decodeSerial(stderr); + if (serial.includes("DONE")) { + clearTimeout(timer); + child.kill("SIGKILL"); + resolve(serial); + } + }); + }); +} + +describe("sketch runtime fidelity", () => { + let root: string; + + beforeAll(async () => { + root = await mkdtemp(join(tmpdir(), "unosim-runtime-fidelity-")); + }); + + afterAll(async () => { + await rm(root, { recursive: true, force: true }); + }); + + it("reads an open INPUT_PULLUP pin as HIGH", async () => { + const serial = await runUntilDone(root, "pullup", ` +void setup() { + Serial.begin(115200); + pinMode(2, INPUT_PULLUP); + Serial.print("PIN2="); + Serial.println(digitalRead(2)); + Serial.println("DONE"); +} +void loop() {} +`); + expect(serial).toContain("PIN2=1"); + }, 30_000); + + it("runs a sketch whose helper signatures use a typedef", async () => { + const serial = await runUntilDone(root, "typedef", ` +typedef struct vektor { float x; float y; } Vektor; +void setup() { + Serial.begin(115200); + Vektor a = {1, 2}; + Serial.println(addieren(a, a).y, 1); + Serial.println("DONE"); +} +void loop() {} +Vektor addieren(Vektor v, Vektor s) { v.x += s.x; v.y += s.y; return v; } +`); + expect(serial).toContain("4.0"); + }, 30_000); + + it("runs a sketch whose header uses constant array sizes and default arguments", async () => { + const header = ` +#pragma once +const int M = 2; +void printFirst(const double mat[M][M], const char *name = "M", byte precision = 2) { + Serial.print(name); + Serial.println(mat[0][0], precision); +} +`; + const serial = await runUntilDone(root, "matrix", ` +#include "mat.h" +double A[M][M] = {{1.5, 0}, {0, 1}}; +void setup() { + Serial.begin(115200); + scale(A, 2); + printFirst(A); + printFirst(A, "A", 1); + Serial.println("DONE"); +} +void loop() {} +void scale(double mat[M][M], double factor) { mat[0][0] *= factor; } +`, [{ name: "mat.h", content: header }]); + expect(serial).toContain("M3.00"); + expect(serial).toContain("A3.0"); + }, 30_000); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 96f044189..54e56b281 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -112,6 +112,7 @@ export default defineConfig({ "tests/integration/serial-flow.test.ts", "tests/integration/compiler-canaries.test.ts", "tests/integration/compile-include-boundary.test.ts", + "tests/integration/sketch-runtime-fidelity.test.ts", "tests/integration/worker-pool.*.test.ts", "tests/integration/concurrent-50-clients.test.ts", "tests/server/services/scalability-stress.test.ts", @@ -153,6 +154,7 @@ export default defineConfig({ include: [ "tests/integration/compiler-canaries.test.ts", "tests/integration/compile-include-boundary.test.ts", + "tests/integration/sketch-runtime-fidelity.test.ts", "tests/server/telemetry-heartbeat-integration.test.ts", "tests/core/sandbox-stress.test.ts", ], From 5c5430b1d2cfd2451e09a14b307c42d08c7114d0 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Sun, 18 Oct 2026 10:06:46 +0200 Subject: [PATCH 3/3] docs: list the differences between the simulation and the Uno The user guide gets a section "Grenzen der Simulation" for students and teachers: int and unsigned int are 32 bits, long and double 64 bits, sizeof, float printing without precision, parseInt without timeout, loop speed, memory size and random(). The changelog notes both fixes and points operators to the section. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 19 +++++++++++++++++++ docs/USER_GUIDE.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c33eaba..4bc31dc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,25 @@ All notable changes to UnoSim. Versions follow [Semantic Versioning](https://sem questions and dialogs fall back to the free Tutor without a Course Content session instead of failing with `INVALID_REQUEST` (Course Content SSOT §11, case G). +- **Sketches with `typedef` types, constant array sizes or default arguments + in function signatures run.** The generated function prototypes now sit + directly before the first function the sketch defines, without default + arguments, as in the Arduino IDE. Before, they stood at the top of the + file, so `Vektor add(Vektor a, Vektor b)`, `void f(double m[M][N])` and + `void f(byte precision = 2)` compiled with the Arduino compiler but failed + in the simulation. +- **An open `INPUT_PULLUP` pin reads `HIGH`.** The simulation now models the + internal pull-up resistor; before, such a pin read `LOW` as if a button + were pressed. + +### Documentation + +- The user guide lists the known differences between the simulation and a + real Arduino Uno (section "Grenzen der Simulation"): `int` is 32 bits wide + instead of 16, `long` and `double` are 64 bits, `Serial.print` of a float + without a precision prints `2.5` instead of `2.50`, `Serial.parseInt()` + returns at once instead of waiting for its timeout, and loop speed, memory + size and `random()` sequences differ. Point students to that section. ## v1.0.0 – 2026-10-09 diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 9f72fa970..48c69fd6c 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -108,6 +108,38 @@ Unter **File → Settings** findest du außerdem: - **Pin Monitor**: Pin-Übersicht über dem Board, - **Toast Duration**: Anzeigedauer von Hinweisen. +## Grenzen der Simulation + +UnoSim prüft jeden Sketch zuerst mit dem echten Arduino-Compiler für den Uno. +Was hier kompiliert, kompiliert also auch auf der Hardware. Ausgeführt wird der +Sketch danach aber nicht als Uno-Programm, sondern als Programm für den +UnoSim-Server. Dadurch weicht die Simulation in einigen Punkten vom echten +Arduino Uno ab: + +| Thema | Arduino Uno | UnoSim | +|---|---|---| +| Größe von `int` und `unsigned int` | 16 Bit: `int` reicht von −32768 bis 32767 | 32 Bit: `32767 + 1` ergibt 32768 statt −32768 | +| Größe von `long` | 32 Bit | 64 Bit | +| Größe von `double` | 32 Bit, gleich `float` | 64 Bit, rechnet genauer | +| `sizeof` | z. B. `sizeof(int)` = 2 | zeigt die Größen des Servers, z. B. 4 | +| `Serial.print` einer Kommazahl ohne Stellenangabe | `2.50` (immer 2 Nachkommastellen) | `2.5` | +| `Serial.parseInt()` ohne Eingabe | wartet 1 Sekunde, liefert dann 0 | liefert sofort 0 | +| Geschwindigkeit | 16 MHz | anders: Wie oft `loop()` pro Sekunde läuft, weicht ab. `millis()` und `delay()` stimmen. | +| Arbeitsspeicher | 2 KB | praktisch unbegrenzt: Ein großes Array kann hier laufen und auf dem Uno abstürzen | +| `random()` | feste Zahlenfolge des Uno | andere Zahlenfolge | + +Was das für deine Programme bedeutet: + +- Überläufe beobachtest du zuverlässig mit `byte` (0 bis 255); bei `int` + und `long` zeigt UnoSim die Grenzen des Uno nicht. +- Gib Kommazahlen immer mit Stellenzahl aus, z. B. `Serial.println(wert, 2)`; + dann ist die Ausgabe auf Uno und UnoSim gleich. +- Prüfe Programme, die von Wertebereichen, Speicher oder genauer Rechenzeit + abhängen, zusätzlich auf einem echten Board. + +Gesendeter Text im Serial Monitor endet immer mit dem Zeilenende (Code 10), +wie im Serial Monitor der Arduino IDE mit der Einstellung „New Line“. + ## Wenn etwas nicht klappt | Meldung oder Verhalten | Ursache und Lösung |