diff --git a/CHANGELOG.md b/CHANGELOG.md index 47055416e..ba751120a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,25 +4,104 @@ All notable changes to UnoSim. Versions follow [Semantic Versioning](https://sem ## Unreleased (integration branch `feature/browser-emulation`) +Version 2 runs the compiled sketch in the browser instead of on the server and +adds the Arduino Nano, the Arduino Mega 2560 and the ATtiny85 as preview +boards. Contracts: REST API `1.1.0` (additive, `1.0.0` clients stay +supported), WebSocket `1.0.0` (unchanged; serial messages may carry an optional +`port`), embedding API (`postMessage`) `1.5.0` (additive). The decisions are +drafted in ADR 0009 and ADR 0010 (`Proposed`). + +### Behaviour changes for teaching + +Check slides and exercises against these points; the user guide section +"Grenzen der Simulation" describes the model, "Legacy" keeps the former +behaviour for the Uno. + +- **Data types are those of the board:** `int` has 16 bit (`32767 + 1` is + −32768, formerly 32768), `long` 32 bit, `double` is the 32-bit `float`, + `sizeof(int)` is 2. +- **`Serial.print(2.5)` prints `2.50`** (two decimals without a precision + argument), as on the board; formerly `2.5`. +- **`Serial.parseInt()` without input waits one second** (the stream timeout) + and then returns 0; formerly it returned at once. +- **Speed is the board's:** `loop()` runs as often per second as on a 16 MHz + board (ATtiny85: 8 MHz); counters in `loop()` without `delay()` reach other + values than before. +- **Memory is the board's:** 2 KB SRAM on the Uno and Nano; a large array can + crash the sketch as on the board. UnoSim reports a stack that grows into the + globals or the heap, but not every memory error. +- **`random()` gives the Uno's sequence.** +- **Pause halts `millis()`.** +- **An open input (`INPUT`) reads LOW**, `INPUT_PULLUP` reads HIGH until a + level is set at the board. +- **Serial input sent before `Serial.begin()` is lost** as on the board; a + system line reports it. +- **The run time counts wall-clock time:** on a device slower than real time + (real-time factor below 1 in the header) a 30-second run ends after less + simulated time. +- **EEPROM content stays per board while the tab is open**; a reload starts + with an erased EEPROM. + +### Upgrade notes for operators + +- **Three images** from the same commit: `unosim-server`, `unosim-sandbox` + (host mock) and the new `unosim-compile-sandbox`; `npm run build:sandbox` + builds both sandbox images. In the Docker profile every compile runs + `arduino-cli` in its own container without network; the backend image no + longer contains `arduino-cli` or `g++` (1.62 GB → 726 MB). The backend + checks the compile sandbox image at startup. +- **New variables** (Compose passes them from `.env`; empty means default): + `UNOSIM_RUNTIME_BACKENDS` (`browser`, `host-mock`), `UNOSIM_DEFAULT_RUNTIME`, + `UNOSIM_BOARDS`, `UNOSIM_DEFAULT_BOARD`. `ARDUINO_FQBN` remains as an alias + for the default board and is deprecated. +- **Docker profile default stays the host mock** until the release is accepted; + a pilot instance sets `UNOSIM_DEFAULT_RUNTIME=browser` (INSTALL_SERVER.md, + "Browser-Emulation im Docker-Profil (Pilot)"). Without the browser backend only the Uno is offered. +- **Return to V1** works with the unchanged `.env` and `storage/` + (INSTALL_SERVER.md, "Update und Rollback"). +- **Local installations** need ATTinyCore 1.5.2 for the ATtiny85: + `sh scripts/install-attinycore.sh` (INSTALL_LOCAL.md). +- `npm run capacity:calibrate` measures compile capacity as well + (CAPACITY_MODEL.md, "Compile capacity"). + +### Added + +- **Tools → Board** with the Arduino Nano (A6/A7 as analog-only inputs), the + Arduino Mega 2560 (pins 0–69, four serial ports with a port bar in the + Serial Monitor) and the ATtiny85 (ATTinyCore 1.5.2, 8 MHz, `Serial` as a + half-duplex software UART), each with its own board view and with warnings + for its pins. The choice is kept per browser; a change stops the run. +- **Examples can name their board**; loading one for another board asks first. +- **Diagnostics** in the Serial Monitor: stack collision, program counter + outside the program, watchdog reset, `sleep` with interrupts disabled. +- **Debug bar:** engine (`BROWSER` or `LEGACY`) and real-time factor. +- **I/O Registry** keeps the static entries during a run and marks what the + run confirmed ("+ runtime"). +- **Tutor** questions name the board; the Tutor knows its data type sizes. +- **Embedding API `1.5.0`:** `GET_STATE` with board, runtime and serial ports; + pins are checked against the selected board; `SERIAL_INPUT` and serial + events carry the port. + ### Changed -- **The Arduino Uno now runs in the browser by default** (local profile). The - compiled sketch is executed by an emulator of the ATmega328P in a Web Worker; - `int` is 16 bit, `32767 + 1` is −32768, `sizeof`, `millis()` and `delay()` - behave like the board, and a running sketch keeps going when the connection to - the server is lost. Pause halts the simulated time. File → Settings → - "Simulation engine" switches to "Legacy" (the former server-side - simulation). The user guide section "Grenzen der Simulation" describes the - model. -- Serial input that arrives before `Serial.begin()` is discarded like on the - board and reported with a system line. -- REST API `1.1.0` (additive: `board` and `artifact` in `POST /api/compile`); - `1.0.0` clients stay supported. WebSocket `1.0.0` and the embedding API - `1.4.0` are unchanged. +- **Sketches run in the browser by default** (local profile): the compiled + machine code runs in an emulator of the board's microcontroller (avr8js in a + Web Worker). A started run keeps going when the connection to the server is + lost. File → Settings → "Simulation engine" switches to "Legacy" (the former + server-side simulation, Uno only). +- `POST /api/compile` returns the flash image as `artifact` (REST `1.1.0`); + the HEX file never leaves the server. ### Fixed - The serial input field could not be typed into while it was empty. +- A sketch printing without pause (`Serial.println(millis())`) froze the + browser; the Serial Monitor keeps the last 40 000 characters. +- The editor keeps the indentation on Enter, indents after `{` and closes + brackets and quotes. +- On phones the panel navigation covered pins of the board view. +- The ATtiny85 drives the PWM signal of timer 1 on PB1 and PB4. +- A hidden browser tab no longer ends a run as "stopped responding". ## v1.0.1 – 2026-10-18 diff --git a/README.md b/README.md index 1aa02e317..31e99d48e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ and an interactive Arduino Preview for sketches. - **Code Editor**: Monaco editor integration for writing Arduino sketches with syntax highlighting - **Compilation**: Compile Arduino code through the UnoSim backend -- **Browser emulation of the Arduino Uno**: the ATmega328P runs in your browser (avr8js in a Web Worker) and executes the machine code of the compiled sketch, so data types, overflow and timing behave like the board; the server only compiles. "Legacy" (File → Settings → Simulation engine) keeps the server-side simulation +- **Browser emulation of the Arduino Uno**: the ATmega328P runs in your browser (avr8js in a Web Worker) and executes the machine code of the compiled sketch, so data types, overflow and timing behave like the board; the server only compiles. "Legacy" (File → Settings → Simulation engine) keeps the server-side simulation for the Uno +- **More boards (preview)**: Arduino Nano, Arduino Mega 2560 (four serial ports, A0–A15) and ATtiny85 (ATTinyCore, `Serial` as software UART) under Tools → Board, each with its own board view - **Serial Monitor**: Real-time output display from the simulated Arduino - **Pause/Resume Simulation**: Pause running sketches to inspect state, change pin values, and resume execution - **Arduino Preview**: A preview of analog/digital inputs and outputs directly in the Arduino SVG @@ -51,7 +52,7 @@ and an interactive Arduino Preview for sketches. - **Frontend**: React, TypeScript, Vite, TailwindCSS, Radix UI - **Backend**: Node.js (TypeScript), Express, WebSocket support -- **Emulation**: avr8js (ATmega328P) in a Web Worker +- **Emulation**: avr8js (ATmega328P, ATmega2560, ATtiny85) in a Web Worker, packaged as the workspace package `@unosim/avr-emulator` - **Editor**: Monaco Editor - **Testing**: Vitest with React Testing Library - **Build Tools**: Vite, esbuild @@ -64,7 +65,7 @@ UnoSim runs on Linux and macOS. Production always runs in Docker; see ### Prerequisites - Node.js 24.20.0 LTS (see `.nvmrc`) and npm -- [Arduino CLI](https://arduino.github.io/arduino-cli/) with the `arduino:avr` core +- [Arduino CLI](https://arduino.github.io/arduino-cli/) with the `arduino:avr` core, and ATTinyCore for the ATtiny85 (`sh scripts/install-attinycore.sh`, see [docs/INSTALL_LOCAL.md](docs/INSTALL_LOCAL.md)) Docker is not needed for development. @@ -76,13 +77,15 @@ cd UnoSim npm ci arduino-cli core update-index arduino-cli core install arduino:avr +sh scripts/install-attinycore.sh # optional: only for the ATtiny85 npm run dev:full ``` Open `http://localhost:3001`. `npm run dev:full` starts the backend (`127.0.0.1:3000`) and the Vite dev server with hot reload; `npm run dev` -starts only the backend. The development profile is single-user, compiles and -runs sketches directly on your machine and accepts only loopback requests. +starts only the backend. The development profile is single-user, compiles +sketches directly on your machine, runs them in the browser emulation (the +"Legacy" host mock as a local process) and accepts only loopback requests. Examples load from the public GitHub repository `ttbombadil/unosim-examples` at `main` by default. Each browser can choose a @@ -102,9 +105,12 @@ changing the server default; see | `npm run check` | TypeScript type-check (`tsc --noEmit`) | | `npm run test:fast` | Run deterministic unit tests | | `npm run test:integration` | Run Arduino/toolchain integration tests | -| `npm run test:docker` | Run Docker sandbox integration tests | +| `npm run test:docker` | Run Docker sandbox integration tests, including the compile sandbox contract | +| `npm run build:sandbox` | Build the simulation sandbox and the compile sandbox image | +| `npm run emulate -- --board ` | Run a HEX file headless in the emulator and measure its speed | +| `npm run check:bundle` | Check the initial client bundle budget and that avr8js stays in the worker chunk | | `npm run test:e2e` | Run Playwright browser tests | -| `npm run capacity:calibrate` | Measure host/Docker simulation capacity and generate review-only Capacity recommendations | +| `npm run capacity:calibrate` | Measure host/Docker compile and simulation capacity and generate review-only Capacity recommendations | | `npm run test:all` | Run unit, integration and Docker gates | | `./run-tests.sh` | Full pipeline: lint, unit tests, Docker build, integration tests, E2E | | `RUN_HEAVY_TESTS=1 ./run-tests.sh` | Full pipeline including the slow parallel-instance stress test | @@ -112,15 +118,17 @@ changing the server default; see For host-specific Capacity sizing, use `npm run capacity:calibrate` and follow the review workflow in [`docs/CAPACITY_MODEL.md`](docs/CAPACITY_MODEL.md). Calibration needs a built application (`npm run build`) and the sandbox image -(`npm run build:sandbox`); it aborts early with a hint if the build is missing. +and the compile sandbox (`npm run build:sandbox` builds both); it aborts early with a hint if the build is missing. ### Architecture Overview The full picture is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). -- **Sandbox Runner Pool** — Manages runner leases for sketch execution. In the documented production path, each simulation runs in a short-lived isolated Docker sandbox with stdout/stderr capture for serial output and pin state reporting. +- **Browser emulation** — The client runs the compiled flash image in a Web Worker (`packages/avr-emulator`, avr8js). `POST /api/compile` returns the image as `artifact`; the run itself needs no server and survives a lost connection. +- **Compile sandbox** — In the Docker profile every compile runs `arduino-cli` in its own short-lived container without network (`Dockerfile.compile-sandbox`). +- **Sandbox Runner Pool** — Manages runner leases for the host mock ("Legacy"). In the documented production path, each host-mock simulation runs in a short-lived isolated Docker sandbox with stdout/stderr capture for serial output and pin state reporting. - **Compilation Worker Pool** — In production mode, the configured Node.js Worker Thread pool handles compilations in parallel via `CompilerWithFallback`. The Compose reference uses 8 workers (the pool runs at most 8); a full pool answers `503 SYSTEM_BUSY`. Current capacity guidance is in [`docs/CAPACITY_MODEL.md`](docs/CAPACITY_MODEL.md); [`docs/CAPACITY_HISTORICAL_PHASE_3_4.md`](docs/CAPACITY_HISTORICAL_PHASE_3_4.md) keeps the historical measurement. -- **WebSocket Layer** — Real-time communication between client and server for serial output, pin state batches, and simulation control (start/stop/pause/resume). +- **WebSocket Layer** — Real-time communication between client and server for host-mock runs (serial output, pin state batches, simulation control) and status. - **SonarQube Integration** — Optional SonarQube scans are wired into the pre-push hook and `./run-tests.sh` when `SONAR_TOKEN` and a reachable SonarQube service are available. Release blocking is controlled by `REQUIRE_RELEASE_GATE=1`. ## Notes for running tests (optional) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 16023fb50..bad9db98b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -23,8 +23,10 @@ Diese Datei beschreibt die grundlegende Architektur von UnoSim mit Fokus auf Dat - Als Entwurf (`Proposed`, noch nicht normativ) liegen `adr/0009-browser-emulation-runtime.md` (Browser-Emulation als Laufzeit, Host-Mock als Rückfallebene) und `adr/0010-compile-sandbox-and-flash-artifact.md` - (Compile-Sandbox, Flash-Abbild, Build-Identität). Bis zu ihrer Annahme - beschreibt dieses Dokument weiter den Host-Mock als einzige Laufzeit. + (Compile-Sandbox, Flash-Abbild, Build-Identität). Der Integrationsbranch + `feature/browser-emulation` setzt sie um; dieses Dokument beschreibt den + umgesetzten Stand. Bis zur Annahme können die Maintainer Entscheidungen + daraus noch ändern. - Externe iframe-API-Verträge liegen in `EXTERNAL_API.md`; Feature-Details liegen in den thematischen SSOT-Dateien unter `../ssot/`. - Versionsverträge: REST `1.1.0` (`1.0.0` weiterhin akzeptiert; `Accept-Version`/`X-UnoSim-API-Version`), WebSocket `1.0.0` (`handshake.protocolVersion`) und iframe `postMessage` `1.5.0`; inkompatible Änderungen benötigen eine neue Major-Version und Migration. - Neben normativer Dokumentation enthält der Tree als historisch oder nicht @@ -36,14 +38,20 @@ Diese Datei beschreibt die grundlegende Architektur von UnoSim mit Fokus auf Dat ```mermaid graph TD - A[Client/Browser] -->|WebSocket| B[UnoSim Server] - B -->|REST / Prepare-Phase| C[Arduino Compiler] - B -->|Docker API| D[Sandbox Runner Pool] - C -->|Hex-Datei| D - D -->|Serial Output| B - B -->|WebSocket| A + A[Client/Browser] -->|REST /api/compile| B[UnoSim Server] + B --> C[Arduino Compiler
Docker: Compile-Sandbox] + C -->|Artefakt: Flash-Abbild| A + A -->|Artefakt, Befehle| W[Emulator im Web Worker
@unosim/avr-emulator] + W -->|Serial, Pins, Status| A + A -.->|WebSocket, nur Host-Mock| B + B -.->|Docker API| D[Sandbox Runner Pool] + C -.->|Hex-Datei| D + D -.->|Serial Output| B ``` +Durchgezogen der Standardweg (Browser-Emulation), gestrichelt der Host-Mock +als Rückfallebene (nur Uno). + ## 🏗️ Architekturkomponenten ### 1. Client (Frontend) @@ -57,8 +65,42 @@ graph TD - `use-simulation-controller.ts` – Simulation-Mutationen, WebSocket-Kommandos und Lifecycle - `use-ui-feedback-adapter.ts` – Toasts, Debug-Meldungen, Glitch- und Pin-Konflikt-Feedback - `useArduinoSimulatorPage` – Composition Root für ViewModels - -### 2. UnoSim Server (Backend) + - `lib/runtime/` – Laufzeit-Schnittstelle `SimulationRuntime` mit den + Umsetzungen `BrowserRuntime` (Worker, Überwachung, Übersetzung in die + Nachrichten des WebSocket-Handlers) und `HostMockRuntime` (WebSocket), + `RuntimeHub` (aktive Laufzeit eines Tabs), `runtime-commands.ts` + (Befehls-Router an der Grenze der Controller), Auswahlregeln + (`runtime-selection.ts`, `board-selection.ts`, `runtime-config.ts`) und + die Worker-Datei `emulator.worker.ts` (eigener Chunk, lädt erst beim + ersten Browser-Start) + - `lib/active-board.ts`, `lib/board-view.ts` – gewähltes Board und seine + Ansicht (Grafiken je Board) + +### 2. Emulationskern (`packages/avr-emulator`) +- **Verantwortung:** führt das Flash-Abbild eines kompilierten Sketches als + AVR-Maschinencode aus; importiert nichts aus UnoSim +- **Technologie:** avr8js (exakt gepinnt, einzige Laufzeitabhängigkeit), + TypeScript, privates Workspace-Paket `@unosim/avr-emulator` +- **Einstiege:** `.` (Emulator, Headless-Lauf), `./boards` (Pinbelegung ohne + avr8js, auch im Server und in `shared/`), `./image` (HEX/ELF, Artefakt), + `./protocol` (Worker-Protokoll ohne avr8js), `./worker` (Worker-Host) +- **Hauptkomponenten:** MCU-Profile (`mcu/`: ATmega328P, ATmega2560, + ATtiny85 mit den Korrekturen an avr8js), Board-Hardware (`boards/`), + Peripherie-Beobachtung (`io/`: Pins, PWM, ADC, serielle Ports samt + Software-UART, Laufzeitfunde der I/O-Registry, Diagnosen), Zeitsteuerung + (`runtime/`), Worker-Host mit Laufkennung und Heartbeat (`worker/`). + Einzelheiten: [README des Pakets](../packages/avr-emulator/README.md), + [SimulationRuntime-SSOT](../ssot/ssot_function_definition_SimulationRuntime.md) + +### 3. Board-Katalog (`shared/boards.ts`) +- **Verantwortung:** Produktsicht der Boards: Name, FQBN, Core, angebotene + Laufzeiten, Freigabestatus (`planned`, `preview`, `available`); die + Hardware (Pins, MCU, Takt) kommt aus dem Emulationskern +- **Nutzer:** Compile-Route (FQBN-Allowlist), `GET /api/config`, Parser je + Board (`shared/parsers/pin-context.ts`, `shared/board-pins.ts`), Tutor-Kontext, + Client (Tools → Board). Vertrag: [Boards-SSOT](../ssot/ssot_function_definition_Boards.md) + +### 4. UnoSim Server (Backend) - **Verantwortung:** WebSocket-Server, REST-API, Compilation-Queue, Sandbox-Management - **Technologie:** Node.js/Express, TypeScript, WebSocket (ws) - **Hauptkomponenten:** @@ -71,7 +113,7 @@ graph TD - `services/sandbox-runner-pool.ts` – Pool für Docker-Sandboxen - `services/sandbox-runner.ts` – Einzelne Sandbox-Instanz -### 3. Arduino Compiler +### 5. Arduino Compiler - **Verantwortung:** Sketch-Kompilierung, Header-Verarbeitung, Cache-Management - **Technologie:** arduino-cli, im Docker-Profil in der Compile-Sandbox, lokal im Backend - **Hauptkomponenten:** @@ -97,8 +139,8 @@ graph TD gestartet. Lokal und als Fallback bei ausgefallenem Pool begrenzt der Gatekeeper (`COMPILE_MAX_CONCURRENT`). -### 4. Sandbox Runner Pool -- **Verantwortung:** Verwaltung von Runner-Leases und Docker-Containern für Sketch-Ausführung +### 6. Sandbox Runner Pool (nur Host-Mock) +- **Verantwortung:** Verwaltung von Runner-Leases und Docker-Containern für die Sketch-Ausführung des Host-Mocks - **Technologie:** Docker-API und pro Ausführung kurzlebige Sandbox-Container. Node.js Worker Threads gehören zum Compiler Worker Pool, nicht zum Runner-Pool. - **Hauptmerkmale:** - **Runner-Pool:** Vorgehaltene Runner-Objekte; Sandbox-Container werden pro Ausführung gestartet und anschließend bereinigt. @@ -109,14 +151,14 @@ graph TD - **Isolation:** Jeder Sketch läuft in eigenem Container - **Ressourcenkontrolle:** CPU/Memory/PID-Limits pro Container -### 5. Execution pipeline +### 7. Execution pipeline (nur Host-Mock) - **Verantwortung:** Sandbox-Lifecycle, Compile-Gatekeeping, Stream-Verarbeitung - **Technologie:** TypeScript, Dependency-Injection via Context-Objekte - **Extrahierte Phasen:** - `server/services/sandbox/execution-phases/prepare-phase.ts` – Compilation mit Gatekeeper - Weitere Phasen kapseln Cleanup, Timeout, Stream und Start -### 6. Parser-Module +### 8. Parser-Module - **Verantwortung:** Arduino C++ Code-Analyse, I/O-Registry, Hardware-Kompatibilität - **Technologie:** TypeScript, funktionale Parser-Kombinatoren - **Extrahierte Module:** @@ -266,12 +308,30 @@ and [adr/0005-browser-scoped-external-examples.md](adr/0005-browser-scoped-exter ### Compile-Flow 1. Client sendet Code an Server via REST (`/api/compile`) 2. Server leitet an Compiler weiter (mit Fallback-Mechanismus) -3. Compiler kompiliert Code zu Artefakten/Diagnosen (mit Cache) -4. REST-Compile liefert das Compile-Ergebnis an den Client zurück - -Die Simulation nutzt denselben Compilerpfad in der Prepare-Phase, startet den Runner aber über den WebSocket-Simulationsfluss. - -### Simulation-Flow +3. Compiler kompiliert Code zu Artefakten/Diagnosen (mit Cache); im + Docker-Profil läuft `arduino-cli` in der Compile-Sandbox +4. REST-Compile liefert das Compile-Ergebnis an den Client zurück, bei Erfolg + mit dem Feld `artifact` (Flash-Abbild, Board, SHA-256, Symbole, + `buildId`); das HEX verlässt den Server nicht + +Der Host-Mock nutzt denselben Compilerpfad in der Prepare-Phase, startet den +Runner aber über den WebSocket-Simulationsfluss. + +### Simulation-Flow (Browser-Emulation, Standard) +1. Client kompiliert (Compile-Flow) und hält das Artefakt des letzten Builds +2. `RuntimeHub` wählt die Laufzeit (Nutzerwahl → Server-Standard → erste + nutzbare) und lädt beim ersten Browser-Start den Worker +3. `BrowserRuntime` sendet `load` mit dem Flash-Abbild, nach `ready` `start`; + jeder Befehl trägt eine Laufkennung (`runId`), Ereignisse anderer Läufe + werden verworfen +4. Der Worker emuliert in Echtzeit und sendet gebündelt Serial-, Pin-, + Status- und Laufzeitereignisse sowie etwa alle 500 ms einen Heartbeat +5. `BrowserRuntime` übersetzt sie in die Nachrichten des WebSocket-Handlers; + die UI wertet beide Laufzeiten gleich aus +6. Pause, Reset, serielle Eingabe und Pinwerte gehen als Befehle an den + Worker; der Lauf übersteht einen Verbindungsverlust zum Server + +### Simulation-Flow (Host-Mock, Rückfallebene) 1. Client sendet `start_simulation` via WebSocket 2. Server validiert Session und acquired Runner aus Pool 3. ExecutionManager startet Prepare-Phase (Compilation mit Gatekeeper) @@ -304,6 +364,11 @@ Die Simulation nutzt denselben Compilerpfad in der Prepare-Phase, startet den Ru | `ws-output-buffer.ts` | Serial-Output-Batching und sichere WebSocket-Ausgabe | | `sandbox-runner-pool.ts` | Runner-Lebenszyklus und Pool-Management | | `arduino-compiler.ts` | Compilation und Cache-Logik | +| `RuntimeHub` | aktive Laufzeit eines Tabs, Wechsel nur im Leerlauf, Zustellung der Browser-Ereignisse | +| `BrowserRuntime` | Worker-Lebenszyklus, Laufkennung, Fristen und Heartbeat-Überwachung | +| `WorkerHost` (Paket) | Maschine im Worker, Zeitsteuerung, Bestätigungen, EEPROM je Board über Läufe | +| `shared/boards.ts` | Board-Katalog, angebotene Boards, FQBN-Allowlist | +| `lib/active-board.ts` | gewähltes Board für Pin-Store, Registry-Tabelle und Sketch-Analyse | | Browser Settings | optionale, nur lokal persistierte Course-Content-Auswahl für Examples und Tutor | | Course Content API | Ermittlung von Default oder request-scoped Browser-Override, immutable Revision und capability-scoped Status | | `ExamplesRepository` / Course snapshot | gemeinsamer Repository-/Ref-Source-Cache, immutable Revision-Snapshots, Examples-Kernvalidierung, optionaler Tutor-Bundle-Status und atomare LKG-Aktivierung | @@ -330,8 +395,18 @@ Der Gateway-Vertrag ist in ADR 0001 Benutzeranmeldung optional ist. Die folgenden Punkte sind eine Architektur-Zusammenfassung und ersetzen die ADRs nicht. -### Sandbox-Sicherheit -- **Isolation:** Jeder Sketch läuft in eigenem Docker-Container +### Browser-Laufzeit +- **Ausführung beim Nutzer:** Ein Browser-Lauf führt AVR-Maschinencode in + avr8js in einem Web Worker aus. Der Sketch-Code wird interpretiert, nicht + als JavaScript ausgeführt, und hat keinen Zugriff auf DOM, Netz oder + Speicher der Seite. Er belastet nur den Tab des Nutzers, keine + Server-Ressourcen. +- **Grenzen:** Fristen und Heartbeat im Hauptthread, Laufzeitgrenze 1–300 s + ohne Pausen, begrenzte serielle Puffer, Prüfung des Artefakts beim Laden + (Board, Größe, SHA-256). Einzelheiten: [SECURITY.md](SECURITY.md) + +### Sandbox-Sicherheit (Host-Mock und Compile) +- **Isolation:** Jeder Host-Mock-Lauf und im Docker-Profil jeder Compile läuft in eigenem Docker-Container - **Ressourcenlimits:** CPU, Memory, PID-Limits pro Container - **Dateisystem:** Read-only Root-Dateisystem; read-only `/sandbox`-Bind-Mount für Sketch und Build-Quellen; beschreibbares `/sandbox-work`-tmpfs für Binary und relative Laufzeitdateien, begrenzt auf 64 MiB und 4096 Inodes; beschreibbares, auf 64 MiB begrenztes `noexec`-`/tmp`-tmpfs - **Timeouts:** Laufzeit pro Simulation standardmäßig 60 Sekunden, je Start 1–300 Sekunden @@ -376,7 +451,7 @@ Sketches): | Wer | Weg | Wirkung | |---|---|---| | Betreiber | `UNOSIM_DEFAULT_RUNTIME=host-mock`, Neustart | neue Läufe starten im Host-Mock, „Emulation“ bleibt wählbar | -| Betreiber | `browser` aus `UNOSIM_RUNTIME_BACKENDS` entfernen, Neustart | nur noch Host-Mock; Boards ohne Host-Mock (Nano, Mega 2560) werden nicht mehr angeboten | +| Betreiber | `browser` aus `UNOSIM_RUNTIME_BACKENDS` entfernen, Neustart | nur noch Host-Mock; Boards ohne Host-Mock (Nano, Mega 2560, ATtiny85) werden nicht mehr angeboten | | Nutzer | **File → Settings → Simulation engine → Legacy** | gilt ab dem nächsten Start, nur für den Uno | | Betreiber | V1-Image (v1.0.1) wieder ausrollen | ganze Rückkehr ohne Änderung an `.env` und `storage/`; zurück auf V2 ebenso | @@ -433,7 +508,11 @@ Der UnoSim Server sammelt folgende Metriken: - **Serial Output:** Bytes pro Sekunde, Drop-Rate - **Pin States:** Änderungen pro Sekunde, Batch-Größen -Diese Metriken sind über `/api/status` und WebSocket-Events verfügbar. `/api/health` prüft nur die HTTP-Erreichbarkeit; `/api/readiness` meldet, ob der Sandbox-Pool initialisiert und bereit ist. +Diese Metriken sind über `/api/status` und WebSocket-Events verfügbar. `/api/health` prüft nur die HTTP-Erreichbarkeit; `/api/readiness` meldet bereit, wenn alle angebotenen Backends bereit sind (Host-Mock: Sandbox-Pool; Browser-Emulation braucht serverseitig nur den Compiler). `/api/status` nennt die Backends. + +Browser-Läufe messen im Worker: Echtzeitfaktor, Pinwechsel und Zeichen je +Sekunde, verworfene Zeichen (Telemetrie etwa einmal je Sekunde). Diese Werte +bleiben im Browser (Debug-Leiste), der Server sieht sie nicht. ## 🔄 Versionierung und API-Kontrakte @@ -441,6 +520,8 @@ Diese Metriken sind über `/api/status` und WebSocket-Events verfügbar. `/api/h `X-UnoSim-API-Version` und den optionalen Request-Header `Accept-Version`; die aktuellen Routen bleiben beispielsweise `/api/status`. - **WebSocket:** Versionierung über Handshake-Nachrichten +- **Worker-Protokoll:** intern, versioniert im Paket (`protocolVersion` in + `ready`); Client und Worker kommen immer aus demselben Build - **Deprecation:** Legacy-Felder und API-Endpunkte werden mit `@deprecated` markiert und nach 2 Major-Releases entfernt --- diff --git a/docs/CAPACITY_MIXED_LOAD_TESTING.md b/docs/CAPACITY_MIXED_LOAD_TESTING.md index ed339b5f5..70be898ed 100644 --- a/docs/CAPACITY_MIXED_LOAD_TESTING.md +++ b/docs/CAPACITY_MIXED_LOAD_TESTING.md @@ -4,6 +4,12 @@ This procedure measures REST Arduino compiles, simulation starts, and active simulations together. It records observations; it does not set production limits or judge a run against assumed thresholds. +> **Browser emulation:** Simulation slots, sandbox starts and active +> simulations here concern the host mock ("Legacy"). A run in the browser +> emulation uses no server resource after its compile; with it, the compile +> limits the server. See +> [CAPACITY_MODEL.md, Compile capacity](CAPACITY_MODEL.md#compile-capacity-browser-emulation). + ## Preconditions - Run on the actual target Linux host, or a dedicated staging host whose CPU, diff --git a/docs/CAPACITY_TEST_RESULTS.md b/docs/CAPACITY_TEST_RESULTS.md index 976cfdf9f..0b3a9af28 100644 --- a/docs/CAPACITY_TEST_RESULTS.md +++ b/docs/CAPACITY_TEST_RESULTS.md @@ -4,6 +4,12 @@ This document summarizes the measured reference results. The operating model, timeout boundaries, production defaults, and acceptance procedure are authoritative in CAPACITY_MODEL.md. +> **Browser emulation:** Simulation slots, sandbox starts and active +> simulations here concern the host mock ("Legacy"). A run in the browser +> emulation uses no server resource after its compile; with it, the compile +> limits the server. See +> [CAPACITY_MODEL.md, Compile capacity](CAPACITY_MODEL.md#compile-capacity-browser-emulation). + ## Historical harness baseline The reproducible BASELINE is 5 simulation slots, 5 sandbox-start slots, and 25 diff --git a/docs/CAPACITY_VALIDATION_QUICKSTART.md b/docs/CAPACITY_VALIDATION_QUICKSTART.md index 3ac0f9d9a..cde001d8f 100644 --- a/docs/CAPACITY_VALIDATION_QUICKSTART.md +++ b/docs/CAPACITY_VALIDATION_QUICKSTART.md @@ -3,6 +3,12 @@ Run this procedure only on the dedicated `UnoSim_Test` staging host. Candidate profile values are validation inputs and must not be copied into production. +> **Browser emulation:** Simulation slots, sandbox starts and active +> simulations here concern the host mock ("Legacy"). A run in the browser +> emulation uses no server resource after its compile; with it, the compile +> limits the server. See +> [CAPACITY_MODEL.md, Compile capacity](CAPACITY_MODEL.md#compile-capacity-browser-emulation). + ## Prepare the host ```bash diff --git a/docs/EXTERNAL_API.md b/docs/EXTERNAL_API.md index 3520e2ed7..8d77a2977 100644 --- a/docs/EXTERNAL_API.md +++ b/docs/EXTERNAL_API.md @@ -25,6 +25,16 @@ includes `protocolVersion`; clients that do not use this field remain compatible. A future incompatible WebSocket change requires a new protocol version and an explicit migration/parallel-operation period. +### Connection to the server + +`START_SIMULATION` compiles on the server. Without a connection it is queued +(`QUEUED_FOR_COMPILING`) and runs when the server is reachable again. A run in +the browser emulation (`runtime: "browser"` in `GET_STATE`) needs the server +only for that compile: once it runs, `STOP_SIMULATION`, `PAUSE_SIMULATION`, +`RESUME_SIMULATION`, the pin messages and `SERIAL_INPUT` keep working while the +connection is lost, and its events keep arriving. A run of the host mock +(`runtime: "host-mock"`) ends when the connection is lost. + --- ## Security @@ -143,7 +153,7 @@ iframe.contentWindow.postMessage( | Field | Type | Description | |-------|------|-------------| -| `payload.pin` | `number` | Pin number of the selected board: Uno 0–13 digital and 14–19 for A0–A5; Nano additionally 20 and 21 for the analog-only A6 and A7; Mega 2560 0–53 digital and 54–69 for A0–A15 (see `GET_STATE` for the board). A pin the board does not have is refused with `success: false` and `error: "pin does not exist on "` (*since 1.5.0*). | +| `payload.pin` | `number` | Pin number of the selected board: Uno 0–13 digital and 14–19 for A0–A5; Nano additionally 20 and 21 for the analog-only A6 and A7; Mega 2560 0–53 digital and 54–69 for A0–A15; ATtiny85 0–5 for PB0–PB5, where A0–A3 are pins 5, 2, 4 and 3 and PB5 is the RESET pin of the chip (see `GET_STATE` for the board). A pin the board does not have is refused with `success: false` and `error: "pin does not exist on "` (*since 1.5.0*). | | `payload.value` | `number` | Digital: `0` or `1`. Analog: `0–1023`. PWM: `0–255`. | --- @@ -201,7 +211,7 @@ iframe.contentWindow.postMessage( | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.data` | `string` | ✅ | Serial data to send to the sketch. | -| `payload.port` | `number` | – | Hardware serial port: `0` = `Serial` (default), `1`–`3` = `Serial1`–`Serial3` on the Mega 2560 (see `serialPorts` in `GET_STATE`). A port the board does not have is refused with `success: false` and `error: "payload.port must be an integer from 0 to "`. | +| `payload.port` | `number` | – | Serial port: `0` = `Serial` (default), `1`–`3` = `Serial1`–`Serial3` on the Mega 2560 (see `serialPorts` in `GET_STATE`). On the ATtiny85 `Serial` is a software UART (TX PB0, RX PB1) and half duplex: input that arrives while the sketch prints is lost, as on the chip. A port the board does not have is refused with `success: false` and `error: "payload.port must be an integer from 0 to "`. | --- @@ -271,11 +281,11 @@ iframe.contentWindow.postMessage( | Field | Description | |---|---| -| `board` | Board id: `uno`, `nano`, `mega2560` (more with later releases). | +| `board` | Board id: `uno`, `nano`, `mega2560`, `attiny85` (more with later releases). | | `boardName` | Name for display, for example `"Arduino Nano"`. | | `runtime` | `"browser"` (emulation in the page) or `"host-mock"` (server-side simulation, "Legacy"): the runtime of the current run, or of the next one while idle. Absent while no runtime is usable. | | `state` | Same values as `GET_SIMULATION_STATE`. | -| `serialPorts` | Number of hardware serial ports of the board: `1`, on the Mega 2560 `4` (`Serial`, `Serial1`–`Serial3`). | +| `serialPorts` | Number of serial ports of the board: `1`, on the Mega 2560 `4` (`Serial`, `Serial1`–`Serial3`); on the ATtiny85 the one port is a software UART. | --- diff --git a/docs/INSTALL_SERVER.md b/docs/INSTALL_SERVER.md index 758759b9b..ba441dfea 100644 --- a/docs/INSTALL_SERVER.md +++ b/docs/INSTALL_SERVER.md @@ -228,7 +228,7 @@ UNOSIM_RUNTIME_BACKENDS=browser,host-mock UNOSIM_DEFAULT_RUNTIME=browser ``` -Dann bietet der Server zusätzlich Nano und Mega 2560 an (Status `preview`), +Dann bietet der Server zusätzlich Nano, Mega 2560 und ATtiny85 an (Status `preview`), und `POST /api/compile` liefert das Flash-Abbild (`artifact`) aus der Compile-Sandbox; der Sketch läuft im Browser des Nutzers und belastet den Server nicht. „Legacy“ (Host-Mock) bleibt für den Uno wählbar. @@ -420,7 +420,7 @@ Ein Rollback von V2 auf das V1-Image (v1.0.1) braucht keine Änderung an `.env` oder `storage/`: V1 ignoriert die neuen Variablen und die neuen Cache-Dateien (`*.artifact.json`); umgekehrt baut V2 Einträge ohne diese Datei neu (geprüft am 23.10.2026). Nach dem Rollback laufen alle Sketches im -Host-Mock, Nano und Mega 2560 sind nicht mehr wählbar. +Host-Mock, Nano, Mega 2560 und ATtiny85 sind nicht mehr wählbar. Die vollständigen Gates stehen in [`RELEASE_RUNBOOK.md`](RELEASE_RUNBOOK.md). diff --git a/docs/OPEN_POINTS.md b/docs/OPEN_POINTS.md index debbdffcf..60546dd21 100644 --- a/docs/OPEN_POINTS.md +++ b/docs/OPEN_POINTS.md @@ -4,6 +4,9 @@ > 2026-10-11 gegen `main` @ `4dcc1ae9` geprüft. Erledigte Punkte werden aus > dieser Liste entfernt; die abgeschlossene Refactoring-Serie mit Verlauf steht > in [archive/refactoring/REFACTORING_OPL.md](archive/refactoring/REFACTORING_OPL.md). +> Die Punkte der Browser-Emulation (`EMU-*`, `AVR8JS-PATCHES`, +> `EDITOR-INDENT`) wurden am 2026-10-24 gegen `feature/browser-emulation` +> geprüft. > Normative Verträge bleiben ADRs und SSOTs. Kein offener Punkt ist mit Risiko „hoch“ bewertet. @@ -26,13 +29,19 @@ Statuswerte: `BLOCKED_DECISION` (wartet auf eine Entscheidung) · `DEFERRED` | T3 | Vom Client gesendete `answerRating`-Werte zählen als Mastery-Evidenz, solange für die Frage keine gespeicherte Evidenz existiert. Wirkt nur auf den eigenen Lernstand, widerspricht aber der deterministischen Mastery aus ADR 0007. | gering | [curriculum-tutor-adapter.ts:493-515](../server/services/tutor/curriculum-tutor-adapter.ts#L493-L515), [learning-planner.ts:193-200](../server/services/tutor/curriculum/learning-planner.ts#L193-L200) | BLOCKED_DECISION | Dürfen Client-Ratings Mastery beeinflussen? | | D1 | LEARN nach schwacher oder mittlerer Antwort, wenn das aktuelle Concept keine frische Frage mehr hat, das Topic aber noch andere Fragen bietet. Die SSOT regelt nur den Fall, dass das ganze Topic nicht mehr abfragbar ist (`content-exhausted`). | gering | [LearningQuestions §Zustandsübergänge](../ssot/ssot_function_definition_LearningQuestions.md) | BLOCKED_DECISION | Gewünschtes Tutor-Verhalten festlegen, dann SSOT ergänzen | | D4 | Tutor-Sessions laufen nach einer absoluten TTL ab (Default 1 h, per `TUTOR_SESSION_TTL_MS` einstellbar), auch bei laufender Nutzung. | gering | [config.ts:412](../server/config.ts#L412), [course-content-session.ts:107](../server/services/course-content/course-content-session.ts#L107) | BLOCKED_DECISION | Absolute oder gleitende TTL? | -| D5 | Ein laufender Sketch hält seinen Simulationsslot bis zum Lauf-Timeout (Default 60 s, Client-Maximum 300 s). Pausierte Läufe enden nach `SIMULATION_MAX_PAUSED_SECONDS` (600 s), jeder Container nach `SANDBOX_MAX_LIFETIME_SECONDS` (2 h). Einen Inaktivitäts-Timeout für laufende Simulationen gibt es nicht. | gering | [input-limits.ts:40-42](../shared/input-limits.ts#L40-L42), [config.ts:246-247](../server/config.ts#L246-L247) | BLOCKED_DECISION | Reichen die vorhandenen Grenzen, oder soll eine Simulation ohne Nutzerinteraktion früher enden? | +| D5 | Nur Host-Mock (Browser-Läufe belegen keinen Server-Slot): Ein laufender Sketch hält seinen Simulationsslot bis zum Lauf-Timeout (Default 60 s, Client-Maximum 300 s). Pausierte Läufe enden nach `SIMULATION_MAX_PAUSED_SECONDS` (600 s), jeder Container nach `SANDBOX_MAX_LIFETIME_SECONDS` (2 h). Einen Inaktivitäts-Timeout für laufende Simulationen gibt es nicht. | gering | [input-limits.ts:40-42](../shared/input-limits.ts#L40-L42), [config.ts:246-247](../server/config.ts#L246-L247) | BLOCKED_DECISION | Reichen die vorhandenen Grenzen, oder soll eine Simulation ohne Nutzerinteraktion früher enden? | | HOSTMOCK-REMOVAL | Der Host-Mock bleibt nach ADR 0009 (E9) als Rückfallebene neben der Browser-Emulation; zwei Laufzeiten bedeuten doppelte Wartung (Runner-Pool, Laufzeit-Sandbox, WebSocket-Simulation, Mock-Tests). | gering | [ARCHITECTURE.md, Host-Mock als Rückfallebene](ARCHITECTURE.md#host-mock-als-rückfallebene) | DEFERRED | Nach `v2.0.0` und einem Semester ohne Rückfall: eigene ADR über den Abbau nach den dort festgeschriebenen Kriterien und Umfang | ## Erweiterungen | ID | Befund | Risiko | Beleg | Status | Nötige Entscheidung | |---|---|---|---|---|---| +| EMU-LINES | Laufzeitfunde der Browser-Emulation tragen keine Zeilennummer; Zeilen kommen nur aus der statischen Analyse. Möglich wären DWARF-Zeilentabelle und Rücksprungadresse. | gering | [IoRegistry-SSOT, Zusammenführung](../ssot/ssot_function_definition_IoRegistry.md#zusammenführung-von-statik-und-laufzeit-beide-backends) | DEFERRED | Lohnt der größere Build (ELF mit Debug-Zeilen) die Zeilen zur Laufzeit? | +| EMU-PINTABLE | Ein `digitalWrite` ohne Pegeländerung über die Pin-Tabelle des Cores ist zur Laufzeit nicht pin-genau zuzuordnen (nach LTO fehlen meist die Symbole); die statische Analyse findet ihn. | gering | [io-usage.ts](../packages/avr-emulator/src/io/io-usage.ts), ADR 0009 E7 | DEFERRED | Zuordnung über `lpm`-Zugriffe auf die Pin-Tabelle kurz vor dem PORT-Zugriff? | +| EMU-EEPROM | Der EEPROM-Inhalt bleibt je Board nur, solange der Worker des Tabs lebt; Neuladen der Seite löscht ihn. | gering | [host.ts:39](../packages/avr-emulator/src/worker/host.ts#L39) | DEFERRED | EEPROM im Browser speichern (localStorage je Board)? | +| EMU-TIMEOUT | Die Laufzeit ist im Browser wie im Host-Mock auf 1–300 s begrenzt, obwohl ein Browser-Lauf keine Server-Ressource belegt. | gering | [messages.ts:14](../packages/avr-emulator/src/protocol/messages.ts#L14), ADR 0009 E8 | DEFERRED | Längere oder unbegrenzte Läufe im Browser, mit welcher Bedienung? | +| EMU-WIRING | Keine Verdrahtung und keine externen Bauteile; Eingaben nur über Pins und Regler der Boardansicht. | gering | ADR 0009 E2 | DEFERRED | Eigene Entscheidung über Bauteile, falls der Unterricht sie braucht | +| EMU-BOARDS | Weitere Boards (ATtiny84, Raspberry Pi Pico mit rp2040js) sind nicht geplant. | gering | [shared/boards.ts](../shared/boards.ts) | DEFERRED | Bedarf aus dem Unterricht | | TUTOR-PROVIDER | Der Tutor funktioniert nur mit KI:connect.nrw und persönlichen Keys; das beschränkt ihn praktisch auf Schulen und Hochschulen in NRW. | gering | [config.ts:667-670](../server/config.ts#L667-L670), [tutor.routes.ts:129](../server/routes/tutor.routes.ts#L129), [Issue #243](https://github.com/MoDevIO/UnoSim/issues/243) | BLOCKED_DECISION | Credential-Modell (nur persönliche Keys oder auch Server-Key) und Provider-Abstraktion festlegen | ## Wartung @@ -40,4 +49,6 @@ Statuswerte: `BLOCKED_DECISION` (wartet auf eine Entscheidung) · `DEFERRED` | ID | Befund | Risiko | Beleg | Status | Nötige Entscheidung | |---|---|---|---|---|---| | A2-DEAD | Die Cache-Lock-API (`acquireCacheLock`) und `drain()` im Gatekeeper werden im Produktionscode nicht genutzt; 48 Testreferenzen hängen daran. | gering | [unified-gatekeeper.ts:259](../server/services/unified-gatekeeper.ts#L259), [unified-gatekeeper.ts:502](../server/services/unified-gatekeeper.ts#L502) | DEFERRED | Entfernen samt Tests (Teständerung braucht Freigabe) | +| AVR8JS-PATCHES | Das Paket korrigiert sechs Abweichungen von avr8js 0.21.1 im eigenen Code (RXC beim Schreiben von UCSR0B, REFS2 und GIMSK/GIFR sowie Timer-1-PWM des ATtiny85, Mega-Vektoren und Pin-Change-Verdrahtung). Ein Update von avr8js kann sie brechen oder überflüssig machen; die Meldungen an avr8js stehen aus. | gering | [README des Pakets](../packages/avr-emulator/README.md#avr8js-0211-corrections-and-known-differences) | OPEN | Meldungen an avr8js (Entwürfe in [human-tests.md](proposals/human-tests.md)); vor jedem Update die Korrekturen gegen die Tests prüfen | +| EDITOR-INDENT | Der Editor rückt `case`-Marken, `else` zu einem verschachtelten `if` ohne Klammern und, bei „Format Code“, klammerlose Anweisungen nicht wie erwartet ein; die Einrückungsregeln von Monaco können diese Fälle nicht unterscheiden. | gering | [arduino-language-configuration.ts](../client/src/lib/editor/arduino-language-configuration.ts), [format-code.ts](../client/src/lib/editor/format-code.ts) | DEFERRED | Eigener Formatter (z. B. clang-format im Browser) oder so lassen | | R10-SSOTREF | Die SSOT TutorQualityEvaluation verweist auf `ssot_function_tutor_model_registration.md`, die nicht im Repository liegt. | gering | [TutorQualityEvaluation:23](../ssot/ssot_function_definition_TutorQualityEvaluation.md) | DEFERRED | Datei versionieren oder Verweis entfernen (Owner) | diff --git a/docs/README.md b/docs/README.md index 08ee44806..7714a7a3d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,7 +55,14 @@ eigenen Reverse Proxy gilt [INSTALL_SERVER.md](INSTALL_SERVER.md). [ADR 0009](adr/0009-browser-emulation-runtime.md) (Browser-Emulation als Laufzeit) und [ADR 0010](adr/0010-compile-sandbox-and-flash-artifact.md) (Compile-Sandbox und Flash-Abbild); sie gelten erst nach ihrer Annahme. -- [../ssot/](../ssot/) – fachliche Single Sources of Truth für UI und Verhalten. +- [../ssot/](../ssot/) – fachliche Single Sources of Truth für UI und Verhalten, + für die Browser-Emulation vor allem + [Boards](../ssot/ssot_function_definition_Boards.md) (Katalog, Pinbelegung, + Freigabestatus) und + [SimulationRuntime](../ssot/ssot_function_definition_SimulationRuntime.md) + (Laufzeit-Schnittstelle, Worker-Protokoll, Zeit- und Speichermodell). +- [../packages/avr-emulator/README.md](../packages/avr-emulator/README.md) – + API, Boards, Korrekturen an avr8js und Grenzen des Emulationskerns. Bei Änderungen zuerst die zuständige normative Quelle aktualisieren. @@ -70,7 +77,8 @@ Bei Änderungen zuerst die zuständige normative Quelle aktualisieren. Umsetzungsplan Browser-Emulation, mit [Vorschlag](proposals/browser-emulation.md) und [Umsetzungsstand](proposals/browser-emulation-status.md); wird nach Abschluss - gelöscht. + gelöscht. Was nur ein Mensch erledigen kann, steht mit Anleitung in + [proposals/human-tests.md](proposals/human-tests.md). ## Archiv diff --git a/docs/RELEASE_RUNBOOK.md b/docs/RELEASE_RUNBOOK.md index f5f1db49b..92707d551 100644 --- a/docs/RELEASE_RUNBOOK.md +++ b/docs/RELEASE_RUNBOOK.md @@ -1,8 +1,8 @@ # Release-Runbook Status: current -Version: 1.0.0 -Datum: 2026-09-06 +Version: 1.1.0 +Datum: 2026-10-24 Grundlage: aktuelle Repository-Gates, `TESTING_STANDARDS.md` und `INSTALL_SERVER.md`. --- @@ -331,6 +331,55 @@ git push origin vX.Y.Z --- +## Vorabversionen und v2.0.0 (Browser-Emulation) + +V2 entsteht im Integrationsbranch `feature/browser-emulation` +([Plan, Abschnitt 9](proposals/browser-emulation-plan.md#9-rollout-und-rückfall)). +Bis zum Abschluss gilt: + +| Version | Branch | Voraussetzung | Einsatz | +| --- | --- | --- | --- | +| `v1.0.x` | Arbeitsbranch → `main` | Pflichtgates | Unterricht, Referenz und Rückkehr | +| `v2.0.0-beta.N` | `feature/browser-emulation` | M1, Compile-Sandbox (P2.4), Deployment (P7.2), Pflichtgates | Pilot-Instanz neben der V1-Instanz, Browser-Emulation Standard (`UNOSIM_DEFAULT_RUNTIME=browser`), „Legacy“ wählbar | +| `v2.0.0-rc.N` | `feature/browser-emulation` | alle Abnahmekriterien unten, Funktionsstopp im Integrationsbranch | letzte Erprobung im Unterricht, nur Fehlerbehebungen | +| `v2.0.0` | Merge-Commit `feature/browser-emulation` → `main` | ein Release-Kandidat ohne neue Befunde über mindestens zwei Wochen Unterricht | Standardbetrieb | + +**Ablauf einer Vorabversion** (Release Manager): + +1. Im Integrationsbranch per PR `package.json`/`package-lock.json` + (`npm version --no-git-tag-version`) und `CHANGELOG.md` auf die + Version setzen; der CHANGELOG nennt Verhaltensänderungen ausdrücklich. +2. Pflichtgates auf dem Merge-Commit grün (CI und `./run-tests.sh`). +3. Annotierten Tag auf diesen Commit setzen und pushen: + `git tag -a v2.0.0-beta.1 -m "UnoSim v2.0.0-beta.1" && git push origin v2.0.0-beta.1`. +4. Images aus demselben Commit bauen (`unosim-server`, `unosim-sandbox`, + `unosim-compile-sandbox`) und die Pilot-Instanz nach + [INSTALL_SERVER.md](INSTALL_SERVER.md) mit `UNOSIM_DEFAULT_RUNTIME=browser` + ausrollen; die V1-Instanz bleibt unverändert. +5. Befunde aus dem Unterricht als Issues; Behebungen als PRs in den + Integrationsbranch. + +**Abnahmekriterien für `v2.0.0`:** + +- Korpus: alle 51 Beispiele mit Referenzen der Klasse `hardware`, Ausnahmen + ausgewiesen; Proben aller freigegebenen Boards grün. +- Browserbedienung: E2E grün; alle Funktionen im Pilot bestätigt; Rückfall auf + „Legacy“ funktioniert. +- Compile-Sandbox: Vertragstest (Docker-Gate) grün, Artefakt nur mit Sandbox, + Build-Identität getestet. +- Leistung: Echtzeitfaktor auf den Referenzgeräten (P0.3), Compile-Latenz und + Kapazität auf dem Referenz-Host (P7.1) im Rahmen. +- Release-Gates: alle Pflichtgates und die Opt-in-Gates vor Major-Releases. +- ADR 0009 und 0010 sind angenommen; die Standardwerte des Docker-Profils + stehen auf `browser`. + +**Nach `v2.0.0`:** V1-Fehlerbehebungen nur noch in `release/v1.x` ab dem +letzten V1-Tag, ein Semester lang und nur für sicherheits- oder +unterrichtskritische Fehler. `main` wird bis dahin mindestens alle zwei Wochen +per Merge (nie per Rebase) in den Integrationsbranch übernommen. + +--- + ## Notfallplan ### Bei Gate-Fehlschlag @@ -369,6 +418,7 @@ Rückfallebene](ARCHITECTURE.md#host-mock-als-rückfallebene). | Version | Datum | Änderungen | | --- | --- | --- | | 1.0.0 | 2026-09-06 | Initiale normative Runbook-Version; der aktuelle Implementierungsstand ist in den verlinkten Architektur-, Security- und Skalierungsdokumenten beschrieben. | +| 1.1.0 | 2026-10-24 | Vorabversionen aus dem Integrationsbranch, Abnahmekriterien für v2.0.0, V1-Pflege über `release/v1.x` (Plan Browser-Emulation, P7.3). | --- diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 3f2b3e3d2..4d340ce78 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -12,7 +12,8 @@ gateway is optional. - `NODE_ENV=development`, `UNOSIM_SERVER_MODE=local` - loopback listener by default - local signed session cookie; no external identity provider -- compilation and simulation run as local processes +- compilation runs as a local process; sketches run in the browser emulation + (see "Browser runtime boundary"), the host mock as a local process - intended for one trusted developer on one machine Local development is not approved for shared, LAN or public access. @@ -31,7 +32,9 @@ exposes local process execution to clients that can reach the listener. - `NODE_ENV=production`, `UNOSIM_SERVER_MODE=docker` - server runs in Docker -- every simulation runs in a separate Docker sandbox +- every compile runs in a separate compile sandbox (see "Compile boundary") +- every host-mock simulation runs in a separate Docker sandbox; browser runs + need no server sandbox - a trusted gateway and valid gateway identity are mandatory; authenticating a person before the gateway is optional - Docker availability and the sandbox image are startup/readiness requirements @@ -73,9 +76,35 @@ ADR 0001 records the original gateway contract; [ADR for user authentication while retaining gateway identity, header and origin requirements. +## Browser runtime boundary + +With the browser backend ([ADR 0009](adr/0009-browser-emulation-runtime.md), +`Proposed`) a sketch runs in the user's browser: avr8js interprets the AVR +machine code of the flash image in a Web Worker. Sketch code is never executed +as JavaScript and reaches neither the DOM, the network nor the page's storage; +its effects are the emulated pins, serial ports and EEPROM of the tab. A +sketch that loops or floods output costs only that tab, no server resource. + +Bounds, enforced in the worker and the main thread: + +- the worker loads only a flash image that fits the board's flash (at most + 256 KiB) and whose SHA-256 matches; symbols are checked against the flash + size; +- every command and event carries a run identifier; events of other runs are + dropped; +- a run ends after its run time of 1–300 s without pauses; pending serial + output and findings are bounded and drops are counted, serial input is + limited to 4 KiB per message and 1 MiB pending; +- the main thread terminates a worker that misses its heartbeat (3 s) or a + confirmation deadline in the visible tab, or reports `error`/`messageerror`. + +The artifact is derived from the user's own compile; the server never sends +the HEX file. The worker module is part of the built client, loaded from the +same origin. + ## Sandbox boundary -Docker simulations use a dedicated non-root container with: +Host-mock simulations in Docker use a dedicated non-root container with: - network disabled, - read-only root filesystem, @@ -217,13 +246,15 @@ development startup reject the flag. ## Production checklist -1. Use only the Docker profile and reviewed immutable images. +1. Use only the Docker profile and reviewed immutable images (backend, + compile sandbox and, while the host mock is offered, simulation sandbox). 2. Keep the backend inaccessible except through the trusted gateway. 3. Configure TLS, gateway secret, trusted proxy and exact WebSocket origins. 4. Verify `/api/health` and `/api/readiness` before routing traffic. 5. Keep Docker sandbox resource limits, rate limits and admission enabled. -6. Run `npm run test:security:inputs`, `npm run test:docker`, the unit suite, - E2E suite, build and security audit before release. +6. Run `npm run test:security:inputs`, `npm run test:docker` (including the + compile sandbox contract), the unit suite, E2E suite, build and security + audit before release. Report security vulnerabilities privately to the instance operators before public disclosure. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 197a76519..057a6e35d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -182,7 +182,7 @@ Hardware. Danach läuft das fertige Programm in der **Emulation**: ein Nachbau des Mikrocontrollers (ATmega328P, beim Mega 2560 der ATmega2560, beim ATtiny85 der ATtiny85) im Browser, der den Maschinencode des Sketches ausführt, nicht ein Programm für den UnoSim-Server. Datentypen, Überläufe, `millis()` und `delay()` verhalten sich -deshalb wie auf dem Uno. Wer die frühere Art der Simulation braucht, wählt +deshalb wie auf dem gewählten Board. Wer die frühere Art der Simulation braucht, wählt unter **File → Settings → Simulation engine** „Legacy“ (siehe unten). Das gibt es nur für den Uno; bei Nano, Mega 2560 und ATtiny85 ist die Einstellung sichtbar, aber fest auf „Emulation“ gesetzt und nennt den Grund. Die Emulation ist ein Modell. Sie weicht in diesen Punkten vom echten Board ab: @@ -194,13 +194,13 @@ Die Emulation ist ein Modell. Sie weicht in diesen Punkten vom echten Board ab: | Pegel, den du am Board setzt | gilt, bis der Sketch den Pin auf `OUTPUT` schaltet | | Analoger Eingang | Der Regler ist eine Spannung von 0 bis 5 V in 1024 Stufen; mit der Standardreferenz liefert `analogRead()` genau den Reglerwert. Am Pin `AREF` (`EXTERNAL`) liegen 5 V, weil dort am Board nichts angeschlossen ist | | PWM | UnoSim zeigt das Tastverhältnis (0 bis 255), keine Signalform | -| Serial | Der Empfangspuffer hat 64 Byte wie am Uno (beim Mega je Schnittstelle); was der Sketch nicht abholt, geht verloren. Text, den du **vor** `Serial.begin()` sendest, geht verloren wie am echten Board; UnoSim meldet das im Serial Monitor mit einer eigenen Zeile. Gesendeter Text endet mit dem Zeilenende (Code 10), wie in der Arduino IDE mit „New Line“ | +| Serial | Der Empfangspuffer hat 64 Byte wie am Uno (beim Mega je Schnittstelle, beim ATtiny85 32 Byte wie im Core); was der Sketch nicht abholt, geht verloren. Text, den du **vor** `Serial.begin()` sendest, geht verloren wie am echten Board; UnoSim meldet das im Serial Monitor mit einer eigenen Zeile. Gesendeter Text endet mit dem Zeilenende (Code 10), wie in der Arduino IDE mit „New Line“ | | Zeit | exakt in simulierter Zeit. Ist dein Gerät zu langsam, laufen Uhr und Simulation langsamer als die Wanduhr; die Kopfzeile zeigt dann einen Echtzeitfaktor unter 1. Der Timeout zählt Wanduhrzeit: Bei Faktor 0,5 endet ein 30-Sekunden-Lauf nach etwa 15 simulierten Sekunden | | Pause | hält auch `millis()` an | | Überlauf mit `int` und `long` | In C++ ist der Überlauf vorzeichenbehafteter Typen undefiniert. UnoSim zeigt, was der Code mit dem Compiler des Uno auf dem Board tut, z. B. `32767 + 1` ergibt −32768. Das ist keine allgemeine C++-Garantie: Mit Optimierung kann derselbe Quelltext anders laufen, auf dem echten Uno genauso | | EEPROM | bleibt im Browser-Tab je Board erhalten, solange du die Seite nicht neu lädst; ein neuer Tab beginnt gelöscht | | I²C, SPI | Die Bausteine sind vorhanden, aber es hängt kein Gerät am Bus | -| Speicher | Größen und Adressen wie am Board (2 KB SRAM, beim Mega 8 KB). UnoSim meldet nur wenige Fälle, etwa einen Stack, der in die globalen Variablen oder den Heap hineinwächst, wenn die Prüfung ihn erfasst; ein Schreibzugriff über eine Arraygrenze fällt wie am echten Board nicht auf und zeigt sich nur in seinen Folgen | +| Speicher | Größen und Adressen wie am Board (2 KB SRAM, beim Mega 8 KB, beim ATtiny85 512 Byte). UnoSim meldet nur wenige Fälle, etwa einen Stack, der in die globalen Variablen oder den Heap hineinwächst, wenn die Prüfung ihn erfasst; ein Schreibzugriff über eine Arraygrenze fällt wie am echten Board nicht auf und zeigt sich nur in seinen Folgen | | `random()` | Die Zahlenfolge ist die des Uno (`randomSeed` und Startwert wie am Board) | Was das für deine Programme bedeutet: diff --git a/docs/proposals/human-tests.md b/docs/proposals/human-tests.md index 226bd7cf9..f727c8a57 100644 --- a/docs/proposals/human-tests.md +++ b/docs/proposals/human-tests.md @@ -1,9 +1,11 @@ # Messungen und Aufnahmen durch Menschen -> **Nicht normativ.** Anleitung für die Arbeitspakete, die eine Person mit -> Geräten durchführen muss (P0.3 und die Klasse `hardware` von P1.14) im Rahmen -> von [browser-emulation-plan.md](browser-emulation-plan.md). Wird mit dem Plan -> gelöscht. Ergebnisse gehen an den Agenten oder in die Statusdatei +> **Nicht normativ.** Anleitung für alles, was im Rahmen von +> [browser-emulation-plan.md](browser-emulation-plan.md) nur eine Person +> erledigen kann: Messungen und Aufnahmen mit Geräten (Teile 1–4), Meldungen an +> avr8js (Teil 5), das README von UnoSim-Examples (Teil 6) und die +> Vorabversion (Teil 7). Wird mit dem Plan gelöscht. Ergebnisse gehen an den +> Agenten oder in die Statusdatei > [browser-emulation-status.md](browser-emulation-status.md). Alle Befehle laufen im Hauptverzeichnis des Repositorys auf dem Branch @@ -194,3 +196,183 @@ und `nproc`/`free -g` in CAPACITY_MODEL.md unter „Compile capacity“ eintrage | Abbruch „requires a built application“ | `npm run build` fehlt | | Backend startet nicht, Meldung nennt `unosim-compile-sandbox` | `npm run build:sandbox` fehlt oder ist fehlgeschlagen | | Zeilen mit 503 | der Pool war länger als 30 s voll; Wert notieren, das ist ein Befund | + +## Teil 4 – Aufnahmen an Nano, Mega 2560 und ATtiny85 (Klasse `hardware`) + +**Ziel:** wie Teil 2, für die 33 Fälle der übrigen Boards (Nano 7, Mega 21, +ATtiny85 5; alle bisher `derived`). Werkzeug, Ablauf und Übernahme sind +dieselben wie in Teil 2; das Board steht im `case.json`, der Befehl ist +gleich. Die Aufnahme liest nur `Serial`. Deshalb übernimmt das Werkzeug diese +9 Fälle nicht, sie bleiben `derived`: + +- 8 Fälle mit Zustandsprüfungen (`expectState`): Nano `it03-06`, `it04-03`; + Mega `it03-04`, `it03-06`, `it04-03`, `mega_pins`, `mega_pwm_duty`; + ATtiny85 `attiny_pwm_duty`. +- `mega_serial_ports`: Er nutzt `Serial1`–`Serial3`, die Aufnahme liest nur + `Serial`. + +**Fälle mit schnellen Flanken** (`mega_interrupts`: alle 5 ms; +`attiny_interrupts`: alle 20 ms) sind von Hand nicht zu treffen. Sie brauchen +einen Pulsgeber, zum Beispiel ein zweites Arduino, das die Flanken aus +`events` im `case.json` erzeugt. Ohne Pulsgeber bleiben sie `derived`. + +### Nano und Mega 2560 + +Wie Teil 2, nur mit dem jeweiligen Board am USB. Ein Nano-Klon mit altem +Bootloader lädt nur mit `cpu=atmega328old`; dann den Fall **nicht** +übernehmen und dem Agenten melden (der Korpus nimmt den neuen Bootloader an). +Für die Analogfälle (`nano_adc_read`: A0, A5, A6, A7; `mega_adc_read`: A0, A7, +A8, A15) die Spannungen aus `events` mit Potentiometer oder Spannungsteiler +einstellen und messen, wie in Teil 2. + +```sh +npm run record:hardware -- tests/fixtures/runtime-corpus/probes/nano_adc_read --port /dev/cu.usbserial-1410 +npm run record:hardware -- tests/fixtures/runtime-corpus/probes/mega_memory_layout --port /dev/cu.usbmodem1101 +``` + +### ATtiny85 + +**Was du brauchst:** einen ATtiny85 (DIP-8) auf dem Steckbrett, einen +ISP-Programmer (USBasp oder ein Uno mit dem Sketch „ArduinoISP“), einen +USB-Seriell-Adapter mit 5 V und DTR-Pin, einen Kondensator 100 nF und einen +Widerstand 10 kΩ. + +**Aufbau:** + +| ATtiny85 | Verbindung | +|---|---| +| Pin 8 (VCC), Pin 4 (GND) | 5 V und GND des Adapters (gemeinsame Masse mit dem Programmer) | +| Pin 1 (PB5, RESET) | 10 kΩ nach 5 V; 100 nF zum DTR des Adapters (Reset beim Öffnen des Ports, wie beim Uno) | +| Pin 5 (PB0, TX) | RX des Adapters | +| Pin 6 (PB1, RX) | TX des Adapters | +| Pins 1, 5, 6, 7 (PB5, PB0, PB1, PB2) | zum Programmieren an RESET, MOSI, MISO, SCK des Programmers | + +**Einmal je Chip:** Ein neuer ATtiny85 läuft mit 1 MHz (Fuse CKDIV8). Die +Fuses für 8 MHz intern setzt: + +```sh +arduino-cli burn-bootloader -b "ATTinyCore:avr:attinyx5:chip=85,clock=8internal" -P usbasp -p usb +# mit Uno als ISP: -P arduinoasisp -p /dev/cu.usbmodem1101 +``` + +**Ein Fall:** Das Werkzeug lädt über den Programmer und nimmt am Adapter auf. +Bricht der Upload ab, stört der Adapter an PB0/PB1 (MOSI/MISO): seine +TX-Leitung zum Hochladen kurz abziehen, dann mit `--attempts 1` und ohne +Upload (`--no-upload`) aufnehmen. + +```sh +npm run record:hardware -- tests/fixtures/runtime-corpus/probes/attiny_basics --port /dev/cu.usbserial-A10K --programmer usbasp --upload-port usb +``` + +Der interne Oszillator weicht ab Werk um bis zu ±10 % ab. Zeitwerte in +`attiny_basics` können dadurch außerhalb der Toleranz liegen, die Baudrate +ebenso (unlesbare Zeichen). Dann den Fall nicht übernehmen und dem Agenten +die Aufnahme geben, mit dem Hinweis auf den Takt; OSCCAL-Kalibrierung ist ein +eigener Schritt. + +`attiny_adc_read` erwartet 2,51 V an A1 (Pin 7), 5 V an A2 (Pin 3) und 0,5 V +an A3 (Pin 2). `attiny_serial_echo` sendet der Adapter selbst. + +## Teil 5 – Meldungen an avr8js (rund 30 Minuten) + +UnoSim korrigiert vier Fehler von avr8js 0.21.1 im eigenen Code +([README des Pakets](../../packages/avr-emulator/README.md#avr8js-0211-corrections-and-known-differences)). +Wer sie meldet, hilft anderen Nutzern und macht ein späteres Update leichter. +Vorher in [wokwi/avr8js/issues](https://github.com/wokwi/avr8js/issues) nach +einem passenden Issue suchen. Am 2026-10-24 gab es keins; #136 (ADC im +Free-Running-Modus) und #178 (Timer 0 und `delay()` mit ATTinyCore) betreffen +andere Fehler. Je Fehler ein Issue, Texte zum Kopieren: + +**1. USART: writing UCSRnB clears a pending RXC** + +> avr8js 0.21.1 clears the RXC flag on every write to UCSRnB while RXEN stays +> set. On the ATmega328P only reading UDRn clears RXC (datasheet, UCSRnA). The +> Arduino core writes UCSR0B in every `Serial.write` (to enable UDRIE), so a +> byte that arrives while the sketch prints loses its interrupt, and the next +> byte overwrites it. Reproduce: a sketch that echoes input while printing +> continuously; send two bytes in quick succession; one is lost. + +**2. ATtiny85 ADC: REFS2 is read at bit 3 instead of bit 4** + +> On the ATtiny25/45/85, ADMUX has REFS1:0 at bits 7:6 and REFS2 at bit 4; +> bit 3 is MUX3 (datasheet, ADMUX). avr8js reads REFS2 at bit 3, so +> `analogReference(INTERNAL2V56)` (ATTinyCore) measures against VCC: 0.5 V on +> A3 reads 102 instead of 200. + +**3. ATtiny85: INT0 and the pin-change interrupt share GIMSK and GIFR** + +> GIMSK holds INT0 (bit 6) and PCIE (bit 5), GIFR holds INTF0 (bit 6) and PCIF +> (bit 5). avr8js hooks GIMSK only for the external interrupt and GIFR only for +> the pin-change interrupt. Setting PCIE does not raise a pending pin change, +> and writing 1 to INTF0 does not clear a pending INT0. + +**4. ATtinyTimer1: PWM mode counts phase-correct and drives no OC1x with CTC1** + +> In PWM mode (PWM1A or PWM1B) timer 1 of the ATtiny85 counts up from 0 to +> OCR1C and restarts at 0, with or without CTC1 (datasheet, "Timer/Counter1 in +> PWM Mode"). OC1x is set +> at 0 and cleared on a match with OCR1x (COM1x = 01/10), inverted with 11. +> `ATtinyTimer1` counts phase-correct when CTC1 is clear, and with CTC1 set (as +> ATTinyCore configures `analogWrite` on PB1/PB4) it drives no waveform at all: +> `updateCompPinNonPwm` is skipped because PWM is on, and `phasePwmCount` is +> not used because CTC1 is on. + +Die Issue-Links in die Statusdatei eintragen (Zeile P6.2) oder dem Agenten geben. + +## Teil 6 – README von UnoSim-Examples (rund 15 Minuten) + +UnoSim liest seit P4.6 das optionale Manifestfeld `board` +([ExternalExamples-SSOT 7.1](../../ssot/ssot_function_definition_ExternalExamples.md)). +Das README des Beispiel-Repositorys nennt es noch nicht und beschreibt +außerdem nur die frühere Simulation. Vorschlag für einen PR in +`ttbombadil/unosim-examples`: + +1. Nach dem Absatz über Headerdateien einfügen: + + > Every example may name its board in `manifest.json` with the optional + > field `board`: `uno`, `nano`, `mega2560` or `attiny85`. Without it an + > example fits every board. UnoSim versions that do not know an ID treat + > it as not given; from version 2 on, UnoSim asks before it loads an + > example for another board than the selected one. The examples of this + > catalog are written for the Uno and carry no `board`. + +2. Den Absatz „UnoSim does not run the sketch as Uno machine code …“ ersetzen + durch: + + > UnoSim 1.x runs a sketch as a program on the server, so a few + > observations differ from the hardware: `int` is 32 bits wide, `double` + > 64 bits, and a float printed without a precision shows `2.5` instead of + > `2.50`. UnoSim 2 emulates the board's microcontroller and matches the + > hardware here ("Legacy" keeps the former behaviour). The examples avoid + > these cases (overflow is shown with `byte`, floats are always printed + > with a precision), so they behave the same in both versions. Keep it + > that way in new examples; the UnoSim user guide lists all differences in + > its section "Grenzen der Simulation". + +Danach die Zeile P4.6 in der Statusdatei auf „erledigt“ setzen. + +## Teil 7 – Vorabversion `v2.0.0-beta.1` + +Die technischen Voraussetzungen erfüllt der Integrationsbranch (M1, P2.4, +P7.2; Plan 9.2). Ablauf und Abnahmekriterien stehen im +[Release-Runbook](../RELEASE_RUNBOOK.md#vorabversionen-und-v200-browser-emulation). +Ein Mensch entscheidet: + +1. **Ob und wann:** Zeitpunkt der Pilot-Instanz (Semesterplanung). +2. **Version und CHANGELOG:** Der Abschnitt „Unreleased“ im CHANGELOG ist + vollständig. Für die Beta wird er zu `## v2.0.0-beta.1 – ` und die + Version gesetzt: `npm version 2.0.0-beta.1 --no-git-tag-version`. Ein + vorbereiteter PR macht genau das. +3. **Tag nach dem Merge** dieses PRs und grüner CI: + + ```sh + git fetch origin && git checkout feature/browser-emulation && git pull --ff-only + git tag -a v2.0.0-beta.1 -m "UnoSim v2.0.0-beta.1" && git push origin v2.0.0-beta.1 + ``` + +4. **Pilot-Instanz:** Images aus diesem Tag bauen, nach + [INSTALL_SERVER.md, Pilot](../INSTALL_SERVER.md#browser-emulation-im-docker-profil-pilot) + mit `UNOSIM_DEFAULT_RUNTIME=browser` neben der V1-Instanz ausrollen. Vor + dem Unterricht: Teil 3 (Kapazität) auf dem Pilot-Host. +5. **Rückmeldungen** aus dem Unterricht als Issues; der Agent behebt sie in + PRs gegen den Integrationsbranch. diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 2bffbda79..f7d3af3f7 100644 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { access, readFile, readdir } from "node:fs/promises"; import { join, resolve } from "node:path"; @@ -14,7 +15,20 @@ async function collect(dir) { } } -await collect(root); +/** Markdown files Git tracks or would track: ignored drafts (`local-temp/`, reports) are not documentation. */ +function gitMarkdownFiles() { + try { + // The git of the maintainer's PATH, as in the other repository scripts. + const out = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "*.md"], { cwd: root, encoding: "utf8" }); // NOSONAR S4036 + return out.split("\0").filter(Boolean).map((file) => join(root, file)); + } catch { + return undefined; // no Git checkout: walk the tree + } +} + +const tracked = gitMarkdownFiles(); +if (tracked) markdownFiles.push(...tracked.filter((file) => !file.includes("/node_modules/"))); +else await collect(root); const failures = []; for (const file of markdownFiles) { const content = await readFile(file, "utf8"); diff --git a/scripts/record-hardware-lib.ts b/scripts/record-hardware-lib.ts index 90d27ed18..251befa84 100644 --- a/scripts/record-hardware-lib.ts +++ b/scripts/record-hardware-lib.ts @@ -1,6 +1,11 @@ // Pure parts of the hardware recording tool (scripts/record-hardware.ts): no I/O, tested in tests/server. import type { StimulusEvent } from "../packages/avr-emulator/src/index"; +/** Toolchain a recording of a case of this board was built with, as `case.json` names it. */ +export function recordingToolchain(board: string): string { + return board === "attiny85" ? "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)" : "arduino-cli 1.5.1, arduino:avr@1.8.8"; +} + /** Baud rate of `Serial.begin(n)` in the sketch; undefined if it never opens the port. */ export function baudOf(sketch: string): number | undefined { const match = /Serial\.begin\(\s*(\d+)\s*\)/.exec(sketch.replaceAll(/\/\/.*|\/\*[\s\S]*?\*\//g, "")); diff --git a/scripts/record-hardware.ts b/scripts/record-hardware.ts index 1e62ea0d5..d7bcfe6c6 100644 --- a/scripts/record-hardware.ts +++ b/scripts/record-hardware.ts @@ -1,6 +1,7 @@ // Records a corpus case on a real board (plan 7.4, class `hardware`): // npm run record:hardware -- tests/fixtures/runtime-corpus/probes/data_types --port /dev/cu.usbmodem1101 // ... --accept "Arduino Uno R3 (genuine)" # turn a matching recording into the expectation +// ... --programmer usbasp --upload-port usb # ATtiny85: upload through an ISP programmer, record on --port // Uploads the sketch, resets the board, replays the stimuli (serial text automatically, pins and voltages // are announced for the person to set) and writes recorded..txt next to the case. import { spawn, spawnSync } from "node:child_process"; @@ -9,20 +10,23 @@ import { tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; import { CORPUS_FQBN } from "./runtime-fixtures"; import { diffOutput, type Tolerance } from "../packages/avr-emulator/tests/corpus-helpers"; -import { acceptedCase, baudOf, lineDiff, normalizeRecording, stepsOf, stripLeadingNoise } from "./record-hardware-lib"; +import { acceptedCase, baudOf, lineDiff, normalizeRecording, recordingToolchain, stepsOf, stripLeadingNoise } from "./record-hardware-lib"; -const TOOLCHAIN = "arduino-cli 1.5.1, arduino:avr@1.8.8"; const args = process.argv.slice(2); const option = (name: string): string | undefined => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined); const caseDir = resolve(args.find((arg) => !arg.startsWith("--")) ?? ""); const port = option("--port"); const bootMs = Number(option("--boot-ms") ?? 3300); const accept = option("--accept"); +// Boards without a USB bootloader (ATtiny85) are programmed through an ISP programmer at another port than the +// serial adapter the recording reads. +const programmer = option("--programmer"); if (!existsSync(join(caseDir, "case.json")) || !port) { - console.error('usage: npm run record:hardware -- --port [--boot-ms 3300] [--accept ""] [--attempts N] [--no-upload]'); + console.error('usage: npm run record:hardware -- --port [--boot-ms 3300] [--accept ""] [--attempts N] [--no-upload] [--programmer --upload-port ]'); process.exit(2); } +const uploadPort = option("--upload-port") ?? port; const caseFile = JSON.parse(readFileSync(join(caseDir, "case.json"), "utf8")) as { board: string; simMs: number; events?: never[]; expectState?: unknown[]; tolerances?: Tolerance[] } & Record; const id = basename(caseDir); const fqbn = CORPUS_FQBN[caseFile.board]; @@ -31,14 +35,15 @@ const baud = baudOf(sketch); // Every attempt starts from a fresh upload: closing the port does not reset the board, so a free-running sketch // would otherwise hand the next recording the middle of its previous run. -function uploadSketch(portName: string): void { +function uploadSketch(): void { if (args.includes("--no-upload")) return; const work = mkdtempSync(join(tmpdir(), "unosim-record-")); try { cpSync(caseDir, join(work, id), { recursive: true, filter: (source) => !/(case\.json|expected\..*\.txt|recorded\..*\.txt)$/.test(source) }); - console.log(`uploading ${id} to ${portName} …`); + console.log(`uploading ${id} to ${uploadPort} …`); // Maintainer tool: uses the arduino-cli the maintainer installed on the PATH. - const upload = spawnSync("arduino-cli", ["compile", "--upload", "--fqbn", fqbn, "--port", portName, join(work, id)], { stdio: "inherit" }); // NOSONAR S4036 + const programmerArgs = programmer ? ["--programmer", programmer] : []; + const upload = spawnSync("arduino-cli", ["compile", "--upload", "--fqbn", fqbn, "--port", uploadPort, ...programmerArgs, join(work, id)], { stdio: "inherit" }); // NOSONAR S4036 if (upload.status !== 0) process.exit(1); } finally { rmSync(work, { recursive: true, force: true }); @@ -80,7 +85,7 @@ const attempts = Number(option("--attempts") ?? (steps.some((step) => step.instr let text = ""; let differences: string[] = []; for (let attempt = 1; attempt <= attempts; attempt++) { - uploadSketch(port); + uploadSketch(); console.log("starting in 3 s …"); await new Promise((resolveStart) => setTimeout(resolveStart, 3000)); text = await recordOnce(); @@ -107,6 +112,6 @@ if (accept && differences.length > 0) { console.log("not accepted: this case also checks pin, PWM or ADC states (expectState), which a serial recording does not cover; the case stays as it is"); } else if (accept) { writeFileSync(expectedPath, text); // the measured values; the tolerances of the case stay as they are - writeFileSync(join(caseDir, "case.json"), `${JSON.stringify(acceptedCase(caseFile, accept, TOOLCHAIN), null, 2)}\n`); + writeFileSync(join(caseDir, "case.json"), `${JSON.stringify(acceptedCase(caseFile, accept, recordingToolchain(caseFile.board)), null, 2)}\n`); console.log(`accepted: expected.${caseFile.board}.txt holds the recording, case.json set to class hardware (${accept})`); } diff --git a/ssot/ssot_agent_policy.md b/ssot/ssot_agent_policy.md index 13cb2bc3e..2508045b4 100644 --- a/ssot/ssot_agent_policy.md +++ b/ssot/ssot_agent_policy.md @@ -15,6 +15,7 @@ Ziel-SSOT für projektspezifische Agentenarbeit; übergeordnete System-, Tool- u - Pfad: Working-Branch -> dev -> main. - Vor Merge in dev: dev in Working-Branch ziehen und Konflikte lokal lösen. - Repo-Klarstellung: Dieses Repository hat derzeit keinen `dev`-Branch. Für die Tutor-Quality-Arbeitsserie ist `main` die Base. +- Browser-Emulation (V2): Base ist der Integrationsbranch `feature/browser-emulation`. Pfad: Working-Branch → PR nach `feature/browser-emulation` → nach der Abnahme ein Merge-Commit nach `main` (Tag `v2.0.0`). Den Integrationsbranch nie rebasen; `main` wird per Merge-PR übernommen. Mergen darf nur ein Mensch. Ablauf und Vorabversionen: `docs/RELEASE_RUNBOOK.md`, Abschnitt „Vorabversionen und v2.0.0“. 2.2 Test-Integrität: diff --git a/ssot/ssot_function_definition_SerialOutput.md b/ssot/ssot_function_definition_SerialOutput.md index 7823c087d..2dd840412 100644 --- a/ssot/ssot_function_definition_SerialOutput.md +++ b/ssot/ssot_function_definition_SerialOutput.md @@ -36,7 +36,7 @@ Der Simulator bildet das Verhalten der Arduino-`Print`-Bibliothek ab. Die Konver ## 3. Transport-Protokoll -> **Browser-Emulation:** Hier gibt es weder `SERIAL_EVENT` noch stderr. Der Emulator liefert die Bytes des USART; der Worker dekodiert sie als UTF-8 (je Port, auch über Paketgrenzen), bündelt sie mit höchstens 60 Paketen je Sekunde und sendet sie als Ereignis `serial`. `BrowserRuntime` macht daraus dieselben `serial_output`-Nachrichten wie der Server (Zeilen enden mit LF, CRLF wird zu LF, `isComplete` sagt, ob der Text eine Zeile beendet). Eingabe vor `Serial.begin()` geht verloren wie am Board und erscheint als Systemzeile. Details: SSOT SimulationRuntime. +> **Browser-Emulation:** Hier gibt es weder `SERIAL_EVENT` noch stderr. Der Emulator liefert die Bytes des USART (beim ATtiny85 die des Software-UART an PB0, Baudrate aus den Flanken erkannt); der Worker dekodiert sie als UTF-8 (je Port, auch über Paketgrenzen), bündelt sie mit höchstens 60 Paketen je Sekunde und sendet sie als Ereignis `serial`. `BrowserRuntime` macht daraus dieselben `serial_output`-Nachrichten wie der Server (Zeilen enden mit LF, CRLF wird zu LF, `isComplete` sagt, ob der Text eine Zeile beendet). Eingabe vor `Serial.begin()` geht verloren wie am Board und erscheint als Systemzeile. Details: SSOT SimulationRuntime. Alle Ausgaben (print, println, write) werden über **stderr** als base64-kodierte `SERIAL_EVENT`-Nachrichten gesendet: ``` [[SERIAL_EVENT::]] diff --git a/tests/server/record-hardware-lib.test.ts b/tests/server/record-hardware-lib.test.ts index 931b546e8..25330861e 100644 --- a/tests/server/record-hardware-lib.test.ts +++ b/tests/server/record-hardware-lib.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { acceptedCase, baudOf, lineDiff, normalizeRecording, stepsOf, stripLeadingNoise } from "../../scripts/record-hardware-lib"; +import { acceptedCase, baudOf, lineDiff, normalizeRecording, recordingToolchain, stepsOf, stripLeadingNoise } from "../../scripts/record-hardware-lib"; describe("hardware recording tool (plan 7.4)", () => { it("finds the baud rate of the sketch, ignoring comments", () => { @@ -41,6 +41,12 @@ describe("hardware recording tool (plan 7.4)", () => { expect(result.basis).toBe("Recorded once on Arduino Uno R3 (serial output, arduino-cli 1.5.1, arduino:avr@1.8.8). Previous basis: datasheet"); }); + it("names the toolchain of the board a recording was built with", () => { + expect(recordingToolchain("uno")).toBe("arduino-cli 1.5.1, arduino:avr@1.8.8"); + expect(recordingToolchain("mega2560")).toBe("arduino-cli 1.5.1, arduino:avr@1.8.8"); + expect(recordingToolchain("attiny85")).toBe("arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)"); + }); + describe("bridge noise ahead of the first line", () => { const matches = (line: string): boolean => line === "bool 1";