From 7e0334dd5a9ae1726e18d7d37d022acd3cd3b977 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 18 Feb 2026 23:39:36 -0500 Subject: [PATCH 01/25] docs: add implementation plan for rp2040js simulator mode Detailed plan to integrate Wokwi's rp2040js emulator as a built-in simulator device. Covers device entry, compilation pipeline, Modbus RTU bridge over virtual UART, debugger integration, and UI changes. Co-Authored-By: Claude Opus 4.6 --- docs/simulator-rp2040-plan.md | 577 ++++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 docs/simulator-rp2040-plan.md diff --git a/docs/simulator-rp2040-plan.md b/docs/simulator-rp2040-plan.md new file mode 100644 index 000000000..6cdc9d117 --- /dev/null +++ b/docs/simulator-rp2040-plan.md @@ -0,0 +1,577 @@ +# Simulator Mode: rp2040js Integration Plan + +## Overview + +Add a built-in simulator to OpenPLC Editor using [Wokwi's rp2040js](https://github.com/wokwi/rp2040js) (MIT, zero dependencies, ~200KB). The simulator appears as a device in the board list. When selected, "Build" compiles for RP2040, loads the UF2 firmware into the emulated CPU, and starts execution. "Debugger" connects via Modbus RTU over the emulated UART — identical to real hardware. + +The user never sees any emulation details. They press Build, the code compiles and runs. They press Debugger, values appear. + +--- + +## 1. New Device Entry in hals.json + +**File:** `resources/sources/boards/hals.json` + +Add a new entry at the **top** of the JSON (so it appears first in the device dropdown): + +```json +{ + "Simulator": { + "compiler": "simulator", + "core": "rp2040:rp2040", + "board_manager_url": "https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json", + "c_flags": ["-MMD", "-c", "-Wno-incompatible-pointer-types"], + "extra_libraries": [], + "platform": "rp2040:rp2040:rpipico", + "source": "rp2040pico.cpp", + "preview": "simulator.png", + "specs": { + "CPU": "Emulated RP2040 ARM Cortex-M0+", + "RAM": "264 KB", + "Flash": "2 MB", + "Note": "Built-in simulator - no hardware required" + } + }, + ...existing entries... +} +``` + +Key decisions: +- **`"compiler": "simulator"`** — new compiler type, distinct from `"arduino-cli"` and `"openplc-compiler"`. This is the discriminator used throughout the codebase to determine behavior. +- Reuses the real Raspberry Pico `core`, `platform`, and `source` (rp2040pico.cpp) because compilation is identical. +- Pin mapping defaults match the Raspberry Pico HAL (DI: 6-13, DO: 14-21, AI: 26-28, AO: 4-5). + +--- + +## 2. Device Type Utility Functions + +**File:** `src/utils/device.ts` + +Add a new utility function alongside the existing `isArduinoTarget` and `isOpenPLCRuntimeTarget`: + +```typescript +export function isSimulatorTarget(boardInfo: AvailableBoardInfo | undefined): boolean { + if (!boardInfo) return false + return boardInfo.compiler === 'simulator' +} +``` + +This function will be used across the UI to conditionally hide/show configuration fields. + +--- + +## 3. Default Device for New Projects + +**File:** `src/renderer/store/slices/device/data/constants.ts` + +Change the default device board from `'OpenPLC Runtime v3'` to `'Simulator'`: + +```typescript +defaultDeviceConfiguration: DeviceConfiguration = { + deviceBoard: 'Simulator', // was 'OpenPLC Runtime v3' + ...rest unchanged... +} +``` + +This ensures every new project starts with the simulator selected, so users can build and debug immediately. + +--- + +## 4. Device Editor UI — Hide Configuration for Simulator + +**File:** `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` + +The board.tsx component currently branches on `isOpenPLCRuntimeTarget(currentBoardInfo)`: +- If runtime target → shows IP address field, Connect button, scan cycle stats +- If arduino target → shows Communication Port dropdown, board specs, Pin Mapping table + +For the simulator, **none of these should appear**. The board panel should show: +- The Device dropdown (so users can switch away from simulator) +- The board preview image (`simulator.png`) +- A simple message like "Built-in simulator — no configuration required" +- No communication port selector +- No IP address field +- No Connect button +- No pin mapping table (pins are fixed by the HAL) +- No Compile Only checkbox + +Implementation: Add an `isSimulatorTarget(currentBoardInfo)` check at the top of the render logic: + +``` +Line 361: {isOpenPLCRuntimeTarget(currentBoardInfo) ? ( +``` + +Change to a three-way branch: + +``` +{isSimulatorTarget(currentBoardInfo) ? ( + // Simple component showing "Built-in simulator" message +) : isOpenPLCRuntimeTarget(currentBoardInfo) ? ( + ... existing runtime UI ... +) : ( + ... existing arduino UI ... +)} +``` + +Similarly, the section after `
` (line 488) that shows either scan-cycle stats or pin-mapping should show nothing (or a brief description) for the simulator. + +**File:** `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` + +The Communication panel (Modbus RTU/TCP checkboxes and config) should be completely hidden when the simulator is selected. The simulator handles Modbus RTU internally and automatically. + +Implementation: Early return or conditional render: +```tsx +if (isSimulatorTarget(currentBoardInfo)) { + return ( + +

Modbus RTU is automatically configured for the simulator.

+
+ ) +} +``` + +--- + +## 5. Compilation Flow — Handle Simulator Target + +### 5a. Compiler Module Changes + +**File:** `src/main/modules/compiler/compiler-module.ts` + +The `compileProgram()` method (line 1341) needs a new branch for `compiler === 'simulator'`. The compilation pipeline is: + +1. **Steps 1-10 are identical to Arduino/RP2040** — XML generation, xml2st, iec2c, debug.c, LOCATED_VARIABLES.h, C/C++ blocks. The simulator uses the exact same `rp2040pico.cpp` HAL and `rp2040:rp2040:rpipico` platform. + +2. **Step 11 (Arduino CLI compile)** — also identical. The simulator target still calls `arduino-cli compile` with the RP2040 core to produce a `.uf2` firmware binary. Arduino CLI must be installed. + +3. **Step 12 (Upload) — this is where the simulator diverges.** Instead of calling `arduino-cli upload` to a serial port, the compiled `.uf2` file path is sent back to the renderer via the MessageChannel port so the renderer can load it into rp2040js. + +Implementation in `compileProgram()`: + +```typescript +// After successful Arduino CLI compilation... +if (boardRuntimeType === 'simulator') { + // Find the compiled .uf2 file + const uf2Path = path.join(buildDir, 'firmware.uf2') // or wherever arduino-cli outputs it + mainProcessPort.postMessage({ + logLevel: 'info', + message: 'Compilation successful. Loading firmware into simulator...', + }) + mainProcessPort.postMessage({ + simulatorFirmwarePath: uf2Path, + closePort: true, + }) + return +} +``` + +The `compileForDebugger()` method (line 2102) also needs the simulator branch. It follows the same pattern: compile for RP2040, then signal the renderer with the UF2 path instead of trying to upload. + +### 5b. IPC Handler Changes + +**File:** `src/main/modules/ipc/main.ts` + +The `handleRunCompileProgram` handler (line 777) passes args to `compilerModule.compileProgram()`. The args array includes `boardTarget` at index 1. The compiler module will read the board info from hals.json and detect `compiler === 'simulator'` to route appropriately. + +No IPC handler changes needed — the existing MessageChannel pattern already supports sending arbitrary data back to the renderer. + +### 5c. Build Button Changes (Renderer) + +**File:** `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` + +The build button handler (around line 267) calls `window.bridge.runCompileProgram(...)`. The callback receives messages from the compiler. + +For the simulator target, the callback will receive a new message type: `{ simulatorFirmwarePath: string, closePort: true }`. + +When this message arrives: +1. Read the UF2 file from the path +2. Load it into the rp2040js emulator instance (see Section 7) +3. Start emulator execution +4. Log "Simulator running" to the console + +--- + +## 6. rp2040js Emulator Module + +### 6a. New Module + +**File:** `src/main/modules/simulator/simulator-module.ts` (NEW) + +This module manages the rp2040js emulator lifecycle in the **main process**. It lives in main because the Modbus RTU client (which the debugger uses) runs in main. + +```typescript +import { RP2040, loadUF2, USBCDC } from 'rp2040js' + +class SimulatorModule { + private mcu: RP2040 | null = null + private running: boolean = false + private executionTimer: NodeJS.Timer | null = null + + // Callbacks for UART bridge + onUartByte: ((byte: number) => void) | null = null + + async loadAndRun(uf2Data: Buffer): Promise { + this.stop() + + this.mcu = new RP2040() + this.mcu.loadBootrom(bootromB1) // bundled bootrom + loadUF2(new Uint8Array(uf2Data), this.mcu) + + // Wire UART0 output to the Modbus RTU bridge + this.mcu.uart[0].onByte = (byte: number) => { + this.onUartByte?.(byte) + } + + // Start execution loop + this.mcu.PC = 0x10000000 + this.running = true + this.executeLoop() + } + + feedByte(byte: number): void { + this.mcu?.uart[0].feedByte(byte) + } + + stop(): void { + this.running = false + if (this.executionTimer) { + clearTimeout(this.executionTimer) + this.executionTimer = null + } + this.mcu = null + } + + isRunning(): boolean { + return this.running + } + + private executeLoop(): void { + if (!this.running || !this.mcu) return + // Execute a batch of cycles, then yield to event loop + this.mcu.execute() // runs until next yield point + this.executionTimer = setTimeout(() => this.executeLoop(), 0) + } +} +``` + +### 6b. Bootrom + +The RP2040 bootrom binary (~16KB) needs to be bundled. Options: +- Include as a `.ts` file with the binary data exported as a Uint8Array (same pattern as rp2040js's `demo/bootrom.ts`) +- Place in `resources/` and load at runtime + +Recommended: Bundle as a TypeScript constant in `src/main/modules/simulator/bootrom.ts` for simplicity and zero filesystem dependency. + +### 6c. npm Dependency + +```bash +npm install rp2040js +``` + +Add to `package.json` dependencies. The package is ~200KB, zero transitive dependencies, supports both ESM and CJS. + +--- + +## 7. Modbus RTU Bridge for Simulator + +### 7a. Virtual Serial Bridge + +**File:** `src/main/modules/simulator/simulator-modbus-bridge.ts` (NEW) + +This bridges the existing Modbus RTU protocol logic with the emulated UART. It replaces the physical serial port with virtual byte-level callbacks. + +The existing `ModbusRtuClient` (`src/main/modules/modbus/modbus-rtu-client.ts`) is tightly coupled to the `serialport` npm package. Rather than refactoring it, create a **SimulatorModbusClient** that implements the same public interface (`connect`, `disconnect`, `getMd5Hash`, `getVariablesList`, `setVariable`) but routes bytes through the emulated UART instead of a serial port. + +```typescript +class SimulatorModbusClient { + private simulator: SimulatorModule + private rxBuffer: number[] = [] + private responseResolve: ((data: Buffer) => void) | null = null + private frameTimeout: NodeJS.Timeout | null = null + + constructor(simulator: SimulatorModule) { + this.simulator = simulator + // Receive bytes from emulated RP2040's UART TX + this.simulator.onUartByte = (byte) => this.handleReceivedByte(byte) + } + + async connect(): Promise { + // No physical port to open. Just wait for firmware to boot. + // The RP2040 firmware has a ~2.5s bootloader delay, but in emulation + // the UART is ready almost immediately. Add a small delay for safety. + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + disconnect(): void { + this.simulator.onUartByte = null + } + + async getMd5Hash(): Promise { + // Build Modbus RTU request frame for FC 0x45 (DEBUG_GET_MD5) + // Same protocol as ModbusRtuClient but using virtual UART + const request = this.buildRequest(0x01, 0x45, Buffer.alloc(0)) + const response = await this.sendAndReceive(request) + return this.parseMd5Response(response) + } + + async getVariablesList(indices: number[]): Promise<{...}> { + // Same protocol as ModbusRtuClient.getVariablesList + // Build FC 0x44 request, send via virtual UART, parse response + } + + async setVariable(index: number, force: boolean, value?: Buffer): Promise<{...}> { + // Same protocol as ModbusRtuClient.setVariable + // Build FC 0x42 request, send via virtual UART, parse response + } + + private sendAndReceive(frame: Buffer): Promise { + return new Promise((resolve, reject) => { + this.rxBuffer = [] + this.responseResolve = resolve + + // Send each byte to the emulated UART RX + for (const byte of frame) { + this.simulator.feedByte(byte) + } + + // Timeout for response + setTimeout(() => { + if (this.responseResolve) { + this.responseResolve = null + reject(new Error('Simulator Modbus response timeout')) + } + }, 5000) + }) + } + + private handleReceivedByte(byte: number): void { + this.rxBuffer.push(byte) + + // Reset frame completion timeout (50ms gap = end of frame) + if (this.frameTimeout) clearTimeout(this.frameTimeout) + this.frameTimeout = setTimeout(() => { + if (this.responseResolve && this.rxBuffer.length > 0) { + this.responseResolve(Buffer.from(this.rxBuffer)) + this.responseResolve = null + this.rxBuffer = [] + } + }, 50) + } + + // CRC16, frame building — reuse from existing modbus-rtu-client.ts + // Extract shared CRC16/frame utilities into a common module +} +``` + +### 7b. Shared Modbus RTU Utilities + +**File:** `src/main/modules/modbus/modbus-rtu-utils.ts` (NEW) + +Extract from `modbus-rtu-client.ts`: +- `calculateCRC16(buffer)` — CRC lookup table and calculation (lines 27-59) +- `buildRtuFrame(slaveId, functionCode, data)` — frame assembly with CRC +- `ModbusFunctionCode` enum +- Response parsing helpers + +Both `ModbusRtuClient` (physical serial) and `SimulatorModbusClient` (virtual UART) will import from this shared module. + +--- + +## 8. Debugger Connection for Simulator + +### 8a. IPC Handler — New Connection Type + +**File:** `src/main/modules/ipc/main.ts` + +The `handleDebuggerConnect` method (line 1114) currently supports `connectionType: 'tcp' | 'rtu' | 'websocket'`. Add `'simulator'`: + +```typescript +handleDebuggerConnect = async ( + _event: IpcMainInvokeEvent, + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', + connectionParams: { ... } +): Promise<{ success: boolean; error?: string }> +``` + +New branch: +```typescript +case 'simulator': + // Create SimulatorModbusClient connected to the running emulator + this.debuggerModbusClient = new SimulatorModbusClient(this.simulatorModule) + await this.debuggerModbusClient.connect() + break +``` + +The `SimulatorModbusClient` implements the same interface as `ModbusRtuClient`, so all existing debug polling logic (`handleDebuggerGetVariablesList`, `handleDebuggerVerifyMd5`, `handleDebuggerSetVariable`) works unchanged. + +### 8b. Debugger Button — Simulator Flow + +**File:** `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` + +The `handleDebuggerClick` function (line 377) currently: +1. Checks if runtime target → requires IP/connection +2. Checks if arduino target → requires Modbus RTU or TCP enabled +3. Runs debug compilation +4. On success, connects debugger with the appropriate connection type + +For the simulator, the flow should be: +1. Detect `isSimulatorTarget` → skip all connection checks (no IP, no port, no Modbus config) +2. Run debug compilation (same `runDebugCompilation` call with `boardTarget = 'Simulator'`) +3. The compilation callback receives `simulatorFirmwarePath` — load into emulator +4. Wait for emulator to boot (~500ms) +5. Call `debuggerConnect('simulator', {})` — connects to the already-running emulator's UART +6. Proceed with MD5 verification and variable polling (all existing code) + +```typescript +if (isSimulatorTarget(currentBoardInfo)) { + connectionType = 'simulator' + // No IP, port, or Modbus config needed + // Compilation will produce UF2 and auto-load into emulator +} +``` + +The debug compilation callback: +```typescript +if (data.simulatorFirmwarePath) { + // Load firmware into simulator + await window.bridge.simulatorLoadFirmware(data.simulatorFirmwarePath) + // Small delay for firmware boot + await new Promise(resolve => setTimeout(resolve, 500)) +} + +if (data.closePort) { + // Proceed with MD5 verification as usual + void handleMd5Verification(projectPath, boardTarget, 'simulator', {}, undefined, false) +} +``` + +--- + +## 9. New IPC Endpoints + +**File:** `src/main/modules/ipc/main.ts` — add handlers +**File:** `src/main/modules/ipc/renderer.ts` — add renderer-side wrappers +**File:** `src/main/modules/preload/preload.ts` — expose via bridge + +### New IPC Calls + +| Channel | Direction | Purpose | +|---------|-----------|---------| +| `simulator:load-firmware` | renderer → main | Load UF2 into emulator and start execution | +| `simulator:stop` | renderer → main | Stop the emulator | +| `simulator:is-running` | renderer → main | Check if emulator is currently running | + +These are thin wrappers around `SimulatorModule` methods. + +--- + +## 10. Firmware Modbus RTU Configuration + +The existing RP2040 HAL (`resources/sources/hal/rp2040pico.cpp`) and the OpenPLC Arduino runtime already include Modbus RTU slave support. The compiler generates `defines.h` with Modbus configuration based on the device editor settings. + +For the simulator, Modbus RTU must be **always enabled** with fixed defaults: +- Interface: `Serial` (UART0) +- Baud rate: `115200` +- Slave ID: `1` +- RS485 EN pin: none + +**File:** `src/main/modules/compiler/compiler-module.ts` + +In the code generation step that produces `defines.h`, when the target is `'simulator'`: + +```c +#define MODBUS_ENABLED +#define MODBUS_RTU +#define MODBUS_RTU_INTERFACE Serial +#define MODBUS_RTU_BAUD 115200 +#define MODBUS_RTU_SLAVE_ID 1 +``` + +These defines are hardcoded regardless of what the device editor shows (which for the simulator shows nothing). + +--- + +## 11. Assets + +### Simulator Preview Image + +**File:** `resources/sources/boards/images/simulator.png` (NEW) + +Create a preview image for the simulator device. Suggestion: a stylized chip/CPU icon with a "play" symbol overlay to convey "virtual device." Should match the dimensions and style of existing board preview images. + +--- + +## 12. File Summary + +### New Files + +| File | Purpose | +|------|---------| +| `src/main/modules/simulator/simulator-module.ts` | rp2040js emulator lifecycle management | +| `src/main/modules/simulator/simulator-modbus-bridge.ts` | Modbus RTU client over virtual UART | +| `src/main/modules/simulator/bootrom.ts` | Bundled RP2040 bootrom binary | +| `src/main/modules/modbus/modbus-rtu-utils.ts` | Shared CRC16/frame utilities extracted from modbus-rtu-client | +| `resources/sources/boards/images/simulator.png` | Device preview image | + +### Modified Files + +| File | Changes | +|------|---------| +| `resources/sources/boards/hals.json` | Add "Simulator" entry at top | +| `src/utils/device.ts` | Add `isSimulatorTarget()` function | +| `src/renderer/store/slices/device/data/constants.ts` | Change default device to "Simulator" | +| `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` | Hide comm port, pin mapping, IP address, Connect button for simulator | +| `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` | Hide Modbus RTU/TCP config for simulator | +| `src/main/modules/compiler/compiler-module.ts` | Handle `compiler === 'simulator'` in compileProgram/compileForDebugger; force Modbus RTU defines | +| `src/main/modules/ipc/main.ts` | Add simulator IPC handlers; add `'simulator'` connection type to debugger | +| `src/main/modules/ipc/renderer.ts` | Add renderer wrappers for simulator IPC | +| `src/main/modules/preload/preload.ts` | Expose simulator bridge methods | +| `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` | Handle simulator in build callback and debugger click | +| `src/main/modules/modbus/modbus-rtu-client.ts` | Extract CRC16/frame utils to shared module, import from there | +| `package.json` | Add `rp2040js` dependency | + +--- + +## 13. Implementation Order + +### Phase 1: Foundation (Week 1) +1. `npm install rp2040js` +2. Add "Simulator" entry to `hals.json` +3. Add `isSimulatorTarget()` to `src/utils/device.ts` +4. Update default device in constants.ts +5. Create `SimulatorModule` with basic load/run/stop +6. Bundle bootrom binary +7. Add simulator IPC endpoints (load-firmware, stop, is-running) + +### Phase 2: Compilation (Week 2) +8. Handle `compiler === 'simulator'` in `compileProgram()` — reuse Arduino CLI compilation for RP2040, skip upload step, return UF2 path +9. Handle `compiler === 'simulator'` in `compileForDebugger()` — same pattern +10. Force Modbus RTU defines in generated `defines.h` for simulator +11. Wire up build button callback to load UF2 into emulator on success + +### Phase 3: Debugger (Week 3) +12. Extract shared Modbus RTU utils (CRC16, frame building) to `modbus-rtu-utils.ts` +13. Implement `SimulatorModbusClient` using virtual UART bridge +14. Add `'simulator'` connection type to `handleDebuggerConnect` +15. Wire up debugger button flow for simulator (auto-compile, auto-load, auto-connect) + +### Phase 4: UI Polish (Week 4) +16. Update board.tsx to show simulator-specific UI (hide irrelevant fields) +17. Update communication.tsx to show "auto-configured" message +18. Create simulator preview image +19. Testing: full flow (new project → build → debugger → see values) +20. Edge cases: re-compile while running, stop simulator, switch device away from simulator + +--- + +## 14. Open Questions / Decisions + +1. **Execution model**: rp2040js's `mcu.execute()` may block. Need to verify whether it runs synchronously to completion or yields. If it blocks, run in a Worker thread or use `mcu.step()` in a `setImmediate` loop. + +2. **Firmware output location**: Arduino CLI outputs compiled binaries to a temp directory or the sketch build path. Need to verify the exact `.uf2` output path for the RP2040 platform. + +3. **Emulator in main vs renderer**: Plan puts it in main process (where Modbus client runs). Alternative: run in renderer and use IPC for UART bytes. Main process is simpler since the debugger already runs there. + +4. **Bootrom licensing**: The RP2040 bootrom is Raspberry Pi proprietary. The rp2040js demo includes a bundled copy. Need to verify redistribution rights or use the open-source bootrom alternative if available. + +5. **Execution speed**: The emulated UART never blocks (runs faster than real hardware). This means Modbus RTU frame timing may differ. The 50ms frame-completion timeout in the existing RTU client should still work since it's based on silence gaps, not absolute timing. From 7b7c4e1e2f606a22d197683b454d9c161ea4a1ec Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 18 Feb 2026 23:44:27 -0500 Subject: [PATCH 02/25] docs: rename device from 'Simulator' to 'OpenPLC Simulator' Alphabetically closer to 'OpenPLC Runtime' in the device list. Co-Authored-By: Claude Opus 4.6 --- docs/simulator-rp2040-plan.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/simulator-rp2040-plan.md b/docs/simulator-rp2040-plan.md index 6cdc9d117..dab42c329 100644 --- a/docs/simulator-rp2040-plan.md +++ b/docs/simulator-rp2040-plan.md @@ -16,7 +16,7 @@ Add a new entry at the **top** of the JSON (so it appears first in the device dr ```json { - "Simulator": { + "OpenPLC Simulator": { "compiler": "simulator", "core": "rp2040:rp2040", "board_manager_url": "https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json", @@ -64,11 +64,11 @@ This function will be used across the UI to conditionally hide/show configuratio **File:** `src/renderer/store/slices/device/data/constants.ts` -Change the default device board from `'OpenPLC Runtime v3'` to `'Simulator'`: +Change the default device board from `'OpenPLC Runtime v3'` to `'OpenPLC Simulator'`: ```typescript defaultDeviceConfiguration: DeviceConfiguration = { - deviceBoard: 'Simulator', // was 'OpenPLC Runtime v3' + deviceBoard: 'OpenPLC Simulator', // was 'OpenPLC Runtime v3' ...rest unchanged... } ``` @@ -416,7 +416,7 @@ The `handleDebuggerClick` function (line 377) currently: For the simulator, the flow should be: 1. Detect `isSimulatorTarget` → skip all connection checks (no IP, no port, no Modbus config) -2. Run debug compilation (same `runDebugCompilation` call with `boardTarget = 'Simulator'`) +2. Run debug compilation (same `runDebugCompilation` call with `boardTarget = 'OpenPLC Simulator'`) 3. The compilation callback receives `simulatorFirmwarePath` — load into emulator 4. Wait for emulator to boot (~500ms) 5. Call `debuggerConnect('simulator', {})` — connects to the already-running emulator's UART @@ -517,9 +517,9 @@ Create a preview image for the simulator device. Suggestion: a stylized chip/CPU | File | Changes | |------|---------| -| `resources/sources/boards/hals.json` | Add "Simulator" entry at top | +| `resources/sources/boards/hals.json` | Add "OpenPLC Simulator" entry at top | | `src/utils/device.ts` | Add `isSimulatorTarget()` function | -| `src/renderer/store/slices/device/data/constants.ts` | Change default device to "Simulator" | +| `src/renderer/store/slices/device/data/constants.ts` | Change default device to "OpenPLC Simulator" | | `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` | Hide comm port, pin mapping, IP address, Connect button for simulator | | `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` | Hide Modbus RTU/TCP config for simulator | | `src/main/modules/compiler/compiler-module.ts` | Handle `compiler === 'simulator'` in compileProgram/compileForDebugger; force Modbus RTU defines | @@ -536,9 +536,9 @@ Create a preview image for the simulator device. Suggestion: a stylized chip/CPU ### Phase 1: Foundation (Week 1) 1. `npm install rp2040js` -2. Add "Simulator" entry to `hals.json` +2. Add "OpenPLC Simulator" entry to `hals.json` 3. Add `isSimulatorTarget()` to `src/utils/device.ts` -4. Update default device in constants.ts +4. Update default device to "OpenPLC Simulator" in constants.ts 5. Create `SimulatorModule` with basic load/run/stop 6. Bundle bootrom binary 7. Add simulator IPC endpoints (load-firmware, stop, is-running) From d5a77c804d91c3f4cd5014440cd5ab01515e733f Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 00:06:50 -0500 Subject: [PATCH 03/25] docs: revise simulator plan - VirtualSerialPort approach and corrected debugger flow Two major revisions based on review feedback: 1. Modbus RTU bridge: Replace duplicated SimulatorModbusClient with a VirtualSerialPort that mimics the serialport event API and routes bytes through rp2040js UART. ModbusRtuClient gets a single constructor change to accept an optional injected serial port. All protocol logic (CRC, framing, retries, parsing) is reused as-is. Eliminates simulator-modbus-bridge.ts and modbus-rtu-utils.ts from the plan. 2. Debugger flow: The debugger button does NOT recompile the full firmware. It only runs first-stage compilation (compileForDebugger) to extract the MD5 hash. For the simulator, the extra check is whether the emulator is "empty" (no firmware loaded), which triggers a warning asking the user to build first. compileForDebugger() needs no simulator-specific branch - it works as-is for all targets. Co-Authored-By: Claude Opus 4.6 --- docs/simulator-rp2040-plan.md | 284 +++++++++++++++++++--------------- 1 file changed, 160 insertions(+), 124 deletions(-) diff --git a/docs/simulator-rp2040-plan.md b/docs/simulator-rp2040-plan.md index dab42c329..57dece3a4 100644 --- a/docs/simulator-rp2040-plan.md +++ b/docs/simulator-rp2040-plan.md @@ -134,7 +134,7 @@ if (isSimulatorTarget(currentBoardInfo)) { ## 5. Compilation Flow — Handle Simulator Target -### 5a. Compiler Module Changes +### 5a. Compiler Module Changes (Build Button Only) **File:** `src/main/modules/compiler/compiler-module.ts` @@ -165,7 +165,7 @@ if (boardRuntimeType === 'simulator') { } ``` -The `compileForDebugger()` method (line 2102) also needs the simulator branch. It follows the same pattern: compile for RP2040, then signal the renderer with the UF2 path instead of trying to upload. +The `compileForDebugger()` method (line 2102) does **not** need a simulator-specific branch. This function only runs the first-stage compilation (XML → ST → C → debug metadata) and never invokes Arduino CLI or uploads anything. It works as-is for all targets, including the simulator. ### 5b. IPC Handler Changes @@ -272,108 +272,126 @@ Add to `package.json` dependencies. The package is ~200KB, zero transitive depen --- -## 7. Modbus RTU Bridge for Simulator +## 7. Modbus RTU Bridge for Simulator — VirtualSerialPort Approach -### 7a. Virtual Serial Bridge +### Design Principle -**File:** `src/main/modules/simulator/simulator-modbus-bridge.ts` (NEW) +Rather than duplicating the Modbus RTU protocol logic in a separate `SimulatorModbusClient` class, we create a `VirtualSerialPort` that mimics the `serialport` npm package's event-based API and adapts rp2040js's UART. The existing `ModbusRtuClient` is then reused unchanged — all CRC calculation, frame assembly, response parsing, retries, and timeout logic stays in one place. -This bridges the existing Modbus RTU protocol logic with the emulated UART. It replaces the physical serial port with virtual byte-level callbacks. +This avoids code duplication and ensures any future bug fixes to the Modbus RTU protocol automatically apply to both physical hardware and the simulator. -The existing `ModbusRtuClient` (`src/main/modules/modbus/modbus-rtu-client.ts`) is tightly coupled to the `serialport` npm package. Rather than refactoring it, create a **SimulatorModbusClient** that implements the same public interface (`connect`, `disconnect`, `getMd5Hash`, `getVariablesList`, `setVariable`) but routes bytes through the emulated UART instead of a serial port. +### 7a. VirtualSerialPort + +**File:** `src/main/modules/simulator/virtual-serial-port.ts` (NEW) + +`ModbusRtuClient.serialPort` is typed as `any` and uses these `serialport` APIs: +- `on('open', cb)` / `on('data', cb)` / `on('error', cb)` / `once('error', cb)` — events +- `write(data, callback)` — send bytes +- `flush(callback)` — flush input buffer +- `close()` — close port +- `isOpen` — boolean state +- `removeListener(event, fn)` — cleanup + +`VirtualSerialPort` extends `EventEmitter` and implements all of these, routing bytes through `SimulatorModule.feedByte()` (TX to device) and `SimulatorModule.onUartByte` (RX from device): ```typescript -class SimulatorModbusClient { +import { EventEmitter } from 'events' +import { SimulatorModule } from './simulator-module' + +export class VirtualSerialPort extends EventEmitter { + public isOpen = false private simulator: SimulatorModule - private rxBuffer: number[] = [] - private responseResolve: ((data: Buffer) => void) | null = null - private frameTimeout: NodeJS.Timeout | null = null constructor(simulator: SimulatorModule) { + super() this.simulator = simulator - // Receive bytes from emulated RP2040's UART TX - this.simulator.onUartByte = (byte) => this.handleReceivedByte(byte) } - async connect(): Promise { - // No physical port to open. Just wait for firmware to boot. - // The RP2040 firmware has a ~2.5s bootloader delay, but in emulation - // the UART is ready almost immediately. Add a small delay for safety. - await new Promise((resolve) => setTimeout(resolve, 500)) + open(): void { + this.isOpen = true + // Wire UART RX: bytes from emulated device → ModbusRtuClient via 'data' events + this.simulator.onUartByte = (byte: number) => { + this.emit('data', Buffer.from([byte])) + } + // Emit 'open' asynchronously (matches real SerialPort behavior) + process.nextTick(() => this.emit('open')) } - disconnect(): void { - this.simulator.onUartByte = null + write(data: Uint8Array | Buffer, callback?: (err?: Error | null) => void): void { + // Send each byte to the emulated UART TX (host → device) + for (const byte of data) { + this.simulator.feedByte(byte) + } + callback?.(null) } - async getMd5Hash(): Promise { - // Build Modbus RTU request frame for FC 0x45 (DEBUG_GET_MD5) - // Same protocol as ModbusRtuClient but using virtual UART - const request = this.buildRequest(0x01, 0x45, Buffer.alloc(0)) - const response = await this.sendAndReceive(request) - return this.parseMd5Response(response) + flush(callback?: (err?: Error | null) => void): void { + // No hardware buffer to flush in virtual port + callback?.(null) } - async getVariablesList(indices: number[]): Promise<{...}> { - // Same protocol as ModbusRtuClient.getVariablesList - // Build FC 0x44 request, send via virtual UART, parse response + close(): void { + this.isOpen = false + this.simulator.onUartByte = null + this.removeAllListeners() } +} +``` - async setVariable(index: number, force: boolean, value?: Buffer): Promise<{...}> { - // Same protocol as ModbusRtuClient.setVariable - // Build FC 0x42 request, send via virtual UART, parse response - } +**Why byte-by-byte emission works:** `ModbusRtuClient.sendRequest()` already accumulates bytes into `responseBuffer` via `Buffer.concat` and uses a 50ms frame-completion timeout to detect end-of-frame. Each byte resets the timer. Since the emulated CPU processes response bytes in batches (within the same `executeLoop()` tick), they arrive nearly instantly and the 50ms gap correctly signals frame completion. - private sendAndReceive(frame: Buffer): Promise { - return new Promise((resolve, reject) => { - this.rxBuffer = [] - this.responseResolve = resolve - - // Send each byte to the emulated UART RX - for (const byte of frame) { - this.simulator.feedByte(byte) - } - - // Timeout for response - setTimeout(() => { - if (this.responseResolve) { - this.responseResolve = null - reject(new Error('Simulator Modbus response timeout')) - } - }, 5000) - }) - } +**No bootloader delay:** Physical serial ports have a 2.5s bootloader delay after opening. The `VirtualSerialPort` skips this entirely — the emulated UART is ready immediately. - private handleReceivedByte(byte: number): void { - this.rxBuffer.push(byte) - - // Reset frame completion timeout (50ms gap = end of frame) - if (this.frameTimeout) clearTimeout(this.frameTimeout) - this.frameTimeout = setTimeout(() => { - if (this.responseResolve && this.rxBuffer.length > 0) { - this.responseResolve(Buffer.from(this.rxBuffer)) - this.responseResolve = null - this.rxBuffer = [] - } - }, 50) - } +### 7b. ModbusRtuClient — Accept Injected Serial Port - // CRC16, frame building — reuse from existing modbus-rtu-client.ts - // Extract shared CRC16/frame utilities into a common module +**File:** `src/main/modules/modbus/modbus-rtu-client.ts` (MODIFIED — minimal change) + +Add an optional `serialPort` field to the constructor options: + +```typescript +interface ModbusRtuClientOptions { + port: string + baudRate: number + slaveId: number + timeout: number + serialPort?: any // Pre-built serial port (e.g. VirtualSerialPort for simulator) } ``` -### 7b. Shared Modbus RTU Utilities +Store it in the constructor and add an early branch in `connect()`: + +```typescript +private injectedSerialPort: any = null + +constructor(options: ModbusRtuClientOptions) { + this.port = options.port + this.baudRate = options.baudRate + this.slaveId = options.slaveId + this.timeout = options.timeout + this.injectedSerialPort = options.serialPort ?? null +} + +async connect(): Promise { + // If a pre-built serial port was provided (e.g. VirtualSerialPort), use it directly + if (this.injectedSerialPort) { + this.serialPort = this.injectedSerialPort + return new Promise((resolve, reject) => { + this.serialPort.on('open', () => resolve()) + this.serialPort.on('error', (err: Error) => reject(err)) + this.serialPort.open() + }) + } + // ...existing SerialPort creation code (unchanged)... +} +``` -**File:** `src/main/modules/modbus/modbus-rtu-utils.ts` (NEW) +This is the **only change** to `ModbusRtuClient`. All protocol logic — `assembleRequest()`, `sendRequest()`, `calculateCrc()`, `getMd5Hash()`, `getVariablesList()`, `setVariable()` — remains untouched and is shared between physical hardware and the simulator. -Extract from `modbus-rtu-client.ts`: -- `calculateCRC16(buffer)` — CRC lookup table and calculation (lines 27-59) -- `buildRtuFrame(slaveId, functionCode, data)` — frame assembly with CRC -- `ModbusFunctionCode` enum -- Response parsing helpers +### 7c. No Separate SimulatorModbusClient or Shared Utils Needed -Both `ModbusRtuClient` (physical serial) and `SimulatorModbusClient` (virtual UART) will import from this shared module. +This approach eliminates: +- ~~`simulator-modbus-bridge.ts`~~ — not needed, `ModbusRtuClient` is reused directly +- ~~`modbus-rtu-utils.ts`~~ — not needed, no protocol code to extract/share --- @@ -393,57 +411,77 @@ handleDebuggerConnect = async ( ): Promise<{ success: boolean; error?: string }> ``` -New branch: +New branch using `ModbusRtuClient` with `VirtualSerialPort` (no separate client class needed): + ```typescript case 'simulator': - // Create SimulatorModbusClient connected to the running emulator - this.debuggerModbusClient = new SimulatorModbusClient(this.simulatorModule) + const virtualPort = new VirtualSerialPort(this.simulatorModule) + this.debuggerModbusClient = new ModbusRtuClient({ + port: 'simulator', // label only, not used for real I/O + baudRate: 115200, + slaveId: 1, + timeout: 5000, + serialPort: virtualPort, // injected virtual port + }) await this.debuggerModbusClient.connect() break ``` -The `SimulatorModbusClient` implements the same interface as `ModbusRtuClient`, so all existing debug polling logic (`handleDebuggerGetVariablesList`, `handleDebuggerVerifyMd5`, `handleDebuggerSetVariable`) works unchanged. +Since `ModbusRtuClient` is used directly, all existing debug polling logic (`handleDebuggerGetVariablesList`, `handleDebuggerVerifyMd5`, `handleDebuggerSetVariable`) works unchanged. -### 8b. Debugger Button — Simulator Flow +### 8b. Debugger Button — Simulator Flow (Corrected) **File:** `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` -The `handleDebuggerClick` function (line 377) currently: -1. Checks if runtime target → requires IP/connection -2. Checks if arduino target → requires Modbus RTU or TCP enabled -3. Runs debug compilation -4. On success, connects debugger with the appropriate connection type +The debugger button does **not** compile the full firmware. It only runs the first-stage compilation (`compileForDebugger`) to generate debug metadata and extract the MD5 hash. This is already the existing behavior for all targets — `compileForDebugger()` never invokes Arduino CLI or uploads anything. -For the simulator, the flow should be: -1. Detect `isSimulatorTarget` → skip all connection checks (no IP, no port, no Modbus config) -2. Run debug compilation (same `runDebugCompilation` call with `boardTarget = 'OpenPLC Simulator'`) -3. The compilation callback receives `simulatorFirmwarePath` — load into emulator -4. Wait for emulator to boot (~500ms) -5. Call `debuggerConnect('simulator', {})` — connects to the already-running emulator's UART -6. Proceed with MD5 verification and variable polling (all existing code) +For the simulator, there is one extra check: whether the simulator has firmware loaded. If the user has never pressed Build, the simulator is "empty" and there's nothing to debug. + +**Complete simulator debugger flow:** + +1. Detect `isSimulatorTarget` → skip all connection parameter checks (no IP, no port, no Modbus config) +2. **Check if simulator is "empty"** via `window.bridge.simulatorIsRunning()` + - If empty (no firmware loaded) → show warning dialog: *"No firmware is running on the simulator. Would you like to build and upload the project first?"* + - If user agrees → trigger full build (`runCompileProgram`), which compiles and auto-loads firmware into emulator. After build completes, restart the debugger flow from step 3. + - If user declines → abort debugger +3. Run `compileForDebugger()` (first-stage only: XML → ST → C → debug files). **No simulator-specific branch needed** — this function works as-is for all targets. +4. Extract local MD5 from generated `program.st` (existing `debuggerReadProgramStMd5`) +5. Connect to simulator and get its MD5 via `debuggerVerifyMd5('simulator', {}, expectedMd5)` — uses Modbus RTU over virtual UART +6. **Compare MD5s** (existing logic): + - If match → proceed to parse debug.c, build variable tree, connect debugger, start polling + - If mismatch → show existing "Program Mismatch" dialog asking user to rebuild/upload. If user agrees, trigger full build and retry MD5 verification. (This is the same dialog that appears for real hardware when the running firmware doesn't match the current project.) ```typescript +// In handleDebuggerClick(): if (isSimulatorTarget(currentBoardInfo)) { + // Check if simulator has firmware loaded + const running = await window.bridge.simulatorIsRunning() + if (!running) { + const response = await window.bridge.showMessageDialog({ + type: 'warning', + title: 'Simulator Empty', + message: 'No firmware is running on the simulator. Would you like to build and upload the project first?', + buttons: ['Build & Upload', 'Cancel'], + }) + if (response === 0) { + // Trigger full build (same as build button), then restart debugger flow + // ... + } else { + setIsDebuggerProcessing(false) + return + } + } connectionType = 'simulator' - // No IP, port, or Modbus config needed - // Compilation will produce UF2 and auto-load into emulator + // Fall through to normal debug compilation + MD5 verification } ``` -The debug compilation callback: -```typescript -if (data.simulatorFirmwarePath) { - // Load firmware into simulator - await window.bridge.simulatorLoadFirmware(data.simulatorFirmwarePath) - // Small delay for firmware boot - await new Promise(resolve => setTimeout(resolve, 500)) -} +### 8c. What Doesn't Change -if (data.closePort) { - // Proceed with MD5 verification as usual - void handleMd5Verification(projectPath, boardTarget, 'simulator', {}, undefined, false) -} -``` +- `compileForDebugger()` — works as-is, no simulator branch needed (first-stage only, no hardware) +- `handleMd5Verification()` — works as-is once `'simulator'` connection type is supported +- MD5 mismatch dialog — works as-is (triggers full build + retry) +- Debug file parsing, variable tree building, debug polling — all unchanged --- @@ -508,9 +546,8 @@ Create a preview image for the simulator device. Suggestion: a stylized chip/CPU | File | Purpose | |------|---------| | `src/main/modules/simulator/simulator-module.ts` | rp2040js emulator lifecycle management | -| `src/main/modules/simulator/simulator-modbus-bridge.ts` | Modbus RTU client over virtual UART | +| `src/main/modules/simulator/virtual-serial-port.ts` | EventEmitter-based serial port mock that routes bytes through emulated UART | | `src/main/modules/simulator/bootrom.ts` | Bundled RP2040 bootrom binary | -| `src/main/modules/modbus/modbus-rtu-utils.ts` | Shared CRC16/frame utilities extracted from modbus-rtu-client | | `resources/sources/boards/images/simulator.png` | Device preview image | ### Modified Files @@ -522,12 +559,12 @@ Create a preview image for the simulator device. Suggestion: a stylized chip/CPU | `src/renderer/store/slices/device/data/constants.ts` | Change default device to "OpenPLC Simulator" | | `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` | Hide comm port, pin mapping, IP address, Connect button for simulator | | `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` | Hide Modbus RTU/TCP config for simulator | -| `src/main/modules/compiler/compiler-module.ts` | Handle `compiler === 'simulator'` in compileProgram/compileForDebugger; force Modbus RTU defines | +| `src/main/modules/compiler/compiler-module.ts` | Handle `compiler === 'simulator'` in compileProgram (skip upload, return UF2 path); force Modbus RTU defines | | `src/main/modules/ipc/main.ts` | Add simulator IPC handlers; add `'simulator'` connection type to debugger | | `src/main/modules/ipc/renderer.ts` | Add renderer wrappers for simulator IPC | | `src/main/modules/preload/preload.ts` | Expose simulator bridge methods | -| `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` | Handle simulator in build callback and debugger click | -| `src/main/modules/modbus/modbus-rtu-client.ts` | Extract CRC16/frame utils to shared module, import from there | +| `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` | Handle simulator in build callback and debugger click (empty simulator check) | +| `src/main/modules/modbus/modbus-rtu-client.ts` | Accept optional injected serial port in constructor (for VirtualSerialPort) | | `package.json` | Add `rp2040js` dependency | --- @@ -545,22 +582,21 @@ Create a preview image for the simulator device. Suggestion: a stylized chip/CPU ### Phase 2: Compilation (Week 2) 8. Handle `compiler === 'simulator'` in `compileProgram()` — reuse Arduino CLI compilation for RP2040, skip upload step, return UF2 path -9. Handle `compiler === 'simulator'` in `compileForDebugger()` — same pattern -10. Force Modbus RTU defines in generated `defines.h` for simulator -11. Wire up build button callback to load UF2 into emulator on success +9. Force Modbus RTU defines in generated `defines.h` for simulator +10. Wire up build button callback to load UF2 into emulator on success ### Phase 3: Debugger (Week 3) -12. Extract shared Modbus RTU utils (CRC16, frame building) to `modbus-rtu-utils.ts` -13. Implement `SimulatorModbusClient` using virtual UART bridge -14. Add `'simulator'` connection type to `handleDebuggerConnect` -15. Wire up debugger button flow for simulator (auto-compile, auto-load, auto-connect) +11. Create `VirtualSerialPort` (EventEmitter mock adapting rp2040js UART to `serialport` API) +12. Modify `ModbusRtuClient` to accept optional injected serial port (single constructor change) +13. Add `'simulator'` connection type to `handleDebuggerConnect` (creates `ModbusRtuClient` + `VirtualSerialPort`) +14. Wire up debugger button flow for simulator: check if simulator is empty → first-stage compilation → MD5 verification → connect ### Phase 4: UI Polish (Week 4) -16. Update board.tsx to show simulator-specific UI (hide irrelevant fields) -17. Update communication.tsx to show "auto-configured" message -18. Create simulator preview image -19. Testing: full flow (new project → build → debugger → see values) -20. Edge cases: re-compile while running, stop simulator, switch device away from simulator +15. Update board.tsx to show simulator-specific UI (hide irrelevant fields) +16. Update communication.tsx to show "auto-configured" message +17. Create simulator preview image +18. Testing: full flow (new project → build → debugger → see values) +19. Edge cases: re-compile while running, stop simulator, switch device away from simulator --- From a1e48dc2ac4575f4f9ca28216b03c0db3760d529 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 00:17:57 -0500 Subject: [PATCH 04/25] feat: add simulator foundation - rp2040js, device entry, SimulatorModule, IPC Phase 1 of the OpenPLC Simulator implementation: - Install rp2040js npm dependency (Wokwi RP2040 emulator) - Add "OpenPLC Simulator" device entry to hals.json (first in list) - Add isSimulatorTarget() utility function to device.ts - Add 'simulator' to compiler type enums (Zod schemas + renderer types) - Change default device for new projects to "OpenPLC Simulator" - Create SimulatorModule with UF2 parsing, firmware loading, and execution loop (step-based with event loop yielding) - Bundle RP2040 bootrom binary (from rp2040js demo) - Add simulator IPC endpoints: load-firmware, stop, is-running - Add simulator methods to renderer bridge Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 9 + package.json | 1 + resources/sources/boards/hals.json | 25 + src/main/modules/compiler/compiler-types.ts | 2 +- src/main/modules/ipc/main.ts | 33 +- src/main/modules/ipc/renderer.ts | 8 +- src/main/modules/simulator/bootrom.ts | 461 ++++++++++++++++++ .../modules/simulator/simulator-module.ts | 121 +++++ .../store/slices/device/data/constants.ts | 2 +- src/renderer/store/slices/device/types.ts | 2 +- src/utils/device.ts | 14 + 11 files changed, 673 insertions(+), 5 deletions(-) create mode 100644 src/main/modules/simulator/bootrom.ts create mode 100644 src/main/modules/simulator/simulator-module.ts diff --git a/package-lock.json b/package-lock.json index 61ada1ebb..6e8b07d25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "react-i18next": "^13.3.0", "react-icons": "^4.11.0", "react-resizable-panels": "^2.0.3", + "rp2040js": "^1.3.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.1.0", "url": "^0.11.3", @@ -25325,6 +25326,14 @@ "node": ">=8.0" } }, + "node_modules/rp2040js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rp2040js/-/rp2040js-1.3.0.tgz", + "integrity": "sha512-Zv31PenIlQMehmOZTKjCl4DO8/eM5KVQEVZo5/QCx6P3IxIzoAsMwVcUQjS2hDLFXjGHYndgNGadJXfWU3Uqvg==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", diff --git a/package.json b/package.json index cd7789b5d..b9ae690bc 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "react-i18next": "^13.3.0", "react-icons": "^4.11.0", "react-resizable-panels": "^2.0.3", + "rp2040js": "^1.3.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.1.0", "url": "^0.11.3", diff --git a/resources/sources/boards/hals.json b/resources/sources/boards/hals.json index cbd98db34..837784cb3 100644 --- a/resources/sources/boards/hals.json +++ b/resources/sources/boards/hals.json @@ -1,4 +1,29 @@ { + "OpenPLC Simulator": { + "compiler": "simulator", + "core": "rp2040:rp2040", + "board_manager_url": "https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json", + "c_flags": ["-MMD", "-c", "-Wno-incompatible-pointer-types"], + "default_ain": "26, 27, 28", + "default_aout": "4, 5", + "default_din": "6, 7, 8, 9, 10, 11, 12, 13", + "default_dout": "14, 15, 16, 17, 18, 19, 20, 21", + "extra_libraries": [], + "platform": "rp2040:rp2040:rpipico", + "source": "rp2040pico.cpp", + "preview": "simulator.png", + "specs": { + "CPU": "Emulated RP2040 ARM Cortex-M0+", + "RAM": "264 KB", + "Flash": "2 MB", + "Digital Pins": "26", + "Analog Pins": "3", + "PWM Pins": "16", + "WiFi": "No", + "Bluetooth": "No", + "Ethernet": "No" + } + }, "Arduino Due (native USB port)": { "compiler": "arduino-cli", "core": "arduino:sam", diff --git a/src/main/modules/compiler/compiler-types.ts b/src/main/modules/compiler/compiler-types.ts index d402eee86..6c4fc92b6 100644 --- a/src/main/modules/compiler/compiler-types.ts +++ b/src/main/modules/compiler/compiler-types.ts @@ -16,7 +16,7 @@ const ArduinoCoreControlSchema = z.array(z.record(z.string(), z.string())) type ArduinoCoreControl = z.infer const BoardInfoSchema = z.object({ - compiler: z.enum(['arduino-cli', 'openplc-compiler']), + compiler: z.enum(['arduino-cli', 'openplc-compiler', 'simulator']), core: z.string(), default_ain: z.string(), default_aout: z.string(), diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index cbb6b0911..d30c408c1 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -18,6 +18,7 @@ import { MainIpcModule, MainIpcModuleConstructor } from '../../contracts/types/m import { logger } from '../../services' import { ModbusTcpClient } from '../modbus/modbus-client' import { ModbusRtuClient } from '../modbus/modbus-rtu-client' +import { SimulatorModule } from '../simulator/simulator-module' import { WebSocketDebugClient } from '../websocket/websocket-debug-client' type IDataToWrite = { @@ -43,7 +44,7 @@ class MainProcessBridge implements MainIpcModule { private debuggerWebSocketClient: WebSocketDebugClient | null = null private debuggerTargetIp: string | null = null private debuggerReconnecting: boolean = false - private debuggerConnectionType: 'tcp' | 'rtu' | 'websocket' | null = null + private debuggerConnectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator' | null = null private debuggerRtuPort: string | null = null private debuggerRtuBaudRate: number | null = null private debuggerRtuSlaveId: number | null = null @@ -54,6 +55,8 @@ class MainProcessBridge implements MainIpcModule { private currentProjectPath: string | null = null // File watchers for auto-reload functionality (using watchFile for better macOS compatibility) private fileWatchers: Map = new Map() + // rp2040js emulator instance for the built-in simulator + private simulatorModule = new SimulatorModule() constructor({ ipcMain, @@ -595,6 +598,11 @@ class MainProcessBridge implements MainIpcModule { this.ipcMain.handle('runtime:clear-credentials', this.handleRuntimeClearCredentials) this.ipcMain.handle('runtime:get-serial-ports', this.handleRuntimeGetSerialPorts) + // ===================== SIMULATOR ===================== + this.ipcMain.handle('simulator:load-firmware', this.handleSimulatorLoadFirmware) + this.ipcMain.handle('simulator:stop', this.handleSimulatorStop) + this.ipcMain.handle('simulator:is-running', this.handleSimulatorIsRunning) + // ===================== FILE WATCHER ===================== this.ipcMain.handle('file:watch-start', this.handleFileWatchStart) this.ipcMain.handle('file:watch-stop', this.handleFileWatchStop) @@ -1294,6 +1302,29 @@ class MainProcessBridge implements MainIpcModule { } } + // ===================== SIMULATOR HANDLERS ===================== + + handleSimulatorLoadFirmware = async ( + _event: IpcMainInvokeEvent, + uf2Path: string, + ): Promise<{ success: boolean; error?: string }> => { + try { + await this.simulatorModule.loadAndRun(uf2Path) + return { success: true } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + } + + handleSimulatorStop = (_event: IpcMainInvokeEvent): Promise<{ success: boolean }> => { + this.simulatorModule.stop() + return Promise.resolve({ success: true }) + } + + handleSimulatorIsRunning = (_event: IpcMainInvokeEvent): Promise => { + return Promise.resolve(this.simulatorModule.isRunning()) + } + // Using watchFile (polling-based) instead of watch for better macOS compatibility // fs.watch can fail when editors use "safe write" (write to temp file, then rename) handleFileWatchStart = ( diff --git a/src/main/modules/ipc/renderer.ts b/src/main/modules/ipc/renderer.ts index ae94613aa..95fca1e70 100644 --- a/src/main/modules/ipc/renderer.ts +++ b/src/main/modules/ipc/renderer.ts @@ -203,7 +203,7 @@ const rendererProcessBridge = { Map< string, { - compiler: 'arduino-cli' | 'openplc-compiler' + compiler: 'arduino-cli' | 'openplc-compiler' | 'simulator' core: string preview: string specs: { @@ -360,6 +360,12 @@ const rendererProcessBridge = { return () => ipcRenderer.removeListener('runtime:token-refreshed', callback) }, + // ===================== SIMULATOR METHODS ===================== + simulatorLoadFirmware: (uf2Path: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('simulator:load-firmware', uf2Path), + simulatorStop: (): Promise<{ success: boolean }> => ipcRenderer.invoke('simulator:stop'), + simulatorIsRunning: (): Promise => ipcRenderer.invoke('simulator:is-running'), + // ===================== FILE WATCHER METHODS ===================== fileWatchStart: (filePath: string): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('file:watch-start', filePath), diff --git a/src/main/modules/simulator/bootrom.ts b/src/main/modules/simulator/bootrom.ts new file mode 100644 index 000000000..19fb95167 --- /dev/null +++ b/src/main/modules/simulator/bootrom.ts @@ -0,0 +1,461 @@ +// RP2040 bootrom binary, built from https://github.com/raspberrypi/pico-bootrom +// revision: B1 (00a4a19114195e20fb817bdfbca1165e157eef37) + +export const bootromB1 = new Uint32Array([ + 0x20041f00, 0x000000ef, 0x00000035, 0x00000031, 0x0201754d, 0x00c8007a, 0x0000001d, 0x88022300, 0xd003429a, + 0x30048843, 0xd1f74291, 0x47701c18, 0xe7fdbf30, 0xf00046f4, 0x489ef805, 0x60012100, 0x46e76041, 0x2100489c, + 0x600143c9, 0x47706041, 0x00a4a191, 0x00001e09, 0x20294328, 0x30323032, 0x73615220, 0x72656270, 0x50207972, + 0x72542069, 0x6e696461, 0x744c2067, 0x33500064, 0x335202d9, 0x334c02fd, 0x33540327, 0x534d035f, 0x345326dd, + 0x434d26d1, 0x34432641, 0x42552629, 0x544425b5, 0x45440185, 0x5657018b, 0x46490137, 0x584524a1, 0x455223f5, + 0x5052237d, 0x434623c5, 0x58432361, 0x43452331, 0x00000045, 0x00505247, 0x00585243, 0x01a84653, 0x02284453, + 0x01a65a46, 0x27585346, 0x2e4c4546, 0x2e545344, 0x3dac4544, 0x48730000, 0x29006801, 0xf7ffd11f, 0x4971ff9d, + 0x680a4b71, 0xd001421a, 0xe793600b, 0x4e704f6f, 0x42b0cf0f, 0x4059d107, 0xd1041840, 0x60383f10, 0x8808f382, + 0xf0024798, 0xbf20f9e1, 0x08896d21, 0x6560d3fb, 0x1c6ebf40, 0x4c614730, 0x21044f65, 0x6da16139, 0x08496d21, + 0xa50bd2fb, 0xf7ff2000, 0x2801ffed, 0xf7ffd1f6, 0x60b8ffe9, 0xffe6f7ff, 0x8808f380, 0xffe2f7ff, 0xf7ffa501, + 0x46c0ffdf, 0x61392100, 0xe75d4780, 0x6d20bf20, 0xd3fb0840, 0x28006da0, 0x4770d0de, 0x43372601, 0xbe0047b8, + 0x3811e7fa, 0xbd007ac0, 0x4042b500, 0xf0002a00, 0xd2f6f802, 0x4670468e, 0x00204700, 0x00002b69, 0x00002b65, + 0x00002c31, 0x00002cfd, 0x00002827, 0x00002827, 0x00002db1, 0x0000284d, 0x0000284f, 0x00002881, 0x00002883, + 0x000028d7, 0x000028d9, 0x000028e7, 0x000028e9, 0x000029bf, 0x00002975, 0x000029dd, 0x00000031, 0x000029e5, + 0x00002a4f, 0x0000280b, 0x00002a73, 0x000028af, 0x000028b1, 0x0000289d, 0x0000289f, 0x00003581, 0x00003583, + 0x0000358b, 0x0000358d, 0x0000363d, 0x00002e61, 0x00002e55, 0x00002fbd, 0x00003119, 0x0000346b, 0x0000346b, + 0x000032dd, 0x00003565, 0x00003567, 0x00003573, 0x00003575, 0x000036c3, 0x000036c5, 0x000036bb, 0x000036bd, + 0x00003831, 0x00003841, 0x00003811, 0x00000031, 0x00003b45, 0x00003be1, 0x0000346f, 0x00003931, 0x000036d1, + 0x000036d3, 0x000036cb, 0x000036cd, 0x000035c1, 0x000035c3, 0x000035db, 0x000035dd, 0x00003663, 0xf380480a, + 0xf0018808, 0x0000ff1b, 0x40004000, 0x400080a0, 0xd0000000, 0x40064008, 0x01000000, 0x4005801c, 0xb007c0d3, + 0xe000ed00, 0x501008b0, 0x08424933, 0x0883400a, 0x4008400b, 0x18c01880, 0x184008c1, 0x4008492f, 0x18400981, + 0x4348492e, 0x47700e80, 0x08514a2d, 0x00434051, 0x4008400b, 0x43180840, 0x40130083, 0x08804010, 0x4a284303, + 0x40100118, 0x091b4013, 0xba004318, 0xa3254770, 0xd10c0c01, 0xd1040a81, 0xd1050901, 0x301a5c18, 0x5c584770, + 0x47703010, 0x30165c58, 0x0a884770, 0x0908d104, 0x5c58d104, 0x4770300a, 0x47705c18, 0x30065c18, 0xa3274770, + 0xd00f0401, 0xd0050188, 0xd0070181, 0x31100f09, 0x47705c58, 0x5c580e89, 0x4770300a, 0x5c180e80, 0x47703004, + 0xd0060181, 0xd0080188, 0x30100f00, 0x30105c18, 0x0e804770, 0x301a5c18, 0x0e894770, 0x30145c58, 0x00004770, + 0x49249249, 0xc71c71c7, 0x04004004, 0xcccccccc, 0xf0f0f0f0, 0x04040506, 0x03030303, 0x02020202, 0x02020202, + 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00010006, 0x00010002, 0x00010003, 0x00010002, 0x00010004, 0x00010002, + 0x00010003, 0x00010002, 0x00010005, 0x00010002, 0x00010003, 0x00010002, 0x00010004, 0x00010002, 0x00010003, + 0x00010002, 0x20e10031, 0x1c492169, 0x1dcd1ba9, 0xbe0022fd, 0x68934a02, 0x60933b40, 0x46c04770, 0x50100a7c, + 0x7d934a02, 0x4a027513, 0x47706013, 0x501009ec, 0x50110000, 0x60012300, 0x60426103, 0x60c3784b, 0x47706083, + 0x68834904, 0x741a2201, 0x781b681b, 0x43135acb, 0x00004718, 0x0000043c, 0xb5706803, 0xd0021e19, 0x230f7899, + 0x24004019, 0x4a122500, 0x189a00cb, 0x60556014, 0x4e100004, 0x8b323428, 0x78258282, 0x2d002440, 0x1924d000, + 0x83341914, 0xd00e2900, 0x2d002154, 0x3120d000, 0x30290649, 0x78011852, 0x181b4806, 0xd0032900, 0x2200601a, + 0xbd70605a, 0xe7fb6019, 0x50100080, 0x501009ec, 0x50100000, 0x1c45b530, 0x77e9b2c9, 0x702a3528, 0x000382c3, + 0x7824ac03, 0x701c3328, 0x00492301, 0x1889405a, 0x77d91cc3, 0xbe00bd30, 0xb5f00013, 0x000c0006, 0xb0850015, + 0x60013308, 0x78239303, 0x786318e4, 0xd1fa2b05, 0x79217963, 0x430b021b, 0x2101270f, 0x910078a2, 0x00394017, + 0x09d26828, 0xffd0f7ff, 0x4a05cd08, 0x601c00bf, 0x9b0350bb, 0xd1e442ab, 0xb0050030, 0x46c0bdf0, 0x50100a08, + 0x7fdb1c43, 0x78023029, 0x00db4903, 0x2a001858, 0x4a02d001, 0x47701898, 0x50100084, 0x50100080, 0x0006b570, + 0x00047f83, 0x2b003618, 0x8ac3d11f, 0x00037743, 0x78193329, 0x29001d43, 0x1d83d100, 0x8aa37fdd, 0x1e50002a, + 0x480a4182, 0x181b0192, 0x61a3189b, 0x42992300, 0x0020d106, 0xffd0f7ff, 0x1945006d, 0xb2db882b, 0x23017723, + 0x003077a3, 0x46c0bd70, 0x50100000, 0xb5702200, 0x77da1dc3, 0x00043301, 0x42907fd8, 0x2380d12f, 0x431900db, + 0x33290023, 0x2b00781b, 0x2380d002, 0x4319021b, 0x7fd31ca2, 0x001d2601, 0x41851e68, 0x77d34073, 0x036d0020, + 0xf7ff430d, 0x1d63ffa3, 0xb2ad7fda, 0x188040b2, 0x80052200, 0x77a21d21, 0x3a017fca, 0x68a177ca, 0x688a3428, + 0x608a3a01, 0x2a007822, 0x7fdad002, 0x77de4056, 0x20a0bd70, 0x43010140, 0xe7cd77da, 0x0004b570, 0x2b0068a3, + 0x689bd015, 0xd0122b00, 0x7ff51d26, 0xd00e2d00, 0x7fdb1de3, 0xd10a2b00, 0x33290023, 0x2b00781b, 0x0020d006, + 0xfef0f7ff, 0x42ab7ff3, 0xbd70d1e6, 0x8ae10020, 0xffa2f7ff, 0xb570e7e0, 0x0004000d, 0x00280011, 0xf7ff001a, + 0x2300fed7, 0x330160e3, 0x002060a5, 0xf7ff74ab, 0xbd70ffcf, 0x0003b510, 0x49034a02, 0xf7ff4803, 0xbd10ffe8, + 0x00003f59, 0x50100a24, 0x50100dc0, 0x68836841, 0x428bb510, 0x2300d003, 0xf7ff680a, 0xbd10ffd8, 0xb5100003, + 0x7fdb3333, 0x2b00302c, 0xf7ffd101, 0xbd10ffed, 0xb5700003, 0x781b3329, 0x000d0004, 0x2b000016, 0xf7ffd003, + 0x2300ff37, 0x00337103, 0x00200029, 0xf7ff4a01, 0xbd70ffba, 0x00003f59, 0x2200b510, 0x48034902, 0xffe4f7ff, + 0x46c0bd10, 0x501009c4, 0x50100f68, 0x2200b510, 0x48034902, 0xffd8f7ff, 0x46c0bd10, 0x501009d8, 0x50100f94, + 0x4b03b510, 0x49044a03, 0xf7ff4804, 0xbd10ff98, 0x0000075d, 0x00003f59, 0x501009c4, 0x50100f68, 0x1dc3b510, + 0x68817fda, 0xd1052a00, 0x2b0068cb, 0x7c0bd10a, 0xd1072b00, 0x23002401, 0x608374cc, 0xd002429a, 0x608b60cb, + 0x684bbd10, 0xd0012b00, 0xe7f94798, 0x2b0068c3, 0x6083d0f6, 0x749c60c2, 0xff4cf7ff, 0xbe00e7f0, 0x1dc6b570, + 0x00047ff3, 0x2b00000d, 0x1c43d11a, 0x2b007fdb, 0x0003d107, 0x781b3329, 0x4153425a, 0x33014a0a, 0x00206693, + 0xfebaf7ff, 0x68022380, 0x4313011b, 0x77f56003, 0x2b006923, 0x0020d001, 0xbd704798, 0xd2fc428b, 0xe7fa77f1, + 0x50112000, 0x2800b570, 0x4b18d00e, 0x58c40080, 0x7fcb1d21, 0x77cb3301, 0x2d0068a5, 0x2102d109, 0xf7ff0020, + 0xbd70ffc7, 0x29004c11, 0x4c11d1f0, 0x7c2be7ee, 0xd0022b00, 0x746b2301, 0x1da3e7f3, 0x332377da, 0x2b00781b, + 0x68abd002, 0xd0022b00, 0xf7ff0020, 0x7cebfe01, 0xd1e42b00, 0x002068eb, 0x60eb3b01, 0xff82f7ff, 0x46c0e7dd, + 0x50100a08, 0x50100f68, 0x50100f94, 0xb5702300, 0x00046885, 0xf7ff742b, 0x7cebff73, 0xd11b2b00, 0x36290026, + 0x2b007833, 0x0020d003, 0xf7ff7f21, 0x2300fe97, 0x7c6a77a3, 0xd00e429a, 0x1d22746b, 0x3b017fd3, 0x220177d3, + 0x7fdb1da3, 0x78313401, 0x405a7fe0, 0xffa0f7ff, 0x0020bd70, 0xfebef7ff, 0xbe00e7fa, 0x0004b5f8, 0xfe3ef7ff, + 0x68020025, 0x35284b19, 0xd01d421a, 0x2b00782b, 0x1d23d003, 0x2b017fdb, 0x2301d104, 0x7fd11ca2, 0x77d3404b, + 0x7fda1ce3, 0x40932301, 0x4a114910, 0x4e124811, 0x660b6653, 0x423b6e77, 0x3801d102, 0xd1112800, 0x66536613, + 0xf7ff0020, 0x782bfe17, 0x1e592200, 0x1d21418b, 0x60023301, 0x230177cb, 0x340884a2, 0xbdf877e3, 0xe7e5660b, + 0x04000400, 0x50112000, 0x50113000, 0x000186a0, 0x50110000, 0x4d08b570, 0xf7ff0028, 0x4c07ffb7, 0xf7ff0020, + 0x0028ffb3, 0xf7ff2101, 0x2101ff21, 0xf7ff0020, 0xbd70ff1d, 0x50100f68, 0x50100f94, 0x2500b570, 0x000e6085, + 0xf7ff0004, 0x42aeff9f, 0x1ca2d001, 0x77a577d5, 0x7fd11de2, 0xd0052900, 0x692377d5, 0xd0012b00, 0x47980020, + 0x7d5b4b07, 0xd0092b00, 0x29006861, 0x68a3d006, 0xd1032b00, 0x680a0020, 0xfe5ff7ff, 0x46c0bd70, 0x501009ec, + 0x4b1eb510, 0x601c4c1e, 0x4b1e2480, 0x601c05e4, 0x04e424e0, 0x4b1c601c, 0xd02c2800, 0x43202401, 0x61dc4c1a, + 0x426469dc, 0x621c4044, 0x62986259, 0x008921fa, 0x605a434a, 0x68114a15, 0x42112202, 0x4914d10b, 0x68094814, + 0x2103404a, 0x4913400a, 0x2204600a, 0x42116b81, 0x220cd0fc, 0x4b1062da, 0x32ff32f5, 0x2280601a, 0x05d24b0e, + 0x2201601a, 0x701a4b0d, 0x61d8bd10, 0x46c0e7da, 0x40010008, 0x0001fffc, 0x4005b000, 0x40058000, 0xb007c0d3, + 0x4006c000, 0x40008030, 0x40008000, 0x40009030, 0x4005a02c, 0x4005a000, 0x50100eb4, 0x0004b570, 0x20001845, + 0xd20142ac, 0xd0002800, 0x4b04bd70, 0x681b0020, 0x479868db, 0x015b2380, 0xe7f118e4, 0x50100dbc, 0x061222e0, + 0x22841881, 0x02d20003, 0x42912001, 0x22ebd907, 0x189b0612, 0x20002280, 0x429a01d2, 0x47704140, 0x1d85b530, + 0xb2e20f0c, 0x33370013, 0xd8002c09, 0x70033b07, 0x01093001, 0xd1f342a8, 0xbe00bd30, 0xb5102200, 0x48064b05, + 0x7a1b725a, 0xd8002b7f, 0x4a054804, 0xf7ff4905, 0xbd10fdf5, 0x50100ab8, 0x50100e58, 0x50100e24, 0x00000b5d, + 0x501008b0, 0x6a024b04, 0xb5106cdb, 0xd101429a, 0xffe0f7ff, 0x46c0bd10, 0x50100ac8, 0x4804b510, 0x7fdb1dc3, + 0xd1012b00, 0xfdc2f7ff, 0x46c0bd10, 0x50100e58, 0x48040003, 0xd8032b03, 0x3b014a03, 0x5898009b, 0x46c04770, + 0x00003db7, 0x00003ee8, 0x02004b02, 0x681b6018, 0x46c04770, 0x4001800c, 0x0004b570, 0x000d2002, 0xfff2f7ff, + 0x230422c0, 0x43290621, 0x0e080552, 0x66103b01, 0x2b000209, 0xbd70d1f9, 0x4a044b03, 0x00016018, 0x43996893, + 0x4770d1fb, 0x4000f000, 0x4000c000, 0x3029b5f7, 0x4c7a7803, 0xd00a2b00, 0x60632301, 0x8510f3ef, 0x0020b672, + 0x479868a3, 0x8810f385, 0x4b74bdf7, 0x781b2007, 0xd1092b00, 0x7fde1d63, 0xb25b7fdb, 0xda252b00, 0x07eb68e5, + 0x2005d502, 0xe7e56060, 0xf7ff0028, 0x2800ff55, 0x2004d101, 0x23ebe7f6, 0x061b2780, 0x01ff18eb, 0xd9f642bb, + 0x301c0028, 0xff46f7ff, 0xd0f02800, 0x18eb4b62, 0xd9ec42bb, 0x0028221c, 0xf0014960, 0x4b60fcf3, 0x601a68e2, + 0xd50e06f3, 0x7fd91da3, 0x1e53000a, 0x4b5c419a, 0x3329b2d2, 0x2902701a, 0x2201d103, 0x33074b59, 0x07f377da, + 0x0673d418, 0x230ad41d, 0xd00c421e, 0x7fda1de3, 0x33080023, 0x4b537fd9, 0xd0032900, 0x20067819, 0xd1b94291, + 0x07b3701a, 0x230cd410, 0xd125421e, 0xe7b12000, 0x681b4b48, 0x4798689b, 0xd0e02800, 0x2301e7aa, 0x431368e2, + 0xe7dc4798, 0x05036920, 0x6961d1a1, 0xd19e050b, 0x228025f0, 0x1943062d, 0x42930552, 0x1843d89f, 0x4293195b, + 0x4b3ad89b, 0x691b681b, 0x28004798, 0xe78dd0d7, 0x002868e5, 0xfee4f7ff, 0xd0052800, 0x18e869e3, 0xfedef7ff, + 0xd11e1e07, 0xd40a0733, 0x01922280, 0xd2064295, 0x18eb69e3, 0xd3004293, 0x2701e77d, 0x21f0e011, 0x06092280, + 0x0552186b, 0xd9004293, 0x69e3e773, 0x185b18eb, 0xd9004293, 0xb2efe76d, 0xd0002f00, 0x0733e761, 0x4b20d518, + 0x469c69a1, 0x2f00681b, 0x69e2d023, 0x42ab9501, 0x9301d900, 0x18a8331c, 0xd2004283, 0x9b010018, 0xd2024283, + 0x4b154660, 0x00286003, 0xfc5af001, 0xd5070773, 0x69a068e3, 0xd0102f00, 0x69e20019, 0xfc50f001, 0xd58706b3, + 0x681b4b0d, 0x4798685b, 0x0028e736, 0x4798695b, 0xd0e92800, 0x4a08e730, 0x68120001, 0x69920018, 0x28004790, + 0xe727d0ea, 0x50100d8c, 0x50100eb4, 0xeb00001c, 0x00003ecc, 0x50100dbc, 0x50100fc0, 0x50100a7c, 0x50100a1c, + 0x6a46b5f8, 0x00330004, 0x781b3329, 0x2b000030, 0xf7ffd023, 0x6963fbcd, 0x001f69e2, 0x69a00005, 0x37401e51, + 0x40192240, 0xd9004287, 0x712a1ac2, 0x68286a23, 0xf0011859, 0x792bfc0d, 0x00206962, 0x616318d3, 0x681b6aa3, + 0x4b064798, 0x681a0030, 0x601a4b05, 0xfd2ef7ff, 0xf7ffbdf8, 0x0005fba9, 0x46c0e7eb, 0x50100f64, 0xd0000018, + 0x4c0cb570, 0x6ce26a03, 0x42930005, 0x6843d111, 0x60734e09, 0xd0052b00, 0x6a602102, 0xfcb2f7ff, 0x72732300, + 0x6ba269eb, 0x18d30020, 0xf7ff63a3, 0xbd70ffb1, 0x50100ac8, 0x50100ab8, 0x0004b570, 0x4d0a2601, 0x4b0a7568, + 0x58d000b2, 0xd0022800, 0xf7ff2101, 0x3601fd7f, 0xd1f42e05, 0x41841e60, 0x0028686b, 0x4798b2e1, 0x46c0bd70, + 0x501009ec, 0x50100a08, 0x2000b510, 0xffe0f7ff, 0x4a052300, 0x4a057513, 0x22016013, 0x42524b04, 0x651a659a, + 0x46c0bd10, 0x501009ec, 0x50110000, 0x50113000, 0x4b0e2220, 0x601ab510, 0x68184b0d, 0x061b23d0, 0x62586158, + 0xfa27f7ff, 0x4b0a2240, 0x009b18c3, 0x430a6819, 0x601a2180, 0x438a681a, 0x4b06601a, 0x230518c0, 0x604300c0, + 0x46c0bd10, 0x4000f000, 0x50100f64, 0x10007001, 0x08002800, 0x000ab510, 0xf0012100, 0xbd10fbc7, 0xb5704b11, + 0x4c11781a, 0xd1072a00, 0x49114a10, 0x60116322, 0x61224a10, 0x701a2201, 0x21284d0f, 0xf7ff0028, 0x2370ffe7, + 0x3b66736b, 0x2500752b, 0x00294b0b, 0x611d0020, 0xfd10f7ff, 0x00290020, 0xf7ff302c, 0xbd70fd0b, 0x50100e20, + 0x50100dc0, 0x50100a38, 0x00003dda, 0x0000144d, 0x50100a7c, 0x50100d3c, 0x780b2260, 0x401ab510, 0x2a202400, + 0xb25bd114, 0x42a3784a, 0x2afeda12, 0x884bd10e, 0xd10b42a3, 0x42a388cb, 0x4813d008, 0xfadef7ff, 0x701c6803, + 0x71043401, 0xfbc4f7ff, 0xbd100020, 0xd1fb2aff, 0x2b00884b, 0x88cad1f8, 0x2a00001c, 0x4b0ad1f4, 0x7fd11dda, + 0xd1012903, 0x77d13901, 0x781a3352, 0xd1012a03, 0x701a3a01, 0xff98f7ff, 0xf7ff2401, 0xe7e1fb8f, 0x50100f68, + 0x50100dc0, 0x000db5f7, 0x00102180, 0x00140089, 0xff82f7ff, 0xd1202d00, 0x30c30020, 0x494f220b, 0xf00130ff, + 0x23fffaf7, 0x005b2255, 0x4b4c54e2, 0x54e21892, 0x791a4b4b, 0xd1042a00, 0x6a924a4a, 0x2201601a, 0x0020711a, + 0x30b9681b, 0x30ff9301, 0xa9012204, 0xfadef001, 0x2d01e023, 0x4b41d111, 0x2a00791a, 0x4a40d103, 0x711d6a92, + 0x681b601a, 0x00202240, 0x9301493d, 0xfaccf001, 0x30270020, 0x2281e7e6, 0x00521eab, 0xd20c4293, 0xd9002b80, + 0x2b003b81, 0x3b08d105, 0x33078023, 0x80a38063, 0x200080e3, 0x1f6bbdfe, 0x2b1f3bff, 0x2b00d839, 0x492ed1f7, + 0x312b220b, 0xf0010020, 0x2328faab, 0x002372e3, 0x00202264, 0x4d2a4e29, 0x701a332d, 0x3a594929, 0x862585e6, + 0x872586e6, 0xf0013020, 0x0023fa99, 0x332b2121, 0x23027019, 0x33ef8763, 0x002763e3, 0x22640023, 0x3740334e, + 0x491f737a, 0x805d801e, 0x815d811e, 0x00383a59, 0xfa82f001, 0x21210023, 0x335a2203, 0x801a72f9, 0x65e3233e, + 0x3d25e7bf, 0x076b3dff, 0x08edd1bb, 0x0022d110, 0x4813219f, 0xfa4ff001, 0x4d120020, 0x0029220c, 0xf0013062, + 0x0020fa67, 0x0029220c, 0xe78130c2, 0xd1a62d01, 0x0020223e, 0xe77b490b, 0x00003df8, 0x000001ff, 0x50100db4, + 0x40054000, 0x00003e6c, 0xffff8299, 0x00003925, 0x00003dac, 0x00003db8, 0x00003ffa, 0x50100eb5, 0x00003ef4, + 0x4b85b5f0, 0x68120016, 0x9003b085, 0xd002429a, 0xb0052000, 0x4b81bdf0, 0x429a6872, 0x22fed1f8, 0x4b7f0052, + 0x429a58b2, 0x68b3d1f2, 0xd5ef049a, 0x69f14a7c, 0xd1eb4291, 0x001d2201, 0x42134015, 0x2380d1e6, 0x005b6932, + 0xd1e1429a, 0x003868f7, 0xfc62f7ff, 0xd0042800, 0x30ff0038, 0xfc5cf7ff, 0x69b30005, 0x2b004c70, 0x22f0d009, + 0x18ba0612, 0x2d009201, 0x0011d106, 0x42914a6c, 0x2000d907, 0xe7c66120, 0x00119a01, 0x42914a68, 0xb2ffd802, + 0xd1f42f00, 0x32294a66, 0x2a007812, 0x3201d1ef, 0x9202402a, 0x27086922, 0xd0354293, 0x21500020, 0xfe6cf7ff, + 0x9a020023, 0x485e334c, 0x2d00701a, 0x20a8d101, 0x42690540, 0x23a04169, 0x42494d5a, 0x402900db, 0x606118c9, + 0x602008c9, 0xfe56f7ff, 0x001a9b01, 0x429a4b51, 0x4854d806, 0x49554b54, 0x60e360a0, 0xfe4af7ff, 0x686269b3, + 0xd9004293, 0x6123e786, 0x27002301, 0x9a01425b, 0x4b4761e3, 0x429361a7, 0x3708417f, 0x334c0023, 0x9a02781b, + 0xd1ac4293, 0x69b26973, 0xd2a84293, 0x21280020, 0xf7ff3024, 0x0022fe2b, 0x32489b03, 0x62636971, 0x68f36163, + 0x4a407017, 0x46942001, 0x228062e2, 0x64220052, 0x324a0022, 0x221f7010, 0x4090400a, 0x094a6825, 0x62210092, + 0x18aa6323, 0x36206815, 0x900163e6, 0xd0004205, 0x2580e74a, 0x05ad69e6, 0xd20b42b3, 0x41bf42ab, 0x0038427f, + 0x41bf42ae, 0x9702427f, 0x98020007, 0xd0034287, 0xd20242ae, 0xd30042ab, 0x002561e3, 0x782d354c, 0xd11a2d00, + 0x05c9020e, 0x35010ec9, 0x0c71408d, 0x008968a6, 0x680e1871, 0xd10e422e, 0x031b0b1b, 0x23806363, 0x63a3015b, + 0x431d680b, 0x0021600d, 0x31482302, 0x432b780d, 0x69a3700b, 0x33019801, 0x681361a3, 0x43180021, 0x4d0e4663, + 0x35286010, 0x222862e3, 0x480b3124, 0xf001782b, 0x2001f93f, 0xbf407028, 0x7020344b, 0x46c0e6fd, 0x0a324655, + 0x9e5d5157, 0x0ab16f30, 0xe48bff56, 0x50100d3c, 0x0fffff01, 0x50100fc0, 0x50100ec4, 0x0001dce0, 0x15003c3c, + 0x00001e1e, 0x000003c3, 0x00001631, 0x000db570, 0xf8c8f7ff, 0x00290004, 0xf7ff6800, 0x6820fd97, 0xbd707125, + 0xb5102200, 0x210d4c07, 0x48071da3, 0xf7ff77da, 0x220dffeb, 0xf0010021, 0x4804f905, 0xf94cf7ff, 0x46c0bd10, + 0x50100a7c, 0x50100dc0, 0x00000705, 0x1dc3b510, 0x2b007fdb, 0x4b05d109, 0xd1064298, 0x33064b04, 0x2b007fdb, + 0xf7ffd001, 0xbd10ffd9, 0x50100dc0, 0x50100a7c, 0x4a0cb510, 0x7fd91d53, 0xd1072901, 0x005b2380, 0x84934809, + 0xf7ff3101, 0xbd10f9a7, 0xd1052902, 0x48052200, 0x302c77da, 0xf99ef7ff, 0xffbcf7ff, 0x46c0e7f3, 0x50100a7c, + 0x50100dc0, 0xb5702300, 0x1d654c11, 0x622377eb, 0x33017b05, 0xd8002d7f, 0x429a3301, 0x1d62d007, 0x230277d3, + 0xf7ff7323, 0x2000ffcf, 0x6883e00f, 0xd008428b, 0x77c21d60, 0xd201428b, 0x73222202, 0xd900428b, 0x2001000b, + 0x2b006223, 0xbd70d0eb, 0x50100a7c, 0x7c82b5f8, 0x02127c43, 0x7cc3431a, 0x041b000f, 0x7d03431a, 0x061b7dc1, + 0x7d824313, 0x43110209, 0xba494e20, 0xba1b0032, 0x2f01b289, 0x322cd000, 0x02494c1d, 0x003a6262, 0xf7ff62e3, + 0x2800ffb9, 0x4b1ad029, 0x2d3f6a1d, 0x4a19d928, 0x68130020, 0x33014918, 0x233f6013, 0x4b17439d, 0x62a361a5, + 0x4a174b16, 0x23806223, 0x61e3009b, 0x61632300, 0xff7ef7fe, 0x68a3353f, 0x195b09ad, 0x68e360a3, 0x60e5195d, + 0xd1072f01, 0x00302300, 0x60f360b4, 0xf7ff74a7, 0xbdf8f86d, 0xe7fc63b4, 0xff6af7ff, 0x46c0e7f9, 0x50100dc0, + 0x50100a4c, 0x50100a7c, 0x50100a20, 0x00003e17, 0x00003de4, 0x50100b1c, 0x00001475, 0x0005b570, 0xf7fe480c, + 0x68a9ffe7, 0x00047903, 0xd9004299, 0x22010019, 0xf7ff0028, 0x2800ff67, 0x4a06d009, 0x6a134806, 0x68917123, + 0x1acbb2db, 0xf7ff6093, 0xbd70f86d, 0x50100dc0, 0x50100a7c, 0x00001475, 0x4b082100, 0x1d5ab510, 0x688277d1, + 0xd006428a, 0x22017b01, 0xd800297f, 0x33051892, 0xf7ff77da, 0xbd10ff25, 0x50100a7c, 0x68024b1a, 0xb5106959, + 0x428a0004, 0x6840d111, 0xd10e2800, 0x6919699a, 0xd10a428a, 0x324c001a, 0x2a007812, 0x69d8d000, 0x491122fa, + 0xf7ff0092, 0xcc0af9cd, 0x8410f3ef, 0x4a0eb672, 0x42916812, 0x2b00d111, 0x2201d00c, 0x731a4b0b, 0x73da3206, + 0x765a3219, 0x769a3a1e, 0x77da3305, 0xfef2f7ff, 0xf7ff4806, 0xf384fbab, 0xbd108810, 0x50100d3c, 0x20042000, + 0x50100a20, 0x50100a7c, 0x50100a4c, 0xb5102100, 0xf7ff480f, 0x480ff979, 0xf7ff2100, 0x480ef975, 0x2b007a43, + 0x2280d003, 0x02924b0c, 0x2110601a, 0xfc36f7ff, 0x22004b0a, 0x33290019, 0x700a3128, 0x4b08701a, 0x33290019, + 0x700a3128, 0xbd10701a, 0x50100e58, 0x50100e24, 0x50100ab8, 0x4001a01c, 0x50100fc0, 0x50100e84, 0x4b052280, + 0xb5100292, 0x2900601a, 0xf7ffd003, 0xf7fffc19, 0xbd10ffc7, 0x4001a01c, 0x780b2260, 0xb5102000, 0x2a40401a, + 0xb25bd113, 0x4283784a, 0x2a42da10, 0x88ccd10d, 0xd10a2c10, 0x48090021, 0xfe5af7ff, 0x49080022, 0xff74f000, + 0xf80cf7ff, 0xbd102001, 0xd1fc2a41, 0xffa4f7ff, 0xffecf7fe, 0x46c0e7f6, 0x50100f68, 0x50100ab8, 0x4e18b5f8, + 0x001524c0, 0x056446b4, 0x4316001e, 0xd103432e, 0xf7ff2003, 0xbdf8f9ff, 0x6a666a27, 0xd0152a00, 0x2f0d19bf, + 0x1e07d812, 0x7807d001, 0x66273001, 0x2e003a01, 0x6e26d0e8, 0xd0012b00, 0xe7e33b01, 0xd0012900, 0x3101700e, + 0xe7dd3d01, 0xd1f22e00, 0x27c04666, 0x02bf6836, 0xd0d5423e, 0x46c0e7d8, 0x4001801c, 0x000cb510, 0x20030001, + 0xf9daf7ff, 0x23042280, 0x20000021, 0xf7ff0052, 0xbd10ffbf, 0xb51023f0, 0x18c0061b, 0xffecf7ff, 0xbd102000, + 0x2002b510, 0xf9bef7ff, 0x220623c0, 0x661a055b, 0x23012200, 0x00100011, 0xffa8f7ff, 0xbe00bd10, 0x26c0b573, + 0x05762401, 0xf7ff2002, 0x2305f9ab, 0x466b6633, 0x00221ddd, 0x00290023, 0xf7ff2000, 0x782bff95, 0xd0054223, + 0x681a4b03, 0x029b23c0, 0xd0e9421a, 0x46c0bd73, 0x4001801c, 0x0005b570, 0xf7ff000c, 0x0029ffcd, 0xf7ff0020, + 0x2200f993, 0x00112304, 0xf7ff0010, 0xf7ffff79, 0xbd70ffd1, 0xb51023f0, 0x18c0061b, 0xf7ff2120, 0x2000ffe7, + 0xb570bd10, 0x000c0005, 0xffb2f7ff, 0x20020029, 0xf978f7ff, 0x23042280, 0x00202100, 0xf7ff0052, 0xf7ffff5d, + 0xbd70ffb5, 0xb51023f0, 0x18c0061b, 0xffe7f7ff, 0xbd102000, 0x4b822280, 0x0452b5f0, 0x4a81601a, 0x68120006, + 0xb085000d, 0xd4390792, 0x4a7e2003, 0x4c7f497e, 0x4a7f6011, 0x4a7f6010, 0x67a2487f, 0x3aff3aff, 0x6c606002, + 0xd0fc4210, 0x00522280, 0x65a26422, 0x60114a7a, 0x29006851, 0x2080dafc, 0x60180140, 0xf94ef7ff, 0x4b762201, + 0x601a2121, 0x609a3263, 0x02d222aa, 0x4a7360da, 0x68196011, 0xdafc2900, 0x60132308, 0x63e32300, 0x22012382, + 0x6563011b, 0x601a4b6d, 0x6c622302, 0xd0fc421a, 0x04402080, 0xf92ef7ff, 0x4b69220c, 0x62da2180, 0x32f54b68, + 0x601a32ff, 0x4b672201, 0x601a4867, 0xf7ff0149, 0x4b66fadf, 0x43eb601e, 0xd100079b, 0x2e002500, 0xf7ffd001, + 0x4c62faaf, 0x00204b62, 0xf7ff6819, 0x4b61f8a7, 0x68191da0, 0xf8a2f7ff, 0x4c5f2601, 0xd01f2d00, 0x22204f5e, + 0x00380021, 0xfe3ef000, 0x70bb2320, 0x26012300, 0x002b70fb, 0x93034033, 0xd0074235, 0x00380021, 0x31202217, + 0xf0003009, 0x2600fe2d, 0x713b2301, 0x72fb2300, 0x003c9b03, 0xd1082b00, 0x4f4f0021, 0x00384a4f, 0xf7fe3109, + 0x4b4efd91, 0x07ab607b, 0x2117d40f, 0x4e4c4371, 0x4a4c3109, 0x18610030, 0xfd84f7fe, 0x4a4b4b4a, 0x4a4b601a, + 0x4b4b6053, 0x4b4b6073, 0xd1002d01, 0x4d4a3304, 0x60ec4a4a, 0x4f4a2400, 0x4b4a612b, 0x602a0021, 0x220160ab, + 0x94002340, 0xf7fe0038, 0x4e46fd55, 0x00210022, 0x94002340, 0xf7fe0030, 0x2380fd4d, 0x005b0021, 0x832b0038, + 0xfd12f7fe, 0x00212380, 0x0030005b, 0xf7fe832b, 0x23c0fd0b, 0x832b005b, 0x4b3a3401, 0x58d000a2, 0xd0022800, + 0xf7fe2100, 0x3401fcff, 0xd1f42c05, 0x20004b35, 0x4b35606b, 0x001c4a35, 0x33040019, 0x42936008, 0x2309d1fa, + 0x33036763, 0x3b0b67a3, 0xf7ff6423, 0x4b2ffa03, 0x64e34a2f, 0x601a4b2f, 0x4a2f2320, 0x4a2f6013, 0xf7fe6013, + 0x46c0fbe5, 0x4000e000, 0x4006c000, 0x40060000, 0x00fab000, 0x40008000, 0x4000b030, 0x000001ff, 0x4000b03c, + 0x40024000, 0x40028000, 0x4002b004, 0x4000a03c, 0x40058000, 0x4005a02c, 0x14003000, 0x50100000, 0x50100f64, + 0x50100eb5, 0x40000040, 0x00000050, 0x00003e19, 0x50100d1c, 0x50100e18, 0x00003ddc, 0x00000fb5, 0x50100e50, + 0x00003f38, 0x50100aa4, 0x00003dec, 0x50100e58, 0x00001729, 0x00003e64, 0x501009ec, 0x00003e50, 0x50100f68, + 0x00000b75, 0x50100f94, 0x50100a08, 0x0000170d, 0x50110000, 0x50110084, 0x20010000, 0x000113f0, 0x50110090, + 0xe000e280, 0xe000e100, 0xb5104b02, 0x691968d8, 0xfe98f7ff, 0x40058000, 0xb5f74b24, 0x4b24681a, 0x601a6884, + 0x69e50002, 0x69633229, 0x1e6e7812, 0x9201401e, 0xd0152a00, 0xd0032e00, 0xf7ff0020, 0xbdf7f90d, 0x195969a2, + 0xd9004291, 0x2d001ad5, 0x6aa3d0f4, 0x791b4a17, 0x5ad30028, 0x28004798, 0xe7eed0ec, 0xfcd2f7fe, 0x69a26961, + 0x00073140, 0x429169e3, 0x0015d312, 0x400d1e59, 0xd100420a, 0x2e00001d, 0x69e1d103, 0xf7ff6a20, 0x6a23f993, + 0x6839793a, 0xf0001998, 0xe7d8fd0b, 0x0033001d, 0x42ab3340, 0x9d01d2ed, 0x46c0e7eb, 0x50100f64, 0xd0000014, + 0x0000043c, 0x0006b5f8, 0xfca6f7fe, 0x2b1f7903, 0xe0a0d000, 0x4b556805, 0x429a682a, 0xe09ad000, 0x2b007b6b, + 0xe096d000, 0x337f7b29, 0x401a000a, 0xd0004219, 0x7babe08f, 0x2b0f3b01, 0xe08ad900, 0x4b4c4c4b, 0x686b6023, + 0x68ab6063, 0x7beb60a3, 0xd0022b03, 0x766273e2, 0x270076a2, 0x2b237327, 0xd818d047, 0xd03b2b1a, 0x2b03d80d, + 0x2b12d051, 0x42bbd025, 0x2301d05a, 0x33047323, 0x331b73e3, 0x23007663, 0x2b1be05c, 0x2b1ed05c, 0x0028d1f3, + 0xfc98f7ff, 0x2b2ae00a, 0xd80bd029, 0xd0312b25, 0x2b282101, 0x0028d1e7, 0xfc08f7ff, 0xf7fe0030, 0xbdf8fdd5, + 0xd0ea2b2f, 0xd0e82b35, 0x2124e7db, 0xf7ff482d, 0x2119fb7d, 0x482c0004, 0x18400022, 0xfc75f000, 0x70632380, + 0xf7ff0028, 0xe7e5fc51, 0x48252104, 0xfb6cf7ff, 0x60032303, 0x2102e7f4, 0x210ce7d9, 0xf7ff4820, 0x220cfb63, + 0xf0004920, 0xe7e9fc7d, 0x481c2108, 0xfb5af7ff, 0x491d2208, 0x2112e7f5, 0xf7ff4818, 0x0021fb53, 0x310d2212, + 0xfc6cf000, 0x766773e7, 0xe7d576a7, 0x7fd21de2, 0xd0ac2a00, 0x73222201, 0x73e21892, 0x76623238, 0xe7a476a3, + 0x7ceb2203, 0x2b024013, 0x3407d19f, 0x77e33b01, 0x4c08e79b, 0x00202103, 0xfd1cf7fe, 0x21030020, 0xf7fe302c, + 0xe79ffd17, 0x43425355, 0x50100a7c, 0x53425355, 0x50100dc0, 0x00003f40, 0x00003e0b, 0x00003e03, 0x4b09b510, + 0x6a5a4c09, 0x78123229, 0xd1002a00, 0x4a084c07, 0x1c486811, 0x60106ad9, 0x62da1c4a, 0x47a04a05, 0x46c0bd10, + 0x50100a4c, 0x00001031, 0x000011b9, 0x50100a20, 0x50100b1c, 0x4baab5f7, 0x9301681b, 0xd40003db, 0xf3bfe088, + 0x4fa78f5f, 0x00382100, 0xf7fe2401, 0x4ea5fdc3, 0x00302100, 0xfdbef7fe, 0x1cb3211f, 0x4aa277dc, 0x77dc1cbb, + 0x00157813, 0x42a14019, 0xe0a2d100, 0xd1002902, 0x2900e0ca, 0x3160d157, 0xd154420b, 0x2b00b25b, 0x0038da4c, + 0xfb9cf7fe, 0x0007786b, 0x2b066806, 0x2b08d00d, 0x2b00d03e, 0x8033d145, 0x88eb1924, 0xdd0042a3, 0x713b0023, + 0xfc76f7fe, 0x8868e04a, 0x2b020a03, 0x2b03d00d, 0x2b01d016, 0x4b8ad133, 0x68192412, 0xd0ea2900, 0x00300022, + 0xfbcaf000, 0xb2c0e7e5, 0xd1262800, 0x68d94b83, 0x788b78cc, 0x431c0224, 0xe7efd0db, 0x2800b2c0, 0x4b7ed00f, + 0x689b2402, 0x30014798, 0x781b1e43, 0xd1032b00, 0x70343303, 0xe7ca7073, 0x34025333, 0x2404e7f3, 0xe7d94976, + 0x7d5b4b74, 0xe7c07033, 0x2b057853, 0x2b09d004, 0xf7fed038, 0xe00bfd3d, 0x56d32302, 0x2b008851, 0x4b6cddf7, + 0x75990038, 0x496d4a6c, 0xfbfcf7fe, 0x4b6c2280, 0x651a0292, 0x06db9b01, 0x2600d50a, 0x4f692401, 0x2d006dbd, + 0x2e0ad004, 0xe082d000, 0x659d4b64, 0x04db9b01, 0xf7fed505, 0x2280ffbf, 0x03124b60, 0x23f8651a, 0x009b9a01, + 0xd008421a, 0x6d1b4b5d, 0xdb002b00, 0x2280e07e, 0x06124b59, 0xbdf7651a, 0x28007890, 0x4b52d004, 0x795b68db, + 0xd1be4283, 0xff86f7fe, 0xfbdcf7fe, 0x8893e7c8, 0x7d514a4c, 0xd0b42900, 0x420b21fe, 0x6912d1b1, 0x009bb2db, + 0x28005898, 0x6843d0ab, 0xd1142b00, 0x782b2260, 0xd1a44213, 0x0a1288aa, 0xb25bd1a1, 0xda9e2b00, 0x2c00786c, + 0x0038d19b, 0xfae6f7fe, 0x601c6803, 0x71032302, 0x0029e754, 0x28004798, 0xe7e5d19e, 0x2a008892, 0x2a80d041, + 0x4935d060, 0x29007d49, 0xe784d100, 0x00a04938, 0x68065840, 0x42b278b6, 0x3401d034, 0xd1f62c05, 0x7869e779, + 0xd00a2901, 0xd0002903, 0x886be773, 0xd0002b00, 0x2102e76f, 0xfbdaf7fe, 0x886be7ae, 0xd0002b00, 0x1dc2e767, + 0x2a027fd2, 0xf7fed802, 0xe7a3fcb7, 0x77c33002, 0x422ce7a0, 0x2101d00b, 0x4b216dfa, 0x659c4022, 0x1e530870, + 0x43b1419a, 0xfbe6f7fe, 0x006443a5, 0xe7663601, 0xff30f7fe, 0x0030e781, 0x42132260, 0xe744d000, 0x2b00b25b, + 0x786bdac7, 0xd0002b00, 0x886be73d, 0xd0002b00, 0x88ece739, 0xd0002c02, 0x2501e735, 0x7fc33007, 0x429d0038, + 0xf7fe41ad, 0x6803fa7b, 0x601d426d, 0xe6e97104, 0xe7dd0038, 0x50110098, 0x50100f68, 0x50100f94, 0x50100000, + 0x501009ec, 0x00003f32, 0x0000045d, 0x501009c4, 0x50113000, 0x50110000, 0x50100a08, 0xf7feb510, 0xbd10fbdd, + 0x4a1ab5f8, 0x601a4b1a, 0x8510f3ef, 0xf3bfb672, 0x4c188f5f, 0x37280027, 0xb2de783b, 0xd1132b00, 0x8810f385, + 0x8510f3ef, 0xf3bfb672, 0x4c128f5f, 0x37280027, 0x2b00783b, 0x2228d013, 0x480f0021, 0xfa8af000, 0xe006703e, + 0x00212228, 0xf000480b, 0x2300fa83, 0xf385703b, 0x00208810, 0xfd4af7fe, 0xf385e7d2, 0xbf208810, 0x46c0e7ce, + 0x00003ecc, 0x50100dbc, 0x50100fc0, 0x50100e84, 0x50100d8c, 0x9001b5f7, 0xfa16f7fe, 0x2b207903, 0xe09ed000, + 0x4b536806, 0x429a6832, 0xe098d000, 0x21284c51, 0x302c0020, 0xfedaf7fe, 0x68134a4f, 0x60133b01, 0x62e36872, + 0x7a3364e2, 0x4b4c9300, 0x601a9900, 0x725a2200, 0x605a3201, 0x72196932, 0x63a263e2, 0x00224694, 0x32516971, + 0x7c306421, 0x227f7010, 0x40029800, 0x28081e50, 0xe070d900, 0x43422003, 0x7a77483f, 0x42af5c85, 0x2500d161, + 0x605d1880, 0x78407843, 0xb240000d, 0xdb002800, 0x68f3001d, 0xd15642ab, 0x189a4b36, 0x78979b00, 0xd1092b02, + 0x69b24660, 0xfbf6f7fe, 0xfc80f7fe, 0xf7fe9801, 0xbdf7fb43, 0xd0472f00, 0x33500023, 0x0023701f, 0x26012202, + 0x701a3352, 0x725e4b28, 0xd0282d00, 0x4a294b28, 0x62236463, 0x005b2380, 0x230061e3, 0x616362a2, 0x61a54a25, + 0x49250020, 0xf90cf7fe, 0x68a3353f, 0x195b09ad, 0x68e360a3, 0x195d003a, 0x60e52308, 0x421f401a, 0x4b1ed003, + 0x60dc6263, 0x481de7cc, 0x626074a6, 0x60c26084, 0xf9f4f7fe, 0x481ae7c4, 0x00050021, 0x35284b19, 0x22286363, + 0x782b312c, 0xf9d0f000, 0xbf40702e, 0x2202e7b6, 0x9b00e004, 0xd0ab2b02, 0x4b082203, 0x2102605a, 0xf7fe480d, + 0x2102fa8f, 0xf7fe480a, 0xe7a5fa8b, 0x431fd10b, 0x50100ac8, 0x50100eb0, 0x50100ab8, 0x00003eac, 0x501008c4, + 0x00003df0, 0x00000b15, 0x00003e17, 0x50100e58, 0x50100e24, 0x50100e84, 0x00000b45, 0x4c09b570, 0x64a04b09, + 0x00214809, 0x63630005, 0x22283528, 0x782b312c, 0xf994f000, 0x70282001, 0x3453bf40, 0xbd707020, 0x50100ac8, + 0x00000e59, 0x50100e84, 0x220023c0, 0x609a055b, 0x49054a04, 0x001a601a, 0x601132f4, 0x609a2201, 0x46c04770, + 0x001f0300, 0x03000218, 0xf7ffb510, 0x2000ffeb, 0xbe00bd10, 0x230122a0, 0x0552b510, 0x68526053, 0x20004a02, + 0xf7fe6013, 0xbd10fc0d, 0x14002000, 0x0004b5f8, 0x001f0015, 0x42b41846, 0x22c0d205, 0x02924b0c, 0x4213681b, + 0xbdf8d000, 0x421c1e6b, 0x1b33d108, 0xd30542ab, 0x00390020, 0xfa5ef7ff, 0xe7ea1964, 0x21200020, 0xfa58f7ff, + 0x015b2380, 0xe7e218e4, 0x4001801c, 0x0005b5f8, 0x0004000f, 0x1b791886, 0x42b41909, 0x22c0d205, 0x02924b05, + 0x4213681b, 0xbdf8d000, 0x34010020, 0xfa59f7ff, 0xe7ee34ff, 0x4001801c, 0xb5f02301, 0x425bb085, 0x803baf03, + 0x220023c0, 0x609a055b, 0x6c9a6a9a, 0x615a2206, 0x02d222e0, 0x2201601a, 0x611a4c1f, 0x6826609a, 0x0032238c, + 0x439a2584, 0x43152003, 0xfbb0f7fe, 0x93012302, 0x60252380, 0x6065011b, 0x60e560a5, 0xd1fd3b01, 0x22042300, + 0x00180019, 0xf994f7ff, 0x2108230c, 0x2002439d, 0xf7fe430d, 0x9901fb99, 0xd1162901, 0x6026220c, 0x43966066, + 0x26080033, 0x60a6431e, 0x60e62002, 0xfb8af7fe, 0x22022300, 0x00380019, 0xf978f7ff, 0x4b042200, 0xb005601a, + 0x2301bdf0, 0x46c0e7cd, 0x40020008, 0x4001800c, 0x4b072090, 0xb5100080, 0xf7fe6018, 0x2200fb8b, 0x605a4b04, + 0x615a60da, 0x625a61da, 0xbd1062da, 0x4000e000, 0x40018000, 0xf7ffb510, 0xf7ffffe9, 0x4b04ff91, 0x2b00681b, + 0xf7fed001, 0x2000fd0d, 0x46c0bd10, 0x50100f64, 0x4a2bb5f8, 0x60134b2b, 0x60134a2b, 0x4b2b2202, 0x4213681b, + 0x2105d008, 0x60194b29, 0x061b23d0, 0x641a631a, 0x641a2200, 0x005b23c8, 0xd1fd3b01, 0x240920d0, 0x25042200, + 0x06002101, 0x3b01002b, 0x6883d1fd, 0x085b3c01, 0x18d2400b, 0xd1f52c00, 0xd9232a04, 0xf7ff26c0, 0xf7ffffaf, + 0x0576ff57, 0x22c02700, 0x683360b7, 0x43934d16, 0x4313b2e2, 0x23016033, 0x60b30029, 0xf7ff0038, 0x2201f93d, + 0x002821fc, 0xf0004252, 0x4b0ff833, 0x4298681b, 0x2380d008, 0x019b3440, 0xd1e1429c, 0x00082100, 0xf9a4f7ff, + 0xfee8f7ff, 0x46be3501, 0x46c04728, 0x4000e000, 0x00200240, 0x4000f000, 0x4006c000, 0x4001800c, 0x20041f00, + 0x20041ffc, 0xb5104b05, 0x60d8220a, 0x48046119, 0xf7fe4904, 0xbf30fa1b, 0x46c0e7fd, 0x40058000, 0x00001b99, + 0x20042000, 0x1809b530, 0xe00c4d5d, 0xba137804, 0x0624405c, 0x00642308, 0x406cd300, 0xd1fa3b01, 0x40620212, + 0x42883001, 0x1c10dbf0, 0xb530bd30, 0x5c434249, 0x0a1c3201, 0xd10541ad, 0x3101b25d, 0x41aa5c44, 0x5d631b14, + 0x35015553, 0x3101d1fb, 0xbd30d1ef, 0x2a084684, 0xb470d33a, 0x3a01e01c, 0x54835c8b, 0x4660d1fb, 0x46c04770, + 0x2a084684, 0x1a43d32e, 0xd1f2079b, 0x1a09b470, 0x08431c05, 0x5c44d302, 0x30017004, 0xd3020883, 0x80045a44, + 0x18093002, 0x19521a2d, 0xd3033a10, 0xc078c978, 0xd2fb3a10, 0xd3010752, 0xc018c918, 0xd3010052, 0xc008c908, + 0x0052d009, 0x880bd304, 0xd0048003, 0x30023102, 0x780bd001, 0xbc707003, 0x47704660, 0x0092a309, 0x33011a9b, + 0x46c04718, 0x7183798b, 0x7143794b, 0x7103790b, 0x70c378cb, 0x7083788b, 0x7043784b, 0x7003780b, 0x47704660, + 0xb2c94684, 0x4319020b, 0x46c0e011, 0x2a084684, 0x0843d32a, 0x7001d301, 0xb2c93001, 0x4319020b, 0xd3010883, + 0x30028001, 0x1a1b4663, 0xba0b18d2, 0x1c0b4319, 0xd3073a10, 0x1c0cb430, 0x46c01c0d, 0x3a10c03a, 0xbc30d2fc, + 0xd3000752, 0x0052c00a, 0xc002d300, 0x0052d006, 0x8001d302, 0x3002d002, 0x7001d000, 0x47704660, 0x1a9ba305, + 0x33011a9b, 0x71814718, 0x71017141, 0x708170c1, 0x70017041, 0x47704660, 0x04c11db7, 0x4608b505, 0xbd0a461a, + 0x02400dc2, 0x24010a40, 0x432005e4, 0xb2d22aff, 0x4240d900, 0x2afe3a01, 0x3a7ed201, 0x28004770, 0xd5004620, + 0x3a7e4240, 0x32800092, 0x0fc44770, 0xd50407e4, 0xd0002d00, 0x42403001, 0x3a01d403, 0xd0121800, 0x3281d5fb, + 0x3080d101, 0x3080d205, 0x2d00d203, 0x0040d00f, 0x2afe3a01, 0x3201da06, 0x0a40dd07, 0x431005d2, 0x47704320, + 0x05c020ff, 0x2000e7fa, 0x06054770, 0x0a40d1ed, 0xe7eb0280, 0x140cb283, 0x14044363, 0x436cb28d, 0xb284191b, + 0x0425436c, 0x191b0c24, 0x14091400, 0x02c04348, 0x430d06d9, 0x18401159, 0x00424770, 0xd0010e12, 0xd1012aff, + 0x05c00dc0, 0x0e12004a, 0x2affd001, 0x0dc9d101, 0x220105c9, 0xd4094041, 0xd5004041, 0x42884252, 0xdb00dc02, + 0x42522200, 0x47701e10, 0x18494301, 0x2800d0f8, 0xe7f6daf8, 0xb5102100, 0xff86f7ff, 0x33820013, 0x440ad410, + 0xdb073a17, 0xdd192a07, 0x43c917c1, 0x07c02001, 0xbd104048, 0x2a204252, 0x2220db00, 0xbd104110, 0xbd102000, + 0xb5102100, 0xff6cf7ff, 0x0001440a, 0x3a17d4ee, 0x43c1dbee, 0xdce92a08, 0xbd104090, 0xb5302200, 0xd5062900, + 0x430507cd, 0x3a010848, 0x2200e010, 0x0005b530, 0xd015430d, 0x160c17cd, 0xd10542ac, 0x0e4401c9, 0x01c04321, + 0xe7f63207, 0x00080005, 0x323d4252, 0x2100e004, 0x221db530, 0x25001a52, 0xff55f7ff, 0x2100bd30, 0x2800b530, + 0x221edaf5, 0x07c51a52, 0xe7f30840, 0x46a42500, 0x2900e00d, 0xe005dc02, 0xda032a00, 0x427f1b89, 0xe0011912, + 0x1b121989, 0x43674664, 0xcb101bc0, 0xd2000864, 0x46063501, 0x460f412e, 0x0864412f, 0xb5c04770, 0xffe2f7ff, + 0xffe6f7ff, 0x1386d3fc, 0x10d2138f, 0x43574356, 0x43674664, 0x133f1336, 0x19891bc0, 0xb5c0bdc0, 0xffd0f7ff, + 0xffd1f7ff, 0x2900d3fc, 0x1989dc02, 0xe0011b12, 0x19121b89, 0x10641076, 0xe7edd1f5, 0x2118b530, 0xff69f7ff, + 0x09244c5d, 0xdafd1b00, 0xd4fd1900, 0x00650082, 0x21004853, 0xdb0242a2, 0x42401b52, 0x00d2e7fa, 0x2401a355, + 0xffc5f7ff, 0x22003109, 0x25002300, 0xf80cf000, 0xfeedf7ff, 0xfed0f7ff, 0xf806f000, 0xb500e78f, 0xffd8f7ff, + 0xbd004608, 0x07642401, 0xdc0342a0, 0x42a04264, 0x4770dd00, 0x47700020, 0xf7ffb570, 0xe18cffc9, 0x2118b530, + 0xff31f7ff, 0x4a3c1401, 0x14c94351, 0x10493101, 0x0142b402, 0x43414839, 0x48391a52, 0xa3482100, 0xf7ff43cc, + 0x4408ff90, 0xe764bc04, 0xf7ffb530, 0x0001fea3, 0x4933d415, 0xfedef7ff, 0xd3011051, 0x10403101, 0x4601b402, + 0x009b4b2e, 0x1ac918c0, 0xa33b2200, 0xf7ff43d4, 0x4611ff88, 0x0013bc04, 0x22ffe749, 0xb530e7fb, 0xffe0f7ff, + 0xdc0a2b46, 0x2b46425b, 0x4824dc06, 0x31084358, 0x1a081109, 0xe7382205, 0x22ff43c0, 0xb530e735, 0xfe74f7ff, + 0xfe6ef7ff, 0xfe70f7ff, 0x01490140, 0x126418d4, 0xd40a3401, 0xda051ad4, 0x41204264, 0xd30c2c1c, 0xe00a17c0, + 0x2c1c4121, 0x2800d307, 0x4813da03, 0x404817c9, 0x17c8e013, 0x2200e011, 0xda022800, 0x42494240, 0xa30d4a0d, + 0xf7ff2401, 0x4610ff46, 0x18844a0a, 0x1a84d202, 0x1aa0d400, 0x22003801, 0x46c0e701, 0x136e9db4, 0x00001715, + 0x162e42ff, 0x2c9e15ca, 0x0593c2b9, 0x0162e430, 0x6487ed51, 0x3b58ce0c, 0x1f5b75f8, 0x0feadd4c, 0x07fd56ec, + 0x03ffaab8, 0x01fff554, 0x00fffeac, 0x007fffd4, 0x003ffffc, 0x001ffffc, 0x00100000, 0x00080002, 0x464fa9ec, + 0x464fa9ed, 0x20b15df4, 0x1015891c, 0x0802ac44, 0x0802ac45, 0x04005564, 0x02000aac, 0x01000154, 0x0080002c, + 0x00400004, 0x00200004, 0x00100000, 0x00080000, 0x00080003, 0x40514ab9, 0x17c4b570, 0x0e120042, 0x2affd051, + 0x17cdd052, 0x0e1b004b, 0x2bffd051, 0x4eb3d052, 0x40314030, 0x43303601, 0x40604331, 0x1b004069, 0x1a9d1b49, + 0xd40d1ad4, 0xda082c1e, 0x00133520, 0x40aa000a, 0xe00b4121, 0x00082200, 0x0013e00a, 0xe0072200, 0xdaf72d1e, + 0x00023420, 0x412840a2, 0xd0191840, 0xd0030fc1, 0x425243c0, 0x3001d100, 0x42b019b6, 0x1892d204, 0x3b014140, + 0xd3fa42b0, 0xd3020840, 0x2a003001, 0x2bfed009, 0x07c9d20a, 0x05db4408, 0xbd704418, 0xd0fc2a00, 0x0840e7e2, + 0xe7f20040, 0x07c8da01, 0x0208bd70, 0x05c030ff, 0x3a20bd70, 0xe7ac1912, 0x44220212, 0x3b20e7a9, 0xe7ac195b, + 0x442b021b, 0x46c0e7a9, 0x4602b580, 0x0fd2404a, 0x469607d2, 0x00490040, 0xd03d0e02, 0xd03c2aff, 0xd03c0e0b, + 0xd03b2bff, 0x3f8018d7, 0x02090200, 0x0a490a40, 0x46941842, 0x09cb09c2, 0x4348435a, 0xd3020c92, 0xd4002800, + 0x02433201, 0x02520dc0, 0x44601880, 0xd10e0dc1, 0xd22b2ffe, 0xd301005b, 0x3001d005, 0x05ff3701, 0x44704438, + 0x3001bd80, 0x00400840, 0x3701e7f6, 0xd2132ffe, 0xd3020840, 0x2b003001, 0x19ffd005, 0x05bf3701, 0x44704438, + 0x0840bd80, 0xe7f60040, 0x02123a10, 0x3b10e7c0, 0xe7c1021b, 0x3701da12, 0x3002d10e, 0x28030dc0, 0xe005d10a, + 0x3701da0a, 0x3001d106, 0xd0030dc0, 0x05c02001, 0xbd804470, 0xbd804670, 0x05c020ff, 0xbd804470, 0x2401b570, + 0x05e44266, 0x0a52024a, 0x09d34322, 0x062d25d0, 0x666b662e, 0xb2f30dc6, 0x0a400240, 0x0dc94320, 0x0a36404e, + 0x6f2d07f6, 0x2900b2c9, 0x29ffd030, 0x2b00d02c, 0x2bffd039, 0x1a5bd02a, 0x0a01337d, 0x0c094369, 0x001403c0, + 0x1b04434c, 0x436c12a4, 0x03491424, 0x0f0c1909, 0x3105d108, 0xd30f090c, 0x028008c9, 0x1a404351, 0xe008d40a, + 0x31093301, 0xd305094c, 0x02400909, 0x1a404351, 0x3401d400, 0xd2092bfe, 0x186005d9, 0xbd701980, 0xd10c2bff, + 0x05c020ff, 0xbd704330, 0x1c59dafa, 0x0e61d105, 0x2001d303, 0x433005c0, 0x0030bd70, 0x46c0bd70, 0x0041b410, + 0x0209d23a, 0x22010a49, 0x188905d2, 0xd03a0dc2, 0xd0362aff, 0x1052327d, 0x0049d300, 0x0d4ba41a, 0x09c85ce4, + 0x43604360, 0x43601300, 0x02241340, 0x34aa1a24, 0x43400020, 0x0a0b0bc0, 0x13004358, 0x15404360, 0x43631a24, + 0x00180bdb, 0x02494340, 0x11401a08, 0x01db4344, 0x301013e0, 0x44031180, 0x461cd306, 0x43644164, 0x1b090409, + 0x3301d400, 0x18d005d2, 0x4770bc10, 0xd0040e09, 0x05c017c0, 0x0dc0e7f8, 0x0fc0e7fb, 0xe7f307c0, 0xbbc9daf1, + 0x979ea6b0, 0x82868b91, 0x80000000, 0x007fffff, 0x2401b5f0, 0x406307e4, 0x46c0e001, 0x0d0cb5f0, 0x1e660fcf, + 0x1b890536, 0xd3030564, 0x424043c9, 0x3101d300, 0xd0030d64, 0x0af61c66, 0x1be4d007, 0x007f2000, 0x07891c79, + 0x3c801289, 0x0d1d0324, 0x1e6e0fdf, 0x1b9b0536, 0xd303056d, 0x425243db, 0x3301d300, 0xd0030d6d, 0x0af61c6e, + 0x1bedd007, 0x007f2200, 0x079b1c7b, 0x3d80129b, 0x1b2f032d, 0xd4581b66, 0x2e2046a4, 0x3720da46, 0x40bc0014, + 0x40bd001d, 0x413340f2, 0x1880432a, 0x0fcb4159, 0x43c9d005, 0x220043c0, 0x41504264, 0x46624151, 0xd1280d4d, + 0xd1070d0d, 0xd01f2800, 0x41401924, 0x3a014149, 0xd0f90d0d, 0xd3060064, 0xd3003001, 0x2c003101, 0x0840d101, + 0x3a010040, 0x1c94d40b, 0xd1040ae4, 0x44110512, 0x441907db, 0x07d9bdf0, 0x43194b20, 0x07d9e000, 0xbdf02000, + 0xd1dd2900, 0xd1db2c00, 0x3201bdf0, 0x084007c6, 0x432807cd, 0x2e000849, 0xe7d9d0e1, 0xda292e3c, 0x37403e20, + 0x40bc0014, 0x2401d000, 0x431440f2, 0x40bb001a, 0x17d3431c, 0x46ace7ac, 0xda082f20, 0x00043620, 0x000d40b4, + 0x40f840b5, 0x43284139, 0x2f3ce7a5, 0x3f20da0c, 0x00043640, 0xd00040b4, 0x40f82401, 0x00084304, 0x430c40b1, + 0xe7ea17c1, 0x00190010, 0xe7942400, 0x7ff00000, 0x0d0cb5f0, 0x05361e66, 0x0ae61b89, 0x0d640564, 0x1c65d002, + 0xd0040aed, 0x21012000, 0x3c800509, 0x46a40324, 0x1e670d1c, 0x1bdb053f, 0x05640ae7, 0xd0020d64, 0x0aed1c65, + 0x2200d004, 0x051b2301, 0x03243c80, 0x44644077, 0xb284b497, 0x4374b296, 0x437e0c07, 0x436f0c15, 0x4368b280, + 0xd3021836, 0x04002001, 0x0430183f, 0x19000c35, 0x4684417d, 0xb29ab288, 0x0c0c4350, 0x0c1f4362, 0xb28e437c, + 0x1992437e, 0x2601d302, 0x19a40436, 0x0c170416, 0x41671836, 0xb281bc01, 0x4351b29a, 0x43620c04, 0x435c0c1b, + 0x4358b280, 0xd3021812, 0x04002001, 0x04101824, 0x18400c13, 0x182d4163, 0x2000415e, 0xbc064147, 0xb293b288, + 0x0c0c4358, 0x0c124363, 0xb2894354, 0x185b4351, 0x2101d302, 0x18640409, 0x0c1a0419, 0x41621809, 0x4156186d, + 0x41472000, 0x02f9bc18, 0x43110d72, 0x0d6a02f0, 0x02ed4310, 0xd1030d0a, 0x4140196d, 0x3b014149, 0x1b9b4e12, + 0x42b30076, 0x006dd20e, 0x3001d307, 0x41712600, 0x43354666, 0x0840d101, 0x051b0040, 0x07e418c9, 0xbdf04421, + 0x3301da0b, 0x3001d106, 0x3101d104, 0xd0010d4f, 0xe7f20849, 0x200007e1, 0x3601bdf0, 0x20000531, 0x0000e7eb, + 0x000003ff, 0x0d1cb5f0, 0x053f1e67, 0x0ae71bdb, 0x0d640564, 0x1c66d002, 0xd0040af6, 0x23012200, 0x3c80051b, + 0x25d00324, 0x2600062d, 0x662e43f6, 0x666e091e, 0x19f60fce, 0x004946b4, 0xd0020d4f, 0x0af61c7e, 0x2000d003, + 0x3f402100, 0x1b3e033f, 0x44b400b6, 0x057f3f01, 0x08491bc9, 0x36016f2e, 0x029c0876, 0x43250d95, 0x13ed4375, + 0x13ad4375, 0x106d3501, 0x1b7603f6, 0x02ccb40c, 0x432c0d45, 0xb2b3b2a2, 0x0c27435a, 0x0c35437b, 0xb2a4436f, + 0x191b436c, 0x2401d302, 0x193f0424, 0x0c1d041c, 0x417d18a4, 0x416d1924, 0xb2919a00, 0x4361b2ac, 0x437c0c17, + 0x435f0c2b, 0x435ab292, 0xd30218a4, 0x04122201, 0x042218bf, 0x18520c23, 0x9c01417b, 0x191b436c, 0x01d90e52, + 0x0144430a, 0xb2811aa0, 0x4351b2b2, 0x435a1403, 0x43730c36, 0x4377b287, 0x19d217d6, 0x417e2700, 0x199b0436, + 0x0c160417, 0x415e187f, 0x18ed1673, 0x260001f3, 0x41753380, 0xd1060fa9, 0x0a690064, 0x0a5b05e8, 0xd2094318, + 0x2204e02f, 0x33804494, 0x0aa94175, 0x0a9b05a8, 0xd3264318, 0x41494140, 0x9a000424, 0x000d9b01, 0x1b644355, + 0x1ae44343, 0xb286b295, 0x0c174375, 0x0c03437e, 0xb292435f, 0x18b6435a, 0x2201d302, 0x18bf0412, 0x0c330432, + 0x417b1952, 0x419c4252, 0xd4022c00, 0x30012200, 0x08404151, 0x431007ca, 0xb0020849, 0x07d74662, 0x4b591092, + 0x4b5918d2, 0xd203429a, 0x18890512, 0xbdf019c9, 0x2a002000, 0x0039dc01, 0x3301bdf0, 0xe7f50519, 0x2100da07, + 0x0fc9e007, 0x0d5207c9, 0x12c9d003, 0x494ee001, 0x20000509, 0x46c04770, 0xd2f2004a, 0x3a010d52, 0x429a4b48, + 0xb5f0d2ea, 0x1b090514, 0xd3010852, 0x41491800, 0x18d2089b, 0x46940512, 0x0c4aa441, 0x090b5ca2, 0x43534353, + 0x4353131b, 0x0212135b, 0x00131ad2, 0x0b5b435b, 0x4363084c, 0x435313db, 0x330115db, 0x1ad2105b, 0x1ad20c13, + 0x435b0013, 0x0d840289, 0xb28d4321, 0x4375b29e, 0x437e0c0f, 0x435f0c1b, 0x435cb28c, 0xd3021936, 0x04242401, + 0x0434193f, 0x19640c33, 0x019d417b, 0x432c0ea4, 0xb2a53420, 0x14244355, 0x0c2d4354, 0x11a41964, 0x1b1203d2, + 0xb28eb295, 0x0c174375, 0x0c0c437e, 0xb2934367, 0x18f64363, 0x2301d302, 0x18ff041b, 0x0c340433, 0x417c195b, + 0x416418db, 0x230018db, 0xb29e4163, 0xb29d4376, 0x437d0c1f, 0x046c437f, 0x19a40bed, 0x0206417d, 0x1b36088f, + 0x077d41af, 0x416e08f6, 0xb295b2b4, 0x1437436c, 0x0c12437d, 0xb2b64357, 0x17ea4356, 0x260019ad, 0x04124172, + 0x042e18bf, 0x19360c2a, 0x3208417a, 0xd2191152, 0x059c0a9d, 0x191017d1, 0x44614169, 0x0000bdf0, 0x000003fd, + 0x000007fe, 0x000007ff, 0xd6dfebf8, 0xb8bec5cd, 0xa4a8adb2, 0x95999ca0, 0x8a8d8f92, 0x81838588, 0x0a5d4152, + 0x17d105dc, 0x414d1914, 0x4363002b, 0x4376b2a6, 0x0c27b2a2, 0x437f437a, 0x0bd20451, 0x417a1989, 0x18d218d2, + 0x42490580, 0xd4024190, 0x34012300, 0x0860415d, 0x07ed0869, 0x44614328, 0xb5d0bdf0, 0xb5d0e011, 0x004c4fb3, + 0xd0010d64, 0xd10242bc, 0x0d092000, 0x005c0509, 0xd0010d64, 0xd10242bc, 0x0d1b2200, 0x2601051b, 0xd40c404b, + 0xd500404b, 0x42994276, 0x4290d103, 0xd301d803, 0xdc002600, 0x1e304276, 0x430bbdd0, 0x430318db, 0xd0f54313, + 0xdaf62900, 0x4644e7f4, 0x4656464d, 0xb4f0465f, 0xbcf04770, 0x46a946a0, 0x46bb46b2, 0x46624770, 0x4694ca18, + 0x2a00465a, 0xe004db20, 0xca184662, 0x29004694, 0x18c0da1a, 0x465b4161, 0x465c413b, 0x465240b4, 0x432240fa, + 0x464d4644, 0x416b4162, 0x46994690, 0x40b3462b, 0x40fc413d, 0x4652431c, 0x41a2465b, 0x469241ab, 0x4770469b, + 0x41a11ac0, 0x413b464b, 0x40b4464c, 0x40fa4642, 0x46544322, 0x4162465d, 0x4692416b, 0x462b469b, 0x413d40b3, + 0x431c40fc, 0x464b4642, 0x41ab41a2, 0x46994690, 0x20004770, 0x47702100, 0xb5002200, 0xf0003220, 0x0008f82a, + 0x2200bd00, 0x3220b500, 0xf830f000, 0xbd000008, 0xb5002100, 0xf804f000, 0x2100e01e, 0xd4e615c3, 0x468cb510, + 0x004017c3, 0xd00a0e02, 0xd00c2aff, 0x3a7f1e51, 0x1a400609, 0x1ac04058, 0x07001101, 0x2000e01f, 0x00030001, + 0x43d8bd10, 0xbd1043d9, 0xb5002200, 0xf80cf000, 0x429a17ca, 0xbd00d100, 0x210143d8, 0x404107c9, 0x2200bd00, + 0xd4be150b, 0x4694b510, 0xf8b8f000, 0x34011414, 0x2100da00, 0x446217cb, 0xd40c3a34, 0xda072a0c, 0x40910004, + 0x42524090, 0x40d43220, 0xbd104321, 0x43d943d8, 0x3220bd10, 0x460cd407, 0x42524094, 0x41113220, 0x432040d0, + 0x0008bd10, 0x322017c9, 0x4252d403, 0x41103220, 0x0018bd10, 0xbd100019, 0x07db0fc3, 0x0e0a0041, 0x2affd007, + 0x0909d008, 0x18894a3d, 0x07404319, 0x00194770, 0x47702000, 0x18c9493a, 0x004ae7fa, 0x4b390d52, 0xdd131ad2, + 0xda1e2aff, 0x0fcb05d2, 0x431a07db, 0x0f4000c3, 0x0a490309, 0x43104308, 0xd301005b, 0x3001d001, 0x08434770, + 0x4770d2fb, 0x0fc8d002, 0x477007c0, 0x1312030a, 0xd1f83201, 0x2a070f42, 0x2201d1f5, 0x22ffe000, 0x02000fc8, + 0x05c01880, 0x21004770, 0x2100000a, 0x2100e004, 0x17c1000a, 0x2200e003, 0xe0052300, 0x17cb2200, 0x40594058, + 0x41991ac0, 0x4c1cb530, 0x29001aa2, 0x0001d103, 0x2000d010, 0x154c3a20, 0xd204d112, 0x18003a01, 0x0d4c4149, + 0x4c15d3fa, 0xd20442a2, 0x18890512, 0x18c907db, 0x43d2bd30, 0x20000d52, 0xe7f52100, 0x3a01d403, 0x41491800, + 0x320bd5fb, 0x0ac00544, 0x4328054d, 0x00640ac9, 0x2400d003, 0x41614160, 0xd3e0e7e1, 0xe7f80844, 0x000007ff, + 0x38000000, 0x7ff00000, 0x00000380, 0x00000432, 0x000007fe, 0x0fcc0d0a, 0x051b1e53, 0x05521ac9, 0x43c9d303, + 0xd3004240, 0x0d523101, 0x1c53d003, 0xd0070adb, 0x20001b12, 0x1c610064, 0x12890789, 0x03123a80, 0x1ad24b62, + 0x32024770, 0x2a0cd425, 0x2511da1c, 0x000b1aad, 0x3208412b, 0x00063507, 0x409040ee, 0x43314091, 0x4363ccf0, + 0x2300151a, 0x4355415a, 0x43574356, 0x12f402bf, 0x19760576, 0x17ed4167, 0x1b80197f, 0x477041b9, 0x2000220c, + 0x004917c9, 0x05093101, 0x0209e7db, 0x43190e03, 0x42530200, 0xd4083220, 0x4119000c, 0x40d84094, 0x22004320, + 0x41514150, 0x00084770, 0x3b2017c9, 0xd5f13220, 0x21002000, 0x47702200, 0xf7ffb5f0, 0xf000fe56, 0x4684f81d, + 0xf83ef000, 0x4660b403, 0xf858f000, 0xf7ffbc0c, 0xe474fe50, 0xf7ffb5f0, 0xf000fe46, 0xf000f80d, 0xe006f82f, + 0xf7ffb5f0, 0xf000fe3e, 0xf000f805, 0xf7fff845, 0xbdf0fe3e, 0xf7ffb500, 0xa431ff7f, 0xff97f7ff, 0x4d2d2400, + 0x07d24e2d, 0x43f6d302, 0x4166426d, 0xd2040052, 0x46a346a2, 0x46b146a8, 0x46a0e003, 0x46aa46a1, 0xa46f46b3, + 0x270146a4, 0xf7ff261f, 0x3701fe2b, 0x2f213e01, 0xbd00d1f9, 0xb2844659, 0x436cb28d, 0x43751406, 0x435e140b, + 0x435ab282, 0x17ea18ad, 0x43d2d700, 0x18b60412, 0x0c2b042a, 0x41731912, 0x46494640, 0x179b009d, 0x432a0f92, + 0x41994190, 0xe6fc223e, 0xb2844649, 0x436cb28d, 0x43751406, 0x435e140b, 0x435ab282, 0x17ea18ad, 0x43d2d700, + 0x18b60412, 0x0c2b042a, 0x41731912, 0x46594650, 0x179b009d, 0x432a0f92, 0x41594150, 0xe6de223e, 0x000003ff, + 0x9df04dbb, 0x36f656c5, 0x0000517d, 0x0014611a, 0x000a8885, 0x001921fb, 0xf7ffb5f0, 0x4d40fdc6, 0x402c000c, + 0x42acd001, 0x0d09d102, 0x20000509, 0x402c001c, 0x42acd001, 0x0d1bd102, 0x2200051b, 0x02ed2600, 0xd5042b00, + 0x406b2602, 0xd4004069, 0x194f4276, 0x4299d504, 0x3601dd0c, 0xe0034069, 0xda0742bb, 0x406b3e01, 0x00100007, + 0x000f003a, 0x003b0019, 0x2a00b440, 0x2b00d10f, 0x005cd00a, 0x34011564, 0x004cd109, 0x34011564, 0x3901d102, + 0xe0023b01, 0x21002000, 0xf7ffe02e, 0x223efbb1, 0xfe03f7ff, 0x468b4682, 0x21002000, 0x22014680, 0x46910792, + 0x46a4a41d, 0x261f2701, 0xfd81f7ff, 0x3e013701, 0xd1f92f21, 0x4653464a, 0x24013a0c, 0x27000764, 0x001b0852, + 0x4193d405, 0x41791900, 0xd1f70864, 0x4153e004, 0x41b91b00, 0xd1f10864, 0x104907ce, 0x43300840, 0x2e00bc40, + 0x4c09d00a, 0xd5014d09, 0x43ed43e4, 0xd10107f6, 0x41691900, 0x41691900, 0xf7ff223d, 0xf7fffe50, 0xbdf0fd4c, + 0x7ff00000, 0x885a308d, 0x3243f6a8, 0x61bb4f69, 0x1dac6705, 0x96406eb1, 0x0fadbafc, 0xab0bdb72, 0x07f56ea6, + 0xe59fbd39, 0x03feab76, 0xba97624b, 0x01ffd55b, 0xdddb94d6, 0x00fffaaa, 0x56eeea5d, 0x007fff55, 0xaab7776e, + 0x003fffea, 0x5555bbbc, 0x001ffffd, 0xaaaaadde, 0x000fffff, 0xf555556f, 0x0007ffff, 0xfeaaaaab, 0x0003ffff, + 0xffd55555, 0x0001ffff, 0xfffaaaab, 0x0000ffff, 0xffff5555, 0x00007fff, 0xffffeaab, 0x00003fff, 0xfffffd55, + 0x00001fff, 0xffffffab, 0x00000fff, 0xfffffff5, 0x000007ff, 0xffffffff, 0x000003ff, 0x00000000, 0x00000200, + 0x00000000, 0x00000100, 0x00000000, 0x00000080, 0x00000000, 0x00000040, 0x00000000, 0x00000020, 0x00000000, + 0x00000010, 0x00000000, 0x00000008, 0x00000000, 0x00000004, 0x00000000, 0x00000002, 0x00000000, 0x00000001, + 0x80000000, 0x00000000, 0x40000000, 0x00000000, 0xf7ffb5f0, 0xa454fe07, 0xfe1ff7ff, 0xda042900, 0x4d214c20, + 0x41691900, 0xb4043a01, 0xa6522701, 0x23012200, 0xce30079b, 0x1b0046b4, 0xd40b41a9, 0x3620427e, 0x413d001d, + 0x40b4001c, 0x40fe0016, 0x41624334, 0xe001416b, 0x41691900, 0x37014666, 0xd1e82f21, 0xb29eb285, 0x14074375, + 0x0c19437e, 0xb284434f, 0x17f1434c, 0x24001936, 0x04094161, 0x0434187f, 0x19640c31, 0x0fa44179, 0x43200088, + 0x18801789, 0xbc044159, 0x323e4252, 0xfd7ff7ff, 0x0000bdf0, 0xf473de6b, 0x2c5c85fd, 0x004fb5f0, 0x157fd250, + 0x3701d04e, 0xf7ffd04f, 0xb404fdb3, 0x0dc20249, 0x02404311, 0xa62b2701, 0x220046b4, 0x427e2300, 0x000d3620, + 0x000c413d, 0x000640b4, 0x433440fe, 0x414d4144, 0xd1050fae, 0x00290020, 0xce304666, 0x41ab1b12, 0x44a42408, + 0x2f213701, 0x0089d1e7, 0x18121089, 0xbc80414b, 0xcc13a417, 0x43783701, 0x437c4379, 0x12c9054f, 0x19c017cd, + 0x02a74169, 0x17cd15a4, 0x416c19c9, 0x188017dd, 0x416c4159, 0x17cd223e, 0xd00842ac, 0x070e0900, 0x09094330, + 0x43310726, 0x3a041124, 0xf7ffe7f3, 0xbdf0fd26, 0x20004902, 0x4902bdf0, 0xbdf02000, 0xfff00000, 0x7ff00000, + 0x0000b8aa, 0x0013de6b, 0x000fefa3, 0x000b1721, 0xbf984bf3, 0x19f323ec, 0xcd4d10d6, 0x0e47fbe3, 0x8abcb97a, + 0x0789c1db, 0x022c54cc, 0x03e14618, 0xe7833005, 0x01f829b0, 0x87e01f1e, 0x00fe0545, 0xac419e24, 0x007f80a9, + 0x45621781, 0x003fe015, 0xa9ab10e6, 0x001ff802, 0x55455888, 0x000ffe00, 0x0aa9aac4, 0x0007ff80, 0x01554556, + 0x0003ffe0, 0x002aa9ab, 0x0001fff8, 0x00055545, 0x0000fffe, 0x8000aaaa, 0x00007fff, 0xe0001555, 0x00003fff, + 0xf80002ab, 0x00001fff, 0xfe000055, 0x00000fff, 0xff80000b, 0x000007ff, 0xffe00001, 0x000003ff, 0xfff80000, + 0x000001ff, 0xfffe0000, 0x000000ff, 0xffff8000, 0x0000007f, 0xffffe000, 0x0000003f, 0xfffff800, 0x0000001f, + 0xfffffe00, 0x0000000f, 0xffffff80, 0x00000007, 0xffffffe0, 0x00000003, 0xfffffff8, 0x00000001, 0xfffffffe, + 0x00000000, 0x80000000, 0x00000000, 0x40000000, 0x00000000, 0x45444e49, 0x20202058, 0x004d5448, 0x4f464e49, + 0x3246555f, 0x00545854, 0x70736152, 0x72726562, 0x69502079, 0x32505200, 0x6f6f4220, 0x01060074, 0x50100dc0, + 0x50100dec, 0x0000044d, 0x0000000a, 0xbe000104, 0x0000004f, 0x0000000c, 0x0000000e, 0x00000001, 0x0003ffff, + 0x00ffff03, 0x00000200, 0x00000000, 0x02ffff03, 0x08000200, 0x37020900, 0x00010200, 0x0409fa80, 0x08020000, + 0x07005006, 0x40028105, 0x05070000, 0x00400202, 0x01040900, 0x00ff0200, 0x05070000, 0x00400203, 0x84050700, + 0x00004002, 0x01100112, 0x40000000, 0x00032e8a, 0x02010100, 0xbe000103, 0x50100e18, 0x50100e50, 0x4d903ceb, + 0x4e495753, 0x00312e34, 0x00010802, 0x00020002, 0x0081f800, 0x00010001, 0x00000001, 0x0003ffff, 0x00290000, + 0x52000000, 0x522d4950, 0x20203250, 0x41462020, 0x20363154, 0xfeeb2020, 0x01000000, 0x000c1000, 0x02000800, + 0x08048008, 0x00000880, 0x20000001, 0x04400004, 0xbe008000, 0x0000001c, 0x00002355, 0x000024c9, 0x0000188d, + 0x00000aa9, 0x000018c5, 0x000017fd, 0x00003dc4, 0x00003dd1, 0x50100eb5, 0x20324655, 0x746f6f42, 0x64616f6c, + 0x76207265, 0x0a302e32, 0x65646f4d, 0x52203a6c, 0x62707361, 0x79727265, 0x20695020, 0x0a325052, 0x72616f42, + 0x44492d64, 0x5052203a, 0x50522d49, 0x03040a32, 0xbe000409, 0x50100e58, 0x50100e24, 0x02020000, 0x00000020, + 0x20495052, 0x505201fc, 0x2008fb32, 0xfd3206f9, 0x3c010216, 0x6c6d7468, 0x65683c3e, 0x3c3e6461, 0x6174656d, + 0x74746820, 0x71652d70, 0x3d766975, 0x66657222, 0x68736572, 0x6f632022, 0x6e65746e, 0x30223d74, 0x4c52553b, + 0x25fc273d, 0x2f2f3a73, 0x70736172, 0x72726562, 0x2e697079, 0x2f6d6f63, 0x69766564, 0x522f6563, 0x763f3250, + 0x69737265, 0x613d6e6f, 0x63626261, 0x65646463, 0x27666665, 0x3c3e2f22, 0x626dfa2f, 0x3e79646f, 0x69646552, + 0x74636572, 0x20676e69, 0x3c206f74, 0x65727ffd, 0x3e60c666, 0x2f3c91f1, 0x6271fd61, 0x2f3c6bfc, 0xbe00ebfb, + 0xbe00be00, +]) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts new file mode 100644 index 000000000..082d8425c --- /dev/null +++ b/src/main/modules/simulator/simulator-module.ts @@ -0,0 +1,121 @@ +import { readFile } from 'fs/promises' +import { RP2040 } from 'rp2040js' + +import { bootromB1 } from './bootrom' + +// RP2040 flash starts at 0x10000000 (268435456 decimal) +const FLASH_START_ADDRESS = 0x10000000 + +// UF2 block constants +const UF2_MAGIC_START0 = 0x0a324655 +const UF2_MAGIC_START1 = 0x9e5d5157 +const UF2_MAGIC_END = 0x0ab16f30 +const UF2_BLOCK_SIZE = 512 + +/** + * Parses a UF2 firmware binary and writes its payload blocks to the RP2040 flash memory. + * UF2 format: 512-byte blocks with magic numbers, target address, and payload data. + * See https://github.com/microsoft/uf2 for format specification. + */ +function loadUF2(data: Uint8Array, mcu: RP2040): void { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength) + + for (let offset = 0; offset + UF2_BLOCK_SIZE <= data.length; offset += UF2_BLOCK_SIZE) { + const magic0 = view.getUint32(offset + 0, true) + const magic1 = view.getUint32(offset + 4, true) + const magicEnd = view.getUint32(offset + UF2_BLOCK_SIZE - 4, true) + + if (magic0 !== UF2_MAGIC_START0 || magic1 !== UF2_MAGIC_START1 || magicEnd !== UF2_MAGIC_END) { + continue // skip invalid blocks + } + + const targetAddress = view.getUint32(offset + 12, true) + const payloadSize = view.getUint32(offset + 16, true) + + if (payloadSize > 476 || targetAddress < FLASH_START_ADDRESS) { + continue // skip blocks outside flash range + } + + const flashOffset = targetAddress - FLASH_START_ADDRESS + const payload = data.subarray(offset + 32, offset + 32 + payloadSize) + mcu.flash.set(payload, flashOffset) + } +} + +/** + * Manages the rp2040js emulator lifecycle in the main process. + * Handles firmware loading, execution, UART bridging, and cleanup. + */ +export class SimulatorModule { + private mcu: RP2040 | null = null + private running = false + private executeTimer: ReturnType | null = null + + /** Callback fired for each byte transmitted by the emulated UART0 */ + onUartByte: ((byte: number) => void) | null = null + + /** + * Loads a UF2 firmware file and starts the emulated RP2040. + * Stops any currently running emulation first. + */ + async loadAndRun(uf2Path: string): Promise { + this.stop() + + const uf2Data = await readFile(uf2Path) + + this.mcu = new RP2040() + this.mcu.loadBootrom(bootromB1) + loadUF2(new Uint8Array(uf2Data), this.mcu) + + // Wire UART0 output to the Modbus RTU bridge callback + this.mcu.uart[0].onByte = (byte: number) => { + this.onUartByte?.(byte) + } + + // Set program counter to flash start and begin execution + this.mcu.core.PC = FLASH_START_ADDRESS + this.running = true + this.executeLoop() + } + + /** Send a byte to the emulated UART0 RX (host → device) */ + feedByte(byte: number): void { + this.mcu?.uart[0].feedByte(byte) + } + + /** Stop the emulator and release resources */ + stop(): void { + this.running = false + if (this.executeTimer) { + clearTimeout(this.executeTimer) + this.executeTimer = null + } + if (this.mcu) { + this.mcu.uart[0].onByte = undefined + } + this.onUartByte = null + this.mcu = null + } + + /** Check if the emulator is currently running */ + isRunning(): boolean { + return this.running && this.mcu !== null + } + + /** + * Execution loop: runs a batch of CPU steps then yields to the Node.js event loop. + * This allows IPC handlers (e.g. Modbus RTU requests) to be processed between batches. + */ + private executeLoop(): void { + if (!this.running || !this.mcu) return + + // Execute a batch of cycles — step() runs one instruction at a time + const batchSize = 100_000 + for (let i = 0; i < batchSize && this.running; i++) { + this.mcu.step() + } + + // Yield to event loop so IPC handlers can run, then continue + this.executeTimer = setTimeout(() => this.executeLoop(), 0) + } +} diff --git a/src/renderer/store/slices/device/data/constants.ts b/src/renderer/store/slices/device/data/constants.ts index cae9d96db..a6419986f 100644 --- a/src/renderer/store/slices/device/data/constants.ts +++ b/src/renderer/store/slices/device/data/constants.ts @@ -2,7 +2,7 @@ import { DeviceConfiguration } from '@root/types/PLC/devices' // Default configuration for deviceDefinitions.configuration export const defaultDeviceConfiguration: DeviceConfiguration = { - deviceBoard: 'OpenPLC Runtime v3', + deviceBoard: 'OpenPLC Simulator', communicationPort: '', runtimeIpAddress: '', compileOnly: false, diff --git a/src/renderer/store/slices/device/types.ts b/src/renderer/store/slices/device/types.ts index 7cddcec70..9dae9bd9b 100644 --- a/src/renderer/store/slices/device/types.ts +++ b/src/renderer/store/slices/device/types.ts @@ -63,7 +63,7 @@ const runtimeConnectionSchema = z.object({ type RuntimeConnection = z.infer const availableBoardInfo = z.object({ - compiler: z.enum(['arduino-cli', 'openplc-compiler']), + compiler: z.enum(['arduino-cli', 'openplc-compiler', 'simulator']), core: z.string(), preview: z.string(), specs: z.object({ diff --git a/src/utils/device.ts b/src/utils/device.ts index 014508e5e..519d5e216 100644 --- a/src/utils/device.ts +++ b/src/utils/device.ts @@ -27,6 +27,20 @@ export function isOpenPLCRuntimeTarget(boardInfo: AvailableBoardInfo | undefined return boardInfo.compiler === 'openplc-compiler' } +/** + * Determines if a board is the built-in simulator target. + * The simulator uses an emulated RP2040 and requires no physical hardware. + * + * @param boardInfo - The board information from availableBoards map + * @returns true if the board is the simulator target, false otherwise + */ +export function isSimulatorTarget(boardInfo: AvailableBoardInfo | undefined): boolean { + if (!boardInfo) { + return false + } + return boardInfo.compiler === 'simulator' +} + /** * Extracts the expected runtime version from the board target name. * This is used to validate that the connected runtime matches the selected target. From 885c7aed33a94c6e84cc50c432459072b2beb5c8 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 00:26:12 -0500 Subject: [PATCH 05/25] feat: handle simulator target in compilation flow (Phase 2) - Skip Arduino upload for simulator, send UF2 firmware path to renderer - Force Modbus RTU defines in defines.h when compiling for simulator - Handle simulatorFirmwarePath in build button callback to load firmware Co-Authored-By: Claude Opus 4.6 --- src/main/modules/compiler/compiler-module.ts | 21 +++++++++++++-- .../workspace-activity-bar/default.tsx | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 1ca4ced6c..5180f8fa3 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -687,11 +687,13 @@ class CompilerModule { projectPath, buildMD5Hash, boardTarget, + boardRuntime, _handleOutputData, }: { projectPath: string boardTarget: string buildMD5Hash: string + boardRuntime: string _handleOutputData: HandleOutputDataCallback }) { let DEFINES_CONTENT: string = '' @@ -769,7 +771,7 @@ class CompilerModule { if (modbusTCP.tcpStaticHostConfiguration.subnet !== null) DEFINES_CONTENT += `#define MBTCP_SUBNET ${modbusTCP.tcpStaticHostConfiguration.subnet.replaceAll('.', ',')}\n` - if (communicationPreferences.enabledRTU) { + if (communicationPreferences.enabledRTU || boardRuntime === 'simulator') { DEFINES_CONTENT += '#define MBSERIAL\n' DEFINES_CONTENT += '#define MODBUS_ENABLED\n' } @@ -2019,6 +2021,7 @@ class CompilerModule { projectPath: normalizedProjectPath, boardTarget, buildMD5Hash, + boardRuntime, _handleOutputData: (data, logLevel) => { _mainProcessPort.postMessage({ logLevel, message: data }) }, @@ -2065,7 +2068,21 @@ class CompilerModule { return } - // Step 13: Upload program to board if necessary + // Step 13: Upload program to board or load into simulator + if (boardRuntime === 'simulator') { + // For simulator targets, send the UF2 firmware path back to the renderer + const uf2Path = join(compilationPath, 'examples', 'Baremetal', 'build', 'rp2040.rp2040.rpipico', 'Baremetal.uf2') + _mainProcessPort.postMessage({ + logLevel: 'info', + message: 'Compilation successful. Loading firmware into simulator...', + }) + _mainProcessPort.postMessage({ + simulatorFirmwarePath: uf2Path, + closePort: true, + }) + return + } + if (!compileOnly) { _mainProcessPort.postMessage({ logLevel: 'info', message: 'Uploading program to board...' }) try { diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index 558cb2af8..e99b46e82 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -279,6 +279,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa message: string | Buffer plcStatus?: string closePort?: boolean + simulatorFirmwarePath?: string }) => { setIsCompiling(true) @@ -310,6 +311,32 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa }) }) } + + // Load firmware into simulator when compilation finishes with a UF2 path + if (data.simulatorFirmwarePath) { + ;(window.bridge.simulatorLoadFirmware as (p: string) => Promise<{ success: boolean; error?: string }>)( + data.simulatorFirmwarePath, + ) + .then((result) => { + if (result.success) { + addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator is running.' }) + } else { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to start simulator: ${result.error}`, + }) + } + }) + .catch((err: unknown) => { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Simulator error: ${err instanceof Error ? err.message : String(err)}`, + }) + }) + } + if (data.closePort) { setIsCompiling(false) } From fddd7c70f010d2b81450576d511718798a34bfee Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 00:31:22 -0500 Subject: [PATCH 06/25] feat: add VirtualSerialPort and debugger flow for simulator (Phase 3) - Create VirtualSerialPort that mocks serialport API over rp2040js UART - Modify ModbusRtuClient to accept injected serial port, avoiding code duplication - Add 'simulator' connection type to debugger connect and MD5 verify handlers - Add "empty simulator" check in debugger button flow Co-Authored-By: Claude Opus 4.6 --- src/main/modules/ipc/main.ts | 42 +++++++++++++++-- src/main/modules/ipc/renderer.ts | 4 +- src/main/modules/modbus/modbus-rtu-client.ts | 15 ++++++ .../modules/simulator/virtual-serial-port.ts | 47 +++++++++++++++++++ .../workspace-activity-bar/default.tsx | 29 ++++++++++-- 5 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 src/main/modules/simulator/virtual-serial-port.ts diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index d30c408c1..156d2aa24 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -19,6 +19,7 @@ import { logger } from '../../services' import { ModbusTcpClient } from '../modbus/modbus-client' import { ModbusRtuClient } from '../modbus/modbus-rtu-client' import { SimulatorModule } from '../simulator/simulator-module' +import { VirtualSerialPort } from '../simulator/virtual-serial-port' import { WebSocketDebugClient } from '../websocket/websocket-debug-client' type IDataToWrite = { @@ -870,7 +871,7 @@ class MainProcessBridge implements MainIpcModule { handleDebuggerVerifyMd5 = async ( _event: IpcMainInvokeEvent, - connectionType: 'tcp' | 'rtu' | 'websocket', + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', connectionParams: { ipAddress?: string port?: string @@ -883,7 +884,25 @@ class MainProcessBridge implements MainIpcModule { let client: ModbusTcpClient | ModbusRtuClient | null = null let wsClient: WebSocketDebugClient | null = null try { - if (connectionType === 'websocket') { + if (connectionType === 'simulator') { + const virtualPort = new VirtualSerialPort(this.simulatorModule) + client = new ModbusRtuClient({ + port: 'simulator', + baudRate: 115200, + slaveId: 1, + timeout: 5000, + serialPort: virtualPort, + }) + await client.connect() + const targetMd5 = await client.getMd5Hash() + const match = targetMd5.toLowerCase() === expectedMd5.toLowerCase() + + // Keep the client for subsequent debug operations + this.debuggerModbusClient = client + this.debuggerConnectionType = 'simulator' + + return { success: true, match, targetMd5 } + } else if (connectionType === 'websocket') { if (!connectionParams.ipAddress || !connectionParams.jwtToken) { return { success: false, error: 'IP address and JWT token are required for WebSocket connection' } } @@ -1121,7 +1140,7 @@ class MainProcessBridge implements MainIpcModule { handleDebuggerConnect = async ( _event: IpcMainInvokeEvent, - connectionType: 'tcp' | 'rtu' | 'websocket', + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', connectionParams: { ipAddress?: string port?: string @@ -1131,7 +1150,22 @@ class MainProcessBridge implements MainIpcModule { }, ): Promise<{ success: boolean; error?: string }> => { try { - if (connectionType === 'websocket') { + if (connectionType === 'simulator') { + if (this.debuggerModbusClient) { + this.debuggerModbusClient.disconnect() + this.debuggerModbusClient = null + } + + const virtualPort = new VirtualSerialPort(this.simulatorModule) + this.debuggerModbusClient = new ModbusRtuClient({ + port: 'simulator', + baudRate: 115200, + slaveId: 1, + timeout: 5000, + serialPort: virtualPort, + }) + await this.debuggerModbusClient.connect() + } else if (connectionType === 'websocket') { if (this.debuggerModbusClient) { this.debuggerModbusClient.disconnect() this.debuggerModbusClient = null diff --git a/src/main/modules/ipc/renderer.ts b/src/main/modules/ipc/renderer.ts index 95fca1e70..a90520197 100644 --- a/src/main/modules/ipc/renderer.ts +++ b/src/main/modules/ipc/renderer.ts @@ -244,7 +244,7 @@ const rendererProcessBridge = { ipcRenderer.invoke('util:read-debug-file', projectPath, boardTarget), debuggerVerifyMd5: ( - connectionType: 'tcp' | 'rtu' | 'websocket', + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', connectionParams: { ipAddress?: string port?: string @@ -281,7 +281,7 @@ const rendererProcessBridge = { ipcRenderer.invoke('debugger:set-variable', variableIndex, force, valueBuffer), debuggerConnect: ( - connectionType: 'tcp' | 'rtu' | 'websocket', + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', connectionParams: { ipAddress?: string port?: string diff --git a/src/main/modules/modbus/modbus-rtu-client.ts b/src/main/modules/modbus/modbus-rtu-client.ts index 21f25c5b9..7f4857352 100644 --- a/src/main/modules/modbus/modbus-rtu-client.ts +++ b/src/main/modules/modbus/modbus-rtu-client.ts @@ -9,6 +9,8 @@ interface ModbusRtuClientOptions { baudRate: number slaveId: number timeout: number + // eslint-disable-next-line @typescript-eslint/no-explicit-any + serialPort?: any // Pre-built serial port (e.g. VirtualSerialPort for simulator) } const ARDUINO_BOOTLOADER_DELAY_MS = 2500 @@ -24,6 +26,8 @@ export class ModbusRtuClient { private timeout: number // eslint-disable-next-line @typescript-eslint/no-explicit-any private serialPort: any = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private injectedSerialPort: any = null private static readonly CRC_HI_TABLE = [ 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, @@ -64,6 +68,7 @@ export class ModbusRtuClient { this.baudRate = options.baudRate this.slaveId = options.slaveId this.timeout = options.timeout + this.injectedSerialPort = options.serialPort ?? null } private calculateCrc(buffer: Buffer): number { @@ -94,6 +99,16 @@ export class ModbusRtuClient { } async connect(): Promise { + // If a pre-built serial port was provided (e.g. VirtualSerialPort), use it directly + if (this.injectedSerialPort) { + this.serialPort = this.injectedSerialPort + return new Promise((resolve, reject) => { + this.serialPort.on('open', () => resolve()) + this.serialPort.on('error', (err: Error) => reject(err)) + this.serialPort.open() + }) + } + return new Promise((resolve, reject) => { try { this.serialPort = new SerialPort({ diff --git a/src/main/modules/simulator/virtual-serial-port.ts b/src/main/modules/simulator/virtual-serial-port.ts new file mode 100644 index 000000000..f5ab768e5 --- /dev/null +++ b/src/main/modules/simulator/virtual-serial-port.ts @@ -0,0 +1,47 @@ +import { EventEmitter } from 'events' + +import { SimulatorModule } from './simulator-module' + +/** + * A virtual serial port that mimics the `serialport` npm package's event-based API. + * Routes bytes through SimulatorModule's UART bridge, allowing the existing + * ModbusRtuClient to communicate with the rp2040js emulator unchanged. + */ +export class VirtualSerialPort extends EventEmitter { + public isOpen = false + private simulator: SimulatorModule + + constructor(simulator: SimulatorModule) { + super() + this.simulator = simulator + } + + open(): void { + this.isOpen = true + // Wire UART RX: bytes from emulated device → ModbusRtuClient via 'data' events + this.simulator.onUartByte = (byte: number) => { + this.emit('data', Buffer.from([byte])) + } + // Emit 'open' asynchronously (matches real SerialPort behavior) + process.nextTick(() => this.emit('open')) + } + + write(data: Uint8Array | Buffer, callback?: (err?: Error | null) => void): void { + // Send each byte to the emulated UART TX (host → device) + for (const byte of data) { + this.simulator.feedByte(byte) + } + callback?.(null) + } + + flush(callback?: (err?: Error | null) => void): void { + // No hardware buffer to flush in virtual port + callback?.(null) + } + + close(): void { + this.isOpen = false + this.simulator.onUartByte = null + this.removeAllListeners() + } +} diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index e99b46e82..27f8b35ee 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -6,7 +6,7 @@ import type { RuntimeConnection } from '@root/renderer/store/slices/device/types import { buildDebugTree } from '@root/renderer/utils/debug-tree-builder' import type { DebugTreeNode, FbInstanceInfo } from '@root/types/debugger' import { PLCPou, PLCProjectData } from '@root/types/PLC/open-plc' -import { BufferToStringArray, cn, isOpenPLCRuntimeTarget } from '@root/utils' +import { BufferToStringArray, cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' import { addCppLocalVariables } from '@root/utils/cpp/addCppLocalVariables' import { generateSTCode as generateCppSTCode } from '@root/utils/cpp/generateSTCode' import { validateCppCode } from '@root/utils/cpp/validateCppCode' @@ -432,13 +432,34 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const isRuntimeV4 = boardTarget === 'OpenPLC Runtime v4' let targetIpAddress: string | undefined - let connectionType: 'tcp' | 'rtu' | 'websocket' = 'tcp' + let connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator' = 'tcp' let rtuPort: string | undefined let rtuBaudRate: number | undefined let rtuSlaveId: number | undefined let jwtToken: string | undefined - if (isRuntimeTarget) { + if (isSimulatorTarget(currentBoardInfo)) { + // Check if simulator has firmware loaded + const running = await (window.bridge.simulatorIsRunning as () => Promise)() + if (!running) { + const response = await showDebuggerMessage( + 'warning', + 'Simulator Empty', + 'No firmware is running on the simulator. Would you like to build and upload the project first?', + ['Build & Upload', 'Cancel'], + ) + if (response === 0) { + // Trigger full build, then restart debugger flow + setIsDebuggerProcessing(false) + verifyAndCompile() + return + } else { + setIsDebuggerProcessing(false) + return + } + } + connectionType = 'simulator' + } else if (isRuntimeTarget) { const connectionStatus = useOpenPLCStore.getState().runtimeConnection.connectionStatus const runtimeIpAddress = deviceDefinitions.configuration.runtimeIpAddress @@ -752,7 +773,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const handleMd5Verification = async ( projectPath: string, boardTarget: string, - connectionType: 'tcp' | 'rtu' | 'websocket', + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', connectionParams: { ipAddress?: string port?: string From d6f4b5179ef0d446c36fbc968493e8f6d16a16c6 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 00:34:54 -0500 Subject: [PATCH 07/25] feat: update device editor UI for simulator target (Phase 4) - Hide Compile Only checkbox, comm port, specs, and pin mapping for simulator - Show simple "no configuration required" message in board settings - Show "auto-configured" message in communication panel for simulator - Add simulator.png preview image placeholder Co-Authored-By: Claude Opus 4.6 --- .../sources/boards/previews/simulator.png | Bin 0 -> 158133 bytes .../editor/device/configuration/board.tsx | 41 +++++++++++------- .../device/configuration/communication.tsx | 13 +++++- 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 resources/sources/boards/previews/simulator.png diff --git a/resources/sources/boards/previews/simulator.png b/resources/sources/boards/previews/simulator.png new file mode 100644 index 0000000000000000000000000000000000000000..003939d210bc9deb35eee704c098b3a63e080c55 GIT binary patch literal 158133 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�kw<+7;wl7c1iUK8== zag)Dk7sNJP&lXUYOminfP%p!CEv)Nvaraegam}g%*7R6dmNosL>A6q?Lt08uUX%?c zY<7s6!O6J@kL^E&U%fJlqa_b<*MP+KML31mH|9O`K@AuJv=fxDx;KNbe(oOh7J!Ez zc@9tSc?~D014P9k$;xMbJG*VUQ6O65RVyROcUg}TralU8gt%S?`8GPWX^-?~! zd3g@~ISW~bsccD;kwi7_5-3kiH74a3UU&h&`qi&w0@v1qOMl%v+d?O0olu*96Nm{^ zbT@M>WQR=nI(qb|025uW94EKaENH%@1Q^C)$$Yu&@BZ%Z8cAImha!U|xP zzhA(gtd|K{bTtzM3HpoyGj>Z*{?U(qB%x%d? zzw@2%$iB$X$<)b)nGmK~+uVlW%l*;dAjlK^X;?5ePFFJ-Gub@>o=l$cWO@*I9q4jb z_qArC^P2H`r4!+J92yHeU*2oRu(kJdI(DxKY@OZ7^IPj;f-*<&ZUW|af+=gpFDGx) zSC|mtV5$;ebjrsICu{imvj=c=7AQp?V%Owf6GR9n1Y{}w3u8ongp5-Fv%Z)t#@t*l z8bh}?hpV=)!4=zg;_=-(^E zxQM|*#;Ao!&tEE$5%4@yu9zfkmDqN2%Ey5j;Nh1KWB2Q)QDMxKNoAJmP@UW4L1N2{ zCri1MC7>L$3lW((W6VJng`9^(H=98X#z@JzK)pPR9DADA zs$xX2U1Gf!Ad~OHOr7ynAQQ84I~3L|M-@$Oqm(8+orN|V8Bq39j(lHgvNKMAutq?) zv1O=;k6g71I|g$Y>9W!7nT%^mfK6CGlh^3Qpy5%sJRF^v!HH6gNA{k;6MK*1Y>Xyv@yzb$F*n8X7vi)S2m0OgXt?1P^7NS~pBUCPSs`S~H{>4<%S|9fBgklVHTZ zHQ7tEqPg8yzVa1uO@ICCUvHE*Y0WX+x@3_*{NWFU$uidamw)+}!j$R0W=xsDN&qDr zCAbm5$T*qBOhDl{zVL-FG~Cw&Y;^CPsg(|EY)rkjNElEE|COKYW-^=cM(vB=6%Q>V@IVH}zU z0%OrM3~1Q!|5~?Akk)-PKhz4O{M`(K?LhHDLAccmw%9S#z$Pzui{9A_y|n4Eo-ZTs z^n4dOmR@7czUF=8J)&oZ&r-9|-0AW3oXG|aFl0m-ivk^{_PvJV(`C_mer(?<>>e#) zF4KpIb;V(ToWrE7q`$F5Weo$R7$$+mNHh`|s=^$LIZqNS>yeG|i8=Unz{}-Dq=Nau zI{7k@%Avr7UzXCD>$Wb(?N@Ka)`1Mx_2p5dY-hEMY=M1@SrV0z$vgwPbOZRYh4H$L zy=QCKbFz&4pW25htIOzYM@AS(!%ilL!YEfUo!Cf!o0uxc#PTqK1k30cNZxs)cV*I} zIo0IC$f`t1f|Ux9n8Yb_U$~UliT`r{Y{3|cfg?>B$>N!1O*cEs#pL~>u_U?EBCwLT z1BBICEbGeQt{X1Hb*nwOHiHG$CrhVg0pLan80(T~MR*QoA{&Qh>Nq?LJoMaQJbT~_ z=IkN@J15t~WO}EXVo0YcOIY*rz8>VVp3J43*IX};xtb3hu9Nkk%`F;iGF5R;px)*xBo6Hh#WU;gr!5@VHz#dS?K zW*ABw^~+!WvLsCX^FRNyxNX&~%*pK!|L_m7b?a7P$^dE&-Vd zR&)__-`tKs#?R+J|9MIDVoaH?ZGG{1tUK&K)tE^oV)Y?MLFj!}sr$@8=w3jx)yq(Kvp zKR?IFTqCZ1%3m6w5@1gH9E-a@$yRR;x9Rm3gD|N*=KkOM(fT7(nkCMK#C2`fjA=pWlI*;l3fdFRY+8C>c*gsXr zE2pY>Y|l}=ezJm^$ZMLc`c}^KFc@V`uS1&Q; z^8n_=vJvpmgeUHXe=~kcCV1nGHwrW5%S<39OCUofaFOY099L_FxlgW3*YsDv`c(nX zuYK)n!g}S!N)&MzkEJ{Ko8SDVxR;q!MG)t)36=zUJ{SaZvSWTn0OJEnLxEN!L7PC% z?+D6dzM2)zv9j!mAWG&-@MkQW?rZ*Lf)$g!2$UMrB`E*#AOA6)fBt!V;~U=)H#tF> z;LF%IW6*S0-+lMp0_xnBz)zM;U{=L3J;yX-<~3s&fFRCm!B{d~^a{v2VQLn(dMui6 zYb#J*=!H&$&Qe+2QXi*wV7U+*UFiIl0`?1ybt&iE?0HCsa>$hFA)$fD`@(mC%u|ik zh0bB|YcdbYfko}DSU}nJ90b)0&X#I8Fjf;#e*C}`Cee$!lSkFB3uCh_W@>XXeyc`+ z>R;$Nac<@FrO%qyJ2NXzwqtAs%Dg5{#KsR10w+S4fWH&lB4nca+yPr2qK0BN zMpQbDp-hCkuHJzw*Dk|`WiE!hJQTeQv#bQ%$-IpWDd4e;it1aIh2y0P4$j4R`i;|g z@|9CKIcvdp3kYqa_}A)L(b7v@u5+}}H88Cqc};i>o(DmhO62r-xa@r$v0TkYoOKZe zb8xF?aMj9g+&&1vJ`OrC9??tEcK$Aq&t7x&GWtxh6L64SpqUdDPk4t@yAoGjBZVvdvJVm$obd+(JRXEI-& z4?&uVUOX-tI>*EZiU5DZ4L3-(G-JdZ16|E@U-R4v`UGYE&9QSETCQ}vb6v7{vU8mr z_;7O^8jI(cnW;?|IJ32B%y3<9Be9=ka%!&qUJR65F|}5=c`4v@A&*n1jPyd+yHF5r z#l#l_WrlJH${J!=43s-HK1AWl@4h;V~J? zE#ca&gShLe4Y+Js0Yh0E#f*n)KsPO0GBQJ%;Hdy=0DGzyP+ua8wFQH_j$&hC<0 zYl>trnThUqiDAlkmH8&FW?{%?Ox$yl3@u`vh&hMMub*af>32^+JF!k$gEGO_)KB|N zgi6F)xQP?+dj~%63CqkJA1-E*tB&FF!6H6<)izu{+>7D9tdyVSd3~8n!%zg{?FlGn zld`J0AjI*xI*!ylJpAGzJoEY)OjbSdd@wO(|(a2sE!1+53EZm;;$jqiDVAAz_?AS4*{!^<73+FPf zM=&K2bK)bjCh#%CnLw;km+Ep;?P>xe-QwIIfuG0t-uJ#I@mZdWCQi{!Om{S0#bosa zUdE@%&yJkWe>x#*!3ChgECMZ)>gkxse6xlM*nJ#h0!095_YoOPT z$CF9Nw#7JLau=^1y$odXe2{58==pJ7J;!z|OpTPq9)ZPC;3(aVt=!an*bt?Z%N_KMl%EuQHqwQr5%?eyS2;bSA*#`%dDaS5Dw$EdxK( zD?lOqmhn(&W-0hz#xoQe_NgA6^t~g97*Mta92wW94z(MX;Rchqd{r0jx@seK zj`U(}F^j%J2DL;Jo=H#wgh`gM)I2jIItqobx;x^|re+AOsR=F05D2VR1p`a zBaF_}@y2w7U%YStdymdwF3uT!YTLxwTfGx%RH?C%o~OlYkN~n6FOr*P-2T#Vi)D6^ z*)PKcb~uZCFoqBA+Jp~mT_v86ktf--@WE^dC*9Ydx}@T4I@P$u{hsHyVB_%q|o z1b^)vA4K_H#RsSZLq+eLI(QJ78Yu_!kP^t&ZF2 zxpuN%dVIVW&7jOAuQVt(@3Rv_L_wMBk|~?4-=x)F6p66K@!0^o4^7|~FOA~RRE&DI z2X#>BffWwMyORF20%duo7_&_Y(x{=xV+)8R`8>=zCc~1{v8W0!p20{@gxj~R!u1;l zv1Nc_6Ile2iItkJRg8(V6=GzP)m+AYBh2}sD3k3!IfGxnaTd=V8pl}GMx5)FY-`Ik z%^nO;&NE+OXW~j$ur+RRlc<#>vLyzLId_u~#m`be*%%?la~Ipxy=K{yiLKia!J5*T z%!&>XM)K1MelKoV-oi%s?;tJ6RGXHHMCHf5^kBiZY3Ivx17cN!87*U7FI& zkn8gQJP!g9_en-dW=h~AC^Ms)ESXG`aaz^`6R@*eP(qNbhU{DfmjpBcx+CJ z+=kzifzmBab*^-bmg8YeoD82p&loNZ47$nbMkcTmcnRb@4h;n+r;(}i_%tB64h;)t zUu(v-f-V6$tqew{%xyGtn#bqgjK?#%jQ^)`K({=vrvftnukxJAlG7fHrGWC{wru;Q z&0umqpe&PP>UnAh-1^+LA6@&BtAzl!6VPl0Zv2k-q8XHFlo6Emvy}bM11AB=Oo()0 zih5F#@W*#(wjSZ+Y=FHd=kU;rqu76@j9I6MIy1HDjcg$-qlsQRu0~D+Gm2C9M8L3- z=^G(pjg!<$n@nzIIa7t&MHxMI2{)|k!>!v^K3|D&2$R(mkQwtfQY-{y#%Am2u>x#fk;83Qtin}m`!JFZ&|ffM z>e!LQ;brbju7faTf^t1TEwXX865@^V8h-rj0X%w92l$Mk(Z9*<>S*Bk8-G5 z#^ou=&*zlT(5-#$IF;;UW3;XfCNAc!Z1!d&n+XcjS zRsfc^iRw-Zv?QRcr9OIpLn*A5wiZH*4f{`OX_In0d1fR5%DAr={LrH%wg3~{%(XMs zSp+yrTSY+KN4FbeO~J)`E?D+SA^kSPZFefWX8pru`ctWoe}M6>CnWwZ@-S| zijQM629$sF^lS3l$}hvLABl>Zh7=YC#`*Kp`^i!B`erQSE*DAib3oZmRN~mEgi@O& zxYaRSxvC2v*tHQ?4s~P0@@`T4(r2jIlk(pMly91unfYcM#~ZZVrPD;4#Q^fR$&{NX z{^m(09oJlJ0#blZPiFjEW5NVCGGu}$!Iq#$mo?8_!H57!fFl#8niqjmkJ&t->GkCJ z2#$;g6YR*yHMxr0a~Xel9Bn+oeQ_O*hm4y*%?X9uGUJ=_p~`Z(9^==l`OP>sUp9?d zGvSBZ^E|cLM0(QW`SOR`bBsC}@`d6Lw;@>boYi%mW;Ba2)lNXB9n(#-t#<3SvZNP! zu3O!=R_iu{Zl{K1r|T^QkPCr$D+7q$A$nI>EueMEe4cqv7mEAqeipl?G8u-Z7F0kW z#TLZ~g^Q^gu>Zs~o;xy$U%h+?XX+W$+-}q?YJZV68YZ*Qando=x7QCn|ta-PyUU$d!Ffmub>1iKl$}wI! zHY?|GXuJ-mXArfRZ3`Wd_Y^m-YqEeDpC#C7S1G#db+ih~5kWZt?}e_nCX(rR^m#LY z&8%n>0&w9XL&h2dzI1k?x>^9cR>E-J#x2`6;^xcNU?>w{Sw6zD9+o1p%?Aw`^4&z} zMD7daSB$Yj`O?WsfMcZ?KY#WO{QA%|PLv(g-5g@qB*!W!w|c)8GgxKZNlki~#FE8> zKrcs<3@2VoFM?GYM=_el9Xrm?BJM~xdzbkg!9f)po1b&sbfp7aF7<=Vqo%bJwsE2tBA zxn5c!jpO36I0pS_{91vE$I<<(OIqFO4Hslm|H=I+uxhO~zf%u_W}U0?qRUk4T2;`} zv8Bac$aW0vd_Zd{pxkcVivh|P1F#nZ<#rPuuOZi=F-1_OSELmv>w2AD^ZE7W`|TPK zod58+RKS{xTpTJX!I5_ExLY8Lj#x3H?>HRDQ%JNQ0f5Ig5m>?aqfdZk> zlnPmP9ocXSgP6uGTUJVv(54kd6f+S#*KRZ{FrZ}O!19bIU@h@jSg82GnYj>ej@R+R z;W4~$coL(NRn)95#4HQ4D3{5S9(uw}qG=G+rEjwVW{#C$nTU;uOs8=&FZLnRZZ*d# zDAR54P|!L6!n$FN;suCmh^jMKw=9qCtCr)cHOsNFKab&}g=Iw#IgcJh)_WT-giX0p z5j|n==meB$G(;hejhFDocpVSEa0Gi!)-V}$p)Mt5h9#$e($C~Vp0f1$I8qYFn0_cm zh8Qd&`Ia%OMImxd9d>mLtBbLK@-^%Fv1uTWF2oPinci&>`|HMfYsa!r5b#BHm)P5-~yo&DA^CdZUImlQrq>9DANH=CaQW<_yTNC0Vl3gL!`Bn#05^Wf=7Qr}?>*I%rV? zOKqR9Q}IwFm_xZxhF+tf3OgvHI}>3b7h%`>AzZs<1nc|rDB3j)_2kf-w-E(3>1Q2F z(wzZiW0Ej8K(tHce}3fS@aZYMdb)}SUl_%!;}%Lz5fP<7DQoF?4Nz`%|JprM#A2z7 zmIfYFa7$vzsz}Br8de3)+$ro@*N69STZx^kdaq)eml@@w|stt&5B;{Bq;xD z9LF2qE$-_yn6#Rx(g4?PB1=zDx~?Xgs7tpMz@*u9nn^YTm2R7U@sxRUSsG-yjw!G= zY?U7c)V^KsM1N`q?J)uh1O%nH>=;6Bs}P9HOE8LL2ue%EXqy>*glZTYu*c`x?z zw;Pl@W!kNtr&i~BzM!n04uUch1_{LU$T0N5(8c*oq^<9TCOV}uVPT`>2bictILyvV zZ=S)UFCD^Im_^0w6?w+=eQ0G^FAmC@FeNQ6#1(FsHOsS1sZhWUL-8yIf+<|Lt`8sH zxe43X^rF9u4qsmsy$A*(@t6Rl#Ksd)W~)QWMDlkvj4@FG&Xgk@oeptmtb%9vkK)af z6B0kS3q7b&OIu2jTxmSvIjpfhH>)}g%I%o4f-;Sl^jz0?Hzi}v7!E8AifVJxX=>w8 z5!==*$EKk!Y#HgnV3&(7H$YF`g%gKTN1n;#5Cnj)dc+ZwWTsBz5aCa76o+U=z;-5>PEH~(#%)VX-low-eX$I5mZqfZM#JtmPUj#F(MwF18L1(%Lq@+GX)DJZ9R*>0Qky6p_Ww9(W&*|r1fcKdE#&ihUG zHI>*Dlo?ZISma`WvSG>QlSCpLEebfZp4R5UN%EOV#1afOO4X^vu=z8jQIUSmRKK|~@1(p?+@H%%TXc#CHCX;Edb}8$hI+AM z(=axT^k7BaLVw=IKvxFE3~P@~GMmgHk};oz9jrt&2-!s3glj}7*Go7uQO3Tr5q`4! zFkU$oN#?c1c1c#$-J<+G?9zecqt_<9DicBV)?%I;PwV2I+~Wl_lQk+|7qVnQWT*v|`lC zMzuCq-QLZ|T4-Wdu#*ynl#5!AuS{J5R6PKic-359qHb|D3Yx81d7%;jZsnHO<+}e? zfRpw@G@q9qFFmeSV6u>R<6?k?ouVJU4z8EOCTGkB8H1ae_ zaIs@9G`4fLIj0n89+a7WBihffPzqukpR8i<@hLp|%2B*}s)SjmPb3;6lRhk;R)+9m z*IVMVOzM*66_jQS5>RHm(~~%FWK(i;7Jb14u3s~N5AE839jp7WylBC*1Ca$~`HzT) zNRkovCxtmC-Ybc0#>f@pgJK+Z2IZ=cvvW0!R~@`@Vgj!nJB0(MCNMft#w=@uGt|k> z5H^Lm%lnXYf;t~i)@zjJh0<@!uZYff0X3}b&SJ}OFSf27z{cSo4EAQxlLPWrguYxJ zo*gyx=q=AhjUA_)Y~nVq%5{wBijPgg7(uxnRq^K8X}o+g5Kw+~%tqbof#aEkGK=bF zO`zNuYm3lCD^L#Y2r;$LEw*kXC^N{+8fJEAlDaM@4}x9wPit53w}9VqL>u@JLuH_^45c+wfgY1XSCnl7_S zyPDG=yV#9smHnC?le(Cj8F#b0I_()qUlRr1G<#0RpIe<%+URJty&kIoEkV-06<{|k z`X+bVLO{|El$Y{CEVQq88kE~H+4MQJdj6IQy6u3KhFzK|Yd>%@<;9HFP7Dy%CNuup zP_~l6m==a)uq@B1hBi*lR;4uRHwVY?)N3a(9u-it3Q3lBzOx;TRw^asf;`VNjyma+HPS?oP}MwsP{@1PC>9AmC7EGAHiVwGuG z$&aLvHMw7#6WiWWXPfG*=2#qm8Qpe(ExiToTD<~WN4hcCZKJQpL2uDR!F9ousV-z- zM?8lJF}2BE#`}FqL?aj?Vafz$F(O#%W<~I$8V--o;gwS%9@u>Z`_9a&@ zC7sS8shBcbH~x6#%9Ve936wj%da3o&lbQlm^Q6~_6`tNU{h~BaTFs1E|F7A{nz^iT z*>)voX@KLpnxK`=>TXWX(sNE9Bh7l#ad7UB+jDt3Lt4j`7GG&j7Spk%_a~D{QtHKh zHX9XbKvzRxsUXt{7_@p+76bV7vQGAYAx!yq9h8}6Pxm!zl@|i#POg!>SCTC&JqsJR zL=uORzQTM~>amT)xZPe|Y^d-`d7bo?v zj8pTyWDb%A(}$T^)lmR1U%*rqn5Z+>9AKgrV|1#DS4Yp_z_D>m&V`t*vN$P&$mWmf zHcduOsShzG76oPFI@j!N6Ejys!2-K5ujm1`tsKCv)hn^KH;3if2>o3SianmlXNqE3 z2)K0{9*rj^-$emx(rtoCtOPfTl(LG0+34OmOt~6XaAKy8*T#XL{N^BDKJH`A&Wl(J zuL%J;PMEShGt{U~J|bcw_w?L6lKD6~pAu82OpgYQ11F%$8WqWmT&iu=kheTXnUo zYFeiNs_S(M2upoj(gsz#Z94_!r83>cj=K~vZ+`9<+b^G`&6_t%4{Scmd|t?s7wQt# z&UQH)1`l&_jKdSN*mrskyWcp2XJ0>y61q|M`lW}l9R(t% zs0ml{{4!Zeh%Y0dhGEL|o&dNQnigdiZNy;}E*nH7TG0erI|&0>VHpFCkN0k0jk|WO z!{&YmE8Hq_E~P%1sjVec#+5GbbD~tnlB9F*i>ct{a(dotWS_Q+T3}(aQWqdTJyXGn z$tuoJqVvcpoSCgkvK!05VmF7H#YPdP5rZc{uf2$kz!+n`A}fkOoZog_gUfh1>e3TA z=h_%r)`vCA^4PVu7efVLMRyUs`7F9~F1qtsDfc4yF+R@NGo?or4B0c81_rxInNlQ* zz(~(VqsyIGK|IsYwQy`=4hJWyc<`Cm@ZzBvRGfay_%^a#Jt8h5@4HLGiVYWOUGs2rSDm z&Rjy);}uEj-P^s;9V~BR%9lX7gKM_vDy;ywd4pE#v;u(U?=-_$0aV;rNwafhkqXQj zQ&zXOGFX1k36URyq$XnN-~68Y)Oz4_J#kvGOoc+!Fi10WRS?tTq#15=Ju%OPW7d6( zdQVbXrACB;u_~YuWV!D&@auW#v2;RTY+~;OeBLf#?({iZY>e&3*2(#GYN(v=zS8?w zP^L7img!wAP!=gfDLrCC1jAA}!LvENfV0h(^)adzaD2Lg*U!vJo$vGe&){U)K`k>V zLL$rvc9ObSx=v+MmqacP5%rss1SxX_6Z2(VYZ#d-1t$uGIWh5x@>*dOiz=6}%le!E z*X>tt#`|}y!n*DlE1U{)jIjz+a~p|iJO#m|$bgC}ewq7_@&-ufC*z2glAg#$r52)8 z3o%m(adx(f@wpJErmHwMIV-AQr%P4L)`3c#)c8{SSquQNG>k!z$byM3x0K-4;j$O9 z6`+`j(UbSEqOXY6!z-|6c^>P#So#EX7YkzG=R6l#F9Vl8_lT7nCiAmkOzYA5j54FgJE8a5VF%$0S zp|B)mAthxRQ*L-slKYliM^i3Fu@&BCPmHj7;G4ucJ3=m)L)M?d^_Q*2dv~nEmccHp z>ax)11+e|Hoa5UC%9dr_Po~_{(=*oToY5c^oo?`MjVZTc3=2(U3xTpS(pF3@o$<`| zG;>)qnYm4x?b6y3S-_;Ip5VzzQGuQ7D~nboEd^S2B`Y{8a4HDsas_r}yQ(_IMt)7ztwVsE%!Sx!d3tmfcniJ5L0y6EuuhVD$eAYW( zP+rRUEo3yNzrT?E>3(>xsfn&knfLJGgR)W9V$zg=aZI30K$-HDo{h7$0Efn_*mrCO z&+b2g*G`r(jXvqIEcn>~C^K^YpUn`&z!N8^Iflf16Sps2s7{7T7!mx?#AxN35ulla z9oLYL%Gk8L3m>@tGI3!K=X~_Jb&+UPrfjNmwK`W0Zu(9o$8I&xNJ9?FQ8J;7PT=hwz7)pxhm#rHEAKqxO_nOxOm2>M-5~>Ps*^m zaxMz4BZ*DDc@G2KIV|hz!a#2k{W%Lgb`?Dt3)zfolmHnF5ErtAI5h1M8*L?N#56(; zrb&j>DwC-vn%wFI5W_q11e7A+*mM;y92&#JFCM`gV^xIt6(|QTTqm0(dd=K{R{s!h0N<1p6rdl)L%$gxa{6kD2D zBnf!u^O84hlV-|}ReIXLX^g z8=ECD+*XYZ9Y5Dq&1-GSp_$(bj@*vp5f@X^U?ZJ~rst!jT6*3W0y4iXVD1zQo5$Db zcC8-uR@+|4{`45U4+_d;%DhjkQD#Gmc8P}VUP+@hZ4|Y{B`SS*%_J`j&5GfNA&_w~ z8v`e1Sl>H`J+Ga@?$=J>RIMOFA_N99HtETn%I1~lT_$zLoDIX{hZ)fguZ-)V761W7 zcqrUKmd%)NSVh*K#>y@S*K8V;$^G)7F7$bIbh{y9zb5s>oqQVoki|1FZ$Nm`7$Z@l z5(hEGErEInRD(#yRfmOgoea8;QnikXAD~>RVa^Xws)y1DLpoF?ov@_l*z=Ha9BG5u zlk)_eiy0e*oQ*=pWFFJS+m!`!E;Gp!E$vh*NimG&{EV=R@_fBl2GxvCxH5IgLzlR9 zV8R8CjcK16<~|Nh_;`H(as2xA6PO6Hh_n3&m>9-nFK#D>lIfZy29?~q=Bs;-42m(n zuvwXRzH_c8;+9s0aC|pk)NhqMFOU2Hae;Mf!hDYMBj zF={D%??rOHrH#V%$NJoYrt#8vG9l6 z@c&${Mu-|M%Dfl5KkeR@iwVlQuTFrjlXVO@yj9ep-8n6^{Cq&U-Sy!<6_n||Rzp%- zH(od>o3k{V0);= zjwnjebuGza)X9<-i{#*m>s9KK9dV7v1Q6RnC3d6?%5qs390GnB*>DbB^$A?Lx)1NW zVl}SYJcQw*Er8tRIjGgEh;7p^ym2qp)hY>N^B=9nCU7XYN$U%`ev{RNg-Y2J-Pw_V ztvJ0c3w{uxQmdoR>}Q?{dqqSsDnSIFq8*~_XFx{7Sd29z@KIpDV}~`#fs}FOJx3DG zc%GWSL5*x)3sWORU^GB%L{R2CQ^U)uC-xo2D@P{~WCu{Qiiqr-gbidYny^QC(xlr}+C8sHSyE#zPfP$Wn?W^Tki$mS zbr4slk@KgqrW<(w&h@xv!wRe{0?WHR^c3=_Rm<{>$o$;!o}BMnH@{sAP$p9zU+SJa zHWTQ+z68qi-Ac}R;ZmaNY0x965MbFMioKEvj>;$rUhIyHeffz}mHI zWlRKA?ngSVWHQ*jd$;sOrXfHeX0wbt?zlr5WxV|I%hKvm1w{zlTeoh-jW^yXW8*R! z4jOkP$f}{he#-1jr3{(zWgd$yCAp1G_MBW4pqo7ptsd^QtE<)WcA(Sg3$s*4+bJk_ zdi+k-dpkgx$4!H>hA}P%D4Q&6h)dM4FXP7L$MiJT_KJ)*1A*gV#s_9<7LK2t!|vCQ zN|?Ai=^ z;u6+%TexBKAa33|B5aG|8aXFIA>*M^uhIA20Avb!xn%FpLGLnOp5k%gCfoJ84lVB!Q}a?7!|ZbDAr#1S$Y?s+b_g zA=T+)wrRW=kxVVzH6kSDI?7$5QkMX-IYZtrNy_8)qU4pFtHo ze`XBB`4HD_9>jaLj$q4*0#@WANn*=oJ-Kc?Z}A>7DXy*5W+!i-2`eV^KeMlYlrZH> zpxoKzYrR(Uq^3z$x^^q3oL)ykRNcFpAj5vj{F?wwc1)lquo9$y`O9BQk6{8SYeB#G z#V-nbBmk-#mtCH??tudbWPLJQwnCJ0k$PQvHgo^`_wSbuPaG4$o5y3%<4=C_lhRi4 z2S50MG>sS@9+q~JbV+hN1YjPEAWeWLP_hjq_eFNgp2-Avwtsx+p@)PO^IC9BYz;|G zWCH%qpZrd)4?&s67h}q5FkLKAR_$hiU*;Aiu2P97n@Fp4OA44o=s2iG4yv(>nYjS_ zj*jE$H%>?^#N!hc%ps2;(}!wobfkrOMeOEeKv}Yh#Qy6Tlf*EI zKO2T>uzTLcF5r~J$0a`_QS8#%Ub?$Y#&QB=NhWq9_8XculBSr-NQsK^Af2F?x}B5k z@kT=p%^GivJW(ur3n-gVuVh_IQWvw_&9x*LGuzuEDEmG#nSyATPgE_8&M_~bj2Dlb zz_0g@Vl3>DR-7~dD6GO{tyt>4jkiNHxize;kgV4?wyE3igFTa(&}P+2=yiQuxpq0O z-86`8LtR+a<6(Iha};W(Cfe~(=YW}SAbq4eFMC=HUz$Mqha)2+fBlXyf4_1ELN>#mdg`t5IjTZ{lQ zWS#>J29BGcOz`L5WZSBV%yT0%C(!bv$z%LY&?b04`sky0{`u#HX@Bs89~2{iK+f+p zc}>~1CZ#nedNn_5t?pTRolZb!vFBHrcq^v77)$?MVagZ!ZgP7CWxB65x$tcP<-}EL zn$#zt9QnfZ>OtMGOg1v0>U8Mf@Ypoo9IN7mgD3Fv;Zqo|d6LyzK~~y6+Ab5g2+xKQ zs~9Hpl36B2SE)yK8dJlX?o4Va2XO1NA}_gNz{C5lSc@$~MT`_Z4CHO(GDZtPqST_` zW$I*2c5TUlk!0zD zMNqaIy_vb6Oc=wBB4jO-9bK(b+q)abOFoWGS8%da!9%-W!K2mv( zD_4pOo$hFYKiM_EBdBtHW!3ziKu!kFF|$zt89V`2L7Bz^4FFE!>({TB{gV-M%rrpg zQYXXbxX7}(F1JxY*HWo=Y`GmM>wUaXaQwLN{b9o&4V)Ct$e41P73(Ml)r?9dKh6guyAUoibG>1ymaU^UKt(7 z$vL267ZEvmi8IUdLqJX#ph=`6pfU5;g5}qdu|nziM8OX?Dq}^#!sV-1VB4BLtn1HV zbzcs{y#+}i5^$4@ZUZ)Ez@{`%l)C~0Zjw=*>fXlHG_g`bI+4*F5ZO%uU8y@Vwf-#6 zO3DpQ{MfjPHGz$_$l^Lq0Oj0HTV`3~Bk^AC%Wmc;^G@+9n(S#uN|BQ5YzoO5>(y)Q z2EU^Vn=Dyf(fmJ+2m&o9-zT4ZQpQDt zf`$c`Kla#T5`P{Y9hLYo&yQmwXmelOhWpnVXkG*U@LcH@*Vr+y6VGSo&Yk$xuYQH+ zo_kKfTwH+E2u>M1?Lb4a2Z){V?}(9C=V<#uuZ z#rChPhb~HjrRlC@AjTHxWl)hllO$c)Fgj7_VrfobTy8UNn1GD!c;XsmzhrT5TQTx3MEf~o1;T_^^<}VOc@dk3d$E>{3knYU3N8v6 zS6ty*AwXs=^{@`x7$#i}k_p}uOpE;Yh#$&J^0q9s|9rX^!1N1xA|;r(DID7ilz zLr9~za~ES`czj%yOsf>5qk&NiiO+y{-Jx+K*B<8yNuoAWVVcJcbr zGkAUUI8IGgFkTK(rYcyj2gnvsuCe=*5iqf(8y6dg)MelDe1s9)IK)sfgKcYU^hJ*E{a)O1XLITQI~gO`k8C5{i&Do(k!rU%6&~x{=v}D&|mlU^-V5y*Gmr+ zMK1sVAOJ~3K~!bRH;j*ufAfEW`&ut#8jPEPS>2b->uA{$-Ma)?#)!Z6wXX^AFnLOA zfq(D!eosJ|0L<)Yvd%Aj;R^zwWUBm5fs6o7wo0bV*e>0N+z)?~DYpYa$g!xX2KWUSl{{1cLbzA^O?^`9Gjrd-~1sfCR5U+Gcsj@vb>;) zxCyTz!I)kHUK38_JT6`3H{EoTSm1Q26XesSUg_ApPWtHwyZJt}0~Vc>m-=!n243x$ zX}jeYGM2@*UFvZ=xpo9))xK7y%&dDYiE1^TPK>3uf-*Cwjpnr_8PX;OD*Q&=yh*Z@ zZOWMlAhj^UTx?;i6k&9{jKgCU96mdXV`pYCHdBEgQR#_H+$492YLt~yaq`P3II+YW zHw^aR^3?;j74ReWZzjUG*Xt_r|XEuwMkDi8PnW^rB;%a zEW;;DHH=k&g1?i@V~s1TfkneZ61?QNM)uQ4S;`d=kYYrchReL>wGm3;|21ijY+Uv! zhJoocB?d6}AJz?6*~T3%!W}dOq`Zt^Jm2+Al*8i0TR_*p3ltnrj1mGaWB%gCCQEi) zWL*zFJJd;A%NQYz5ywU3x+n($sy;A9ft2wQj-8pp(X%r+a&{UgXKR=Z98^LJmRB&1 zD^j@u6hz@YVOXP>?ZU|NUThv&j`b^wSl#1cu-nD*t_-@eE+VRYrP3;7KTMBHO5((8 z($o~?eBaG9C|j2G{o&!^zqtg;=X*7i?G|Ih3o#M3M3v>K%Zk86x9yj{^d(`m|M-vp zC@yLh-yk^Oa?33em%Z=4`-Byfb&~ZGq{vFip1D4Ok+EBXFu{;OO2$gC<0RP(%4zj2 zCR8yN$fPfVDz_mZlKGN#vn+`(K950A=J#Bmu5B6%1Yj~{PQY}tlUXw!&1`U<7st$a zH<>c`rwLv%**J~>G?_Ceah@yX8o4i)SutaqiC>!Ot*&}Cc3J_(`2g}IP+s`OXa~v~ zjv*-1FjS_@`^jfRji!Y>35y#EiTI_kIWjV%g$>K-7_)JrnKD%=iUp}ul42PXX|VME zL&c9UQ;#uIw=olXI6G4lP(Cp+gA*suNR4l~S`)yOPEG_Vy02}BwB)j$0@f_=mPvix z@&bl?b11qooT!dm#wb*gt7(a&UQJH!85$0(5BB&Mg)D#yn@>;=JBX!>samhY_qENV zDMjKtn#*|X+Khb0LniAAIP=FyTqX=xW6~0jPJl8=!je;od`V<8hKm7f{qUZ!gi7v> z=w`E?*8s1~R}8gD4B3dbkm1%SAQZ%?`|R+4soo1Vi| zse+nMffTmCv_v8_>slxl@>1TkW@HtX6>}IYW-(B3u%cjNSurDuUU5L>!=^g&#Qn|l z7DczW36vej`QG5*;Qw|Bl;37hZUxL~_mGw?a6N((nJtsB2&M#OGG7(rASg2djG4)F zI}hu;Tapj$lntB*2l$DpO|MRwXahRZaIbN_|#6XBqssxxSS20(vpjJd=#DN^ENs$55W6G^2yflb3Ub>{Tf}l+1%Ln=&{^1{l4Quu)U&`BW zzg^fgL75L7W2^)~vRB4zj~_pthyp|+=)oUa>vaF}SbPWw?u=U#@aS%4DU{Zda-U4< zBEz9eoY~m~LV_|`v1VR#J#|YHthI!SS>fC-%bCcaxjn^22&`nwWK9H4?vD(fz)a9r z!$ae5`kNEHvSBiKf;%&>$(S|MTm?fkKEKolI=#M3Mk$x$h3+f8eLJwcSjTNQ=EcU* zZj3SkG<6wjK3+Sf+=x#uu%FJDGVc?$)Dzuk0*!z&(SxAFh+AqvZRj1Q3H@B55dbY-|>%CQ3LyJ|#v$wMI!!qb$bla+xf;vo_ZB zcVn=pOBzJcs2~g!YmoW-fBIZT%WoH#uxtT|*H0FkiNJeyXK zspcK59qhqyKY#krov~5$EcE6)6fzcEJ4O`N#5FF*PVxd&j6|L-Dd!5Ln!MtN;?FXyaC#mSQ`Q2U;m9|WqZ%HerrtD5E;lyprI4xN$!HZ0p ziddShO0Z+H6PGceir%_GI3?LRb_UvL1PoA zpV!V^i+`K$dPzB0B3*3p8c;%%rB~Fkw&|ISB|PDI*@k#gwOmQ=X)In9d6rU% zT}&8ckLZi_X#(Z%tXQ$)ZwCejrkA?^B{AhoplmL0dLmTdNHb_aTs-4a4HiDTl1H8s23Se_A1&Xl*5Ka&`&BtzLUp)2#07&=|nL4>&= z#?h%Vrt2|IPgn8A(X)8<$SE9~ETd`_5ISs&V7f>Nx7Ly`I#eUV1LZnHSgfy&19WFS z^cOM`6CLg;qQ98I@?t^2xht1|I2pZXG_4sEW=AKZzQzBW<`x_;wamzhIZigMsQRHK z#nCvZ)dN)Ob(H-GM<+^{sMIk&UBcPfGNvmb%8`Q_GVqZ>J*1$AXT&{Z%oHOrdg69m zg%_3J`Xvl@WwCwT2zG5;g|*AOu%gRGZ^lBmX9Qey-V`4MOQkqIF@$2&gPJj{B=Ic~ zeMx%5OV~g>UP(W9o|Tlj(GXz{ea+l^CkxwQ{W{Otctk`BI5Z&(k!wvlhl%#M>Z6lY zUnE>^0_AV7Sh3=NzXZw`F#nvd8?br1=Ks@ueD~dVi#wVzS!PqyRZKArDrynP=)xtC zQBZ{LZVG*nDXZ4CaoHKyO}Yd{S?A|J|9Np4lX0v2TKhLEQ)V_b!Jj}*KqmNddj(QH zP!td04}p~~b+TeIWf~9MhVE;Es+K&lrkR=AWZ2BU)yO)`O=V;}%?4E&Dl zn=xh9R`Zyg@Re!Pu%O#Nt>@ir5VgA9?cA{!@QD1k+O$&!tzL=dlS>;jeCGIU(zqfR z^ZqkSpFi!4r-jBb|GkOZYFy8Juic!|qcRCQ=WaK@Tqf4EP=~CX#4pduu1(akl8j^B zgJgOqAAw8D9M1@UQpO@FdvXX?9;V7Q9G|Y?WZlNTV`JF&=5ZW8J%!1djVRNN*eReE zYFov5V_xc;6V^zk=rLi9N>p^oT84?zg`Txz&EIf|tRI`O;HmV_ORj_(6hifihi|y-{VTBW7b=F0{>q#o1j^r9 zv0}ylaS4mHFgY2(Fa= zWC;_O-+AYq!jx%LFeyuuxoBMIGR>q`AmtcUor`XKy1)s#OjP5s7)xhT8tatVoP*z~ zgrmkW(@A5E2{UoiEp)3df%5#7Y4^-3$hEp=>1{bNYL>l*BGRoJ7klkmjp2L^;7*LP zPNx8QhlDwsL?ly1%LuV)!l30u1UiIu0ZO`8>#>8mz!q2a=u{1Rj!)s`BWDGW>&T+! z6i|=Z!`Xvlvs{Qho{S0=KMKl>p^B01m?jwnegzpOlac4rr5^irQLD-^25wV#JC;(P zuAGOg1N3xdrB<5pR8uBx%8yt&&Di?*Oc|wmgqfsQbD7<+2*8mIpRwkQD;eo#UST2| z=N1#nCuk~2s;8h~#E9F$f=j^gR*5=|4C3@wrtr>EIIy~)HBpPe#=rTS%NVcahb~@@DJ^5DFGG49T!%o%-?|N# zH4~nUaZh@rh6hLV1Nn8 zX^;q#APE8>2__K?A}LWTY0r_*@n4Q>d+oK?-nG{ov<|d)Eorq{d&MLYlqdpVKwtoo zNDw&(0+XkEdOC*+FL;07eP7k9>Z-1)P7_dd&YYgAdhfn_zkBQZefcw;s!~lXOeHWA z?5375rVN=mYX_%D%+yEW!Ligl&GqwrIB8I)!t2ug6JIvI`COkJX;%$#a7o&b|kpeuElCHO3H zt1=(k{2rxqf(hVKzcDkE1DeHIFWxk&;6PF%I?*1PwxuAdKwS~+r7xl_1wSg8u}Sj@ zYRKk#bH8ZCsbDC6Cwz?UG-F0$JBUk>1fKc6i>NpSKbpn1T!7oIxDeM}v>E%h41yPQr6~8#X_D1Gs)LvCvPuHndMwa&P z@OsWs!JOZvt9j|QoI~2&P1j51To zYJ#hhfT;wKm0W3nubkHel;!taVx~nTe2!7&PH77HXd0Jq$>Y73@5J>7wn;=bec~D0 zg&kT5*l*p_-d&yT_SvXG*MaiahKGm$^YsE{<|*$x-}00@0a}$l+c`!mRjL`98Yn;0 zv?BH8YJik#l?r67n4ShmaV`q-3he5W)(B<_0-DmI(avhhP9Ji;Pqkv+ui7l1r*llF zYpEq5om(u7DKXu1e68%279Ny7N=scYKUXcS9XhvrO)Gk~JI-2N*XcQ2OHfw9ihrpo zGpMgqP;RxJR^P9b&Py@xR-mlEbN2Y1Etha=Cd3=3=J4n%NAT;nMscd(!0y?CITqJo zgA323Ux@%9=~?UrowOKaOGeSaZE1>;^kRY+n2u6n-AYW-Rc$ya%_%sU^ny+bVVJ#P zr8C!btD=@`JEm@RqIjx8K-0Te-jZe$+742*VbSE%j@@d|Rho9`-(`AFXpR=aoX@9* z&6b$VWe(?&VXBljkDD*uj`v-50WR8{mnIQ8N+B$3o(LH$-Q%<6{yI?p_Zv5E{4X|b z+GM&4b@1vWPg%{^T~JO4+zvxg%~11q35wjjbbhl2RA~n(0$lq4^*~Y&WN9HGttF?m z=(M)0V9D>*53B&FfJ~u*!B{kVbH3@ZRMY2QW2Q3|bo3v`;Aax(Qt5AJga)o(pQ&?F zjb2z!a(()F9h~Qum!8j{+=;MQPpC#`IelhSQ%*-;Gd^G`dba!fXt`-CxS-K*MTc7o zl%=F+!X2sEPA?QOG9Tg96La|SGjHPALz9SlhA|(w5@~HY#s_TzuWZR9mSzY^dY5*H zlBjNiXHtN~5^b8bu1)7FBDeV+8GEt#vl-N--P4l1T@{$vltO;9gj_azU}SZod8^e) zP1+J&u63^s@*|9x3YIn>R~v6sgp$;6TYx+jT;(KIKKioRzuI(^3Z#QnHZGtqoW!*k z4dMLL8SivMgU)7QgxAxOER>&A7>})?_^D_u5Gq&t;}0#$-mVzI$2x& zwJSXzt$?=Oxi6Pad_ILQ=G3!T423Gbr^ZZBZuM&Schzc~wfMdY%Ji>>NkNPxB{w$Z zHA!EB^2B@*BXcnxe{%#6Klcs}&AOP5veJk``amcC*rXmw(iBiDFB`2;%3Ufbn{LLH za-yu5B?U&L;#IXgEd8_su2Ol*mNY9Fl_^CC;MxnYSZ7*_lu!q*3KOz-($_gDpy9B0 zvz7FGre@D#8%aIzl(yU!|4EQkgy^KNv)0p=uUOYQDj*-uV9%h1+ppS%>vnIzu0gg` z@0)>2tLLt8D=N3QT7UXEOUY9A`B^xqd!vG0g^xSni$?gqd2$=xbU~ z_+DE#u9Q~GozqI6TM`@w*vveR&O~_Z^-=ufg~K>f%%NcC z3?Qd{waLKZw@tSUOqxPSMKS?Tf6_fi3>}*{r4rLR#B?ZS=5|j-z7zPdmr2wA8 zn)5#5umIO8NF8vgXH9L|l=cw7=kN@yl5{S{b`XGVAFl74y5OZC3G`}A&1OQm{Fbzh zrj$LUrp+pA^n;ryHLk;nittL)*pZKL=aoBf^WM$aGvHuTFV|T%>6x@hoQI(7IL`k& zI5_zCHgDcM+v!>Dny>39)((?s#uVGRcPY)ao%=i8;H89sMu5=>l+)T;`kqebd!~S~ zo@UL2lC<``Tsk*;R;P4Yt!FnHr`@@vX`!G@O_`ufpdu(U*lRuawB5OPL_Y>!Nh1dX z!z=(PprY*&tr+Fl#^}^64o?Mm^tBN@^wKe$2>Vd>dQD-F1e6to1(1_TNL}xwXov<> z5tPjotz!8OiM#!up-#8s7P6wI%K8k2J4uV1*AROq$pax5g?>jv{tl|)TpEq zSsX}%1{u;}Kw1W6sVe5ewtXoUQd`w3i9k>M&sF}BgoH~$ky;C z;GMz@HrjLe=#@Kg>m}Q8@dnfH*s=rDF)B8i|9YOIj?PC17pFC4+qVDRh7BA3t1c*a zK)_~~cJjk-BuRFvt+fJzR`Y2Uo!rWdyA;WEeU0WI=D5T^)h^l3)*s$dCjCw|-DYcS zwvU~T-!83J>pE&!4Dw_3Hw9(RnV`&E5o&>b2v0v#Lr$G!NV^d!^x;0f!`-VO{QpvfkfuuasU}ZdLSFKv-CESsBDdH z*6zk7qbbMK8u{$Rrz@SX%BHq+ya0#AJ(*g?;B4_do0O)|$rwPcloU0PsO382s3fLt zF`YYElx$Bbj+MpSoq=onQq`<_Zj*qr6z-bu65y-><%C8OwPiGR27JX9tAx3*q7Z&G zho15{K63d5xb5=o*gKfP)(si4_Dk0;)12~r1m(Zjuwlc0-38^9asZWXS5PHpDdwA= zUMn!xYgY=&DEK)KcVZ6&BR`kk~Ivy=5|KvkliCPhC?s+5$!Of+SJ@;eg+1IjNS z!ALZKFf%9-)>TkuYL&LbG-HJ1CAR54#Tr~h?`nI+r0%ukF`3qr6c1P!1aT1-3b=6B z23)jr2pfAGAecjr-I?Mt5e=D47UjTH=B_!5EIK638W?cJ++kmGsAtO-N=_d0v5$8~ z#_`tSQB2QUh(uehZR2VBG@G)ZjX>F$?Gs-)8-&CQMi^j6FUvbCNV)(3AOJ~3K~xn% zMk<-bbI6xY;bWI?!)=%Cz`hMxY~9d<$hD;pH7l0YKf9CtJDaXeY07i9Z4;FLn=UA? z@(wNc$KU9_H1k=C5j8U(H(OdOrncNQq@TBxrrb(XS`IDJ_bhjPoq}>RjjSD^q0t=c z!K7Vbt^T@BXwk{>ZzYVRg~GI^d^Uh`y8>%F^iWVv!mdo-GOKZ!Xe9yVQWRrkaz;S; z7q5+q`FaF{2`HPo)?)gWu2TBcq<%J|uvyx&V)E4#DWlOcfJ^d}#cW;Gl35wc!8{IJ zG=w`oa1hsBHh_K)$XkHV*s5|Qy`bqP)<^0iI2S2O0+R*7Czc%-B?p)aftL@5c<9OJ z@T+HEM{h8}Dl=tJmuQfbVzf~-%=YG>!pzH910`QL8Xu;K4_L3xe#X*s~wjG49r z&TGYpmrI`2GG8}({!$g8IyFzW5&#+r8udbjj?oO%+j&Ma?YA9)YdL4FlYMO^4ViMV3R{k!>R$>2;~?!H8qVx(`EeP)e$`S;!&K4 z1`xOdaH$a{s94duwZVdABb1VuN$#%zoiUX&e2g#C_=44~Oot}=cvDd9J_Gpln&zpAQZW{{5|6x6XCCuC^%=$FY6# z;yB@y!zpXUWR`Xk{@9QsCQE6Q9Ji5{xY8eM zEAwzOO?W9yyA`;u1e6=mL22HI7OSxb&CZUFy_TTdYW|&`nNEKP3d$AWOnNqpK&Uy! zmW{F51ss`)@aU^2@z4v0aJ0ne>s~l^t`dn{nX9xEG^T3Pamt`}taYwy>TD-~M}M+7 zei?X0fGfd?3UGo+WTRQ!dffrsefxWH+0HBmU7(lcwacu|CnFoPw*az~C#eLVv7d9v z0w!YMg(DH}ee@YT^yEtzn+uQ~*n+uIWI9?U>0eC2N`tfPj|deSNM@d6;@9Fzmn&1^ z(}EkiiGQ457hqu=JNrW1e#KVYuzMJLH}qhLwfG}Ol)FH{YtBDS*>RkI)8F6!-*!Q{ z(_iEnzJ8^Eb0()>uTcs=ScbIQj(K=3*SVZIujPDNfz(<)yV=;J5sQZ~B}(&@`Inu; zS_wU8V!oX`v)T|r1DP0U5oRun@YVa4y7 z)QOfLEd!3!!YhJo(s{~!qm{o*>Xhaot59aN=@52YhFh9JpHslW{oC>BTd%{7mkndc z2fPT#GE*vw5i*EObXT@TEF$mFeC%KWHYNZ(`A!+%`^EEk?8UcnYK~_43}AaGyN={x zt6!YvWKw_uTc-Rc#S2q9l+j`oFa(gb%2>Hz3Ib7IW-V?~O?p3;kaa_Zv!l3f?>5|W z>2_SQIgfoCdoY}}5tbLEzzQuA&3YhrvX5uOwP{V+ahyNv@9+Qbx}bbE>`Vu9INJcB zgQ-`v+LfBx-|e8>$P&>BFgLSgbh4MNu6;LvvOtl=5|J9c%I|#T0zoxERb)R3SeToWi>2OKNF2w zii{+hZ$#sA3Atzvebzj#-@65O-+41`Jg`B0%RK;mMlx3us{~+a)h@un3@kkTP7(M1 z`b|9i?3*|`Rf3)AN5=6{ER+#CrlOYAwzlXKP84pD4wCYjiBDXTtdi24Qq>a?^1dgj zSw+_Nv`r@}n%^0W=1Zx~DcpS7F5Iy10_@w|gI)bT2Hgm5RFM127KMk}e#))Rj9t>@K{Oid#9h z6vYsXWf!E1$(o-S4OuIIN!@8`wvtL@noy{znA)p(u{L%HJD3Az$1s#B<2_eigu6a; z6K>qU5gTEEIfz&a7jR*t$TGLKkC_N~{?I(`ee6X%^xQibEjS3=Uc`=XK$z)n2?%p8 zQ4-u0YK?A5wW2wj>q!Hrxrsq=zHLkST>6kB_MVQIjuj$<0y3pZY|e)Gz%>`++TEM5 zXSfHOa~Ax#Xf*sVOw|Ej?l1W4xi1CEv$k#jSr?Sgp1o;%CTAKD+MZ~gX*$jLR|*2F z4N9G?Ni>yZnA|&k#!}a>_4-%3KT4zX(|ld6`D}c@#)M6PWFVGCRTs)JtZWZX7c3l} z4)Es43?6&_O+5ehNi28+2)(?d0a00GDiG1pX4463&RUr(eF!9%H>m_h$FOCBzJ%6E zz!^>EYBFY1i?B3@j8#ORU6c+?*IsfFzI@lMc+Z77^s$aK_02dmod8imuD;sb3?^gX z)e}Yh)q_vrv6qkG@I(lIV2k867m9pl1om@s17l1lATygO6mX``n3_HP;>MpW`oH+u z8AO(VGAxu!1^AhqNdXQ5k!Z=5N!O8TSPlWl!Ge>OrWA)x&El0qBlyJ&zrj1x5yEUgY&R=L<5J`x zklx0c-)pLSRm|doXGzeMfFG-RCB;W9YUifRC;ikWT+5PxsA6Fjz8#@IYojzfj$R)) zc+qBj@wO{*{YA#dTrLLiJr_kAc=GLOeE-oG@bL48F;;XDIRk)`M=>zvEnVM1nSIfv z&uQM{;%jGd&DkF@_=*wkc6{q3MjbdA_gb!YM zA>MOf8!qUxu{rM}>sbiPCHUE#n9p6-06kA$b)ftwy}iBveHWC^vCTc#K)FWPKLe)b zwbLjye%$ldd|mO}nwE;<_a!JNImA*qE0E$FvxPE{>BD5%!tse&oEV?S zldm1Y&tEx?S&N#YhuCGMsvOERWs9TMF=k@P8%~19w10yL0x&64qU9R%wQ9;N#Yq!3 zYe-8?FgB&2|2bl1uK*q*c*WCr&)xxi;UhQWy1kn)=otayncvLen?HFRKYQU2#^Zhz ztQ>5o&y)})D0?<6*G93#U@Mc-r2%gY*pd`&P2&>yRR}f#X(lS&G-ak`z+n-Sa#_l1 zx^{?gdIVcDA#T{e1vl^Agv++~V>lb3*9CmX7K?)IWKarenortXuEmdgoyR;=pnNb% ziQ0d@^;2Hw{WGR2>4}=ET7M=c(dILonOWD`{MzR9-nEyl#~FGzgR-P2ISxueM3oQ= zk&W^BGA3pVc=hBAe)j696crhpngi31tV|ywJA)wUZ!9L?I0ec{oo#bqZ3Ge+#pq|o zA)5viCYUPmKQlr)jN$s8_>4D`0$O_;PQPvIDH!U{)3O> zr!TyTlM6P=*&!4HhtP?v64rUdKq5DcybfK zCPhg^({|w4kl?PUFo}MvfNS>*<2{${#NJ^Ky9P2C@ME~rgo5oQDPDNS_Z~8!F5Nov z-2Ex1LD{mb|EI67?|)biO}Puot9WMCgQ2eCbZdrEVPdJ`A1i(KxlEsxuKjGf|9l2z zR=zS((N#@Z;8HbP0m%@yZR$Cf!Dws?<|Y1Wjjpm0aa<-ZNh(rUt;3zvvgQ~zg#xOVK&2mfSSrGI7^pP|FPOuP zSMJAkS6qs>-g*m9KJy|bqYMg84#g+~&(Fb*Ld4W&lR_Od$@5=mnG`41saD~EB}X+S zsuJKOeUA+wi&jmoo{{F3i_$^?z7@ic7m$nQaoMh6+;r(K>>0{p=YWUd9tZutjfjOp zMA)!V3~gy{!gZXdp!~1<`uhH17nIK?PS%}EmjdSr1=<1SbH(CQyCsYiUK*6ySdgGB z+AD*%EYkycp&Vg4aB*lRmcXx<-h3Nx9vQ*dLWr`RmEf zhlu&cmL(c8!_dT-Zh2B#GqN2C3}XQmN!_AGEGGMy$J z^7?@Ci4!NTo0^*XS{%n+^YyuYg1YnXQlL!=oWG!4HQTDDtog*MDVzG#)#eP$#ie&R z2xBb7E{;qTF%vja8~d%}r|{abQ#d*{i-}^0LgXTFv#`7jEH^8fGZkJbRq6QB%b2yP zrA%j3mb_+yRJ!XFXK2jJCUA^`0sqQB5L5~7p;Xp#Jxc(+Tp|Fv@LgZL!K@?}qbMLt z0l~m&Mv9BjR4u7kl`3QO^BN#$>|43WR4b-=@qLUSC?W_8BB*5S2;RaxHsw6*7|!FO ztpnJ%xgWcSd$47|M_<-K&PfWBq?=}l@4VWFy3J#F)?bzaapzBlal+@kpyA(J#De$fV1mvv3&htPu*IU6Lqu?v{!L7qas`JO>8&Pn zs>jq^8P;7 zEY{Sd@>ZpohxGhqJe27l&VVWDiwwu~l8z!QlmpBbLrfI|OqFenFGM&wH81t0*&=at zdI6(TbC@YX`WctuBeFcnZPwO^OoLLhw196C0Gck@VbYUK4zYrnLoL^~5j&O%-r^i1 zOGar4wQOHkXI`DSo4~tBqsrl=4aNA zhAZVbXG#&KOChF9G3LrKMyBR5K3Bx()B?t4i#R<~#OdjI%q#>bNnDqQ&@z>-nL@>S z)Nl+~QyUJ$Bv4E%d?kfPOjI{3j+F#Z*;FEANinIk#l{fy3kQAW_qbj z9Sh(}05&yb{tJtU*sGbPJ!zVTg-jfx*CXKi*wmND_Q8H^+1Q8S{sHu5fqXVX-i^dm z-J5Zc_Z&&t;@ILh*Jsw1(mY>+yV7T+3(Bj2BIe66=F7lLA;5Go#?*X(QATc`n#AeZIZVtJrEYYg z9HJP)L~Rpb8M)1zY0)r^*<2EmlDuW3NXKwIZ2{?++-T-ROSw&gc8nkiFcUyl?K>*N zu>z^W&Aer5$EGoJ0N06;&$t-KWzg%p*t%f=!@W6d8|cHvo(%eYU?Ah7C+i`fvEjQh zGOmTJ@4}@_!6@%ahplSox2i^*^ktSxdPcK9UtV>f{D0;1`G2(Ln)31E$FG^5o@Sue zC0(EL`Er!H>*!KoX$rg>K)I?R*XGO$z^E@dRhG5l+hqhb1F({a=yV{K1Xe|{0QBTc z0dvI=Wzc->Vs;_G#C!oKrYAAAP{jD`Jf`MLm|38v9m%x?s?tndBE&}Y2NRx6`Azzx zX-1b~AL46H8fqksC5U-6xl>`5epiu$I3)NM;j#9#Q$W^^q;|FM+Q?=+^k#hwi$v>}ug;SkKujcdlf7}J-E_dxxpi6;Or@*@zl&i!{Yj_ZlG`?S^39(_J z(fs&Qppg=t^zpjBROXszs!~Ah)xknJ#9XhM||3`cF>?w3QosOcfj~_pN_4M@g z*WV4AvYJ)SmgvCxd7MvXpH2^L_xiP`K>GZ@8$h`gMXMTfEz1-)uB0dNm4I@p$q7x{ z=0$(50V8n}Ge_96F;gsxZ@CnND9AsWvSayMjx4DuO^umNBp6V}y4pql4I)XWV%04H zIO%#Lbe<{x)K`CQt;WUj5i zzuEV#?mGub(Vg;HM1d|S*KS5LP_7q_n+eZaoi2Sn{j8EV9MLar{J#WU<}x$Yn3X;0 zS1yH#zd58iI+C)R<{k#2Rin0(@+77hh~yc^hy#`hwc$Dr*!zKjT?FZj=ZY^l6PIAe zfr&9AFxw7X*OBWbx}3h|iuPX>I*c{I1dg>*T;44TL>(yqpP5YNe_nG?K6dQbRn(LL zTzbCvl+Sc2$9BQB)sO!y;^*C|oCg%>f^uh|+z6PIx9E3D(`vetRE?&JYkbC%h(zVXO zv@H63qG209x@kQr^G&Ln>9fR-oTQL-LD`&@GXcuhv17-?eBA}*_2atrKWKe&v(MLZ zW@qA=Yq_3tVQkgV8qIYz1j=UPcR{&5P;PdH>VGSZG-aA}nNQ60Bn^JzcoGbifUo5w zfl)+De#bN;iL5pVD$&(R(GbySwc1yu>Q@qYCFWxjl%~Z>1W4!`mj;bQ<8&mn(TZ8! zOU_6oQd&*eX5|`Hy%aFN8#QIeasCg_^Zu8$)|9Q$(b20;pFaJK^9hvCG-F;0=p>4f zCZr^?Gd?)QC5jfGa|{X*Qwz zOi-?9?NSHZ1gcRpUUUYM=<@WOmf}Z^=yATE4PA3uQ(jk49v>fPpx0$*v-vs=AgvBW zJ=0qa?V}U+-7dJEY4dd#lsnng)w`BVRZWp;^QUM$tk9GcjA-`6R_DJFGuO|$pj@TS zazMGBwhGAlxqxPv^2u5XxCtn0CnoBniEu6;opheke#Vu8APo)y?^8{;UidO+!vvt2 zMiq+u?Sz#Y(l;_+6HEhf1hdIRl5Z@gVy)tpl=e&lx9U@_7K$-oNS@&ALrml3F->j zrK$?7nzB?q3sv`7V*#!U%1ZD$Q2yWc@8ADVEXyi(`s;67?2F^rNTO9`_VnD4n}&PUH2*T*&SJ?G2wMjCT$(>BkXajt`V%z5&~weyT} zEY6!}p6lR0r5*Q}@3~)mF6YK|kzPEXe9p0B$10ym?rSPwNSXM%T~qFqv9@=;NhwN< z$EI7;k}sOB>tD4M$XY$O9@eE}(-N<0UU4&xxwGfDn%}wneF~JPXue)AP`*6Tl-CWE z)pw?hLN|>J{5|h^53ac43fy|-Cp(@#H*2OoS8x7>0IuD||z zVeDi&Bpw;@JMX-M_rCYN0zBXU{`ZBsleyk_4<~7*3oxfw$j&TbMNO;Tqn0 z>n#Cf(vzUUbyEl+%?NUw3;#a(z}Kor1E`itFaOZoTzZS^rZ{ zJtctr!4G~AzxmB?@YA3EQ~-l>yylu~@WvZ&$o|}M#~rda{El`ot$bfg?wb2vGA}5GWNCDa`P@mtJ~FzSp&o2AmT?w3T~6_nY+8A0>rF z(o%z>`1z}^zA9@XSbyXrAHg%vJR|@3%$shy2?q`wsL){7u3fTkoImGBT5)}x?}tD9 zVcA;>Ui_YafBDN_%30xeJdfNbUVG6+7YR+cHohlq2JWqOFxpw`X z@L8lm7nIiqE~>FrRnU|E$;?7(LuImHgs8S(($J!Mb%WThnxVR&+%{#?DNzJvYRdoX zdT7c7WopV-te2)t2B9xylw?w5E_dB^moOay>d$`mGhrlTgrEA{xQDy zz3&OLy7I~^@xTKQ2qPksp>{)X``zFDU12!9?iauKg)l$@IyIK>eCIm?k_0z`I{}ak zoxnh?wQXh7OhgX7*1;9h+3MI1hSSgt3%2t1@a1t`+xZ~o?QWWEIOKmYST zmwo11zVL-F2#p?o_+g<1|MA@Ldyc{VCXkVa(ov^U>)2Rl(&w+yI(03)kMy8sNhH)^e#>#rREzat&?TAdJR{ByD~-_4>jY}X}ob`4Y(bFpp8eqt1hY2 zl-CE8Po6w^+2rITeahpkIauCLM?+#hu}eJpIYIUzx-ukSOl}b z{oB9A?YG}9TEaKJ@eN`0WQ=4wWZIwk%x6S-NKZ~P*t-d zSdv*2)Hol44K*_|d@^{hpW4??e)1F1IJy2b=yJ_u*zbS;`(-Y)JP-&kzW8DRI06>w zNgyXk5b)LP-fDfSDW}cGYDy;adY&Vmr!Rf!OY#{7IhuBP z7I?NeFa2Su+oY8xK=)cLI=gr87SN{-L4%KQC4Yyu{=KDqpwVI`w z?o_M#wx=m~LAmq&K9iuVnzBU(*D8>tou!d65>VBLN2#6+j2ar5f7-035mt1RiwcC}i<7jzKWyx#jiTUyeagCVeP`a(*0x=Y!AZnWA98bCMQ*wPv#dbQhG{;bfg& zLoJzr+#o-=k@r|CcCqxPKF0=P#?jbD<8IM)Nr1LaGw!@gQ@szlRdjZzG&y&#KU1K5 zX-ZRG7f|LFX#f}hufR2tov9f*aSw>j`QKRNOa$BF_vpQ>CHqp_eh zo_X~Fb1i%x_m4DF^EHJ}&WYcVo_sC^5U!0hBF#xZg0fnoRCwYwx)!y9bbZQe2Nbb@ za#c@WWL|FZ=m>u0idMkf2$Wj^`QpcSLAlwM)PeH9+`oVSKV4IwGADB4#0i?OsVQG+ z+xEJEvT9aJ2=#ptpvZ)%G1I(9kS1u72~i8yfGBFp1ZrxN4?XmdfE&jkV3UaS?`g0M zfr{Wmjf>zBULju>=zV@}s8TkF*|9!C_kdEpHrwf1@Kdl4`sH7j~r&bx=lWLcr#y>@kQN zLJ|Gmd>%oWbmX3Jo!k$eO`6-eMw+3y1~p&v9)xXpT1xUU>bzQeW36` z+Hl^q5Rpb4o9Bi5%QMaUDFARSJXZu|zUST(*j4b=wQ?+t6xZ|F)s)u)m^J#k3d$BW z*&3iW-)Y)XS}&Cfopt7EYQ9Tqzo|g48b5V|XPW1x1%^g6=xWMJm3Iv&r+vyAbVEi+ zMomyAh!CX6e5kb&q-ZuI10%zt_CY2@jf%cT0yyv2bQPL6`5Cnf-b1ENZJ1#2?Qefu zfRR8!O^haEYF`97f*AdMYR=`xsA*kIe(gkGtJ#x2YDRh!G-(zlP*I~&|0x+Y0h>NV znxGj(M5fHB-Oqpi^8)MyDAJ1{K!(dX^K%L+G!0WIA=B5-s7Z0%)U+sUP&*@&COGgt zg#^-unk)m}sIf9wiJy^aYg$b!cGW2QntQ;#Q&$764rKhKHNleLMInJg0k7pesbvyS zNgrzXN;?W#G~sfcT+?6t#b1c^gZs!pFOEfXvYMx99pS!_Mzlz%|CsAgExsMowE~uE z%eLjT1g#zFQ|6qg5mQ^HNuHoe(B&E9TK?*<{z?Q$&WB?Y zta%Oz`20PP5ztaVO9;<7_l9G0@6-oPiwozXz|P-_M$*$I!RKity;=(RSz3!eA3(VR zFew148jiKqU}t}8N@P}b)>c1>Eji0ND8=1Xy>Jxn^ z^~VPe9QbEzt|=cse*Ds@sVN3}t-7X6Kvk_nead7kWOQU4T(G9*s76VDBpD)2f%F?w z)1uZuEt2oan5lu0SrNRb0g}NfXp)iBlu4#eGa=v8)J+CS?U4XPt&9wkW^FQY^?Op= zBon3iGySR$d};;E1X~Ii)Xr&Y-n4F zTsa1X7t)8|O}f>;>d!3iqXnS8_BEfWuljq^7KfEEx>omAw8A=HX{8881+fTBI-1G% zl`7L||7;Biwg)cNF>A-F1^e_Wstp|sATFh8)`D_<`cms)ELQ3=U}^p?rY7fLqP=T@ zU8;N~R=kFLT%*xf!M_#A%9;t@$^Bv(=z#9c?ynU->gQ99Uaya0@7$-}{6wBfP^S6% zs?|1MHv?rd9%ZJg#SjRoRg&pYvm@A&(a_vS(;c-4GAL?}WNKt4{GNcV#XhJ}P;23J z1UxcKf<04e2+VpfpUL~l{P`IHgma?rk3ijsk*7av1AT*iqyjzdnY~-lvR*J~l_$OET-N1S zSLweLxTiq5F4%3YK-qDef3a`hzJI>vn)0z@$1a(ko~EX}CZMbgj#_}4;K(!xq?)oq zCZ_;J(=ttL)B?!Z_@4e(f-JQR0u(`pjEbf}jzcC+P$lpY?5F`yQzk%b6(nBE7tM|Q zj-bFI7SyEqe44fi{2F=POj~H@ccZCZWUd5e&XwTHbA4lJP)J+ zg+2{VBPj8V@HzFCj&}B}{u+X4`h4=)+()j3b5y@C0heZB1#Y?{_?~m25JCXewNSv| z{j}I<9x>OY^x=MMK$iaH`Qdpc7?Zv{3p_u(Un`bzUpWTP7SG30(er6-_iWXa<&2re zgO$b#1{l&zy4kKqDNT7fJHPmzdd*p*rPF_FHO19=?plswgo~8Mog{3v0$>$-)^ctX z6^y@qX;5CvIq2A`DaUaihhnQH{F zGDHnbQqU$t=e1fAQcE+Y0iKMSjEv7wQz)4i!Iv*BNystjOC}JJ@O;m|d`7*d*h({Q zv_V`O!9j~#sIQhls1?8X9R8msY|c^FO-+m6X{k&#DRT_I)MrZOt6HJvck6p= z&B66%WHDDK>RQZ34>DjYrrzxPS}j}0N&|DFpskwo(+;w=Ui7lBNNb9V3Y08qTCacP zVo;-!RSy1yVNgFaUey&VWgOT{1{-1;+Q`8cO{9Mzl_$&oy)u71CIGzHL zj?M3>q1Hzqcgmy`WH_E`tZI_f`RKlD!4CDU>)bUUhig-&&3RJu)b*x|VWfdXi$f?) zl+GIH!@p{dPYXtzlaABrflg~+=^!Z`N1vwxhJtb$kU56d-`2h1{pt1ay#_KVfRSdZ z9d^1`&DN>=q5>4h)j5-%+#j_z@LCEU6oB|jo4<8^YCTawtiG?8A+71oWe9hPj6&!fZ&IV8x^Jj`FFRtZme5NL#%4pT;h-F@Hn09BrO>KGD zcr2|^o%Um!)1%in_c?d^Yg+#Gwbj53ozekdiwcjBN_Dle+YxFc08D+*tNAyz^mJ^1 z`p{L_s_wA{!&Uc%Yh2vsV|g?^Q=oicW@hG_>kZ1PEh&T3;3x)U(SJ&9n9N7bcw{sh z9H#$IgG(9|bWHx$lo^i0XYje28lucznIKKu4AfEPstmT%?NqIa_o?PdhQ}AjCPUOY zsHU!K(jX<(($XnMs^zPo!fUvmbbyxnK}jo)ser7$S8ZgVz`^@TYpsXe4nuE754~6Y z(&_x*v`=35hwI_`6i~I`McS0EK+R{S1v3pGRPa`S;`J+8g9-s^El}Z31s45Weak8s zan78BT0~S(&@;;S+!LL*nzU5_)H(A#$4*-UR6yVyv>tcbyk9R&r3IR1d({k-X>mBy zuQVuE^K@&%l#TF{1T;zS;gkm0sv*Nt>#Ksi36!d)Be5=5Z8WvaA<@zmKw1ZStu1vG zl#S_sY2if#zy<|M+VKgu4oGW%o%KHYDqWNLS151BOj|D&?N4lpkkkd`2K1+iO?{nY z66uOwe6P$$nJ$5a=41vUG4Mqs&?L{4|4;in$xsxml<6r`=I82TPMcZDGy~k5Y;`>_i2}l3VteV)LRZT5HL-X`t_+qr$~;Ty=<81Dwief?H9zf1tk0pLx~ipCqn_!HtPlQbcWwf` zNlKE@wvC2tG&2j}!1W&QL0`}hBg_3$Yln4X?y^z}8XX}+!pWexPwGMmb*$yf-u zYPMx?mjb71kJ?5rt&PwZs(xYxFx3#$M5@52pOdlCq^#y^)n55*Uc>LS5J~y~r2)AW zvlB@!De0I73I$J1wNY@^Ks3&kfTX@)wF;=IQUO)pE4ZnTQa>l7uNO)1lU^f?t{*s#&K2KRuUD znMT^xkQQhZ0Cj%)Jf*h+tKOr3({tAIqfypsNl6Q73btv%hi8>%UQNv$hv$~hPys>D zUV6RFgr~C=l+&hY$GT{xSTn)dk?3U7D(#N*LwccC##^G7Ap`tNlKN;hhEA|T}{nu-(65CrqaOd zZRVcdW25zI<+8LepzGv*rz7T9(^`=>U#}Y|A3b_>Kh4*x3Cd(zG?OyjNCAo|IZW5k zK(D0^Su0Jn-2>Y0b@k7y2jO0Z~Rz}Z59r36V8Iv7pPYGRsSPT*(sc)iJft=E)x zJ;8ENwmbw;2t+Ylroe(Oi4YD<{y1VIP}T?^DJ`m1q-!JoQV|rBK`VK_k`J7Ik^vvF zS_Mofis87X!A_aQbJ!L$42&Ql1w@SHWj$X4%t?KBQCJ9T$$Q;f*DlyANUl-u+zr}LP(uR6YEafbs$CAt|+GZ~%(k#QVX<`mKjQdhfv zafPzhntr?caV9~Tn)0=)r73gcsohfRRMQ&qWDX_G*P8Cpj96<8dTH}(qdBw!<@I7#D>+Qvd%F}!pVRc2)_^)@ zI1tLs488YUX3d44(_CINy3iB=4{0tuO9Ny%xnon5VpM=IhbX(S4^+pQibG)ih;lx&&keWtzoETumuC zQ$KcPBCRy_Gd;hxVs_ne-`y0@bFEqvnV+U|X~qC!I_hHv){AN)93X<5Xm&IW(?lHb zUlgKPHu>UK%6wVzji&sNab$d=&0gu>cVhuteVSD;uGXR^_!8(MWc>_0mpBr`b6JVa z1S-PKf@b(L17~5DVlrUc`B8&)=v;c(2*wmek4_(dp zX?muWrbZRcL|94aUz6XRG{dNIS4d!4HMmkd;uhffJ~F-!*R_SdG9RmIhUeba*ZMZG z0M!7n)i}3jL{m;cnda;3R?B?N!$?h8iyUa-gLEY8QUqIT^YvxyMTUVY%-*FtmvIxjAW)=#Vo-bmdOo(<7*$x6CNa|&x zqpK!u6L8hav%GlssvxT4*J@;gc)_LDUe{T=^d$RB>3TP!bGJX;PpGt>2pFnwUqA9N`C^M>9 z(}2|Uq(8h?KlHQRJbbp#?@Td+?sLyB3N$lcYs9xk-c$3@=x?o(w;q5jz>&&SFy`O@ zuH#^4b{6B)bC@fZQHosz)c!0Vk?o-zQ4_0zt|Nj&5|Bmnr*AT0pnEsTgBhax!Qzf ziWV_^$3lPBMPELHe8v@E_C5NS6_oh|6QO;+figAagR7}2YjFh)Dp8`Rb7Y$x%vR>* zl^*D&K(&>>Sp?6-=dsdxbnov{V7)28-=UhX6%@4|fM_SKiF(!V)@puf6K0JMIRa)% zWlYY@V|uP2fV>d9n3xYRF&v3g z#o`fSFzX`g#G)M!=6&Q`AZthH>+!I$ClBAVDk)H!{?rBK7NYv%SOD z=m)lRAJA6g)*q_HKc3no^~K|@{+wFPxwG$E&94>xmak=)c{Urreq1#}>FO5y@;Y1h z>Ri33MPP$r8)*X7ZbqikQ*9Ta)tg(hic9)=W&TZ4ax~96ZNbt&9|5Dp)VY?x+z6CK zkdJK~J2i#rGB8<+Fg6?D(8xI6JT{84*&@Q&MHriuoy3@2A*_=MWa4+}VjrZ5f_0_n zgmg=?5hiqU6R?QHC(7@rGir`>Qq2nyJjW8y*qC*3(e{nld%;F*=y5RM#2CoC*tTI1 zwnY>CViWrMfO1@MK}11l(#jkU3&jE^W*0Czy?_Py7@3do&d4O*K6VNte8@6xgkA;8rXP_6&*ES=gSE{L2 zOcBZE)kh+>GXGYYi)k3Hud3<9Y`(d!3IHbmjSR6K6m*PwP|)kr*W0k+Rk^Arvug@K zx?gGy_fC@D6Z6^Xu;As4V)6^qFCi0uEJ7Ei8PE==X$$-^(TsFGa{qFHSzMWxC(GW8CF-%#16e)JpD7ey0ieOR7_n201n{jnS!j%-MMy zExLI0g*WihJI62|dnh7HmQ;`MFCO}&##hwAcpdcD5O~>>*+RSyPso9B8jw2Lo z`YIjxp)rl}oCdLjqT@+KZ%Y#(kEqR$P` zYcJr+y<719t1iZ{RX{GD$F9wr(BIcnX?~OT(I>F965_SImwsa@sN;HwL-NTOj^|-= zW)?@sr*S%PP{SIP4qWD- z`!>p@8RXmmQMrIz#>L*9n{e}`J8|Klg^ifS*1ik|L|gVM0b(WrEta!jIVSaMwO(f= z&?^CDHD9kfC|CRkDXD$+K)Ej9qXNnb)@fi#{lBi6ckK@)0cH8H3bd6F{|aCbToOr` zQltn82KY?K|CxJ{Nt*`YW)2ll76b_r(5ZUe7)vT&$;D|vt_D=3Qyes|4^b+VgFhafWxUkD5M6r#)&4E!_u2n=F z7R8rcEXD||e%O`=-ztHX|8wkSQ(Z6s03ZNKL_t*E97UKxgrXZEkc=oBnJ5F_X2f_2 zmK`7{&cd-NsM#3Egt-2~LEN-=2)lD-3_4|O9Uel~^9kcd;9%1Y7DAy`-v#ByhbkTA znbwkp;iXJt&5oqz$Qj0x(v$@`s@iR(aD4*CMq@}yveoD^3Ir84s!+A42th3nr)%Bn zTzCQ$xYAmDedV9k|G~98Vny>(dL%TgD8flaBoU61Bz_@n(p27b-)X;WUK)N3kQslL zj6r55uV&T@T~#ZSdb_J%`E{mLp?Re>U=MqwW6ESTe~0CoAwuL zzZIdOZZc;#AXk6W_5N*YCC5g`FjcZ}WG==7&mYH&$7eAeWe|9Mh`k(wFcwgDPzItR zXlgDySp-oTR#+r9!-Fqi5F;Z#RogD{Lli6*Bwn%%@$vKb9nJ!A2~O5Wq2$2wHyZ_|JcBGhFD(ck zi2N+9oQv7woCKEnp$pe`F|#l)U=Mcu^O$QL0EN@IbhD3-9Nde`h8^Uhd0eo0Gx9#$ z__|5eH!~3dWlMv=R?Y`F!Wq+)PoF;hbpY3|mib!2w*rAF-%8t{T!GDc^K}Z8^#KX^ zv^HPYf%76WcUm)UW&%k^Jg5LrD?Bvw`K`_|X*G&!o}52R5}KeG^LKICKf&dMi%VO1 zi$5?wxs ziAAAKkV%7W`i#||w{sDc30Tb7IC63nlc9$ra~8hy=Rk?h9|2r~R9J#v3b21u4?cOr9&FDSvC$83 z!RC!MZAi5)x(Iq}%pmx?lX~oqDgQ}RGqcD}oEpdR$$89XHsHrkzky%AbsE!7pJ>W~ z(US!&*;!+(tORM zX|z)TVG&r|7q)U?zgu=LaGegArO!*gCUR5PLF>9vD4EyJsZHz_n<`g1WT2|5`U{-EMwQ8gU{ZwA6xx-Y{|sf zwPmQ%#*=GI2hy!IDATtgetOGvn`518f~DQq@ln%yL1D@E2k_R>F&rOVkoPQC>PIu! z>doO}H|)h#+jAI*3)nKe5xv=*SRy!4hW&BPoU>SFyP(|sCs-fF*{%a#&C==%tuU*! zgh{7<%Ku3-Nu|)aQwF_FDI;X?CO zgQ=2%v|M1-&l}bF?p081r~O?sP!4SauJzB>wP+Wl#TxOs0m|CYK|z^(fhJzoHF*2v zDV!|ZI9BlSt)IRopd2`TVotRYB5N0L)x{fe>%jxqF_eWB2MFkIjve^611e^3j85U9 zM;^yJ?;OS6-P>`?m3wjNh1)Q^VE`Eqn4KwM zad6*uY}>j4$k`GAH9uRx%dfwQUq1UP4v)+LISL6jTq`5ZC;YgG-2)CjbIZlp>dj-T z7i0J4T2NL~J@;@uKv|y0AS?DOcWnXXqa$M&oeu?+?|b59Ja=MFOxY#72acbE6~+j{ zl32K?iezmY8@&);zT-w*bzn$n@q0$bf#eENNt?(wt7X2f5BpbXxLr`LGEx&|ZC-VnZ=GM)Xt(BN*#zW9+C;M` z?0VLn`1j-)#&l%{=I0@dCT?0CRvod`$l}#J6rF;~;_I86f(^xz6owRiElV})Wjvh% zi$$SGX=u_a7Drt#j*?%l8F(49YnX7GVNZ6bKVSZd*|J&M`*7 zP8i>Iq5h!VMaf!@tWyYj4(l9x062O3$cJNVhIvPSsu#c+Ng1#$0#2A>0Qa$zdm<4lbn6Y zWfgcqT`^un*Q*_;#}d_UyceI(!|-h%*OHH`l2bbqGhaAYJ_Bva>r42C$O5l-AFuAA zbm>2%i3ULj`%Z=NYmE&|JnY;+t+_oI!8O=mv73Gt=3vj`BMzkfq>}kP6^>W{Cg8F^ z^QsT4Zt#>u0vp>bleS^Ppl=;wW2ockK0`EVwsr-3_izVKKlj0+wTPb#DH9JwJJQ44 zzb0yiv{%Y5KUoNFrtvr6DTvn1b$-25(O-!4Ao0GPAg)K@{Y-3VfQesQN*z2Omy1@o89tdADoBvc`;_vSN_GE>DQ2c79zK|gv{-o_1vpPTZB#jEr!>< zhC%qVgs}${iy&`I0v}jBRGG)e{`|I|Yy)N3^rx%Xz=@ZZefZVNl*6bH=aqOmX-U+4 z{)C1ne5}P!7lE(RUHtkZ)q>ws(hi!|>JWI_K>10EwM=&`>SzTV)!Py7lBm#;!=IHf9#aec#i zVFw~`L}NPh*Ap$7v}b4QeA})8JRZmO_IsXLh_qhc2$o-sG{ESt1VNWla$Y01zQ6?B zWGK3>Bqt=O5E$^H78T|+o>DS4WhvU)`^6Eg!BbV;e|6#BPsgA}VT4X=Zq1V_8yT0h zgzr9fE*5od{Z=BTrlV#(6Oc=5B~zC z0Mh(H^;yE5y1gK|>=ge>H)-Xr#_DsRdv0kp%?Y3;ucAX9hPlh7eFxF(NwI*bz%2@f zM5Fnq{cple)Axe&qC8%csu3l>*?_mnBl;1u-J0LRJSDI z*oJgOJea)ipdQ|?5CR_F>DUb>DPtSy^+?O|Kh+AyZT8$F25e70n6-`e2a3K5^gP_+ zxmr5n!l4<*t+~9Q|2x0Gsw!96_J8=m$GcdK?0s2Tt8XFuc2Y6;`0R5zxMl$uL;JV8 zh-Mg<6nrCW-t)FC8Ra_FJw@3Vg}$oy+oUy=Q z)m2!|*vtGb2KrnN#%uo8?>9#Q;SKe84<9EyLcyM-To<1t_|WQ`u)@lyDFab`JEN7o z5k9UDF9)LP2xSs?ykPmi!o41Mt8T-o_JmUUg$ywNyMuX5>T!j07Hxy_pxGKtO4^$G z$q*$%Ih7f3tz;N#kTbGq7_HiUNf_|2_8~gz8ZJpFO7!KOjeYly+%*up6lmWIvP@Z; z4G0u({jLhp8Z0^fDW)qJ{I^Zb8c;{(_B7EfBjnlWG1W}5(WH`>EPUBU2vl> zW%6tAmC$5PY_5t!n&>=T`V)wPuLS7{;}${MeCH#BO4$K1*|40j4o40# zqSd>J8k0B1SOvfMK(n&&C)g4}B;J@e$M5jqc^^>2$TNMXq-IS}m?{->Q}+tVXlX3_ zvKjF?xckf*aBed3UP(d4rhU~&o!{SVo@RTda22sarKnMgIYQmcN9g55@rG?%M@4_D z#2yjOzz$Qf?O;pY@&f^g{oE(UI#;L7Z3&w_20kJx1sN+;$`cSN1bRT50a4<1@QWRo zVGkv0X1n<8tZ{xYl5}P%U;!_u+;8tSiUMpcvD50WMl9odgI@Xgxz8!;UtoZNoycri zeffNb;hQgKED-MC!vBgPVD}}-G$3*U7gqhB(75hrcNK)fpi7#^+0hL)!h2ORUxdw| zqor?eR}7ASyI}B40vbRp`9j{rvPq=Fa4sFNCQm~x?wFVD9|C_LnoQ#K(kB9aE{O*g zbWBkHOl}P?CGH&K$zlkGMORnvu^II9vl7E!KjnIx`=}y>U97-?L7OSNcMPNWNH14m zCfy;2NfQ<$7m{LWAt1_hGXom(yfB@DkqQ6Stt1hDI8@`K?MFe`nO-}p;sT+br`ye~ zf~bO#3WCOrFWwK4kDY~`C_aY0Shvz!aH@$rs+Yd-77bz3e+E!VzR?ddO`3YtGQV$6 zZF*r@efs-xt8+#7AQEDS2v?BsJr;dgYg#IFEyC?=4A*r@85a0b%t(JrC)Ue_R3P^Z z+>K3bejSjF%QnUXM$m#m@~$YA8XiBc*I=zUMMs1QubS*GB=x=nmDEd14~n|D&1>Jr{sAVqz=B3<4;j)d>zZ=uLg_ zVUEj8N!lyy=*RPJ6>mD9i-Sn-bKGnGjmYfDPXQ8@Kc($FazA*8#Gk3WyfUn7L~jYp z4E|yiIX>{v#dTV3lgHi!wfe@Sc1%TH@*FtTZa9-o?p<<|mX<|UVM#K3hN>l%I~l{( z0Jv(2PF*}W+XbVjyVK2aeFVvsu02*_Pm)V#eV4%(Z_6C;Lf3ahk257xW?iO=4M`Q4 zu6uXTh!J{*KEGK5QfQC!QB{<`h&~@TeY?1q|dl^lKt`T_m|2&b(+N zj-b4%fioU5`8X3$nw2ANbIA7Y2951coleiGr`P*IW-#);LVQ1YJS%?8CXn&TJavY99@j z)EngVA2<>e`hI*4xUk@ZCu+M)S-P&f2t)*Ie8(o=miLy#<>ZJ>w*_Yl1W1SpLD>i?Q*7bkk#vnM!04fi$6Cf0NYO zeu5@Rqn?C4nvZv)lc^Ajld#k78@=YNfG84UD^J|g^urgvvj9eUsyTIe*=%5l7{ro8 zE;&Nh>#em*FzO;FB+UZ;{MSHyag`V4Fw!Y;k8loQ=#(uRqD0`u7~0pb(83yOhpn{r z!eLK+NWB86j_wJENPZGEFQsjWMj*-B=-X(DhsHS<<|%JN0oplJag|)J{xh7J)r}9S zbN>OKlv1OOT$lPllA~tl$38{``ldi-$~~aE@OASj2VVV#`AhGijU_8h4miz&jXeMB z_8}R4=Qjt;w!>>5Xy6~zDhs`_qIf0WFTYHBiGmL=3|wc(**QtJTT<+>yGg@aqXx^Z z-KGLX`(pqW_(v047QZ&Wuhnfv#qoZolMR`<{$ecXPF&Y>&ui>|iw+S%O&4DFulsbN znY0D=+WKtCwpY%-0c%n0+R*RkKBWCrs=X}Eb?Zc&kAgnI_+;H?M7Ut+ z{oThjf6_j?8^T{YVkrqp|XSD334?TswiFF$v*k$YDefOp_UL&&w zJCz&T`P*p?lImwOtY5Z^n;xVc%srl#4YboJE9hvegZ zv-`5!&rf3kP=TA?^R>KVUh82UGDG?6_5EctSEX3z)UwuDJ84IIaXNP*_GG9!a|Jt9 zxza@5NY~PI=qz_N;{KG^LfNF(R%xD{;Mz%ko-aKQ)VPe9V`PIQp&a5K^N*VL2Phi8 zrKX`l-H<>aO2e19zKWd%EwTno%D5EI**iHBw@Q9#g65`JSM^~WxEO7SU%hCtH?uwy z*AnP3v%X$)*OoiJgx!kC^~|j7cN{@P+RSaxR85Y?RLg~k+oRJz)N)F%szS-Cv!(Hh zNyN280T3b&MP2&s=;YN}czVzp>g8^|=f!ZfwbyubZWLCq!83-9IGdp&mmSnJk|B(e z<37>h=Cf{(l-6Jn^vs)cvp|#;ZIqW6e_e5jy!#gzqb5f%hA-m2LBH`n=KZ>(+KV9g z@*UzhYW-I-*<+iiZ{-x@eN#0E0Z(RR3y##=*)VLWKIrCM+;MF>DuGB7|M0rWD&V$# z@UcH`RE-g0=NJ4~^7ury852W!g=ZoVs~UVtF9QV^k`h9cdt|EmNqYoQPijz-UY{fw zu5BOD+v|(y18h!RmBzNieoA8qQsyY;t(Q*%kRVW5qpaR+q+l;3*9^md@$mXo^w#2= zXZ?4)AlEO2k9#KOO9kSuLsLC?`RlAAPd=m9*>zpf4Ka3(N0U2vo42T4Zw)~)JwXTU zNRMf%!eK;ixGC!hgF0T7;>rR#!~VgRY(f9>lYUDDT%3+xkrf7^rg@_|GSMA=jnHop zYXsSNAscXVo1ks=!!;hk+tk~w>w9{GMD~F|C=4L(O8IMNSXfx^uOpFtNX$7Jb3z&Gm_!R#8Vi-kH1^-w}9%)f6@i zgv|w=HPesUX;9&c{BN1$ruTr#<~@<==v#8KeWrN(If9ryF{Z3(~pg@$*DGOrKbA}m59$} zI5Hu3dS(uZeU`f{OQET*qoZKdFbNi~{f3lo|C&Dxkk)4s{J=e_3>Bi4{4g;fyFoN+ zfacuK)^o_Ryb4Dg^u$~cc#KUei4>AHY|AiGbvWu4=uW7vE7gBDlfc+ociz82vew{Z zO7!nyJ$Azw3yrHku^g*;icD737`iw&8{vMAb5p{^UTD`U}TFCMn z?X>WL8o(R9(byAuIS58j(ERn)!hmMF>W`lDEsupWe$?_$2EJ5xx}L{w0KaENc<};7 zOO-_&$T0ou5&`ygN!?0>$!mjbgH1_7UEnFa$ipz3>H0{}>z?WxH?Q;Ah)+`D7F*#v zuQ5qacngWZ>%*o0m&<@^GHJ5MS&zdjy258nnL*TWZ@lq79IoI!)mYUhVbLx)(U(bC z8jqW%O`%xP=f4l^RuBEO8}Z^lI^%NuQ_H&;03%pQ3>pxKaPaPB-g7`&QY6(IRPyxf zZ0*9GQ>Q%l+8q)Sve_*v->!{p&znt%F)@JLYL=YIsOGprQTdKtYfLn~^t&X=%GrpI(A|E<|2AFq@*e6*(r9AtuzSC<-rHgAfbE>3FhcUf8VuNMn| z|1;Ky?1%0z>^c~9hwW{;+aX?kz7n6MMEC4&J|U;^%idB$kp-~V1mv&ersnY>F8eLg z>_$#|+GaR#)b4Rrn_zl(#=di{J~p$}G*ncXCq+gBxX~Y7qP5a9NcL0!&n1TKK;NW~{7nLk#)omso2aF!PMxuo6T(-tGEs7NC)M49KP$P1h*Z;g^| z#Gt+2q3wo{ZXL6cjF<#%ceWWvKFSDNf~ak_@EW{o67KVPcib@`Tq(RcUZ9ch>ll&u zZp<%{M3(%%&A{C(kN`=*k$Bf79U1P@yiKn+GFk955k5+%sm~py17u!OmQhxD5}QLW zeu$Y!7yMucH@Ccku@jhd-shdM;z{Y~TjWA8vGj|4pNY@4+t!r(Xb7)RFd>r_*8!wt zHVDsDzf1W{j7H}7khB#O#cLi2cXe__uTmfwr6h|Bhk7Q-3uQ=(c4ea)NV4g>jgLrx zM;7qWdHqjz$zlYmAjld2;|X9gjM9tLFo!f(o`d;$277V;2(S_L(EUcPd$A39(IQ3t zo&~d7pOeLtJTuL2(Mf;oj8$_^SH=2tIrEXOW=U@pj+CJ|%;X>Ds5#M`&Mbk;$30%0 zF+3C?f&9pH9!|j3*e8B|&_mtD@{`rq`h1M@ZYMK_QxU?xnRTwzyc?3KnQ&}R6^rezI z_UN8Tr!ifU-&sF7X{0jtezVYI&zqDtAV3FK#TJ6f90Z!TOLPf_6S6(BD)VIbjK$A3 z#4Ojv&Tuh?($`YmEgYuwd?#Zz6tM~|aJRx8%rqDKtDU7cRwH{}Q9bvU2Ca0mM+#(O zHak_o40dd241$h#7g0N~4J5pQbpz&vhApwjNQc`>5C_kO68KspaoN2;mn+gyIh;lW zb_#!M9zr;E%G;;8)61zL$VP3ogXCWZiEfh>6QaYj8O~Qz!Ro{6a2UOpmfR-6)Mv7V zP2PS-X@@_*xJ;7FgeJG=rNUK%AGDCC!mo(`;c=$$1Xy-4be;2bUT=24C@;?=y`JMG z?)+v8LfY*Ki`Dw>!f$$D@6z9&OST>oC`c-%%di7C)g7)gX&7pf-ycHaaMe>7Kx8c1 zj4?$flB62Ey|&!uMHF;Gw*kp_3)p9hy5^e-06^eWyUI-M=HsIUdoli0_Z4T)%dB0+ z$JyFhG4{YU4#UTpMQ6S6WyusmMmkVBj9YmD6b6>S#Pr_=qZl%c;3srmM_lF)DP}TZ z29m7S>cZN!PD;-$H_*lfU-uXGTjYMimCf7H_xe;=s>Gtttl2&E@jUNyJ-*b#B>t=S z?)?Gs?X7>WpfQ5^^FLYu&a0=(!jZz)`%Dhf>Dg(N@I+7|MNWTCUXoAm6ZytVVi@Mp z{gVsvC|U5u8ohpC{e&~ZKTBMwKVAxG>@J4Ib(_~;Vg2?~kjffbf?2*I`3@o6a`uGz zhb{_q5Y65C{}39Q;!ABb!~YnC7!JIz2g-xJnymHZ6`wL37$^pqk$K%Mw}-W(t4iw3 zyF7`5{vlVkXKr^=QLe~fj}FNa%+qq*XKpCuXjnVoM(=t!;;X}EY(lLCe`@_jVr@nA zEcjsI^x(~)n~aIYv!{=dxUUdnKYr?LKbS=b7fl0`t;nZh3Kk8c55~|sWd3r9N$I1Z z@pCBiuV;NZ?YVyUP8re(gW&j~mnodXmY=~EwhtAS^XO`#(b45PWoT%#fD&WGC{In7 z*Zh?RA|E47tb-zqAIC$=K>+M?J+};j$n7S^+u@d5Mg#c=ZJ1^s7B$p?`Bn>xXOa+x4#E4 znP*-r1i9PqRp>l-5WxXn9&B5&xN#Efj!LRSi!{S~_83fv26g9SxUfDKJU=6~VOXlzX>wFOxvWNcC#pu5(zVNU&~gw_SA+dJ1MUe~RbR3^7kB11MIsN`o~~|A zx405V;7fVExV;cug5ShnZ~Vzd|Dl??cg5~zmaB6vo2GJ3&u3 zzf^X5h3>tuYmC2*^}7qM*dn^p@G|Djsegufa(eb_ZMK{Zk*$X)U(NuH`N#mkyw z!}r*Pt(1|@N%!FEbE3AI;|>mXbxj84;hJYXP_3q>=oFV3((QxbWl|}r9u=$l@1p;Z zN9+FM9NKTt;ZPTV7WMzK|B4ZQ!Sj0}-Ovc3PvHfAiHNc}F?pXOliPF4j3`?IOpV{! zNT6*c`1geL&WE(Gqt?lFB_VQ&4o2z(x>3m&9D`b>t+;D8))%80SDAKW>56E!&CGbg z0G(|nH3?EMP{?87uI&H<0m|*|JswWRpfMR(f?mAZUnG;`=#Bj<)lA5yMwb~<%RgRg znaR8yWJ>fp-}m+@tzi9z<8f^g+2wux2scV^#|%5apHxMe8S=*@+GDh5N#uoyA(9T& z>xq?76i^ubN)S%2P`PD{&HZ6T(Rd0vb6Q34afP2LJ7<698$8xS>mX~y;QCC@uHI)U zs=uGxIuz=8ne6_x$&PUzV!7HUmpr;s0UvyufXAWH!eO`f+4}1cDQ7?S#_JX8>+B2Z zC=|@rPAR3ah#M@HR*~ZYhZd?jtKYZ%F|)Jv7KF>7or&eAPp;>cQFsNxsJnbJ4IU5V zs3L86B-|#!_t+x0cQz>#HdnPi!XbP;&P1VhTUXlvevfORuYP{6TU(OLjWYucX?-?5 zk0=3$`?v6oM(|uD(P5llL){!FU>=`F(aZs?QmVWI2Ox^!n`BNFTA^X)W3GuTIQ#L; zI^+Iy2C@=uwu1M5f`E_WD7#L@%Fv-7QHI=M&Lzm`&iJku@Knq> zUDe~69d+koOahkBEIw*g^18XhMBl1u%bW&BW^2keB0G)D*^1P|qgzRZ5$_gUxw&AqnV-W;* z0~O8@S}QYBV7>1ZQuYrF$^zFhtTfr_1}4Na=wP8l05iw zr$~k=0VPlRW##RaBy$9EU{ap_^ZMAAWEAg`-g=@p;WbPRS_Ya5intp0i+eUk z4SEJKF@+#}3Q(_Th?~RCX4|Q_EC7Ax;9TrOrcL-X4H7uQhMRx{Y98L!SQ_^I{80&VvMI_Pl=^|i#O7enxw0>-SINi2fU92KAC ziw*0S!hjQxtPVF6(bq?+Z-3B);}cG`{75J2l(5*eLp%;8h2F+9-5!rHDC!`7lHW!R zHshC&@8sHlbIuJblb?Rd9Q~P2Hp&-#g*ch$8-245zy2kljvmz2L^UwgbNz|;FrfPP zr(C|f);k6ddAT2P(r+3Wm^s%Gz?H?>c(?r<9>d&gSK*V9OjM&jG*)ePQlGv_F9qiC zQ&o%8Rbr8=%`TKWG9d5RRXLulKhq?PCEKQihN1t*xz5+qI~H=2qXs*!mss7J+Z(3b(01Yq;l#6 zeP6chk8Q335PIcj_07>QM$)^GdK)g-)z;!znLTyaR~l?-dJoi|4L>JB9bIm~j#4ss z1XQ&1u^=}Xh8Wj)`*%MoDAY4B004+p1r4QsU(+$5*X?Ru2alnht>bH=BvF`@@PEa!+{&&d=eL;97y= zmMO%w`??EED&tEDq3RyW*V4it1{<&c4jfXnvK;cW8UR8X8l>WIDhW}K13rm6ubSUe z=b-rY5Q;6;7N`W5cB4pFBi=(%mMAZ91FWm8J2}ojAJ!&p`)_`HmZ>x8d+t#|zF3V9 zAz7PVT1yD1g&Nktv6(U%`3*R9eH~(WG=rYg#sQ(CUW_=v{xTn3R37s>WCtLWPV6>d zm0#cd8EoS{{$cL*!~PIs85Lb~nn1`%WXoSh0qo9GBJGq_~V( zA_=$_?{)=WRk5jZQFk;~PC6^t#CkNPbo zA}({}I$}yROv4Q^soPnUG;ojNc8^#M9BsE%^64Z zDe~>xH)Xo7-STI;n^6*z*c$&BK(wHMY|<6RyzTdS3n(euq4J<8Pd(DCZS=? z*@<%~1!%2W+tP$J&4g{J-6elM29$l9e4cHg#cf2vS!u>;3E)eb91*fgnILkmeb+`k z28bm7zJlHnl7E7JJ(KUtPWId69$jLFT6$+}ILCl~>ThE(az~ z(nJGXV=%*u6CVR=({9{f(sGy9CT_D+-Mfo3F*Sgyz7qOK1#Rm6nLWYCgJ~2BDrA+I zXE^fhBTD=Us8z9!hBDZirk%czWf8~r$eUyR?1Dp4cC)h>v(YCdRE3hPgN)ym+J6@r zqNp=I!w=xA@Y01y{5XU_|33PJqADh&@?jsN@#0K4-{$Y5hf=*b#q)lyd2-x^>*!&To!obwN5l(LRgKuBToUJ zX{$wE&wY}1OaJsp3CpS!kf572*~%@3r^QTb)uT_2Z;d2y46UeqYk*=(lWftDQl*tp zwb2GoHj2FN#YasV;*w~8id<2Lw{rO;s=eOH3_BcwTWiuwkxfvG_2XB$;^ayN#Pd59 zrj5B2ENqIffqVyU(+VH1(k-mmDi1yq7wW22OmPn;LmZd(JhCmpnNPj>)yhVBQk?Q1 zN;>=xCH;Cy1Ex7kS90P`%p)Ms^>8sTrgHP5Yw%KCGbCju_D{`}ziK1^0dx1Sa(XaR zSM9GAO3Ad<>`+{WNaYoSQoW6Fr*K}TWA;!SoPrP5$57AUc%33xa&Er1%M@2xsDgb{flt-` z`M`w&^@}ghmy<>$gFi3Lzjejdkr9kspVCky#sb91y;ei2k16OAOHAP^)6`heHtX2| z3DnbnZj~5LBw4;9%l*}o*V(<48!$$vN;`)JkjQtK;maS-#-EelDC*AHl^Eth|EVU- zh{Zy8+OV`_zfEhpqOn;C#Th5p=?;P8+q>RHa~f?$g$gXALt$uKab)RZIwz?K;?2#t zKqXnaES`j|`vD@8mW#g&rb`El7HLR!#(0z{5tE4c~3MT*;2enY9 zESxzB8QmoR_UU^7*B3Jq0~Rb@9Y^l#qZ2E`YI3+~ytYV)aHgWntXEX=^!Uo3vG6~? z7E$h}Di5aPCL6=ICt>CgFXOu9i1P=+7<8Jy?(L%OikaZ9n-K^uJ`^Q4ps%Gco(+2S zZ>guY+k)j!wvE2G^p`6sKP4G1y_xu&Evwr-EIKh)f-dib*Sp zsapups+YX2?x<-MM%5?MfF@$e-Fccia<9K<9g|DX)NZaRZjXbiAIp?MkzR5e`{4XNMd z4mjiwJ(HK3Y&-eJ!(?^%Pm#UFl%AOC+GIEg)*IsGWDk~<06Pf&EAEA`JUrKt8Cg38 zYM8C@f-zu~!j3zB&eCwJK)nh@uUQFniX4Vw_haJ5rMjYk9r|&ky5dgGh{WY6k}DEj zuQ*yegA+|8;uZyWugy;8q;NKbm9(i6AZf3cLO-q6W~Nd$*4KtG6)85=w^siw-caFuhITFiY4idbfCiJ zF4cwY)~Ds4#Adu}Li{_atkQ*x4&>$Z6nbTDZXxZYHeNtKXeNRzk7PJA$*?bhJc@h{ zB`GkIV6BhvGTxLHwKk@%%Lu==d3Veo3sl^sgK|Klq)g*k zDNszhbSX>TUVM2sJ>fu~=%=rVl{^zhH+%LxDR?fo>(AdTo4-kbW9%L?KmofTXushn zvu~(S-6~9loM@aloI*Amr;ZW_KXuE1BKydOIj?Uu^)~ZoOvtB||T@ zMHq6$znhlD-;^S`%GMQo-q5?gLw8hh9mQvjj%(yGMyS|5RrB-tzdJBk+_fMuRN;~c zb>r`|95^tA-IN)Pr2xs65q||kaSA)2%R$9!gB%sBWN5}}p(UJY@VVOsF@xO|H0_L8 zVyqtL4A?j%Gh>DBQ222H zApsRlq@%CDPl7cDzG&Io&Q>O;zc4S)f0wMZx4(o4+awu(4nr!NmWA7ZnFIOO)Nx$D z)RHa^vYi?g+L=Qj-~6I2cBddj7Y`Mv{P7Oc!VlR>$LSUajD zfCZ_@L%xoy8Et6F<*?^)l%k?I@z3Y?plHWhte`$(Q}_|HpU{Z4M!fF`j5sqt8RD_h z?1yEgkV2LFqt8vNZu}?V402Z!5!m3-6^e*%W0fnqry|L#V~^PH*^A|ztG@o-M(!X+ zL%Td1%|$&M@*5o0hgiA}mx9PMkO_wljRBm2p-`syvC3WErzDyKs$zRrb0lsu|7( zS0U{8s_zg-4MC-3^4BbbrJmY-uJZ1PHAtu3XiAa^L9;5w-GaM?u}7m?X?yjEy)4W_ zVx&MAxy1GJ53b1RpY#&jC%89iPjxln3EV7|iByW4Rw0angPs&bj%j`Tqnn955&61n zO*At~gwG;GkPy-I9}ID21)vQMjf~5QFHakq7^PJ@Tk%GN^D&v*`NEO4C&+#*-mU7O z@r)!6sHmadg6cNtJWq>7W-e|Y#ag#nQKm$8yF>L=&hHn2b<)dgQGX+qk96niaxAjM zcz#lPr+#mY=81^;Rhc+)uW!(FtIrhYHFM5m#b72Fm!+j=K>B^SGHUye4!WhFt1s;U zQhDuG1Q%5sm{H%koIBc-ig5p>&WCHapjJ;8MD~qQqio>Y`clq(K;v>(G{iN=DJ6@g)5S#ZY8uhHNC2cmT-xDe{!<~set{d?YZR(RuG#lr;X&05bD;OU~)SQXHu6AjgHHi1!WrX=HD?QZRuY=)^{W9G}GNW2HDYct^;A>?!iWQp_VYD5*JRL4Bj0HpE>QnxTR zwSz#0kkM9@%Il)$(w6mwnOl0lLyWe}trZ&#t6K7Ah(^vXdxF}uMtt1iuR}{;XQWA^ zGrxSc^ih5rmU9~h{l#}8=aJRg{3DYly@iT&)309mpRTLEXh5O9e?7ir0T>-vLi zGMd9`Uu~lvN>~zYz;G?87Ou5ux(Rk6`kb=;1`1l?AoXop4vYasL<9+OUhJQL4}>h9 zxMr&3o%SSZ1~r;h__;YFmOOpBFm1+(Zr07=SeY_zfyJSf^B)B)YD?||SP$_u4r+R6 zvl_J(3!$uEC`R|#5A#9afSOWNcGS{()KFpCWCW#o0q=7ZZ`~!VSUH(0X6T!qDfaH7<%8sx_4Y7Mk zQz$wlEh-r@Q;I%EYos@JsJ@(n& z(PYe3277w$Ng-MG8s&h9nIg$JY{^H`Q_Qg6{zP*wsZZufTB=3|>%Qj7b0Gh4t*B1Z zL{uEnzl~TNla?kc@3)*yrvne(yqG?oLb`4iMoTKyr^ZR~4pa(q+2TmFaT3uQK^_Y8 zv&rtl3c@_Qh zKIx-vPlwQi_iHwa3~>={%e)<(uEw@FEG~Hrqj&=p%%L%Tb(8{?|CR4k?~izvWmiyI z*P2M~KjVa3ESV)3Nw+w-e+|Lw>rD|Yp7Hi zlsYgi)Pe$R(J8C_@Y^{O8{Em;`{q)@PSa@OGXteEVlUHa1~3h7dQQq89enHKMn)oG zZ=iXQV(bb_0a~34^Ivpa&cumvV9HxyQ}1M+m>Hy7c=z$%N}iN(yCaaluH%>S#W?c~ zmA&)0{_TR6Lr~|i7p*wZ;f zu2oGA)P<`pRg-2{96lMs1Td|Fnw_)F`g{7BLz3!sY8J@=!KV-TCazEX{l_w+<;5t9p+2=sI4aT<9riU_KSaLyY)1 zey7>@oTJvrh;>%yNn_TuV$q+sbuxT3L?xFF8`qBw9)7?hpL8LZ6$$EEc8 z+pEa3W6bBFxG6W{nE`nww7<(*00Tmm`e#@U+@GuOcf!eK?nI?CT89Pg@5ezwA?#QUpvcI43TsUAJSwlT)M zA0M`1gPzQzFrNv~@JM^rHF0)$A(V?f}iZ^h>R*n%roxF@(&UU3i@ zxVAzq6_vB!NwV~m@aWL9=hzL=Lex`omUa{~I+h@akw-uPo$%}Z{1?~MHf8PWRJqkX zv6`1=5B33}R{}VJa>-`P3IXz#;*}`flz57ijXyY^fZO_?V(s&vMjupBf|Z2fC6!cF zsmw_#i4{rGO|2@Uk$V@>^m*l=1el1^B&)1ftFTm1WEGBQH4-pV^oI6Xhs3pK>Hfx} z3@U)L+_6%1H3}WR(kas{W@tdDD9-B}P|#O2M$$$tHZi+0c)HkWv(F?l(aaJIRlhlA z1L#)9Dri%-GHXi+)wxUMe$N2y|3o5CrHRwTa!hQDuY7SQ_*ZQ12wgQdGhQRNV%}>% z1595$T*2gB1peyM+)>)~I9LP{y@Bnn(bKz&0uo<;L%DTmN;GJu9GQq(n$n?NMrI{j zrvLHVrBLmU4F9>~W}}BPTcn;YYkax4@N=Ppz)`8cx=`9~1{bq&u(41^CNy0)s=Xvx zd?Y5dkB$bLTYtp*Wg1ThU_Jj$49h(gtq@W4xWE$K(9K$Ad_hZf-xRJHb6GDOn^M=Y zq9274Hb$S`O9F(Fjd|~m3^vEQMaIaEp{|lEIIKr!oI>G!OhHE0de1s69g-jqTdsGw zh%p?&acpX(={HSuqWtM+9)V85s=Nm5)AZT_cj!}V@G9TZhh~rdj~1ZU+sEhQ4+;@4 z_S(9ZQ+AyWDMR@Ie-E$6bgB$y=+`x`3i>f1kO0GW1-XI^%9|jANIsmbv}V3g>RdeA zd_iX0T+77EiESZczs+J=(j5UwPzo6{O?d<+J%nVRMt+_88i18a&MD*1d8Bx+RqGXR z@AD5gp!+6xrg|_|ufkWen}v#rYA7bDO5aQY(bVP}x;DCj7EWOHK@{J7sX`RZXA^Tb zTO%>4{>qyc0aTepo-GE;_Dq9d8`x*fm5@!;%eBYN}4=^p6JsIxB?tLh-bT__Ef@|9)%Yrjbu+j3Gac zq`{bM7K07}&3+TFTxV3ot>G{)u&!5=02>yHP$i;xC8FPs5S~{=(v8Y1=WFB|<$tco z{+fEzaU!H9fd&(ZF&%;I$nT9?n}$*1FY29$s}7Ej@yNr0p8?`vxP=6$+QYwcMCls_5E$Vz^sDW7C1&`<{DAxEih!ycCLBvcZ|%IKp1=9kf;>_o{`*0v3WSS#?-$gy&KDA2T8 zF%A?nBXOHV)hlbGX}cj&cJurGl%x-rpS7;Fvfs%phEeeFl7><{-+K1Yb)UwqinJwAB2nzzN_Nyu6P|&vU;Z~jbcQP+L$q%S>Iy^HZl4>( zqFLJO98>#@+pYDR3WoZ)vvg}3G`Ez({DB;Rr-cdxgL!PLq%|IMXX<@@M7>m&6k6i? z%}mN;%R;_nl<FCz|vXD|%&2be}G*1gke-!f_B=YkKE9*_<=!5tX)MP8W`nx7HN3j#CKb__{OX|zovkq%nsF_f3OTT2E`TWk*Swy5c ztxupts<}e`fw>?C#-8^LoE>JX*UgOk7XyL-&JYJejZ#HS8%tm6&`4+)TJ?>aJkK72 z?FkRv3E!sK=}0Mo(^*=L_oj=Bi%mmKjl|vMHoyDItU4V`h9x385T+g>OuqcMMCCLz z&pdl{iq%c$sIwaQOrjT<(`M;LyU;sNFa}^*xYKMdcvb-UD-6jKDwRCtNQ9=2Kl#%5 zf+-mGFcpSc%&@7W<3$ekc}&W4I7f>SYw5XCDXrcMt-iun#-ksI7e6Pj*0Kmh2%#_h z;9mO`a66zqGi(n3RdRcX}N>A0p9AgME<1@$U8f#c+zR?uhvCUKmXvgrL ze5!AE9tR~uZ_Z3qe)+h|QIAHj+t7Zm`M%1yB#liq=u;ZGq_)6hWZgnuM~hTOTpSY- z(&vwS=I#!@!6ej3i6wvwV7;}KZi|=WRbZ;huDkrKob5ny-{MZfJ!&TPIYP$3PgC(1 zd6+bfCeV*3{?nS)1i#dfJhT*oltREsj4A;7FsCdJ=$#H%*)vyRpitvbdR)O$%cxlK zT>zGnHX|1#(;0q6Fn_?g<*JJjkBf{5)x4~}cxyxr&de!td)@rJKiv-!J-RR|f(IhWvCQ)W~0B;fvVtyd< zSb0ZNnpAB-X=r9pD^kceJ7_J_vr|6ac@87Lg4BNJ?dt~o6N(cMNeuIk_npE_rp@tA zUGKZ54b2@>%>iC&!nUs;YOpZ#gq6Ytmv}GQQabbm^;qN>akV`G?#WH9coiqA?@x0k zJnb|e+@I2Nq2tTIx)Y(HU&nJdJ4F7iQlvlHA5iY!1!2}~pzCIMPgLTx$<@H~Ig#=t z0poL(7ZvRWY86)!2>qe7W4xqEkP&gL7 z5&atX!K>~%oeiw8{dKK~0In$Hdr}+>pjWaEE=y+!u5DL;>0a>VBKnX?U|#6D)J~qs zpH#fPHRyKeW>#wqB4SSuoHDHi9|668O(o=yG%i%gTFQ;NkW1-CQwUJ8%rdzt0flaFjkH<n$2hhL=kaW*@Z8)S z8Qhhc#&d;s@j2jN%7c;bIFZAQig9}> zh9i*hU}a=w?RtVx0>L$TuSmhtfpGtOl0@)w4;A#Oi+$WCbfDLAB3?(`zmeGChmZ-# zb$`hD(hag>7dASr6?dGn{qI&T@X7JNLp&v?111X1@H05G#|!IBdfy8#*E@YWjFb+H ztkd}Zix>?7l&c6Izq}_qt-)$}1^xfKfOxwqFBcr9-#)#Xpdcf^HbG?S72X`#d<;$h z?o+5_QS$^m4t|rv|NEKRbFg(V4A@{D6Fhd0+)|s?zTgxIAB+)tHSE zFtwUmv}#J2G|OFYFrThrgrUwA@O~cSig}oexoEe`NOiU+1*H~(MEO^-aQ%73h_1;K z$5y*c8nD@oFa!RQS{E z8abgppZ#F5weyzupOpylLet?<6Y~6&1nXj>Zei7Z*7{eJlhAC5^_Ce57h63ag^^aE z3L+q5xT&AJEcs_Xt^s}&2fV^^z7qe@w!c%1^y{rptC{CJAe)JtUH)C#37uGBU-r-U z513$8l25(<;Dj5XQvYHd_Ku~q%b*#H|6emk^~Wp%og<(SU!My=*(RY`JkBS3QaSY& zfOfahavkI6;(r8SkOR#Y@3_b|sX)Nzc9qKjLp|^HReUyQ!K+HjJ*T0BasD5keGeC6 zrPfZlTmIV~gk26$;n;pB^Uvygr6uydQ2U?>|QHqlR=wMCJW95J})M zUYzn^AQb7+JbjWY#=5D-Id8eEJL~rGU&_SLjYJ32XDfAkq`tYB>`NEkYoO7U?XdK` zyZ*7^N*9O>$<+qi7uK~F`;QSBF@U+a3J3has0mB0ty9xp)t174x!)oNLO{@Ki%gIj z`|`1|vHfPSan1V=L6iRx)Pu;7TCJ~~zS!(~Q3s_K#o}{2Zr}bfRJ)za0G{aTKTrO{ z;yeThEek;I+F#W?TWvU!z-x(@S({5@r*46(>DK#aST$ZSdbWeohl`CDT5xc1((~>P ziUSvv`DN>WZ6)usKc>H){tAdrC_DA3>0f03Kl)5Akr$riJzn3<#Kv~J1F~Fwdsix3 zO8^bIG?YgZT>bwBVFaU>e2Nqm6(yW2>hw6$=}G^WKZ+1q@xb@D7r{=aqbcii#5Q`l zrsz{UwBJuHBRPFCmM+g#5vw1UijJ zVk*uNJ3)kou=ELdTrU<99irZ}RP3;<{>MfM2(36^Dk47DRlYd6DcDm#+_wiyWxZ$_ z>Hm^krv@9B0DOzV$Ki37ZWsGHlL5?@&wbDOcSH(^l;|WRLV?bQ+dn%B7Rq{Z<5E45 z|NKe^@mUT4C#>MB_FwtBqWrZE*AAES=b3Krm+HosG*0kYg=}0i=>=ii?*E@D zx}N{O>jNpGXei=2xA*fIB^!0kuxas@)IW4D{to0CkntY%&&U=Kn4kU4{vLj5K?#tA z>!4}FrONy7nVA_zdLHVY+c}xQfq^3pvm0SuKCv39|9Xy3^na1= zG&ujiT6iF2usX5K*zIPseYTpokVFWw14r2|pOSy&#P(NCAZ%}c*&myyrp`>4g>1I1 zX7jpr``_$L{)403jwZ`xD&~>^B)pJ(Dfh7w9YM)|?S$s9GK3-#Zn{6;UL`u<_oyWT zrF98Bbp8R3+9c>ks)nb<{QP_*dXL+?e9=%BkpD?3dJUsED9&hjJS1C93;2%-M0P+fRlR~=6la58GPKnVH&p$M{984w=ybh=({j-SW0RsH^p^}*`@^G;v=O(_oM6|6v& znDvdG-~Jt=`!6TwOTA7O{!oGd)2mckzx1E=RDk|M3W|Z}^13Ft19-@-nrc|yeE#7d z6C;Gy7Y~pd_i~%{21w>1_{*^#e!IDaW9VpD4)>&l7TfjkKyfl_aXFce^^kc_RD-wCOwjX ztcftmF4BKp5V^mSP6y{ljP4adwg7Ecx^35{5fKo)-9U@S`rT!}mH|v>HO~R7{jiI7 zEFAQv#8v2EtgOieeg7rnKp^h!@p36bt!Vuox&$tA=UI;B_OaGB6wL0^1M3C*yf7TKH zACK_k$CWx(m)odO98iSqD{6y3^kKlOmTk7Qs}sRp7aQ1k#3$;MB0fQdQy)M zx>K@~&>Cn6W#wvB8YcLG7P`}$zi_Du5_oixPHeYcnu2>@Q74gz9?DHyWuV4!yhp=D zV`EL>i0_e`)8%!?BL1A~dPER4#Bz`9U^wIR)VF-WGL+T_l)t>mv0ckuYqDMzTm|(P zTy)v0rq2aNGInz@@$t##))$E}&x|`{x#V)6J_pA+T90`P542qhcyW65!(|Ty`&Ezh z)C!GgPj7(U5qK-k@R*9cPSllw(__{9-I=R@0V6vgf>x|jdlDc!=vi&C->NwWZ3a9# z7*{Z<1wn0_JEpUFzk#kv^jrgz=6_AQKuw)qMkLtKwN$%O+WR~RG$1Q$%d^O%%V{n4)fH2R zSl1Gp+@4(mFnfP2;wjpvu6eTnL+pW(nQ`G&hAkD1pZ}eABzGk}UNdq`lC;u1(FUBl z1^Q$zX#FC-qDDXZKYPFWO3J^!Z5rf*nU*eIn#IjWV!nEAgL(r$InA?eowZjF`I|JX zY6~R(W~M2T-@hn z5T>e}ck5~wc|G6$xhVV=>oalFiNqC?cm#PcZ1Za6>{imVCvkWv&&mFgoxt)zuceN@ zi1_*4v${rA8}k8kIt5eEeP#XRlHv;#DcGc2l~2Nx{ex=+Qp>Yv&{fKVT&`=!^i6kF zjsnbq+MeyZH~!qwug#h#C(HFJ!-dP#81w1}`CQS50guGi*rybvpYbSfn_}{Fr33r&8(4es9AerfELv2u)DdPxp zfg+4aN>$LBnNd#74Eyr?lJ7Efr3u6@i;^q}qnH9J!~lb$hK3tgNGPZeH;{mnT}HMr zL5- z%T!uhaHLL#lEeK;O3i7#87)Gd<1TzqT`f(!i;kt3itEo7?{N z(4+a4pSgwuPWO+w3fyStg)Z=R5$;FUsd?X?LcuRIb@>z0 z7%TJe&-1mWTkR=uJ-EG0lH&k&Q-E{JQqrY?^xZd5zylV{q2F>H!U2B-V}L2d)6??< zB!kSH-=GbDG_QMn+f-2I1 zZ@Pvm!e+#wk_cMX&I~d<`L*8o;9Q6O_H4mk95r<^C#}K$K^by#PU>^~Q>Rx2xLvjA zjoL%@xVjrCu;mo}q!xPbwMC1uhkMDoH63xauzzE7)G+oe+1hnpOqFY`4R4z>Ly_xo zlY7J)>y&Ur|5`oH?Q@8I#d7oEiQJFoa40|vHC79TTw=8|* z)@O`A%7p)iqqf|S=jR3=<&JH27A#=m$5xRun}~V`C!=-NK!yubF1XQCOZBnyv;Y01bH$-QN37oGwd8g!lUJMjQqQ;#hb;CfCS%B4rZ^6MM!lP1(j2-m5Jg zt){IBzU|#Swy5Q1TE9&t(j9i89=QwRs6{eQJLgQW$E>|~W!FsxYd=3cB>>MamaTN? zFV>V5WlVUg8#06VH4hQM32X6)fV}Xc8NZ}Pf%9yW<+f`>I-u6}^_8>! z{-34Py60=!D6Zr$*CMj!%olwK1bsS>XutKm3(BgR3!sJ?a>_O=RgJR1Lo_bpm4_() z9t!*IaQ@MzzE)Z1t|Annv1$M2Oe<%}Aot|*kYyUsBlpwxhW~I*jr1~?2Kb30kK%d4HUoi`lyIy^qqz!4xso6N+jp)!U;MmLa;_diTx;-Gc%-T9(;%+4 z!Z7F4{vtbY$dBTk7e||>GE+)g%>$Zat6;WN&zgPJ;KVvc&ptFvT*aPfNKMPW0{Z^e zsp%Arq^wJHfCv=N9=@KM%RoaLmYY_PBug%K^OXv=CTnm{6V!qcr*~b&r~VfGprk=F z$(>#CyC+ef3LzmZebuoSG+z z(^EFg=UAzj|eiz8*6MwPg1g*zz&Z^0odqG^tLL}w*bi@5crij#l609n1T2*OZja~|&>FJAdSs&i_p<#OXu^F5G~ z?P-=vL?nTUM$yopyeJ`E z3J=%)EVY3;$(DIn(!spD#MfxA9Xd5(dQ5WnpN`U~Hpp0qqihv&5}_~95f|{uMMM); zLAR%Yi_~Gb^4{`ApA@J_56w-d#Q?VZy&*U_Ka9<-MuyLOK1xQmt~xTKBv?0DZEIhSWNT*b8@#IO3JPgg_tQVbL?_KcZgT3Huxv~ATbhq^1C4qnYQ?+ z1gMF|Br>e_c&1>s0L6&>q9VXcBvTwR>}3t2Ofj$sLXAw`^WR(miHCM-akUfoAJJOb z_>Xkk25F9IC1t{8hbak((DcFyE)p!M#(Ymk!q6x|=(E5L)zu-gq40N+jEDK8LdXJz z?561gDZfQL z{%F#S@25)&L9~*6ZB}+_jM9e*)rl;dF;V%Ew5~@4>vi?HtjX_GEC^zgHNwQlHu0u- z$$mqnaH%apT9p~eo$~_DN5xGhNJ7s@(<*k9uPlQ1Bz+x9UYF^+h&;(zzn|?2lXtH_ zYm?JJjPJ+r2*J}E{cs9!_v^RkDUg}{l^qg;E}+Nlwc_LcsVR+scA3zFT$kkcvd z2us1hfhN_zpmm?<^koUT{aOS_%(ZwL!;f1LGZ2dM6PH4zR|?C)O|x)a;8=Wt{ucH{ zY|`3_L2a7FY6${I-O--7Y+u)8J&17@sGJ`(jU`M^?2w>r^<5DYF2jQUk>oMn51pB| z;U^2?xR?z!LJIV_C)!AwSlJ<+jUdq3c7Nk%>D_Cf|McvFnE*@=n$udT^@P8l;%qqbk?xeF0WPUE0=DYQ)6 zVoUq?(H(;K$D}VG1BiuhM$pe`i=eRRVeDTSDB<#ZSZ6^k*$~I1frU=xPv&4gH-Ka@@f>UwFm8()-l&)-m zAv5|$Mx7SsEv_|(61f)vF1D?KkgQBn0$deWUGP5nL(aaNebB**;$c-k2%lbFi(pAj zW{3i2@l0l@$V7xhDI#57Tp~8+SxoVBHs)6uWd@&=^n1o5dbpgy5Yw2X1caa>Ihu+S zH~WNye}0K2dH=V)x1_5gN%}=c(Zq~2`oWsU9w9oo-I}Bi8po{U1ky_hFjZeB1oGRs z!8wQdAYD>ofK@n*o(`-I41VBijM;O0o4$AVg%uIC3K~`<{8%hT1@3DDipg9bTE}@HAHHH$8vPaw@D>r z;*t=lX6Q0p+A6tptnV|~=UTO_9DIg2#l_>q^d)zZi3r7^0(5MUKH^MLX~7g;{4KQY2)*?|cUid%2z=^lk$4wj>kr zf57jJ=wr98@Z-eMa=$Re+WGMcS-Y-Cq%6yH*3=*f(&nI3#Z2MZK?;CMZ`#}Zm6`sA z=q{lAF@6Wz&zb^?21fErTrqByg%yCJ3f`aZ`0bMtyV%*hj%`@_Tpde6cvyN8#VQxF zARUr)LU*$3ew>O`GY_B2WEQjeNv-PdipOI&i)R!tM)c4Masug&!^v25MW(?tz8~fh%0ME zQR2?gL=;BOLp(VnQmk6sp3%yDyrt?ye3z<;Fr{b*g&0{RnneX6Nu`HBg=kW^p28*^ zKLX{aUqB>GubFkegFe#Gx()pXnOJA~zK&F=tcD7{VM4$vtYq-M)rnA*W*VY!ka z2#BC`fX9(ZpPi%%APg26)=|)DK!x1lVOno54n3t|Kc&s6NvzMG5US51(bpvAatQ^~ z-sLoN@mHXQyJiw~52>WoO0vjN02`Xq2xSvW@ORTuXf9hGou@Oi4C&@~P70{K6qY{lyFjL=AI?G}t>cQlaP@AlYfTYC6yc|0k3_L8^Kj=$S zGB5@Ku!?uIR4)YO!oe4|gA74@)Su#H{~VE_6wS6cV&9D3BLpoXQyieq3vW)68uJpx zM9T>lRY)oJ6$v5`cH_^fsiE|cWVCauOzbRZSsHHhUd17a)ERL(NHk9BP()VlMPSNj zX+WhsP^-!O3KA&)Dc4%k83-*ort?FVy#VE-jM#)4GR!Zd+rKhdba;%&*qqE!JN@zx z8We?$3Q5@a{&F#VjcU|S7AfjV8_LcFn7Klq#U)cQAs9te5#yN~M%zIBw(#pQ9qN)aePj8svneE15mM)fg>N!? z9??Xkf>*rePpTkKw}0L-%&cWun2~$zWQfj}R8CE@*N$ghrjbg&vBSXx%FA~r`;|a^ z7lc)e=2Hzo1&}mC);3UN*@WBS2*>2p&^1rUk5Q1(j-lWi< zt5OmDP039URDAw$)=cQUAq=qdmQmjb0a0iWxC;bVg;m4-9ie&IlzuoKPhbW?JaT4m zT3d=FTpQ<|r7|qZrU_Au|LD!N91eWzbAFU-5r(3$S(@`znedOr_& zk)I-dNP(e_WvA&^bcYtG4@U1|8!h`CT;Y&Kco1o2bVw)caKfwt5mstvlTXFeyV2y& zMt4dZ889G7$K))6z_^Hpi7jh$+l@dts=G7fFp0zfI+!(nZY-Hs7r!Nomu41}w%& zz^Evd{$SI~=ohnu^~dO);1-$6kB60{+4vT%6fZ!J!K|AvFr#uTK1H8Ls_kdoZWMsk zL&x8Y9#Dy$SZpaPuaLKom3&8H4Tb}0Stdlc$&Y}W^ht0SR#o)T6v^FWHvNx~c{f^{ zte&L&#C7=Al8dcse|?m10hhm{YDjVu1f_t!cqC;tik1RuG~v$<2+ROw3~t`o938Co zhA$>^(xKtbf!8ozXpzB=A`@5q5h_%{7${_ubWJ`+-(91rdVHuZg*pQYpm<%je>3nj z=+;-WsHtgw_R^nw!e&L*B$oDp*<(dZV^E+z=e6#eGAEXJBzw?@_zn|O_`NR#E#rrX zwc|_3dCyJzGXHHT&y4jSMDw?z7c_rU%7e?V?(Z$t7~ce}%+KWYM?=jWsNDO=WS|(z zRWMd^!gIVph1CK9vM8G4eeLXT1_zz86orlBRv3EQ;Vu$YYYeT}^fAW28S`hdp=g~v znUQEXh7F49V8$niDj}~*Xh-pIZcW+^=PL2jM$O@lPmWP@DdsiYHWRog%}bbBS;2YD z2b{pT#mb!LD*vG4b6;5)mBeyRPO|fX8Yae2V>SC4mnJzCwDU+K=ge5ynCSYn@TW~ty;{J;1P2jX#Ss2_`( z)(5DJ$oE2%;IFhGWpdah2L>I^85Kv}98K>ow6Pu^pZ5EIL(D^0W7fhAO)qp2jTRQi zOH8ulLI4}>8c_DLYk{+_-)QwZL}kd~rX;KNz4W=gy^{h*$Vf6bljiaPLR9N^gg`Y{ zj@*OuG9Sk+e;5w<5(=#|ii$L)hX za)Zqf+>e73@+>ZQ<;ILq0#EFxO}=5VKpNutSVZtsl1 zt;+*3EMIsKAr4+&g}MCp$tb(cW;0GlsEc7In}68}`uY@nc_na1<4Ob)kq0$XXh#eN z^YhZ7XB|fif!HeY6D zQx)tm$r4#4V*_mP1`_a1*-yOnvzg~1Xs;jV-kP!atLYozqoe%RTbg8%Ch_0bwdtK( z$Tb7q>zI*z?#Fu};o-MKnc>$w>V}z222P{Q;-lT~Gd~91nD%g{qf#!^T%GhIhDbC` z>h9@jAEKc5vOMk8RKt^S1GH3Kzj1_}lmj|Z9y*jB)-PYCTm_Jj1iC|iPYF~{>Gw^P z&A0o$@3Gz)-I3dEb_LQ=17?}}jvU*uNM5iL>BDZ^mxQ|btu5{7$AhaG+% zscmZahiV}_VRAa({hpP@S7m{QWC*eCiDb7o7D*V2+Q^_yNq$-wHDilo| z6#izrx-6x@{pCP^qa;i3&$z8WCnDf->v8*G=@jn+Fl{b=%QB%7K>2UVCY7y+88m2It&KUtc_+jQ^s>HwSFZ|}#_=p@W4`+Tjy>p^oQy7e>cTr6X zA!|^AHCj*0;+o!4(jqN19*h(95N%8}AA5w9ySsD#^6UF47rPTK7K2Xj;~<2~Lv!8j zBQ=45*RSZ)xx4YSHXqC;JA^eTPYW@6h~iH^{&+vt>8as_^*$jJ{8YS;OvA@cZRz>( zB4FqB2qO_X!o_-7LtqDdj%frrLgX~N!!MkTCB@2LQWyp#JrJ%B070se zg*~X=bIqRg?WJ~YMYKs*oZp62eS+#$My;VF0Wm8Xe?Z%mCF@}vQ$2A)n>P^FtoMj2UJyS@J;IUxV#=m;6XOV z*$YBmoKjNgK0uGZ)AaWd2|)^rC+nX1$9C6(%DZEsIM0iI_?Jy8Diyx{!Vcei&kCs1 zkBqZZhTkO&NqMr6mdE)`TO!<4hOJCdJ99+4T4BYZks2P2LBqlbq@>1Nt{3KWhBAW1Ag<&KP-=J>?h77Z?vqYMcXW5r?YxZUX zLsx!p^p(w*Iy_HM+s?ru5d!cscm-oupBhGE(3N8GR=8LC>`sM@<@%x&ZKnP)a2C=m@S1${QnP6QzVokR(e_vPlUMK&}ViTgIE;c}&iJc9m2RFY?eW1T^#O=q20PGWLwSHHO za=zctka%TPmC=tPLZ{`!ACC9vztgL~9AtDJ=up>yqYAGsJb4_mNnuAZE93Vo+IvOr)!^oS>#TSV5j zb3%Ga@UnrM^R<%X@G}J=cK%oGBU_r^tdhb{>h(yaSW@&e4ZV0!2S}H1Pd&O7)jn(E zPsx&Em74)s;I_RoWoFix(@Slm&0n4x}|WVa;&z<>Bh= zvuf1XMZ^tx;o=OR+lj7k=^#@Tt*_V`6xidVy`c$hhzd*;e7D~FGlzO1==F=wxjOFq zR7T@AT*DEwua91)HvhZ`?Yz>;&(iKdCa8n@vAJTSM62vFcFkC3E&P5B2Z&%h#PpaN z(VA6 zfnwB&0~!wK-m5GCJ*6Dh&+dPAj4P|F3s`1q;+`+w1t=6`w_n%k&jY1L67gJNe}R7 z32;`{Qinf4KIvoF=+u$gp1%WXn z7=_8g{$|}@!?s)6x&niPzf^YXaONKnA?G20`dIZ3!Erfw!0OF@;|%*UoVob*tfia^ zK`qIb^RB0aAU!@_C_a@r$^;5|v18PQ@67&k=P-J0WqnO7^(~4_? zLS?EfW!YSV3%6ubQ`vpJ$3*qyDTdCqY8!*nPG70qLR!s0rVvf|#T2z`)8_&3pmOgV zK~VNZKpU=8N%6f8=y-h?*z-7b;{VyyP7*$U2xWD-ySGI3F z%&sBQ)AeHe{ydXJGG5jB{*|s;uPf@k@AFT(jwDkh-bwz?tgGX~@nU7|D?{An0!<{J zh4o#K3lEf%_UeHv$I7;4k=m}ChX(zdZ7KYgAA)#PE0Gyh2$K!Ef?A7iNTp90oz+0=^0f`wS z({!e~Maq7G9kQXb@Au^~B#fa?)tnlnSxRNdNSIGly(Xq$Z*8y<4g}>(5%n6HdVQ3+ zUI9&$?311rHrV#Q=d@f7Cq>lyM%gw?qD`KDizgJg7JW7Q$mUVKVhp1s)vhy})<%_s zSC6%Z{YNy=yv(qxV4x&p+l0z46VHID|ZnsGi?gLRh>#QY=ENvUS(noS+nP z_~MtfsWBYRpD`vTvQYb~CDeXyextWCGB!v1e)tF_O@fq>#W>VHx_en44lWNbYe2>8 zmzEXA73tU=bYRYS^{yZ=_p%Jz9X)G7BTpSWlc|+tsVLjC@TM zG60Q)#Aw)1WnLOUVg@NsjCJ_^eXzbotuT573De=@0{q}kbBfc+1yX&hJ!l zUWtWI(xb+X7Sh~2A)I4*RhP>LtLtr^<3Sp2ebG^Aua^sCuU>vsaPi)fgIXJ!-?Adb zKUsy!`J=(Grn}H7RzJRB)5aDsVheG=&sYWnM!dcDjQM<@@L!-n<&d39&6zy!@k2DM z2A_`{AraVWoolOVZ78t@O7kQ`b}{P2*k{1n7ZFRKN|}_fMQ6ZZ9S9~pzG`>iflWk5 z(|;x!C~atH;BxiBJP<~$HVRsWJ|`NCJU?2o{;ov85b-wttOjdc%aELx70t@i3BOMR zFrHI6^f&l3OG3b>rMQaBxzX(pYj(sqs%DZ`VQR|UtLI5LzQ#B^6$1SnX%TEo$|z0F zBH{gTiamv3;AkN0zLk|LzG5cl7!-;`urt;5!cTwZ|L)W2e1}UxF|0`ThsFxjeuptp z&t@bR8PiM5L@u#YRw>OaOqRD_sGCF~J(Nj;MFaQyTFuU8m#2n~+b>0HiYsP5wg7vt zH_ij*Z$5Y^9j;$={edo?SXAc7X4LqijEQY4pLXvc{NH6!hFsLdxCwf5=zDsz zJzeX%x~gMIn=p5Ej=~Xx7WxVzQVowREd@w;@Ckvot0{wwS*ifw-{vQ1ILKg zw=e+1!eD`#0M@|Lot6bVQRlnpz>rT**M;IgGw7uPW8|WC=aM7S1o*2&2}lK_(q>5Y ze`|?|T^I-`iuq56&O>rGUHZ><6A0eC=*#dQpvx%3c^zUtvV)rzat%6uJt1JG!&@e{ z5BFQmy$oIcoUsU*Bj6FR#%bRdDSB{>tyMBJa9frLYmO)zl}A5Rj)=%3S7}p4r664s z2o_R44o}K#%=>~6r8bP-J>`5ubGN!xzB^U5<_0{Y{(VjX83NVlp#I2ngkRO&oMg3U!u)@a#9=AjZcP;-|5c7?P6p zZ&`4HYZ!HML=~qUCyUGG?Iw~ed~W}~yZ{~AKzy+1 zhp}jFY4Tfy+rqeUV^2?&V)dIFCnFauNNFyM<=QQtUY|()3TPVlpDgj{vt*M}`FJTm z~)oC^V!M;;mzB#)JzwLFG4Rf<%SoT;Hu67-G||@>ce2Fn0OKSz;aoAr608=rxO}Z3 z&8M=IR>vpTIBT1FqE*a6F!luYECGp(<(>R|#{Q5*T1-bFoSu+rEF(*ZlQi;;4^`ZiilJ!nNHYXz)Z?2X9A< zmW+J8p%^h!>lz;+7Q4V#P3&U!3(5J0>RN8<(#xE9M zB=*vKB{>`(@}m81a>Mj=B8T<9Mm`Yk%yxZo59rjF5QEMk8)`bC&&tgmXp=El#GxMd z)ZOq5f4lk=>>ogafNHGtn4w3<50r%B^LTA*53Ne3cTbtlt;y?Ij|Fvus?f_iz1m+z z1t#N9>2L+EjyH^|Ij&S+7*w=JLt1yf$j~&X{0_G9NvO?$9|6TAfobLN)HL<=tx=&5 zSha2KRgzPS*7lY+ZLX&r92`r73axH$EEMtzBn&U*e|*|n%rs6kRld>b{yf|))Ht(d zGnQ2^QMI0w_8)m5+*{o%wOGjD;&+Bsuh!|izelv&>Ilq!`e`Ph_L8`ZqHOiTul4czINM(pbi2%Mc_*tiVt91$ zEVJI`3&rTl@#DGQ((9nBrT~S#Zn?5x*0O&Zub48WmI>QJDE)3gsA6eQ(i8&i%~+%J z2D(L{6Gm_%%#`1CCt%b>RYz-0V~DcFKJWLh-SuG2&bMgSTK%md7jLYuJ%v1`A!FGT z4L_54yAO$tUbx&WBO&7oopWgJ#pP-Uu!0gE_0Y__bheY>MD?#9-v(qbKX^AxziO{;};Z z6k2)Evh|XI$TZEFV_c&x+RSJUNXBL{Jt6}hP`Um-De#<$f0Uw5{ASz{f%LxhVxgAF z<^9YC+Th0Q_!|aysxPTwlz4a7N5L&0GWnjfu#h9+y{whDlw85WSlb4$1Os-X5HV#H zYVERQJ?m=-x3mX0@LY?KeV%s7WfiiKXE?QN-KSjb)1Nn=PH(t853=|i2n;$|29|V_ zQI)WC^Cp;t;h@)arL2LtZ-zLa2vh9!?X^nytWLMv7juPa3q-s%?beTDh7|SO@FbUm z^GIx7Zif3ipDr4d7!h*HA^Ikl3B~kD@cHKQ=ac8O6Z7`|ffwH!V|T#O6cc+NY6tax z12-Kbf8Pd_>$S;2x>Xosj}yY%p6H6znNdZ1u#xZBg3xO7E`%w&siiA&i&50Ux1na= zYm+a8zKWKSmqJZ}?U5*!kS+q7G8WsSuJKcINQ!wV(i|$`LE$ zxYx*(w7(DqY)??l59E|#tUXL%r?GPPR{K?=5Mr#*$PJo1bbq!ls3SVK?g;2rSJR+# zakGZoO7n#{n?!OmcWSripd=`&M2~XbEzuJu+u#JZhMhOrR`R*Wr2j!{bu*Ng{tn-l z$Skwn^Kd0#wE;P$%NLs`$arqyk+#rlW=;kckE=461E|J+x4l@zFeA9cLuuvL*O4fl z?OR{HNz2JfQR5O$anjWErdGVbDMoAy)~eC^B;-DFb2PO#7@i0gX;ep-a=Y>JaP8&}lSoO)7{N4;0iG6$ zNMLfDZ@Tgth-LELwIngiC=Fx%a=?)zpw+)J!*qDWK<%hRg-)j2?{Qjeu7J_U<=}#w z$f}~Twk5>QC*S`m^H+r&gBWxVhG8XFrQ%1I!bN_! zt6wA*qp{6T5gXekobwPKSq46L2u8x~v!Mo5_0^TF1*c3}3?V>MMvi3ZMwq9fuMSj~ zi(bE&@0N>Oc zVJEhM!($&ZDcv`2{Mhadp>sD#VS>UFtNWq?bbc zEoE$HQl@p(ogrM-^~Tgf9{ z1f(J2sF|{?;TqBeD!!t0oAkxwfcH#2!)@bij+R`|T{k^Bcmn+SREC zvXLJi{bg4Dmqm10^>7H56^bQBY0}HBzeg#t3nvn47PxxOUSii2Q_pN9hi*Gn;aG;= zi5}XI;qd_;CQE^Xdfd=>VOuQ%<+sTZpFla@-_U{3P*pUDTFzCXIku#>k&@Ny;mTm9 z(oQNhT^JQXm4aKPqKSv*c!3aYot+va=J{zOK3yfZP(!x&ncu}-H4sN;nh?wBT8u~| z#EuwFnnFE^L&gKCAjOkM90*MIDniB23OCMwkwqtzs%DdpxBsp*ddQ;*?#N?QOvlPq z3#dQE9I88x=ly&4G3`bvAF4oaKz}76oW4s;xc(%#rD<>Tju#Qyh~wF?V}18RE=2>o zU`T1FL{6QQa&z2U%VEhup?E~nz3jnJiB#Q^aY8_*Yt%FV7nN$$mzGxwd5^-L;$*?e zJEvPhz|MKxZS+MnnRtu^7Wx-w7JJ6hq>CsXyVHv5U_m#FFs1rvIBn zM|t7Ig!b4U8Lb-9Makkys-%Mxh)+!%yW~~nQ%aF zXpm znxiQ0QwjfZUD31(pnv(s1TiUz+;l!to^pw}UX}Vm`YEc@3eNM6|4;MH=AR9-Na32| z!uF){fpn!GdoptJRuf&I+S0Jt7kK?b#almbcZvOMi92+og?X<&4O+}Cy@#|bBtzlI zh26y#W>#yJWS4jdg`xl*819l(S|#bPXd8Pop$U2p#{2tHWerGp zm_c#7sQyv;vdeKfE~J`KZChq7f$;p}y;wvQo@iY%uy!+r#sqQEur{1D&RRyCrOki7 zP2^%VxZfrNz{ow4C19cm+*M$WOz0!Ivp89T*Ru}Dol={s$Y1~qF;gCz{_+ba5m{nj zZktF800mnlr;-5(PpNt1GIs0VLVxJd&S28`{D1h|rg+itF$axRe`|J!>WfIo=bNA+ zoK`kyCY?A?8fD)9e*#q-s^n{=AYJp0Mw^W^YV!Q^r2_KhygW|cJAp^h0K$!DuTV@ur6_ z?bKyk#THvcnqJ2ozg#S;8xo@zX01a-Dd-KGG9Mq=Vpazl=Uu9IGfSenCj85&1lNQ_ zDXuq3emo@cx+HfovcTG2$3W(|WGc79mLf20dcnhyG_SQ5=1>?i3c)U41Vr0;EX$lp zD^$Z13YYMJM_uiFu3(}s1VJ*@n++ZI=+zSFIes>8rBoGkuzkAKMa*1JpsX+f$7cWR zt_~~3Y+27N?d3XFvE3*oQ|s+I8m*RuLJB1clJNSWgh{vnrp={oI;vAZoPzSNa5GLX zm0Cc5*Kg7cY~A>S8z24qjmGA`^z)gr17*=>SocX!S^f7I#UnpAqtGLTB)FN;<6t0{ zo3X;n+~BDbZ2Rr<|5ZQgJY%=v*bRttXUZ+e2>cOyAG7-rJu-HF zV$*nb>p>{?GsD5^LoARW(6HyB2{oFR*+5-;Ljq6O=_2fO5VgA!fhcA3S_i;%@`MF;yeU5|$g0*~4}vSKc5qvLl4^7%r+FvUaD z3!1>JcAHVC`_aKIH2G3qQ?{me&u&Y!Q76>r>6z2JWFY`c&Ir5&G`19_2d)lao+;ha zd|lsZt{+7=Xn)(YnRUr*oWclwC{Y#OOHOGGdh6=iWeN!P0z1w z?WvZUx~^&mZAUFihyu+<17VA`?zYQSn};f{v(aL2c8&5_nVrC>ZO%G(MY)aCDaTf1 zLH(LdI_6D%LiK)tMeU0bV;5vpvsNh5OJu>%<)9%VQG~50MlDDUM@u-@&N@n?1t?7@=p1A z*1d{3%tJuf)@B)GO@5o>NcD{4#wKjaQXGvPbnCXv>1#Nk`Dq}aTH+eX*VQd|YEWu6^r?P_qNm`b6Vp){y ztfgm%m+R2e=RKGRf(~rFHYH^d={k&%@Bm;ASXwXy3_ph^0w7IlzDMPPI+mFEvKdLc zqkKLOu#5;8a-L}K8IV+=Qgnt_@eBE1n+ao(=Uw;{4A)WJ^w7?q& zUw|+O1^~;g~%o^c+HA7IUi=K^XAIRF7-X=uhc?yLIH zTF=lkz@hKy8JO+QY);(d1jyV2nOiiQTk>^C5F<&-Tt`$ZZOZ1Q2&QMfF@}4&vDfXn&GJ#A^EF%~uqQZgA z(`3d!ynh?nu%FjG=}g6Om;KDEGSV}Umc%6Hc9RQ~4U_rGgEIViWH<}pyV4Hwj|&UjKj z!`XJTAsD+2OPgK~l=+Us{F?y}o4a(MwO|_3gS27^y|A5n2-|X!=x{?c1S|7$xi=H7 zwqRm@VMEqOf!1u?W~XUjZa~?A+9CKK z9x9Z76dmc%Yc>XDj+JFY2KbG~={$E6>2~8e;+i})8=`j`ycz%4*v?iqeV}<8Vs>v0 z0}EIVlog!T;lO^Chwly*CfqFn4bFzlXVxMMnB1d$2&(1)VdiA)Z-CXz*M9FJ2==B- z`qqK9xt@qz1T=x8O;Pmpp%35Ny9F(xwTA;nH)%trHq3dmh)4di39{x6-1Y2SZJ50G zi1my}fIXal?{MM9C1Fbp@x*D#PLVcKJ4=sIj}(-TEJXTEYJoRofj17ye|_Wq?`}4> ze%B9za*O3MY)~KqjmtTj&CP*w5^xB}1XM9blTK{{Jhj<4kHv)y$%=L!u1G@;jEo0# z=xLhnMaC;W^eP?Tn;#nQhR~{eV>r-yd=9vr2kO4(;ep{w`T6u_lb~!j+axF(aCV!u z&BIzSq12N}%bM_}?%xuD-1oY>L?pp^R`L*8?&Ls=!0fy(d4dFJJ1^zm54}Qd;ca`->=$3Xpi23!v4J&FW>y5N345RnCSJMa;oU{hIZ)mvR^t2 z{Mv)EDb0}U*xN`1KyIn57uEyiBi=G|t;{1b*Gbi)%#rZ50;kM@vSfrzX4kS#Y+w^Y z>>UDnmOAv771~XBVaZ${!OXo5!@?$SsC`-f4Lw(T=y0A*0+a=0Nx+n-gWXUacxHlL zV|va(z+%Tzj@yC4@kiTvh}@8AV>{Q34e|F5MRxPnJ&W+=fSC~sdaABG%Q1qFUg*$< zCLbyQ3Cue5K+r}b41k*J^il_j`*fI?yux95n6;y`PZD_SJug!2QyA%E`lz`@92Xey_4Kk_W{nIDDC$z6;gcD2F^rwpnjOaOrPk@07Oa-7*cGjZm7DJcK!zoqoI zsRe!+7I>qee1GG9-(P<8gYRy(c7E3nG9`wu$^JbIU;FC1!_u6vb(j}4IYGlHpMhjK^gF^KcVA;t_|Y^oRerIVQv7uQ#yCIUl}^xJa&OYIOw~);n|WAqemq+^k(r zmcMhkK7q20j=IkXkSzc?Fe4Di5j|e+qK9R@?xFSLsR(Jdg-e7?R3>(gY!^{ z8-dz}-;+7pqoABP$oQs&jq4#POA*W%CO52Tj>)k*&5mKHTLRtd);bA<$xt}KlJiLB z*Exo?spDAJs5KjgsAoabdC7c_dsI0lZ73Vt=UIoBAfq`@c2&=$jodf@c85In64z&) zosQ+GllR}7qa!d<0jyj?Kz6$Il-+}U#8bAQ{LOdY`5&jeQ;lE7#*>bbS|GK+t1a+r z0Lnl7Pn)gne|{8{55bY3%)YRl=3$sl+Z>syiO{Sg$}nbJtwlY4N0qni+wLzErD{9>8$arm#q2fAeJ%p?Y5p+>&ntJ`^>tvP)#xqH8wmS zcFj688>Ym{vFY6s=+K(v0ocQlyH|JkkyRXz1YB`+q`~?Ktw{94hED$G#-}&OoEKU z>C8P^Zhg%DbmkD)j)$=MK6*4sP*(WWhR(T_v&AB>!SFJ3$xgHT zRlppx7htcP%K$Dxl1fkn8{UsLkM2JdE(Z9H_f_@R_2m7&>Q!s+ZTcvZMlK z=9H(-eE&D!`NkhjdXu%!WG6}oO)ZdG;HUP|U%5#>URoOb;p4yh?`yU7f0oGx1uACQ z;$+j7(B?3eV{AJj78R>(9+x5_N9s_L;Zp<3Bzn=qbY3j6C>gTlHr11abixj=$P-`DXmx*T zG__I7bFu*PX&04a@gP#n<)vAj^)u*1huY%2Cmk$WtzKwZUgoE~fU~*FlPW|m`O?Es zKLLl=ASkO@gPq5#F2V6!`IJ29L@Ae22{}q(W_uA0N095~dzDHE5gdPhj$JHiOGgl+ zZ8<(YennPnE?9tTBP+BML(9o$Eje^D{gF(WxKO@Q6hQ`MtBKCfCPUGDju_pc@XGOP zB)>oYJ9jPjwNNaYymer7!bSEn>3Q)v3dkdmT=m@4?DxO*wRgUkg7T+);M2FI7Wf4% z@M-tdUvT)J{jr-*Zw~$K98WT&SPMi7-pX7Bg-SFyh>cuT2_ zWVLKoEf}d0?hY5Sg_TPchrmfh9?Ov)A3w<>NCFyx*~x3Vr_ceB4ltHXMxAZlzfDIR z|Em1F>ey}h6jK8kUmeQmiBqo90kONj^OOmiKkwBmW3$hjdv16rqTMS+_ER>8yCflu z>)?Rd+21`^%3<5}p#76Sce!Wxm4Hk|H{+$VpE9FAO2mtSZ9L`R;Pm*}|M?HT{>|SX z_r?!?-kj3krxr*p@QYjEje_#+<=ex5_3+1kv{u{r?MyC{r&lXBaRQW`hemy6vw0o` zWg{M9L@|1sk*!;H^`|WJCYvz;000P!Nkl*f~0-ME(oO^$2SV=mJz^Qbn>o;At*s-1B@Pw6!v&>F*|fvfdlT&G zmAUT^mAnjt-LZI zBV&2Un$|PQgI{Ra_Q|NntNu*g9Fz7I+n%{&GO+D)>*P+|&-uLztGoMm%4JFW5ag3T zcXoGQ$x3Hsf(}I1Q&wo3wc*_erA!Gyo0@ElUIgb0h`qQf5`tYlAxQ`#BH8il7;%$BdnAAF#7~^vO%C1M2S4YO z%RbE?%5fI615t&t-gCp_%dzKCDVT^V zZsxg5j_*ptde4V_o?f*+oP%oFZT-G$MqxnNwCVKOn4_Di%}$;D)8G8YZ~V70Z>;)h z?3KPJwLoftU(5n;6qK1${@X`?`Ms^$#y`zwv&tze9Bi4@=6l;PLda{im?`@2l!~Y- z^%_9tCtOI;{l77j+a!(7wepcB5&T;4eHAF1-m?~LEQloE=QdM;$}#ZmZIUA39D)jU zv)%n^UP;iE8r30)tKmHrx#$aV98rLc2Kc9)PehT3#9`w2=Z=Gg1Nj~(ziwWMghCR8 z9cb~Z2y$NVQ=ly7x&@<<_2~&a7{Z-dKL@TH-yLR-A9|dfkK4_?&#~=a_viDfoN&_P zcJ>p?#W#_mv*eEyTs2uhIh%5mm z5PH=!6ElDO-h1!-VamS##WvdXQ>g_$tp(mFD63BSCqMj;TL&AzlgR~rQN-@>Hj~O4 zSl4P#Whwa|vkf!v{QQu(xO>KXkIrF& z*C~eCYq*>#$2}_3X^Cn!=b5~vS6#>b{%GKyc~9H#`BT*A9wGSI_ZtoKS4Ua2ZpX$w z`YciT?RXUC=x1I>^wJ^Gio%H7@QUjg^4g)Q-0|mFUi#1ZOFv#OfBfy#h||4Tg*h{4 z^LNJbJU>KS@N#QsPoDeF=imKD|Lcm!j^pVowLoft&&mRC6qFYh7V`i1@elst_U`sS z&J}_aVHg&@z{|#A#N@0Wh26mOGlA!MnIQK3Fphj9pl>v$BQNx%>+6a{lNG0?36yEo z>&jWpS7|sHgG$cH>T_7wWQyA8Rm7g>$8mftd3tmuJXfb`f9%V7R`p5Gv>J`utFCAg z#-7=%laSm)e{fjl_ax^B`#yS>;> z>BxQeLdm9qla3Vv6O)%P10B^lrKNd}lP&Fw2@G^y$0XAlnNN%Tyc9m-D2m+tjdjcj zZiVA4=;_PM`aO?%{7MJd+}hDwH-hJe`xZ(?_0R^1=T#qi)pMk=OhWyf9o5+ z^?}wreWeyiE%2FH;EjSZXZ53nANB9H8sqs~t`fDwoEIS5jk|%L_5HBj4T6jpcyW-8 zy*Lv^?0io`5c71##S7U%UK^{E-8f<^&kwSG97R5f#IYY|;>h#8jK^(^z^K;op6^9| z>_wjMM=|#$A!w|IkxI2DUY1j)tS_O^ZJq{4l%+FxVp+XWXk00ecRs#?0X7;%Q!B9@xR$O4;AazD-(5||>oov32~!spcO_1(SN4O(}3 z?3Gk$mQI(Ain6BKppgL?5|U{{9$8F`z#>8HMW*}lD+W%w{MP;qmu8emHZNxrPl;0H zxC!7#?QiG7(q=Qq9?wr>B=3jDlQnHErH+4q$gG1?+LkZ?&3@117_p8m|j+owq=%Jp@J4YP){ zdo0GI9hc86{{&)mL3ED&h!86~Xl*jF$0Kbh>RE5SZNpIbKL4OHaVPR4FK$Os#^)bO z#Bq3D2|};y!EZ$#c8Y!8`ZrH~Ggb5YsfRoLL27|tt_9vWDF1S&la7{JAhkehfz$%2 z1wOwPNJ07YyLRcFrxr*pkXj(Mz!%H{DJXxz7BrnsYJt=OsRdFCe10vEg7W8g?b16> zEs$CuwLoftFPH^VQ2v4~XgZzL0;vU33#1nK{8}Id<x0YYJt=OsRh1Z7Dz$)3$~!?bW#hX7Dz3S yTHy0*ffSTKziXG?d1`^w0;vU33w*&W@c#jo8TwUWMbC%;0000 -
- - -
+ {!isSimulatorTarget(currentBoardInfo) && ( +
+ + +
+ )}
- {isOpenPLCRuntimeTarget(currentBoardInfo) ? ( + {isSimulatorTarget(currentBoardInfo) ? ( +
+

+ Built-in simulator — no configuration required. Press Build to compile and run. +

+
+ ) : isOpenPLCRuntimeTarget(currentBoardInfo) ? ( <>
)} - {!isOpenPLCRuntimeTarget(currentBoardInfo) && ( + {!isOpenPLCRuntimeTarget(currentBoardInfo) && !isSimulatorTarget(currentBoardInfo) && (
-
- {isOpenPLCRuntimeTarget(currentBoardInfo) ? ( + {!isSimulatorTarget(currentBoardInfo) && ( +
+ )} + {isSimulatorTarget(currentBoardInfo) ? null : isOpenPLCRuntimeTarget(currentBoardInfo) ? ( connectionStatus === 'connected' && timingStats && timingStats.scan_count > 0 && ( diff --git a/src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx b/src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx index 1f71585c6..47df727c8 100644 --- a/src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx +++ b/src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx @@ -2,7 +2,7 @@ import { communicationSelectors } from '@hooks/use-store-selectors' import { Checkbox, Label } from '@root/renderer/components/_atoms' import { DeviceEditorSlot } from '@root/renderer/components/_templates/[editors]' import { useOpenPLCStore } from '@root/renderer/store' -import { cn, isOpenPLCRuntimeTarget } from '@root/utils' +import { cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' import { useEffect, useMemo } from 'react' import { ModbusRTUComponent } from './components/modbus-rtu' @@ -21,6 +21,7 @@ const Communication = () => { const currentBoardInfo = availableBoards.get(deviceBoard) const isRuntimeTarget = isOpenPLCRuntimeTarget(currentBoardInfo) + const isSimulator = isSimulatorTarget(currentBoardInfo) const isRTUEnabled = communicationPreferences.enabledRTU const isTCPEnabled = communicationPreferences.enabledTCP @@ -47,6 +48,16 @@ const Communication = () => { } const memoizedIsModbusTCPEnabled = useMemo(() => isTCPEnabled ?? false, [isTCPEnabled]) + if (isSimulator) { + return ( + +

+ Modbus RTU is automatically configured for the simulator. +

+
+ ) + } + return (
From e6c650e47e356d59c078c153feec870ba2843d3e Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:02:41 -0500 Subject: [PATCH 08/25] fix: force fixed Modbus RTU defines for simulator compilation When the simulator is selected, the user doesn't configure RTU settings (communication panel is hidden), so rtuSlaveId is null and MBSERIAL_SLAVE is missing from defines.h. Force Serial/115200/slave 1 for simulator. Co-Authored-By: Claude Opus 4.6 --- src/main/modules/compiler/compiler-module.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 5180f8fa3..26018534a 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -755,10 +755,17 @@ class CompilerModule { // 3.2. Device Configuration DEFINES_CONTENT += '//Comms Configuration\n' - DEFINES_CONTENT += `#define MBSERIAL_IFACE ${modbusRTU.rtuInterface}\n` - DEFINES_CONTENT += `#define MBSERIAL_BAUD ${modbusRTU.rtuBaudRate}\n` - if (modbusRTU.rtuSlaveId !== null) DEFINES_CONTENT += `#define MBSERIAL_SLAVE ${modbusRTU.rtuSlaveId}\n` - if (modbusRTU.rtuRS485ENPin !== null) DEFINES_CONTENT += `#define MBSERIAL_TXPIN ${modbusRTU.rtuRS485ENPin}\n` + if (boardRuntime === 'simulator') { + // Simulator forces fixed Modbus RTU settings over emulated UART0 + DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial\n' + DEFINES_CONTENT += '#define MBSERIAL_BAUD 115200\n' + DEFINES_CONTENT += '#define MBSERIAL_SLAVE 1\n' + } else { + DEFINES_CONTENT += `#define MBSERIAL_IFACE ${modbusRTU.rtuInterface}\n` + DEFINES_CONTENT += `#define MBSERIAL_BAUD ${modbusRTU.rtuBaudRate}\n` + if (modbusRTU.rtuSlaveId !== null) DEFINES_CONTENT += `#define MBSERIAL_SLAVE ${modbusRTU.rtuSlaveId}\n` + if (modbusRTU.rtuRS485ENPin !== null) DEFINES_CONTENT += `#define MBSERIAL_TXPIN ${modbusRTU.rtuRS485ENPin}\n` + } if (modbusTCP.tcpMacAddress !== null) DEFINES_CONTENT += `#define MBTCP_MAC ${FormatMacAddress(modbusTCP.tcpMacAddress)}\n` // OBS: This is giving us an empty string and this is being printed as a space From b05cfab9f2ca27e6faad89b812ffe9221efee9ef Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:11:28 -0500 Subject: [PATCH 09/25] fix: correct UF2 firmware filename to Baremetal.ino.uf2 Arduino CLI outputs Baremetal.ino.uf2 (not Baremetal.uf2). Co-Authored-By: Claude Opus 4.6 --- src/main/modules/compiler/compiler-module.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 26018534a..3cfe6b88f 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -2078,7 +2078,14 @@ class CompilerModule { // Step 13: Upload program to board or load into simulator if (boardRuntime === 'simulator') { // For simulator targets, send the UF2 firmware path back to the renderer - const uf2Path = join(compilationPath, 'examples', 'Baremetal', 'build', 'rp2040.rp2040.rpipico', 'Baremetal.uf2') + const uf2Path = join( + compilationPath, + 'examples', + 'Baremetal', + 'build', + 'rp2040.rp2040.rpipico', + 'Baremetal.ino.uf2', + ) _mainProcessPort.postMessage({ logLevel: 'info', message: 'Compilation successful. Loading firmware into simulator...', From de234bc6481f8e08322db169f6f2ca67a41dcaee Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:21:30 -0500 Subject: [PATCH 10/25] fix: use rp2040js Simulator class for proper clock and UART support The raw RP2040.step() doesn't tick the simulation clock, so timer-based peripherals like UART never process bytes. The Simulator class properly advances the clock on each instruction, handles CPU wait states (WFI), and runs 1M iterations per batch for better throughput. Co-Authored-By: Claude Opus 4.6 --- .../modules/simulator/simulator-module.ts | 56 ++++++------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 082d8425c..f9a6cf740 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -1,5 +1,5 @@ import { readFile } from 'fs/promises' -import { RP2040 } from 'rp2040js' +import { RP2040, Simulator } from 'rp2040js' import { bootromB1 } from './bootrom' @@ -44,12 +44,11 @@ function loadUF2(data: Uint8Array, mcu: RP2040): void { /** * Manages the rp2040js emulator lifecycle in the main process. - * Handles firmware loading, execution, UART bridging, and cleanup. + * Uses the Simulator class which properly handles clock ticking, CPU wait states, + * and timer-based peripherals (UART, etc.). */ export class SimulatorModule { - private mcu: RP2040 | null = null - private running = false - private executeTimer: ReturnType | null = null + private simulator: Simulator | null = null /** Callback fired for each byte transmitted by the emulated UART0 */ onUartByte: ((byte: number) => void) | null = null @@ -63,59 +62,38 @@ export class SimulatorModule { const uf2Data = await readFile(uf2Path) - this.mcu = new RP2040() - this.mcu.loadBootrom(bootromB1) - loadUF2(new Uint8Array(uf2Data), this.mcu) + this.simulator = new Simulator() + const mcu = this.simulator.rp2040 + mcu.loadBootrom(bootromB1) + loadUF2(new Uint8Array(uf2Data), mcu) // Wire UART0 output to the Modbus RTU bridge callback - this.mcu.uart[0].onByte = (byte: number) => { + mcu.uart[0].onByte = (byte: number) => { this.onUartByte?.(byte) } // Set program counter to flash start and begin execution - this.mcu.core.PC = FLASH_START_ADDRESS - this.running = true - this.executeLoop() + mcu.core.PC = FLASH_START_ADDRESS + this.simulator.execute() } /** Send a byte to the emulated UART0 RX (host → device) */ feedByte(byte: number): void { - this.mcu?.uart[0].feedByte(byte) + this.simulator?.rp2040.uart[0].feedByte(byte) } /** Stop the emulator and release resources */ stop(): void { - this.running = false - if (this.executeTimer) { - clearTimeout(this.executeTimer) - this.executeTimer = null - } - if (this.mcu) { - this.mcu.uart[0].onByte = undefined + if (this.simulator) { + this.simulator.stop() + this.simulator.rp2040.uart[0].onByte = undefined + this.simulator = null } this.onUartByte = null - this.mcu = null } /** Check if the emulator is currently running */ isRunning(): boolean { - return this.running && this.mcu !== null - } - - /** - * Execution loop: runs a batch of CPU steps then yields to the Node.js event loop. - * This allows IPC handlers (e.g. Modbus RTU requests) to be processed between batches. - */ - private executeLoop(): void { - if (!this.running || !this.mcu) return - - // Execute a batch of cycles — step() runs one instruction at a time - const batchSize = 100_000 - for (let i = 0; i < batchSize && this.running; i++) { - this.mcu.step() - } - - // Yield to event loop so IPC handlers can run, then continue - this.executeTimer = setTimeout(() => this.executeLoop(), 0) + return this.simulator !== null && this.simulator.executing } } From 071519f8b554421ee5015097c611239e5ff9a175 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:26:47 -0500 Subject: [PATCH 11/25] fix: use Serial1 (UART0) instead of Serial (USB CDC) for simulator On RP2040, Serial maps to USB CDC while Serial1 maps to UART0. rp2040js bridges uart[0], so the firmware must use Serial1 for the simulator's Modbus RTU to communicate through the virtual UART. Also fix "undefined" display in debugger log for simulator target. Co-Authored-By: Claude Opus 4.6 --- src/main/modules/compiler/compiler-module.ts | 5 +++-- .../_organisms/workspace-activity-bar/default.tsx | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 3cfe6b88f..2a7f582e8 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -756,8 +756,9 @@ class CompilerModule { // 3.2. Device Configuration DEFINES_CONTENT += '//Comms Configuration\n' if (boardRuntime === 'simulator') { - // Simulator forces fixed Modbus RTU settings over emulated UART0 - DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial\n' + // Simulator forces fixed Modbus RTU settings over emulated UART0. + // On RP2040, Serial = USB CDC, Serial1 = UART0. rp2040js bridges uart[0]. + DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial1\n' DEFINES_CONTENT += '#define MBSERIAL_BAUD 115200\n' DEFINES_CONTENT += '#define MBSERIAL_SLAVE 1\n' } else { diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index 27f8b35ee..4eae2a507 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -876,7 +876,11 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa }) const targetDisplay = - connectionType === 'tcp' || connectionType === 'websocket' ? targetIpAddress : connectionParams.port + connectionType === 'simulator' + ? 'simulator' + : connectionType === 'tcp' || connectionType === 'websocket' + ? targetIpAddress + : connectionParams.port consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', From 281fd3439e6b4d7febbf6ce1a3594e56930cb451 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:39:01 -0500 Subject: [PATCH 12/25] fix: add simulator reconnection case to debugger variable polling The reconnection logic in handleDebuggerGetVariablesList only handled 'tcp' and 'rtu' connection types. When the first poll attempt failed for simulator, the client was nulled and subsequent reconnection attempts fell through to the error case returning "No connection type stored", causing all variable values to display as "-". Co-Authored-By: Claude Opus 4.6 --- src/main/modules/ipc/main.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index 156d2aa24..7b65ae242 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -1081,7 +1081,16 @@ class MainProcessBridge implements MainIpcModule { this.debuggerReconnecting = true try { - if (this.debuggerConnectionType === 'tcp') { + if (this.debuggerConnectionType === 'simulator') { + const virtualPort = new VirtualSerialPort(this.simulatorModule) + this.debuggerModbusClient = new ModbusRtuClient({ + port: 'simulator', + baudRate: 115200, + slaveId: 1, + timeout: 5000, + serialPort: virtualPort, + }) + } else if (this.debuggerConnectionType === 'tcp') { if (!this.debuggerTargetIp) { this.debuggerReconnecting = false return { success: false, error: 'No target IP address stored', needsReconnect: true } From ee9e4e5eadc873dfdd4beb90061561cd09d6a693 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 04:43:34 -0500 Subject: [PATCH 13/25] fix: skip Modbus TCP/RTU check for simulator in debugger polling The polling setup in workspace-screen.tsx bailed out early when neither TCP nor RTU was enabled. For the simulator, both are disabled (Modbus RTU is auto-configured on the main process side), so the polling never started and all variable values displayed as "-". Co-Authored-By: Claude Opus 4.6 --- src/renderer/screens/workspace-screen.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index d603cc694..8d60080ed 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -5,7 +5,7 @@ import * as Tabs from '@radix-ui/react-tabs' import { useRuntimePolling } from '@root/renderer/hooks/use-runtime-polling' import { DebugTreeNode } from '@root/types/debugger' // Note: Logs polling is now handled by useRuntimePolling hook -import { cn, isOpenPLCRuntimeTarget } from '@root/utils' +import { cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' import { appendToDebugPath, buildDebugPath, @@ -230,7 +230,7 @@ const WorkspaceScreen = () => { console.warn('No runtime IP address configured') return } - } else { + } else if (!isSimulatorTarget(currentBoardInfo)) { if (isTCP && !debuggerTargetIp) { console.warn('No debugger target IP address configured') return @@ -243,7 +243,7 @@ const WorkspaceScreen = () => { } let batchSize = 60 - if (isRTU && !isTCP) { + if ((isRTU && !isTCP) || isSimulatorTarget(currentBoardInfo)) { batchSize = 20 } From acdc954831cf776bd6f26ab2e777cba6f1a68f0f Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 05:01:52 -0500 Subject: [PATCH 14/25] feat: cosmetic improvements for simulator UI - Wire start/stop button to control simulator state (start re-builds, stop calls simulatorStop). Button is enabled for simulator targets and shows the stop icon when the simulator is running. - Sort board list alphabetically so OpenPLC Simulator appears next to OpenPLC Runtime entries instead of first in the list. - Hide core version suffix (e.g. [5.4.3]) from the simulator entry in the board dropdown since it refers to the underlying rp2040 Arduino core, not the simulator itself. Co-Authored-By: Claude Opus 4.6 --- src/main/modules/hardware/hardware-module.ts | 8 +-- .../editor/device/configuration/board.tsx | 6 +- .../workspace-activity-bar/default.tsx | 56 +++++++++++++++---- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/main/modules/hardware/hardware-module.ts b/src/main/modules/hardware/hardware-module.ts index c628a82f1..ad8c2695f 100644 --- a/src/main/modules/hardware/hardware-module.ts +++ b/src/main/modules/hardware/hardware-module.ts @@ -182,11 +182,9 @@ class HardwareModule { }) }) } - // TODO: Improve error handling and return type - // if (availableBoards.size === 0) { - // return { success: false, data: undefined } - // } - return availableBoards + // Sort boards alphabetically by name + const sortedBoards: AvailableBoards = new Map([...availableBoards.entries()].sort(([a], [b]) => a.localeCompare(b))) + return sortedBoards } async getBoardImagePreview(image: string) { diff --git a/src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx b/src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx index 532d28040..7d3dd9c46 100644 --- a/src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx +++ b/src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx @@ -90,7 +90,8 @@ const Board = memo(function () { const handleDeviceValueAtFirstRender = () => { const boardInfos = availableBoards.get(deviceBoard) if (boardInfos) { - const coreVersionAsString = `${boardInfos.coreVersion ? ` [${boardInfos.coreVersion}]` : ''}` + const showVersion = !isSimulatorTarget(boardInfos) && boardInfos.coreVersion + const coreVersionAsString = showVersion ? ` [${boardInfos.coreVersion}]` : '' const initialBoard = `${deviceBoard}${coreVersionAsString}` if (initialBoard === formattedBoardState) return setFormattedBoardState(initialBoard) @@ -342,7 +343,8 @@ const Board = memo(function () { viewportRef={deviceSelectRef} > {Array.from(availableBoards.entries()).map(([board, data]) => { - const formattedBoard = `${board}${data.coreVersion ? ` [${data.coreVersion}]` : ''}` + const showVersion = !isSimulatorTarget(data) && data.coreVersion + const formattedBoard = `${board}${showVersion ? ` [${data.coreVersion}]` : ''}` return ( state.deviceDefinitions.configuration.runtimeIpAddress) const isDebuggerVisible = useOpenPLCStore((state) => state.workspace.isDebuggerVisible) + const currentBoardInfo = availableBoards.get(deviceDefinitions.configuration.deviceBoard) + const isCurrentBoardSimulator = isSimulatorTarget(currentBoardInfo) + const applyEarlyCommentWrapping = (projectData: PLCProjectData): PLCProjectData => { return { ...projectData, @@ -319,6 +323,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa ) .then((result) => { if (result.success) { + setSimulatorRunning(true) addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator is running.' }) } else { addLog({ @@ -401,6 +406,25 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa } } + const handleSimulatorControl = async (): Promise => { + try { + if (simulatorRunning) { + await (window.bridge.simulatorStop as () => Promise<{ success: boolean }>)() + setSimulatorRunning(false) + addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator stopped.' }) + } else { + // Re-build to get a fresh firmware and start the simulator + void verifyAndCompile() + } + } catch (error) { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Simulator control error: ${String(error)}`, + }) + } + } + const handleDebuggerClick = async () => { const { workspace, project, deviceDefinitions, workspaceActions, consoleActions, deviceActions } = useOpenPLCStore.getState() @@ -451,7 +475,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa if (response === 0) { // Trigger full build, then restart debugger flow setIsDebuggerProcessing(false) - verifyAndCompile() + void verifyAndCompile() return } else { setIsDebuggerProcessing(false) @@ -1227,19 +1251,31 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa void handlePlcControl()} - disabled={connectionStatus !== 'connected'} - className={cn(connectionStatus !== 'connected' ? disabledButtonClass : '')} + onClick={isCurrentBoardSimulator ? () => void handleSimulatorControl() : () => void handlePlcControl()} + disabled={isCurrentBoardSimulator ? isCompiling : connectionStatus !== 'connected'} + className={cn( + isCurrentBoardSimulator + ? isCompiling + ? disabledButtonClass + : '' + : connectionStatus !== 'connected' + ? disabledButtonClass + : '', + )} > - {plcStatus === 'RUNNING' ? : null} + {(isCurrentBoardSimulator ? simulatorRunning : plcStatus === 'RUNNING') ? : null} From ece33b3561360d2a6e2f4369198f08a04e5cd72b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 08:47:54 -0500 Subject: [PATCH 15/25] perf: use setImmediate and larger batch size for simulator speed The stock rp2040js Simulator class uses setTimeout(0) with 1M iterations per batch (~8ms simulated time). In Node.js, setTimeout(0) has a minimum ~1-4ms overhead, causing the simulation to run ~4x slower than real time. Replace the stock Simulator with a custom execution loop that: - Uses setImmediate instead of setTimeout(0) to minimize scheduling overhead between batches - Increases the batch size from 1M to 4M iterations per batch - Includes a local SimClock implementation to avoid deep-importing from rp2040js internals (not exposed in the package exports) Co-Authored-By: Claude Opus 4.6 --- .../modules/simulator/simulator-module.ts | 174 ++++++++++++++++-- 1 file changed, 157 insertions(+), 17 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index f9a6cf740..f25cd37bb 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -1,5 +1,5 @@ import { readFile } from 'fs/promises' -import { RP2040, Simulator } from 'rp2040js' +import { RP2040 } from 'rp2040js' import { bootromB1 } from './bootrom' @@ -12,6 +12,107 @@ const UF2_MAGIC_START1 = 0x9e5d5157 const UF2_MAGIC_END = 0x0ab16f30 const UF2_BLOCK_SIZE = 512 +// Execution loop tuning: nanoseconds per CPU cycle at 125 MHz +const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns + +// Iterations per batch. The stock Simulator uses 1M which yields ~8 ms of +// simulated time per batch. With setTimeout(0) scheduling overhead (~1-4 ms +// in Node.js), the simulation runs noticeably slower than real time. +// Using setImmediate + a larger batch keeps up with real time. +const ITERATIONS_PER_BATCH = 4_000_000 + +/** + * Minimal simulation clock that satisfies the RP2040 IClock interface and + * exposes `tick()` / `nanosToNextAlarm` for the execution loop. + * Avoids a deep import from rp2040js internals. + */ +class SimClock { + readonly frequency: number + private nanosCounter = 0 + private nextAlarm: SimAlarm | null = null + + constructor(frequency = 125e6) { + this.frequency = frequency + } + + get nanos(): number { + return this.nanosCounter + } + + createAlarm(callback: () => void): SimAlarm { + return new SimAlarm(this, callback) + } + + linkAlarm(nanos: number, alarm: SimAlarm): void { + alarm.nanos = this.nanos + nanos + let item = this.nextAlarm + let last: SimAlarm | null = null + while (item && item.nanos < alarm.nanos) { + last = item + item = item.next + } + if (last) { + last.next = alarm + alarm.next = item + } else { + this.nextAlarm = alarm + alarm.next = item + } + alarm.scheduled = true + } + + unlinkAlarm(alarm: SimAlarm): void { + let item = this.nextAlarm + let last: SimAlarm | null = null + while (item) { + if (item === alarm) { + if (last) { + last.next = item.next + } else { + this.nextAlarm = item.next + } + return + } + last = item + item = item.next + } + } + + tick(deltaNanos: number): void { + const target = this.nanosCounter + deltaNanos + let alarm = this.nextAlarm + while (alarm && alarm.nanos <= target) { + this.nextAlarm = alarm.next + this.nanosCounter = alarm.nanos + alarm.callback() + alarm = this.nextAlarm + } + this.nanosCounter = target + } + + get nanosToNextAlarm(): number { + return this.nextAlarm ? this.nextAlarm.nanos - this.nanos : 0 + } +} + +class SimAlarm { + next: SimAlarm | null = null + nanos = 0 + scheduled = false + constructor( + private readonly clock: SimClock, + readonly callback: () => void, + ) {} + schedule(deltaNanos: number): void { + if (this.scheduled) this.cancel() + this.clock.linkAlarm(deltaNanos, this) + } + cancel(): void { + this.clock.unlinkAlarm(this) + this.scheduled = false + } +} + /** * Parses a UF2 firmware binary and writes its payload blocks to the RP2040 flash memory. * UF2 format: 512-byte blocks with magic numbers, target address, and payload data. @@ -44,11 +145,17 @@ function loadUF2(data: Uint8Array, mcu: RP2040): void { /** * Manages the rp2040js emulator lifecycle in the main process. - * Uses the Simulator class which properly handles clock ticking, CPU wait states, - * and timer-based peripherals (UART, etc.). + * + * Instead of using the stock Simulator class (which uses setTimeout(0) and + * a 1M-iteration batch that runs ~4x slower than real time in Node.js), + * we implement our own execution loop with setImmediate and a larger batch + * size to keep up with wall-clock time. */ export class SimulatorModule { - private simulator: Simulator | null = null + private mcu: RP2040 | null = null + private clock: SimClock | null = null + private running = false + private immediateHandle: ReturnType | null = null /** Callback fired for each byte transmitted by the emulated UART0 */ onUartByte: ((byte: number) => void) | null = null @@ -62,38 +169,71 @@ export class SimulatorModule { const uf2Data = await readFile(uf2Path) - this.simulator = new Simulator() - const mcu = this.simulator.rp2040 - mcu.loadBootrom(bootromB1) - loadUF2(new Uint8Array(uf2Data), mcu) + this.clock = new SimClock() + this.mcu = new RP2040(this.clock) + this.mcu.loadBootrom(bootromB1) + loadUF2(new Uint8Array(uf2Data), this.mcu) // Wire UART0 output to the Modbus RTU bridge callback - mcu.uart[0].onByte = (byte: number) => { + this.mcu.uart[0].onByte = (byte: number) => { this.onUartByte?.(byte) } // Set program counter to flash start and begin execution - mcu.core.PC = FLASH_START_ADDRESS - this.simulator.execute() + this.mcu.core.PC = FLASH_START_ADDRESS + this.running = true + this.executeBatch() + } + + /** + * Runs a batch of CPU instructions, then reschedules with setImmediate. + * setImmediate fires at the start of the next event-loop iteration, + * avoiding the ~1-4 ms minimum delay of setTimeout(0). + */ + private executeBatch = (): void => { + if (!this.running || !this.mcu || !this.clock) return + + this.immediateHandle = null + const { mcu, clock } = this + + for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { + if (mcu.core.waiting) { + const { nanosToNextAlarm } = clock + clock.tick(nanosToNextAlarm) + i += nanosToNextAlarm / CYCLE_NANOS + } else { + const cycles = mcu.core.executeInstruction() + clock.tick(cycles * CYCLE_NANOS) + } + } + + if (this.running) { + this.immediateHandle = setImmediate(this.executeBatch) + } } /** Send a byte to the emulated UART0 RX (host → device) */ feedByte(byte: number): void { - this.simulator?.rp2040.uart[0].feedByte(byte) + this.mcu?.uart[0].feedByte(byte) } /** Stop the emulator and release resources */ stop(): void { - if (this.simulator) { - this.simulator.stop() - this.simulator.rp2040.uart[0].onByte = undefined - this.simulator = null + this.running = false + if (this.immediateHandle !== null) { + clearImmediate(this.immediateHandle) + this.immediateHandle = null + } + if (this.mcu) { + this.mcu.uart[0].onByte = undefined + this.mcu = null } + this.clock = null this.onUartByte = null } /** Check if the emulator is currently running */ isRunning(): boolean { - return this.simulator !== null && this.simulator.executing + return this.running } } From cb81bcb9f850bfaab4a9f905a16ba9d2545b0f0a Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 08:56:43 -0500 Subject: [PATCH 16/25] perf: dynamically calibrate simulator clock to match wall time The OpenPLC firmware busy-waits in loop() checking micros(), so the CPU never enters WFI. A JavaScript ARM emulator can only execute ~30-40M instructions/sec, but real-time 125 MHz needs 125M/sec, causing the simulation to run ~4x slower than real time. Fix by dynamically measuring the sim-to-wall speed ratio every 10 batches and scaling the nanoseconds-per-cycle accordingly. If the simulation is running at 0.25x real time, cycleNanos is multiplied by 4 so timers and millis() advance at wall-clock rate. Clamped to 1-8x nominal to avoid instability. Also reverts batch size to stock 1M to keep event loop responsive for Modbus I/O. Co-Authored-By: Claude Opus 4.6 --- .../modules/simulator/simulator-module.ts | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index f25cd37bb..624131e12 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -12,14 +12,15 @@ const UF2_MAGIC_START1 = 0x9e5d5157 const UF2_MAGIC_END = 0x0ab16f30 const UF2_BLOCK_SIZE = 512 -// Execution loop tuning: nanoseconds per CPU cycle at 125 MHz -const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns +// Nominal nanoseconds per CPU cycle at 125 MHz +const NOMINAL_CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns -// Iterations per batch. The stock Simulator uses 1M which yields ~8 ms of -// simulated time per batch. With setTimeout(0) scheduling overhead (~1-4 ms -// in Node.js), the simulation runs noticeably slower than real time. -// Using setImmediate + a larger batch keeps up with real time. -const ITERATIONS_PER_BATCH = 4_000_000 +// Iterations per execution batch. Kept at the stock 1M so that each batch +// yields to the event loop frequently enough for responsive Modbus I/O. +const ITERATIONS_PER_BATCH = 1_000_000 + +// How often (in batches) to recalibrate the speed ratio +const CALIBRATION_INTERVAL = 10 /** * Minimal simulation clock that satisfies the RP2040 IClock interface and @@ -146,10 +147,11 @@ function loadUF2(data: Uint8Array, mcu: RP2040): void { /** * Manages the rp2040js emulator lifecycle in the main process. * - * Instead of using the stock Simulator class (which uses setTimeout(0) and - * a 1M-iteration batch that runs ~4x slower than real time in Node.js), - * we implement our own execution loop with setImmediate and a larger batch - * size to keep up with wall-clock time. + * The OpenPLC firmware busy-waits in loop() checking micros(), so the CPU + * never enters WFI. A JavaScript ARM emulator can only execute ~30-40M + * instructions/sec, but real-time needs 125M/sec. To compensate, we + * dynamically measure the sim-to-wall speed ratio and scale the clock tick + * per instruction so that timers fire at real wall-clock time. */ export class SimulatorModule { private mcu: RP2040 | null = null @@ -157,6 +159,12 @@ export class SimulatorModule { private running = false private immediateHandle: ReturnType | null = null + // Speed compensation: scale cycleNanos so simulated time matches wall time + private cycleNanos = NOMINAL_CYCLE_NANOS + private wallStartMs = 0 + private simStartNanos = 0 + private batchCount = 0 + /** Callback fired for each byte transmitted by the emulated UART0 */ onUartByte: ((byte: number) => void) | null = null @@ -182,28 +190,53 @@ export class SimulatorModule { // Set program counter to flash start and begin execution this.mcu.core.PC = FLASH_START_ADDRESS this.running = true + this.cycleNanos = NOMINAL_CYCLE_NANOS + this.wallStartMs = performance.now() + this.simStartNanos = 0 + this.batchCount = 0 this.executeBatch() } /** * Runs a batch of CPU instructions, then reschedules with setImmediate. - * setImmediate fires at the start of the next event-loop iteration, - * avoiding the ~1-4 ms minimum delay of setTimeout(0). + * + * Every CALIBRATION_INTERVAL batches, we measure how much simulated time + * has elapsed vs wall time and adjust cycleNanos so the simulation keeps + * pace with real time. This compensates for the host CPU being slower + * than 125 MHz worth of emulated instructions. */ private executeBatch = (): void => { if (!this.running || !this.mcu || !this.clock) return this.immediateHandle = null const { mcu, clock } = this + const cn = this.cycleNanos for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { if (mcu.core.waiting) { const { nanosToNextAlarm } = clock clock.tick(nanosToNextAlarm) - i += nanosToNextAlarm / CYCLE_NANOS + i += nanosToNextAlarm / cn } else { const cycles = mcu.core.executeInstruction() - clock.tick(cycles * CYCLE_NANOS) + clock.tick(cycles * cn) + } + } + + // Periodically recalibrate so simulated time tracks wall time + this.batchCount++ + if (this.batchCount % CALIBRATION_INTERVAL === 0) { + const wallElapsedMs = performance.now() - this.wallStartMs + const simElapsedMs = clock.nanos / 1e6 + + if (wallElapsedMs > 100 && simElapsedMs > 0) { + const ratio = simElapsedMs / wallElapsedMs + // Scale cycleNanos inversely to the ratio: if sim runs at 0.25x, + // we need 4x the nanos per cycle to catch up. + // Clamp to reasonable bounds (1x-8x nominal) to avoid instability. + const correction = 1 / ratio + const clamped = Math.max(1, Math.min(8, correction)) + this.cycleNanos = NOMINAL_CYCLE_NANOS * clamped } } From 10de6c90ac4b04ee7ce6b752a53a964c34f2ac66 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 09:08:52 -0500 Subject: [PATCH 17/25] perf: add WFI to firmware loop for simulator real-time execution The OpenPLC firmware busy-waits in loop() checking micros(), executing millions of useless instructions per scan cycle. A JS ARM emulator can only do ~30-40M instructions/sec vs the 125M/sec needed, causing ~4x slower than real-time execution. Fix by adding __asm volatile("wfi") at the end of loop() when SIMULATOR_MODE is defined. WFI puts the CPU to sleep until the next interrupt (SysTick at 1ms, UART RX, etc.). The emulator's execution loop already handles WFI by fast-forwarding the clock to the next alarm, skipping idle time instantly instead of stepping through every cycle. This makes the simulation run at near real-time speed. Changes: - defines.h generation adds #define SIMULATOR_MODE for simulator builds - Baremetal.ino: WFI inserted at end of loop() under #ifdef - simulator-module.ts: reverted dynamic calibration hack (no longer needed), kept setImmediate and custom SimClock Co-Authored-By: Claude Opus 4.6 --- resources/sources/Baremetal/Baremetal.ino | 8 +++ src/main/modules/compiler/compiler-module.ts | 1 + .../modules/simulator/simulator-module.ts | 61 +++++-------------- 3 files changed, 24 insertions(+), 46 deletions(-) diff --git a/resources/sources/Baremetal/Baremetal.ino b/resources/sources/Baremetal/Baremetal.ino index 61cf6273a..dce6cf64f 100644 --- a/resources/sources/Baremetal/Baremetal.ino +++ b/resources/sources/Baremetal/Baremetal.ino @@ -339,4 +339,12 @@ void loop() modbusTask(); } #endif + + #ifdef SIMULATOR_MODE + // In the emulated RP2040, busy-waiting wastes host CPU executing millions + // of useless instructions. WFI sleeps the CPU until the next interrupt + // (SysTick at 1ms, UART RX, etc.), allowing the emulator to fast-forward + // the clock instead of stepping through every cycle. + __asm volatile("wfi"); + #endif } diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 2a7f582e8..5ba799006 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -758,6 +758,7 @@ class CompilerModule { if (boardRuntime === 'simulator') { // Simulator forces fixed Modbus RTU settings over emulated UART0. // On RP2040, Serial = USB CDC, Serial1 = UART0. rp2040js bridges uart[0]. + DEFINES_CONTENT += '#define SIMULATOR_MODE\n' DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial1\n' DEFINES_CONTENT += '#define MBSERIAL_BAUD 115200\n' DEFINES_CONTENT += '#define MBSERIAL_SLAVE 1\n' diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 624131e12..50c84823c 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -12,16 +12,13 @@ const UF2_MAGIC_START1 = 0x9e5d5157 const UF2_MAGIC_END = 0x0ab16f30 const UF2_BLOCK_SIZE = 512 -// Nominal nanoseconds per CPU cycle at 125 MHz -const NOMINAL_CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns +// Nanoseconds per CPU cycle at 125 MHz +const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns -// Iterations per execution batch. Kept at the stock 1M so that each batch -// yields to the event loop frequently enough for responsive Modbus I/O. +// Iterations per execution batch. Each batch yields to the event loop via +// setImmediate so that Modbus UART I/O can be processed between batches. const ITERATIONS_PER_BATCH = 1_000_000 -// How often (in batches) to recalibrate the speed ratio -const CALIBRATION_INTERVAL = 10 - /** * Minimal simulation clock that satisfies the RP2040 IClock interface and * exposes `tick()` / `nanosToNextAlarm` for the execution loop. @@ -147,11 +144,11 @@ function loadUF2(data: Uint8Array, mcu: RP2040): void { /** * Manages the rp2040js emulator lifecycle in the main process. * - * The OpenPLC firmware busy-waits in loop() checking micros(), so the CPU - * never enters WFI. A JavaScript ARM emulator can only execute ~30-40M - * instructions/sec, but real-time needs 125M/sec. To compensate, we - * dynamically measure the sim-to-wall speed ratio and scale the clock tick - * per instruction so that timers fire at real wall-clock time. + * The firmware is compiled with SIMULATOR_MODE which inserts WFI at the end + * of each loop() iteration. When the CPU hits WFI, the execution loop + * fast-forwards the clock to the next alarm (typically SysTick at 1ms), + * avoiding millions of wasted busy-wait instruction cycles and allowing + * the simulation to run at near real-time speed. */ export class SimulatorModule { private mcu: RP2040 | null = null @@ -159,12 +156,6 @@ export class SimulatorModule { private running = false private immediateHandle: ReturnType | null = null - // Speed compensation: scale cycleNanos so simulated time matches wall time - private cycleNanos = NOMINAL_CYCLE_NANOS - private wallStartMs = 0 - private simStartNanos = 0 - private batchCount = 0 - /** Callback fired for each byte transmitted by the emulated UART0 */ onUartByte: ((byte: number) => void) | null = null @@ -190,53 +181,31 @@ export class SimulatorModule { // Set program counter to flash start and begin execution this.mcu.core.PC = FLASH_START_ADDRESS this.running = true - this.cycleNanos = NOMINAL_CYCLE_NANOS - this.wallStartMs = performance.now() - this.simStartNanos = 0 - this.batchCount = 0 this.executeBatch() } /** * Runs a batch of CPU instructions, then reschedules with setImmediate. + * setImmediate fires at the start of the next event-loop iteration, + * avoiding the ~1-4 ms minimum delay of setTimeout(0). * - * Every CALIBRATION_INTERVAL batches, we measure how much simulated time - * has elapsed vs wall time and adjust cycleNanos so the simulation keeps - * pace with real time. This compensates for the host CPU being slower - * than 125 MHz worth of emulated instructions. + * When the CPU enters WFI (waiting), the loop fast-forwards the clock + * to the next alarm instead of stepping through idle cycles. */ private executeBatch = (): void => { if (!this.running || !this.mcu || !this.clock) return this.immediateHandle = null const { mcu, clock } = this - const cn = this.cycleNanos for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { if (mcu.core.waiting) { const { nanosToNextAlarm } = clock clock.tick(nanosToNextAlarm) - i += nanosToNextAlarm / cn + i += nanosToNextAlarm / CYCLE_NANOS } else { const cycles = mcu.core.executeInstruction() - clock.tick(cycles * cn) - } - } - - // Periodically recalibrate so simulated time tracks wall time - this.batchCount++ - if (this.batchCount % CALIBRATION_INTERVAL === 0) { - const wallElapsedMs = performance.now() - this.wallStartMs - const simElapsedMs = clock.nanos / 1e6 - - if (wallElapsedMs > 100 && simElapsedMs > 0) { - const ratio = simElapsedMs / wallElapsedMs - // Scale cycleNanos inversely to the ratio: if sim runs at 0.25x, - // we need 4x the nanos per cycle to catch up. - // Clamp to reasonable bounds (1x-8x nominal) to avoid instability. - const correction = 1 / ratio - const clamped = Math.max(1, Math.min(8, correction)) - this.cycleNanos = NOMINAL_CYCLE_NANOS * clamped + clock.tick(cycles * CYCLE_NANOS) } } From 95ada4073bd417f6be655814eba6209ac3b09b76 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 09:12:03 -0500 Subject: [PATCH 18/25] perf: pace simulator execution to wall-clock time With WFI in the firmware loop, the emulator fast-forwards idle time instantly, causing the simulation to run ~2.5x faster than real time. Fix by comparing simulated elapsed time to wall elapsed time after each batch. When the simulation is ahead, schedule the next batch with setTimeout(delay) to let wall time catch up. When behind or on time, schedule with setTimeout(0). Co-Authored-By: Claude Opus 4.6 --- .../modules/simulator/simulator-module.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 50c84823c..07718f417 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -154,7 +154,11 @@ export class SimulatorModule { private mcu: RP2040 | null = null private clock: SimClock | null = null private running = false - private immediateHandle: ReturnType | null = null + private timerHandle: ReturnType | null = null + + // Wall-clock pacing: track start times to keep sim time ≈ wall time + private wallStartMs = 0 + private simStartNanos = 0 /** Callback fired for each byte transmitted by the emulated UART0 */ onUartByte: ((byte: number) => void) | null = null @@ -181,21 +185,26 @@ export class SimulatorModule { // Set program counter to flash start and begin execution this.mcu.core.PC = FLASH_START_ADDRESS this.running = true + this.wallStartMs = performance.now() + this.simStartNanos = 0 this.executeBatch() } /** - * Runs a batch of CPU instructions, then reschedules with setImmediate. - * setImmediate fires at the start of the next event-loop iteration, - * avoiding the ~1-4 ms minimum delay of setTimeout(0). + * Runs a batch of CPU instructions, then reschedules. * * When the CPU enters WFI (waiting), the loop fast-forwards the clock * to the next alarm instead of stepping through idle cycles. + * + * After each batch, compares simulated time against wall time: + * - If sim is ahead: schedules next batch with setTimeout(delay) to + * let wall time catch up, keeping timers accurate. + * - If sim is behind or on time: schedules with setTimeout(0). */ private executeBatch = (): void => { if (!this.running || !this.mcu || !this.clock) return - this.immediateHandle = null + this.timerHandle = null const { mcu, clock } = this for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { @@ -210,7 +219,11 @@ export class SimulatorModule { } if (this.running) { - this.immediateHandle = setImmediate(this.executeBatch) + // Pace simulation to wall time + const simElapsedMs = (clock.nanos - this.simStartNanos) / 1e6 + const wallElapsedMs = performance.now() - this.wallStartMs + const aheadMs = simElapsedMs - wallElapsedMs + this.timerHandle = setTimeout(this.executeBatch, aheadMs > 1 ? Math.floor(aheadMs) : 0) } } @@ -222,9 +235,9 @@ export class SimulatorModule { /** Stop the emulator and release resources */ stop(): void { this.running = false - if (this.immediateHandle !== null) { - clearImmediate(this.immediateHandle) - this.immediateHandle = null + if (this.timerHandle !== null) { + clearTimeout(this.timerHandle) + this.timerHandle = null } if (this.mcu) { this.mcu.uart[0].onByte = undefined From ea0f2d1e4065ade02668f3718048645935ec2268 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 09:17:53 -0500 Subject: [PATCH 19/25] fix: stop simulator on project open/create, window reload, and app quit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simulator had no lifecycle cleanup — it would keep running in the background when opening a new project, creating a project, reloading the window, or quitting the app. This wasted CPU resources and could lead to stale simulator state. Add simulatorModule.stop() calls to: - handleProjectOpen / handleProjectOpenByPath / handleProjectCreate - handleWindowReload - handleAppQuit Note: loadAndRun() already calls stop() before starting, so re-building the same project is already safe. The single SimulatorModule instance prevents dual-simulator scenarios. Co-Authored-By: Claude Opus 4.6 --- src/main/modules/ipc/main.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index 7b65ae242..fb9a3407c 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -614,10 +614,12 @@ class MainProcessBridge implements MainIpcModule { // ===================== HANDLER METHODS ===================== // Project-related handlers handleProjectCreate = async (_event: IpcMainInvokeEvent, data: CreateProjectFileProps) => { + this.simulatorModule.stop() const response = await this.projectService.createProject(data) return response } handleProjectOpen = async () => { + this.simulatorModule.stop() const response = await this.projectService.openProject() if (response.success && response.data?.meta.path) { this.currentProjectPath = response.data.meta.path @@ -657,6 +659,7 @@ class MainProcessBridge implements MainIpcModule { handleProjectSave = (_event: IpcMainInvokeEvent, { projectPath, content }: IDataToWrite) => this.projectService.saveProject({ projectPath, content }) handleProjectOpenByPath = async (_event: IpcMainInvokeEvent, projectPath: string) => { + this.simulatorModule.stop() try { const response = await this.projectService.openProjectByPath(projectPath) if (response.success && response.data?.meta.path) { @@ -768,6 +771,7 @@ class MainProcessBridge implements MainIpcModule { } } handleAppQuit = () => { + this.simulatorModule.stop() if (this.mainWindow) { this.mainWindow.destroy() } @@ -832,7 +836,10 @@ class MainProcessBridge implements MainIpcModule { this.mainWindow?.maximize() } } - handleWindowReload = () => this.mainWindow?.webContents.reload() + handleWindowReload = () => { + this.simulatorModule.stop() + this.mainWindow?.webContents.reload() + } handleWindowRebuildMenu = () => void this.menuBuilder.buildMenu() // Hardware handlers From eafd669107bc8ee6b0375d333baa195c19f67f6b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 19:24:49 -0500 Subject: [PATCH 20/25] feat: migrate simulator from RP2040 to AVR ATmega2560 Replace the rp2040js emulation core with avr8js targeting ATmega2560 (Arduino Mega). The new emulator loads Intel HEX firmware, emulates Timer0/1/2 + USART0 + AVRClock peripherals, and uses SLEEP-based fast-forwarding for near real-time execution speed. Key changes: - New simulator-module.ts using avr8js CPU with ATmega2560 configs (256KB flash, 8KB SRAM, correct interrupt vector addresses) - Intel HEX parser supporting extended segment/linear address records - UDR read-hook byte drain to prevent RX data loss during ISRs - Compiler targets arduino:avr:mega with SIMULATOR_MODE define - Board config (hals.json) updated for Arduino Mega pin mappings - Baremetal.ino uses AVR SLEEP instruction instead of ARM WFI - Old RP2040 simulator preserved as simulator-module-rp2040.ts Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 10 + package.json | 1 + resources/sources/Baremetal/Baremetal.ino | 10 +- resources/sources/boards/hals.json | 27 +- src/main/modules/compiler/compiler-module.ts | 19 +- src/main/modules/ipc/main.ts | 6 +- src/main/modules/ipc/renderer.ts | 4 +- .../simulator/simulator-module-rp2040.ts | 254 ++++++++++++ .../modules/simulator/simulator-module.ts | 369 ++++++++++-------- .../modules/simulator/virtual-serial-port.ts | 2 +- 10 files changed, 503 insertions(+), 199 deletions(-) create mode 100644 src/main/modules/simulator/simulator-module-rp2040.ts diff --git a/package-lock.json b/package-lock.json index 6e8b07d25..007d1dea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@tanstack/react-table": "^8.10.7", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", + "avr8js": "^0.20.0", "clsx": "^2.0.0", "cva": "npm:class-variance-authority@^0.7.0", "dompurify": "^3.2.4", @@ -11550,6 +11551,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avr8js": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/avr8js/-/avr8js-0.20.0.tgz", + "integrity": "sha512-FXScGqctUpVr0mxFAceWSyKRrPbXftu+RfKCwu4Ie2bNel+1KdUbMF6TdCjwBXFBMxjDueDq7k/hzw2hLGtTWg==", + "engines": { + "node": ">= 8.0.0", + "npm": ">= 5.0.0" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", diff --git a/package.json b/package.json index b9ae690bc..f93d1f5f6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@tanstack/react-table": "^8.10.7", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", + "avr8js": "^0.20.0", "clsx": "^2.0.0", "cva": "npm:class-variance-authority@^0.7.0", "dompurify": "^3.2.4", diff --git a/resources/sources/Baremetal/Baremetal.ino b/resources/sources/Baremetal/Baremetal.ino index dce6cf64f..a55b64469 100644 --- a/resources/sources/Baremetal/Baremetal.ino +++ b/resources/sources/Baremetal/Baremetal.ino @@ -341,10 +341,10 @@ void loop() #endif #ifdef SIMULATOR_MODE - // In the emulated RP2040, busy-waiting wastes host CPU executing millions - // of useless instructions. WFI sleeps the CPU until the next interrupt - // (SysTick at 1ms, UART RX, etc.), allowing the emulator to fast-forward - // the clock instead of stepping through every cycle. - __asm volatile("wfi"); + // In the emulated ATmega2560, busy-waiting wastes host CPU executing + // millions of useless instructions. SLEEP (opcode 0x9588) is detected + // by the emulator which fast-forwards the clock to the next timer event + // instead of stepping through every idle cycle. + __asm volatile("sleep"); #endif } diff --git a/resources/sources/boards/hals.json b/resources/sources/boards/hals.json index 837784cb3..d3b2f3291 100644 --- a/resources/sources/boards/hals.json +++ b/resources/sources/boards/hals.json @@ -1,24 +1,23 @@ { "OpenPLC Simulator": { "compiler": "simulator", - "core": "rp2040:rp2040", - "board_manager_url": "https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json", + "core": "arduino:avr", "c_flags": ["-MMD", "-c", "-Wno-incompatible-pointer-types"], - "default_ain": "26, 27, 28", - "default_aout": "4, 5", - "default_din": "6, 7, 8, 9, 10, 11, 12, 13", - "default_dout": "14, 15, 16, 17, 18, 19, 20, 21", + "default_ain": "A0, A1, A2, A3, A4, A5, A6, A7", + "default_aout": "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13", + "default_din": "62, 63, 64, 65, 66, 67, 68, 69, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52", + "default_dout": "14, 15, 16, 17, 18, 19, 20, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53", "extra_libraries": [], - "platform": "rp2040:rp2040:rpipico", - "source": "rp2040pico.cpp", + "platform": "arduino:avr:mega", + "source": "mega_due.cpp", "preview": "simulator.png", "specs": { - "CPU": "Emulated RP2040 ARM Cortex-M0+", - "RAM": "264 KB", - "Flash": "2 MB", - "Digital Pins": "26", - "Analog Pins": "3", - "PWM Pins": "16", + "CPU": "Emulated ATmega2560 at 16MHz", + "RAM": "8 KB", + "Flash": "256 KB", + "Digital Pins": "70", + "Analog Pins": "16", + "PWM Pins": "15", "WiFi": "No", "Bluetooth": "No", "Ethernet": "No" diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 5ba799006..9d295a961 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -756,10 +756,10 @@ class CompilerModule { // 3.2. Device Configuration DEFINES_CONTENT += '//Comms Configuration\n' if (boardRuntime === 'simulator') { - // Simulator forces fixed Modbus RTU settings over emulated UART0. - // On RP2040, Serial = USB CDC, Serial1 = UART0. rp2040js bridges uart[0]. + // Simulator forces fixed Modbus RTU settings over emulated USART0. + // On ATmega2560, Serial = USART0. avr8js bridges usart0. DEFINES_CONTENT += '#define SIMULATOR_MODE\n' - DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial1\n' + DEFINES_CONTENT += '#define MBSERIAL_IFACE Serial\n' DEFINES_CONTENT += '#define MBSERIAL_BAUD 115200\n' DEFINES_CONTENT += '#define MBSERIAL_SLAVE 1\n' } else { @@ -2079,21 +2079,14 @@ class CompilerModule { // Step 13: Upload program to board or load into simulator if (boardRuntime === 'simulator') { - // For simulator targets, send the UF2 firmware path back to the renderer - const uf2Path = join( - compilationPath, - 'examples', - 'Baremetal', - 'build', - 'rp2040.rp2040.rpipico', - 'Baremetal.ino.uf2', - ) + // For simulator targets, send the HEX firmware path back to the renderer + const hexPath = join(compilationPath, 'examples', 'Baremetal', 'build', 'arduino.avr.mega', 'Baremetal.ino.hex') _mainProcessPort.postMessage({ logLevel: 'info', message: 'Compilation successful. Loading firmware into simulator...', }) _mainProcessPort.postMessage({ - simulatorFirmwarePath: uf2Path, + simulatorFirmwarePath: hexPath, closePort: true, }) return diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index fb9a3407c..cba680c4b 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -56,7 +56,7 @@ class MainProcessBridge implements MainIpcModule { private currentProjectPath: string | null = null // File watchers for auto-reload functionality (using watchFile for better macOS compatibility) private fileWatchers: Map = new Map() - // rp2040js emulator instance for the built-in simulator + // avr8js ATmega2560 emulator instance for the built-in simulator private simulatorModule = new SimulatorModule() constructor({ @@ -1356,10 +1356,10 @@ class MainProcessBridge implements MainIpcModule { handleSimulatorLoadFirmware = async ( _event: IpcMainInvokeEvent, - uf2Path: string, + hexPath: string, ): Promise<{ success: boolean; error?: string }> => { try { - await this.simulatorModule.loadAndRun(uf2Path) + await this.simulatorModule.loadAndRun(hexPath) return { success: true } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } diff --git a/src/main/modules/ipc/renderer.ts b/src/main/modules/ipc/renderer.ts index a90520197..547b77eb5 100644 --- a/src/main/modules/ipc/renderer.ts +++ b/src/main/modules/ipc/renderer.ts @@ -361,8 +361,8 @@ const rendererProcessBridge = { }, // ===================== SIMULATOR METHODS ===================== - simulatorLoadFirmware: (uf2Path: string): Promise<{ success: boolean; error?: string }> => - ipcRenderer.invoke('simulator:load-firmware', uf2Path), + simulatorLoadFirmware: (hexPath: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('simulator:load-firmware', hexPath), simulatorStop: (): Promise<{ success: boolean }> => ipcRenderer.invoke('simulator:stop'), simulatorIsRunning: (): Promise => ipcRenderer.invoke('simulator:is-running'), diff --git a/src/main/modules/simulator/simulator-module-rp2040.ts b/src/main/modules/simulator/simulator-module-rp2040.ts new file mode 100644 index 000000000..07718f417 --- /dev/null +++ b/src/main/modules/simulator/simulator-module-rp2040.ts @@ -0,0 +1,254 @@ +import { readFile } from 'fs/promises' +import { RP2040 } from 'rp2040js' + +import { bootromB1 } from './bootrom' + +// RP2040 flash starts at 0x10000000 (268435456 decimal) +const FLASH_START_ADDRESS = 0x10000000 + +// UF2 block constants +const UF2_MAGIC_START0 = 0x0a324655 +const UF2_MAGIC_START1 = 0x9e5d5157 +const UF2_MAGIC_END = 0x0ab16f30 +const UF2_BLOCK_SIZE = 512 + +// Nanoseconds per CPU cycle at 125 MHz +const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns + +// Iterations per execution batch. Each batch yields to the event loop via +// setImmediate so that Modbus UART I/O can be processed between batches. +const ITERATIONS_PER_BATCH = 1_000_000 + +/** + * Minimal simulation clock that satisfies the RP2040 IClock interface and + * exposes `tick()` / `nanosToNextAlarm` for the execution loop. + * Avoids a deep import from rp2040js internals. + */ +class SimClock { + readonly frequency: number + private nanosCounter = 0 + private nextAlarm: SimAlarm | null = null + + constructor(frequency = 125e6) { + this.frequency = frequency + } + + get nanos(): number { + return this.nanosCounter + } + + createAlarm(callback: () => void): SimAlarm { + return new SimAlarm(this, callback) + } + + linkAlarm(nanos: number, alarm: SimAlarm): void { + alarm.nanos = this.nanos + nanos + let item = this.nextAlarm + let last: SimAlarm | null = null + while (item && item.nanos < alarm.nanos) { + last = item + item = item.next + } + if (last) { + last.next = alarm + alarm.next = item + } else { + this.nextAlarm = alarm + alarm.next = item + } + alarm.scheduled = true + } + + unlinkAlarm(alarm: SimAlarm): void { + let item = this.nextAlarm + let last: SimAlarm | null = null + while (item) { + if (item === alarm) { + if (last) { + last.next = item.next + } else { + this.nextAlarm = item.next + } + return + } + last = item + item = item.next + } + } + + tick(deltaNanos: number): void { + const target = this.nanosCounter + deltaNanos + let alarm = this.nextAlarm + while (alarm && alarm.nanos <= target) { + this.nextAlarm = alarm.next + this.nanosCounter = alarm.nanos + alarm.callback() + alarm = this.nextAlarm + } + this.nanosCounter = target + } + + get nanosToNextAlarm(): number { + return this.nextAlarm ? this.nextAlarm.nanos - this.nanos : 0 + } +} + +class SimAlarm { + next: SimAlarm | null = null + nanos = 0 + scheduled = false + constructor( + private readonly clock: SimClock, + readonly callback: () => void, + ) {} + schedule(deltaNanos: number): void { + if (this.scheduled) this.cancel() + this.clock.linkAlarm(deltaNanos, this) + } + cancel(): void { + this.clock.unlinkAlarm(this) + this.scheduled = false + } +} + +/** + * Parses a UF2 firmware binary and writes its payload blocks to the RP2040 flash memory. + * UF2 format: 512-byte blocks with magic numbers, target address, and payload data. + * See https://github.com/microsoft/uf2 for format specification. + */ +function loadUF2(data: Uint8Array, mcu: RP2040): void { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength) + + for (let offset = 0; offset + UF2_BLOCK_SIZE <= data.length; offset += UF2_BLOCK_SIZE) { + const magic0 = view.getUint32(offset + 0, true) + const magic1 = view.getUint32(offset + 4, true) + const magicEnd = view.getUint32(offset + UF2_BLOCK_SIZE - 4, true) + + if (magic0 !== UF2_MAGIC_START0 || magic1 !== UF2_MAGIC_START1 || magicEnd !== UF2_MAGIC_END) { + continue // skip invalid blocks + } + + const targetAddress = view.getUint32(offset + 12, true) + const payloadSize = view.getUint32(offset + 16, true) + + if (payloadSize > 476 || targetAddress < FLASH_START_ADDRESS) { + continue // skip blocks outside flash range + } + + const flashOffset = targetAddress - FLASH_START_ADDRESS + const payload = data.subarray(offset + 32, offset + 32 + payloadSize) + mcu.flash.set(payload, flashOffset) + } +} + +/** + * Manages the rp2040js emulator lifecycle in the main process. + * + * The firmware is compiled with SIMULATOR_MODE which inserts WFI at the end + * of each loop() iteration. When the CPU hits WFI, the execution loop + * fast-forwards the clock to the next alarm (typically SysTick at 1ms), + * avoiding millions of wasted busy-wait instruction cycles and allowing + * the simulation to run at near real-time speed. + */ +export class SimulatorModule { + private mcu: RP2040 | null = null + private clock: SimClock | null = null + private running = false + private timerHandle: ReturnType | null = null + + // Wall-clock pacing: track start times to keep sim time ≈ wall time + private wallStartMs = 0 + private simStartNanos = 0 + + /** Callback fired for each byte transmitted by the emulated UART0 */ + onUartByte: ((byte: number) => void) | null = null + + /** + * Loads a UF2 firmware file and starts the emulated RP2040. + * Stops any currently running emulation first. + */ + async loadAndRun(uf2Path: string): Promise { + this.stop() + + const uf2Data = await readFile(uf2Path) + + this.clock = new SimClock() + this.mcu = new RP2040(this.clock) + this.mcu.loadBootrom(bootromB1) + loadUF2(new Uint8Array(uf2Data), this.mcu) + + // Wire UART0 output to the Modbus RTU bridge callback + this.mcu.uart[0].onByte = (byte: number) => { + this.onUartByte?.(byte) + } + + // Set program counter to flash start and begin execution + this.mcu.core.PC = FLASH_START_ADDRESS + this.running = true + this.wallStartMs = performance.now() + this.simStartNanos = 0 + this.executeBatch() + } + + /** + * Runs a batch of CPU instructions, then reschedules. + * + * When the CPU enters WFI (waiting), the loop fast-forwards the clock + * to the next alarm instead of stepping through idle cycles. + * + * After each batch, compares simulated time against wall time: + * - If sim is ahead: schedules next batch with setTimeout(delay) to + * let wall time catch up, keeping timers accurate. + * - If sim is behind or on time: schedules with setTimeout(0). + */ + private executeBatch = (): void => { + if (!this.running || !this.mcu || !this.clock) return + + this.timerHandle = null + const { mcu, clock } = this + + for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { + if (mcu.core.waiting) { + const { nanosToNextAlarm } = clock + clock.tick(nanosToNextAlarm) + i += nanosToNextAlarm / CYCLE_NANOS + } else { + const cycles = mcu.core.executeInstruction() + clock.tick(cycles * CYCLE_NANOS) + } + } + + if (this.running) { + // Pace simulation to wall time + const simElapsedMs = (clock.nanos - this.simStartNanos) / 1e6 + const wallElapsedMs = performance.now() - this.wallStartMs + const aheadMs = simElapsedMs - wallElapsedMs + this.timerHandle = setTimeout(this.executeBatch, aheadMs > 1 ? Math.floor(aheadMs) : 0) + } + } + + /** Send a byte to the emulated UART0 RX (host → device) */ + feedByte(byte: number): void { + this.mcu?.uart[0].feedByte(byte) + } + + /** Stop the emulator and release resources */ + stop(): void { + this.running = false + if (this.timerHandle !== null) { + clearTimeout(this.timerHandle) + this.timerHandle = null + } + if (this.mcu) { + this.mcu.uart[0].onByte = undefined + this.mcu = null + } + this.clock = null + this.onUartByte = null + } + + /** Check if the emulator is currently running */ + isRunning(): boolean { + return this.running + } +} diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 07718f417..57cc5cc57 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -1,200 +1,213 @@ +import { + AVRClock, + avrInstruction, + AVRTimer, + AVRUSART, + clockConfig, + CPU, + timer0Config, + timer1Config, + timer2Config, + usart0Config, +} from 'avr8js' import { readFile } from 'fs/promises' -import { RP2040 } from 'rp2040js' -import { bootromB1 } from './bootrom' +// ATmega2560 specs +const CPU_FREQ_HZ = 16_000_000 +const FLASH_SIZE_BYTES = 256 * 1024 +const SRAM_BYTES = 8448 // 8192 SRAM + 256 extended I/O (CPU adds 0x100 for low regs/IO) -// RP2040 flash starts at 0x10000000 (268435456 decimal) -const FLASH_START_ADDRESS = 0x10000000 +// SLEEP opcode – the firmware inserts `__asm volatile("sleep")` at the end +// of each loop() iteration. We detect it before execution and fast-forward +// the clock to the next timer event, avoiding millions of idle cycles. +const SLEEP_OPCODE = 0x9588 -// UF2 block constants -const UF2_MAGIC_START0 = 0x0a324655 -const UF2_MAGIC_START1 = 0x9e5d5157 -const UF2_MAGIC_END = 0x0ab16f30 -const UF2_BLOCK_SIZE = 512 - -// Nanoseconds per CPU cycle at 125 MHz -const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns +// Nanoseconds per CPU cycle at 16 MHz +const CYCLE_NS = 1e9 / CPU_FREQ_HZ // 62.5 ns // Iterations per execution batch. Each batch yields to the event loop via -// setImmediate so that Modbus UART I/O can be processed between batches. +// setTimeout so that Modbus UART I/O can be processed between batches. const ITERATIONS_PER_BATCH = 1_000_000 -/** - * Minimal simulation clock that satisfies the RP2040 IClock interface and - * exposes `tick()` / `nanosToNextAlarm` for the execution loop. - * Avoids a deep import from rp2040js internals. - */ -class SimClock { - readonly frequency: number - private nanosCounter = 0 - private nextAlarm: SimAlarm | null = null +// --------------------------------------------------------------------------- +// ATmega2560 peripheral configs – register addresses are identical to the +// ATmega328p defaults exported by avr8js, only the interrupt vector addresses +// differ because ATmega2560 has more interrupt sources. +// Vector addresses are word addresses matching the datasheet. +// --------------------------------------------------------------------------- + +// ATmega2560 vector addresses (word addresses). +// IMPORTANT: ATmega2560 has TIMER1_COMPC at vector 19 (word 0x26) which +// ATmega328p lacks. This shifts Timer1 OVF and all subsequent vectors by 2 +// compared to a naive mapping from the ATmega328p table. +// Vectors verified against avr-objdump of compiled firmware. +const mega2560Timer0Config = { + ...timer0Config, + compAInterrupt: 0x2a, // vector 21 + compBInterrupt: 0x2c, // vector 22 + ovfInterrupt: 0x2e, // vector 23 +} - constructor(frequency = 125e6) { - this.frequency = frequency - } +const mega2560Timer1Config = { + ...timer1Config, + captureInterrupt: 0x20, // vector 16 + compAInterrupt: 0x22, // vector 17 + compBInterrupt: 0x24, // vector 18 + // Note: TIMER1_COMPC at vector 19 (0x26) not modeled by avr8js + ovfInterrupt: 0x28, // vector 20 +} - get nanos(): number { - return this.nanosCounter - } +const mega2560Timer2Config = { + ...timer2Config, + compAInterrupt: 0x1a, // vector 13 + compBInterrupt: 0x1c, // vector 14 + ovfInterrupt: 0x1e, // vector 15 +} - createAlarm(callback: () => void): SimAlarm { - return new SimAlarm(this, callback) - } +const mega2560Usart0Config = { + ...usart0Config, + rxCompleteInterrupt: 0x32, // vector 25 + dataRegisterEmptyInterrupt: 0x34, // vector 26 + txCompleteInterrupt: 0x36, // vector 27 +} - linkAlarm(nanos: number, alarm: SimAlarm): void { - alarm.nanos = this.nanos + nanos - let item = this.nextAlarm - let last: SimAlarm | null = null - while (item && item.nanos < alarm.nanos) { - last = item - item = item.next - } - if (last) { - last.next = alarm - alarm.next = item - } else { - this.nextAlarm = alarm - alarm.next = item - } - alarm.scheduled = true - } +// --------------------------------------------------------------------------- +// Intel HEX parser +// --------------------------------------------------------------------------- - unlinkAlarm(alarm: SimAlarm): void { - let item = this.nextAlarm - let last: SimAlarm | null = null - while (item) { - if (item === alarm) { - if (last) { - last.next = item.next - } else { - this.nextAlarm = item.next +/** + * Parses an Intel HEX string into a Uint16Array suitable for the AVR CPU. + * Supports record types 00 (data), 01 (EOF), 02 (extended segment address), + * and 04 (extended linear address) for flash sizes >64 KB. + */ +function parseIntelHex(hex: string, flashSizeBytes: number): Uint16Array { + const flash = new Uint8Array(flashSizeBytes) + let extendedAddress = 0 + + for (const rawLine of hex.split('\n')) { + const line = rawLine.trim() + if (!line.startsWith(':')) continue + + const byteCount = parseInt(line.substring(1, 3), 16) + const address = parseInt(line.substring(3, 7), 16) + const recordType = parseInt(line.substring(7, 9), 16) + + if (recordType === 0x00) { + // Data record + const fullAddress = extendedAddress + address + for (let i = 0; i < byteCount; i++) { + const byte = parseInt(line.substring(9 + i * 2, 11 + i * 2), 16) + if (fullAddress + i < flashSizeBytes) { + flash[fullAddress + i] = byte } - return } - last = item - item = item.next - } - } - - tick(deltaNanos: number): void { - const target = this.nanosCounter + deltaNanos - let alarm = this.nextAlarm - while (alarm && alarm.nanos <= target) { - this.nextAlarm = alarm.next - this.nanosCounter = alarm.nanos - alarm.callback() - alarm = this.nextAlarm + } else if (recordType === 0x01) { + // End of file + break + } else if (recordType === 0x02) { + // Extended segment address (address << 4) + extendedAddress = parseInt(line.substring(9, 13), 16) << 4 + } else if (recordType === 0x04) { + // Extended linear address (upper 16 bits) + extendedAddress = parseInt(line.substring(9, 13), 16) << 16 } - this.nanosCounter = target } - get nanosToNextAlarm(): number { - return this.nextAlarm ? this.nextAlarm.nanos - this.nanos : 0 + // Convert byte array to 16-bit little-endian words for the AVR CPU + const words = new Uint16Array(flashSizeBytes / 2) + for (let i = 0; i < flashSizeBytes; i += 2) { + words[i / 2] = flash[i] | (flash[i + 1] << 8) } + return words } -class SimAlarm { - next: SimAlarm | null = null - nanos = 0 - scheduled = false - constructor( - private readonly clock: SimClock, - readonly callback: () => void, - ) {} - schedule(deltaNanos: number): void { - if (this.scheduled) this.cancel() - this.clock.linkAlarm(deltaNanos, this) - } - cancel(): void { - this.clock.unlinkAlarm(this) - this.scheduled = false - } -} +// --------------------------------------------------------------------------- +// Simulator module +// --------------------------------------------------------------------------- /** - * Parses a UF2 firmware binary and writes its payload blocks to the RP2040 flash memory. - * UF2 format: 512-byte blocks with magic numbers, target address, and payload data. - * See https://github.com/microsoft/uf2 for format specification. - */ -function loadUF2(data: Uint8Array, mcu: RP2040): void { - const view = new DataView(data.buffer, data.byteOffset, data.byteLength) - - for (let offset = 0; offset + UF2_BLOCK_SIZE <= data.length; offset += UF2_BLOCK_SIZE) { - const magic0 = view.getUint32(offset + 0, true) - const magic1 = view.getUint32(offset + 4, true) - const magicEnd = view.getUint32(offset + UF2_BLOCK_SIZE - 4, true) - - if (magic0 !== UF2_MAGIC_START0 || magic1 !== UF2_MAGIC_START1 || magicEnd !== UF2_MAGIC_END) { - continue // skip invalid blocks - } - - const targetAddress = view.getUint32(offset + 12, true) - const payloadSize = view.getUint32(offset + 16, true) - - if (payloadSize > 476 || targetAddress < FLASH_START_ADDRESS) { - continue // skip blocks outside flash range - } - - const flashOffset = targetAddress - FLASH_START_ADDRESS - const payload = data.subarray(offset + 32, offset + 32 + payloadSize) - mcu.flash.set(payload, flashOffset) - } -} - -/** - * Manages the rp2040js emulator lifecycle in the main process. + * Manages the avr8js ATmega2560 emulator lifecycle in the main process. * - * The firmware is compiled with SIMULATOR_MODE which inserts WFI at the end - * of each loop() iteration. When the CPU hits WFI, the execution loop - * fast-forwards the clock to the next alarm (typically SysTick at 1ms), - * avoiding millions of wasted busy-wait instruction cycles and allowing - * the simulation to run at near real-time speed. + * The firmware is compiled with SIMULATOR_MODE which inserts a SLEEP + * instruction at the end of each loop() iteration. When the CPU hits SLEEP, + * the execution loop fast-forwards the clock to the next timer event + * (typically Timer0 overflow at ~1 ms), avoiding millions of wasted + * busy-wait instruction cycles and allowing the simulation to run at + * near real-time speed. */ export class SimulatorModule { - private mcu: RP2040 | null = null - private clock: SimClock | null = null + private cpu: CPU | null = null private running = false private timerHandle: ReturnType | null = null - // Wall-clock pacing: track start times to keep sim time ≈ wall time + // Peripherals (kept alive so they process register read/write hooks) + private timer0: AVRTimer | null = null + private timer1: AVRTimer | null = null + private timer2: AVRTimer | null = null + private usart0: AVRUSART | null = null + private clock: AVRClock | null = null + + // RX byte queue – avr8js USART accepts one byte at a time (returns false + // while rxBusy). Incoming bytes are queued and drained after the firmware + // reads UDR (via the read hook), ensuring the RXC ISR processes each byte + // before the next one overwrites rxByte. + private rxQueue: number[] = [] + + // Wall-clock pacing private wallStartMs = 0 - private simStartNanos = 0 + private simStartCycles = 0 - /** Callback fired for each byte transmitted by the emulated UART0 */ + /** Callback fired for each byte transmitted by the emulated USART0 */ onUartByte: ((byte: number) => void) | null = null /** - * Loads a UF2 firmware file and starts the emulated RP2040. + * Loads an Intel HEX firmware file and starts the emulated ATmega2560. * Stops any currently running emulation first. */ - async loadAndRun(uf2Path: string): Promise { + async loadAndRun(hexPath: string): Promise { this.stop() - const uf2Data = await readFile(uf2Path) - - this.clock = new SimClock() - this.mcu = new RP2040(this.clock) - this.mcu.loadBootrom(bootromB1) - loadUF2(new Uint8Array(uf2Data), this.mcu) + const hexData = await readFile(hexPath, 'utf-8') + const progMem = parseIntelHex(hexData, FLASH_SIZE_BYTES) + + this.cpu = new CPU(progMem, SRAM_BYTES) + + // Instantiate peripherals – they register read/write hooks on the CPU + this.timer0 = new AVRTimer(this.cpu, mega2560Timer0Config) + this.timer1 = new AVRTimer(this.cpu, mega2560Timer1Config) + this.timer2 = new AVRTimer(this.cpu, mega2560Timer2Config) + this.usart0 = new AVRUSART(this.cpu, mega2560Usart0Config, CPU_FREQ_HZ) + this.clock = new AVRClock(this.cpu, CPU_FREQ_HZ, clockConfig) + + // Wrap the UDR read hook so that after the firmware reads a received byte, + // the next queued byte is fed into the USART. This ensures the RXC ISR + // has consumed the current byte before the next one arrives, preventing + // rxByte from being silently overwritten when interrupts are disabled + // (e.g. while the CPU is inside another ISR like Timer0). + const originalUdrReadHook = this.cpu.readHooks[0xc6] + this.cpu.readHooks[0xc6] = (addr: number) => { + const result = originalUdrReadHook?.(addr) + this.drainRxQueue() + return result + } - // Wire UART0 output to the Modbus RTU bridge callback - this.mcu.uart[0].onByte = (byte: number) => { + // Wire USART0 TX to the Modbus RTU bridge callback + this.usart0.onByteTransmit = (byte: number) => { this.onUartByte?.(byte) } - // Set program counter to flash start and begin execution - this.mcu.core.PC = FLASH_START_ADDRESS + // Begin execution this.running = true this.wallStartMs = performance.now() - this.simStartNanos = 0 + this.simStartCycles = 0 this.executeBatch() } /** * Runs a batch of CPU instructions, then reschedules. * - * When the CPU enters WFI (waiting), the loop fast-forwards the clock - * to the next alarm instead of stepping through idle cycles. + * When the CPU hits a SLEEP opcode, the loop fast-forwards the clock to + * the next scheduled timer event instead of stepping through idle cycles. * * After each batch, compares simulated time against wall time: * - If sim is ahead: schedules next batch with setTimeout(delay) to @@ -202,34 +215,63 @@ export class SimulatorModule { * - If sim is behind or on time: schedules with setTimeout(0). */ private executeBatch = (): void => { - if (!this.running || !this.mcu || !this.clock) return + if (!this.running || !this.cpu) return this.timerHandle = null - const { mcu, clock } = this + const { cpu } = this for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { - if (mcu.core.waiting) { - const { nanosToNextAlarm } = clock - clock.tick(nanosToNextAlarm) - i += nanosToNextAlarm / CYCLE_NANOS + if (cpu.progMem[cpu.pc] === SLEEP_OPCODE) { + // Execute the SLEEP instruction (advances PC, adds 1 cycle) + avrInstruction(cpu) + // Fast-forward to next scheduled clock event + const nextEvent = (cpu as unknown as { nextClockEvent: { cycles: number } | null }).nextClockEvent + if (nextEvent && nextEvent.cycles > cpu.cycles) { + const skipped = nextEvent.cycles - cpu.cycles + cpu.cycles = nextEvent.cycles + // Account for fast-forwarded cycles in the batch counter + i += skipped + } + cpu.tick() } else { - const cycles = mcu.core.executeInstruction() - clock.tick(cycles * CYCLE_NANOS) + avrInstruction(cpu) + cpu.tick() } } if (this.running) { // Pace simulation to wall time - const simElapsedMs = (clock.nanos - this.simStartNanos) / 1e6 + const simElapsedMs = ((cpu.cycles - this.simStartCycles) * CYCLE_NS) / 1e6 const wallElapsedMs = performance.now() - this.wallStartMs const aheadMs = simElapsedMs - wallElapsedMs this.timerHandle = setTimeout(this.executeBatch, aheadMs > 1 ? Math.floor(aheadMs) : 0) } } - /** Send a byte to the emulated UART0 RX (host → device) */ + /** Send a byte to the emulated USART0 RX (host → device) */ feedByte(byte: number): void { - this.mcu?.uart[0].feedByte(byte) + this.rxQueue.push(byte) + if (this.rxQueue.length === 1 && this.usart0) { + const accepted = this.usart0.writeByte(byte) + if (accepted) { + this.rxQueue.shift() + } + } + } + + /** + * Tries to deliver the next queued byte to the USART. Called after the + * firmware reads UDR (via the read hook). This pacing ensures the RXC ISR + * processes each byte before the next one arrives, avoiding data loss + * from rxByte overwrites. + */ + private drainRxQueue(): void { + if (!this.usart0 || this.rxQueue.length === 0) return + const byte = this.rxQueue[0] + const accepted = this.usart0.writeByte(byte) + if (accepted) { + this.rxQueue.shift() + } } /** Stop the emulator and release resources */ @@ -239,10 +281,15 @@ export class SimulatorModule { clearTimeout(this.timerHandle) this.timerHandle = null } - if (this.mcu) { - this.mcu.uart[0].onByte = undefined - this.mcu = null + if (this.usart0) { + this.usart0.onByteTransmit = null + this.usart0 = null } + this.rxQueue = [] + this.cpu = null + this.timer0 = null + this.timer1 = null + this.timer2 = null this.clock = null this.onUartByte = null } diff --git a/src/main/modules/simulator/virtual-serial-port.ts b/src/main/modules/simulator/virtual-serial-port.ts index f5ab768e5..d620e1a94 100644 --- a/src/main/modules/simulator/virtual-serial-port.ts +++ b/src/main/modules/simulator/virtual-serial-port.ts @@ -5,7 +5,7 @@ import { SimulatorModule } from './simulator-module' /** * A virtual serial port that mimics the `serialport` npm package's event-based API. * Routes bytes through SimulatorModule's UART bridge, allowing the existing - * ModbusRtuClient to communicate with the rp2040js emulator unchanged. + * ModbusRtuClient to communicate with the avr8js emulator unchanged. */ export class VirtualSerialPort extends EventEmitter { public isOpen = false From 4dd6da2ba5852d2f2767944b9b591c1c119ae5d4 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 19 Feb 2026 19:53:53 -0500 Subject: [PATCH 21/25] perf: decouple SLEEP fast-forward from batch instruction budget SLEEP fast-forwards no longer count against the per-batch iteration limit. Instead, use two separate limits: a real-instruction cap (100K) for actual CPU work and a sim-time cap (100ms) to prevent runaway batches during idle periods. This makes idle time between scan cycles essentially free, allowing each batch to cover more simulated time with the same wall-clock cost. Co-Authored-By: Claude Opus 4.6 --- .../modules/simulator/simulator-module.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 57cc5cc57..aedfa1bf5 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -25,9 +25,14 @@ const SLEEP_OPCODE = 0x9588 // Nanoseconds per CPU cycle at 16 MHz const CYCLE_NS = 1e9 / CPU_FREQ_HZ // 62.5 ns -// Iterations per execution batch. Each batch yields to the event loop via -// setTimeout so that Modbus UART I/O can be processed between batches. -const ITERATIONS_PER_BATCH = 1_000_000 +// Maximum real (non-skipped) instructions per batch. SLEEP fast-forwards +// don't count against this budget, so idle periods are essentially free. +const MAX_REAL_INSTRUCTIONS = 100_000 + +// Maximum simulated time per batch (in CPU cycles). Prevents runaway +// batches when the firmware is mostly idle (SLEEP fast-forwards could +// cover seconds of sim time without hitting the instruction limit). +const MAX_SIM_CYCLES_PER_BATCH = CPU_FREQ_HZ / 10 // 100ms // --------------------------------------------------------------------------- // ATmega2560 peripheral configs – register addresses are identical to the @@ -208,6 +213,8 @@ export class SimulatorModule { * * When the CPU hits a SLEEP opcode, the loop fast-forwards the clock to * the next scheduled timer event instead of stepping through idle cycles. + * SLEEP fast-forwards don't count against the instruction budget, so idle + * periods between scan cycles are essentially free. * * After each batch, compares simulated time against wall time: * - If sim is ahead: schedules next batch with setTimeout(delay) to @@ -219,23 +226,23 @@ export class SimulatorModule { this.timerHandle = null const { cpu } = this + const simCycleCap = cpu.cycles + MAX_SIM_CYCLES_PER_BATCH + let realCount = 0 - for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { + while (this.running && realCount < MAX_REAL_INSTRUCTIONS && cpu.cycles < simCycleCap) { if (cpu.progMem[cpu.pc] === SLEEP_OPCODE) { // Execute the SLEEP instruction (advances PC, adds 1 cycle) avrInstruction(cpu) // Fast-forward to next scheduled clock event const nextEvent = (cpu as unknown as { nextClockEvent: { cycles: number } | null }).nextClockEvent if (nextEvent && nextEvent.cycles > cpu.cycles) { - const skipped = nextEvent.cycles - cpu.cycles cpu.cycles = nextEvent.cycles - // Account for fast-forwarded cycles in the batch counter - i += skipped } cpu.tick() } else { avrInstruction(cpu) cpu.tick() + realCount++ } } From 3ae167b3ff3101480f08763403da542fb387da73 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 24 Feb 2026 10:59:04 -0500 Subject: [PATCH 22/25] Revert "perf: decouple SLEEP fast-forward from batch instruction budget" This reverts commit ef785388c3c38380e5b8c34598ca38733deea615. --- .../modules/simulator/simulator-module.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index aedfa1bf5..57cc5cc57 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -25,14 +25,9 @@ const SLEEP_OPCODE = 0x9588 // Nanoseconds per CPU cycle at 16 MHz const CYCLE_NS = 1e9 / CPU_FREQ_HZ // 62.5 ns -// Maximum real (non-skipped) instructions per batch. SLEEP fast-forwards -// don't count against this budget, so idle periods are essentially free. -const MAX_REAL_INSTRUCTIONS = 100_000 - -// Maximum simulated time per batch (in CPU cycles). Prevents runaway -// batches when the firmware is mostly idle (SLEEP fast-forwards could -// cover seconds of sim time without hitting the instruction limit). -const MAX_SIM_CYCLES_PER_BATCH = CPU_FREQ_HZ / 10 // 100ms +// Iterations per execution batch. Each batch yields to the event loop via +// setTimeout so that Modbus UART I/O can be processed between batches. +const ITERATIONS_PER_BATCH = 1_000_000 // --------------------------------------------------------------------------- // ATmega2560 peripheral configs – register addresses are identical to the @@ -213,8 +208,6 @@ export class SimulatorModule { * * When the CPU hits a SLEEP opcode, the loop fast-forwards the clock to * the next scheduled timer event instead of stepping through idle cycles. - * SLEEP fast-forwards don't count against the instruction budget, so idle - * periods between scan cycles are essentially free. * * After each batch, compares simulated time against wall time: * - If sim is ahead: schedules next batch with setTimeout(delay) to @@ -226,23 +219,23 @@ export class SimulatorModule { this.timerHandle = null const { cpu } = this - const simCycleCap = cpu.cycles + MAX_SIM_CYCLES_PER_BATCH - let realCount = 0 - while (this.running && realCount < MAX_REAL_INSTRUCTIONS && cpu.cycles < simCycleCap) { + for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { if (cpu.progMem[cpu.pc] === SLEEP_OPCODE) { // Execute the SLEEP instruction (advances PC, adds 1 cycle) avrInstruction(cpu) // Fast-forward to next scheduled clock event const nextEvent = (cpu as unknown as { nextClockEvent: { cycles: number } | null }).nextClockEvent if (nextEvent && nextEvent.cycles > cpu.cycles) { + const skipped = nextEvent.cycles - cpu.cycles cpu.cycles = nextEvent.cycles + // Account for fast-forwarded cycles in the batch counter + i += skipped } cpu.tick() } else { avrInstruction(cpu) cpu.tick() - realCount++ } } From 8097eee43b5326249c2860e37e9a56ba67a88c7b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 24 Feb 2026 17:19:35 -0500 Subject: [PATCH 23/25] fix: resolve simulator USART deadlock, expand SRAM to 63KB, remove rp2040js - Fix rxQueue deadlock: feedByte() only attempted USART delivery on empty-to-non-empty transition; if rejected (rxBusy), the queue stalled permanently. Add batch-start retry to break the cycle. - Expand emulated SRAM from 8KB to ~63.5KB (full 16-bit address space) with linker flags (--defsym __DATA_REGION_LENGTH__/--stack) so avr-gcc uses the extra space. - Add ld_flags support to compiler module and board type schemas. - Remove rp2040js dependency and delete dead RP2040 emulator files (bootrom.ts, simulator-module-rp2040.ts). - Fix stale references: RP2040 -> ATmega2560, UF2 -> HEX. - Replace hardcoded UDR address 0xc6 with config reference. - Fix ModbusRtuClient listener leak in sendRequest error paths. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 9 - package.json | 1 - resources/sources/boards/hals.json | 3 +- src/main/modules/compiler/compiler-module.ts | 8 + src/main/modules/compiler/compiler-types.ts | 1 + src/main/modules/hardware/hardware-types.ts | 1 + src/main/modules/modbus/modbus-rtu-client.ts | 36 +- src/main/modules/simulator/bootrom.ts | 461 ------------------ .../simulator/simulator-module-rp2040.ts | 254 ---------- .../modules/simulator/simulator-module.ts | 49 +- .../workspace-activity-bar/default.tsx | 2 +- src/utils/device.ts | 2 +- 12 files changed, 71 insertions(+), 756 deletions(-) delete mode 100644 src/main/modules/simulator/bootrom.ts delete mode 100644 src/main/modules/simulator/simulator-module-rp2040.ts diff --git a/package-lock.json b/package-lock.json index 007d1dea6..a7471c361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "react-i18next": "^13.3.0", "react-icons": "^4.11.0", "react-resizable-panels": "^2.0.3", - "rp2040js": "^1.3.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.1.0", "url": "^0.11.3", @@ -25336,14 +25335,6 @@ "node": ">=8.0" } }, - "node_modules/rp2040js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rp2040js/-/rp2040js-1.3.0.tgz", - "integrity": "sha512-Zv31PenIlQMehmOZTKjCl4DO8/eM5KVQEVZo5/QCx6P3IxIzoAsMwVcUQjS2hDLFXjGHYndgNGadJXfWU3Uqvg==", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", diff --git a/package.json b/package.json index f93d1f5f6..4ba3b5cc3 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "react-i18next": "^13.3.0", "react-icons": "^4.11.0", "react-resizable-panels": "^2.0.3", - "rp2040js": "^1.3.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.1.0", "url": "^0.11.3", diff --git a/resources/sources/boards/hals.json b/resources/sources/boards/hals.json index d3b2f3291..f905da3b8 100644 --- a/resources/sources/boards/hals.json +++ b/resources/sources/boards/hals.json @@ -3,6 +3,7 @@ "compiler": "simulator", "core": "arduino:avr", "c_flags": ["-MMD", "-c", "-Wno-incompatible-pointer-types"], + "ld_flags": ["-Wl,--defsym,__DATA_REGION_LENGTH__=0xFE00", "-Wl,--defsym,__stack=0x80FFFF"], "default_ain": "A0, A1, A2, A3, A4, A5, A6, A7", "default_aout": "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13", "default_din": "62, 63, 64, 65, 66, 67, 68, 69, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52", @@ -13,7 +14,7 @@ "preview": "simulator.png", "specs": { "CPU": "Emulated ATmega2560 at 16MHz", - "RAM": "8 KB", + "RAM": "63.5 KB", "Flash": "256 KB", "Digital Pins": "70", "Analog Pins": "16", diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 9d295a961..20e10d424 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -1015,6 +1015,14 @@ class CompilerModule { ] } + if (boardHalsContent['ld_flags']) { + buildProjectFlags = [ + ...buildProjectFlags, + '--build-property', + `compiler.c.elf.extra_flags=${boardHalsContent['ld_flags'].map((f: string) => f).join(' ')}`, + ] + } + buildProjectFlags = [ ...buildProjectFlags, '--library', diff --git a/src/main/modules/compiler/compiler-types.ts b/src/main/modules/compiler/compiler-types.ts index 6c4fc92b6..2d3bdea8f 100644 --- a/src/main/modules/compiler/compiler-types.ts +++ b/src/main/modules/compiler/compiler-types.ts @@ -35,6 +35,7 @@ const BoardInfoSchema = z.object({ user_dout: z.string().optional(), c_flags: z.array(z.string()).optional(), cxx_flags: z.array(z.string()).optional(), + ld_flags: z.array(z.string()).optional(), arch: z.string().optional(), }) diff --git a/src/main/modules/hardware/hardware-types.ts b/src/main/modules/hardware/hardware-types.ts index b9200af49..6d69ec4c1 100644 --- a/src/main/modules/hardware/hardware-types.ts +++ b/src/main/modules/hardware/hardware-types.ts @@ -13,6 +13,7 @@ const BoardInfoSchema = z.object({ core: z.string(), c_flags: z.array(z.string()).optional(), cxx_flags: z.array(z.string()).optional(), + ld_flags: z.array(z.string()).optional(), default_ain: z.string(), default_aout: z.string(), default_din: z.string(), diff --git a/src/main/modules/modbus/modbus-rtu-client.ts b/src/main/modules/modbus/modbus-rtu-client.ts index 7f4857352..539b9690e 100644 --- a/src/main/modules/modbus/modbus-rtu-client.ts +++ b/src/main/modules/modbus/modbus-rtu-client.ts @@ -176,14 +176,23 @@ export class ModbusRtuClient { await this.flushInputBuffer() return new Promise((resolve, reject) => { + let responseBuffer = Buffer.alloc(0) + let frameCompleteTimeout: NodeJS.Timeout | null = null + + // Forward-declared so the timeout handler can reference them for cleanup + const cleanup = () => { + this.serialPort?.removeListener('data', onData) + this.serialPort?.removeListener('error', onError) + if (frameCompleteTimeout) { + clearTimeout(frameCompleteTimeout) + } + } + const timeoutHandle = setTimeout(() => { + cleanup() reject(new Error('Request timeout')) }, this.timeout) - let responseBuffer = Buffer.alloc(0) - - let frameCompleteTimeout: NodeJS.Timeout | null = null - const onData = (data: Buffer) => { responseBuffer = Buffer.concat([responseBuffer, data] as unknown as Uint8Array[]) @@ -192,18 +201,14 @@ export class ModbusRtuClient { } frameCompleteTimeout = setTimeout(() => { + clearTimeout(timeoutHandle) + cleanup() + if (responseBuffer.length < 5) { - clearTimeout(timeoutHandle) - this.serialPort?.removeListener('data', onData) - this.serialPort?.removeListener('error', onError) reject(new Error('Response too short')) return } - clearTimeout(timeoutHandle) - this.serialPort?.removeListener('data', onData) - this.serialPort?.removeListener('error', onError) - const receivedCrc = responseBuffer.readUInt16BE(responseBuffer.length - 2) const calculatedCrc = this.calculateCrc(responseBuffer.slice(0, responseBuffer.length - 2)) @@ -222,11 +227,7 @@ export class ModbusRtuClient { const onError = (error: Error) => { clearTimeout(timeoutHandle) - if (frameCompleteTimeout) { - clearTimeout(frameCompleteTimeout) - } - this.serialPort?.removeListener('data', onData) - this.serialPort?.removeListener('error', onError) + cleanup() reject(error) } @@ -235,8 +236,7 @@ export class ModbusRtuClient { this.serialPort!.write(request as unknown as Uint8Array, (error: unknown) => { if (error) { clearTimeout(timeoutHandle) - this.serialPort?.removeListener('data', onData) - this.serialPort?.removeListener('error', onError) + cleanup() const errorMessage = typeof error === 'string' ? error diff --git a/src/main/modules/simulator/bootrom.ts b/src/main/modules/simulator/bootrom.ts deleted file mode 100644 index 19fb95167..000000000 --- a/src/main/modules/simulator/bootrom.ts +++ /dev/null @@ -1,461 +0,0 @@ -// RP2040 bootrom binary, built from https://github.com/raspberrypi/pico-bootrom -// revision: B1 (00a4a19114195e20fb817bdfbca1165e157eef37) - -export const bootromB1 = new Uint32Array([ - 0x20041f00, 0x000000ef, 0x00000035, 0x00000031, 0x0201754d, 0x00c8007a, 0x0000001d, 0x88022300, 0xd003429a, - 0x30048843, 0xd1f74291, 0x47701c18, 0xe7fdbf30, 0xf00046f4, 0x489ef805, 0x60012100, 0x46e76041, 0x2100489c, - 0x600143c9, 0x47706041, 0x00a4a191, 0x00001e09, 0x20294328, 0x30323032, 0x73615220, 0x72656270, 0x50207972, - 0x72542069, 0x6e696461, 0x744c2067, 0x33500064, 0x335202d9, 0x334c02fd, 0x33540327, 0x534d035f, 0x345326dd, - 0x434d26d1, 0x34432641, 0x42552629, 0x544425b5, 0x45440185, 0x5657018b, 0x46490137, 0x584524a1, 0x455223f5, - 0x5052237d, 0x434623c5, 0x58432361, 0x43452331, 0x00000045, 0x00505247, 0x00585243, 0x01a84653, 0x02284453, - 0x01a65a46, 0x27585346, 0x2e4c4546, 0x2e545344, 0x3dac4544, 0x48730000, 0x29006801, 0xf7ffd11f, 0x4971ff9d, - 0x680a4b71, 0xd001421a, 0xe793600b, 0x4e704f6f, 0x42b0cf0f, 0x4059d107, 0xd1041840, 0x60383f10, 0x8808f382, - 0xf0024798, 0xbf20f9e1, 0x08896d21, 0x6560d3fb, 0x1c6ebf40, 0x4c614730, 0x21044f65, 0x6da16139, 0x08496d21, - 0xa50bd2fb, 0xf7ff2000, 0x2801ffed, 0xf7ffd1f6, 0x60b8ffe9, 0xffe6f7ff, 0x8808f380, 0xffe2f7ff, 0xf7ffa501, - 0x46c0ffdf, 0x61392100, 0xe75d4780, 0x6d20bf20, 0xd3fb0840, 0x28006da0, 0x4770d0de, 0x43372601, 0xbe0047b8, - 0x3811e7fa, 0xbd007ac0, 0x4042b500, 0xf0002a00, 0xd2f6f802, 0x4670468e, 0x00204700, 0x00002b69, 0x00002b65, - 0x00002c31, 0x00002cfd, 0x00002827, 0x00002827, 0x00002db1, 0x0000284d, 0x0000284f, 0x00002881, 0x00002883, - 0x000028d7, 0x000028d9, 0x000028e7, 0x000028e9, 0x000029bf, 0x00002975, 0x000029dd, 0x00000031, 0x000029e5, - 0x00002a4f, 0x0000280b, 0x00002a73, 0x000028af, 0x000028b1, 0x0000289d, 0x0000289f, 0x00003581, 0x00003583, - 0x0000358b, 0x0000358d, 0x0000363d, 0x00002e61, 0x00002e55, 0x00002fbd, 0x00003119, 0x0000346b, 0x0000346b, - 0x000032dd, 0x00003565, 0x00003567, 0x00003573, 0x00003575, 0x000036c3, 0x000036c5, 0x000036bb, 0x000036bd, - 0x00003831, 0x00003841, 0x00003811, 0x00000031, 0x00003b45, 0x00003be1, 0x0000346f, 0x00003931, 0x000036d1, - 0x000036d3, 0x000036cb, 0x000036cd, 0x000035c1, 0x000035c3, 0x000035db, 0x000035dd, 0x00003663, 0xf380480a, - 0xf0018808, 0x0000ff1b, 0x40004000, 0x400080a0, 0xd0000000, 0x40064008, 0x01000000, 0x4005801c, 0xb007c0d3, - 0xe000ed00, 0x501008b0, 0x08424933, 0x0883400a, 0x4008400b, 0x18c01880, 0x184008c1, 0x4008492f, 0x18400981, - 0x4348492e, 0x47700e80, 0x08514a2d, 0x00434051, 0x4008400b, 0x43180840, 0x40130083, 0x08804010, 0x4a284303, - 0x40100118, 0x091b4013, 0xba004318, 0xa3254770, 0xd10c0c01, 0xd1040a81, 0xd1050901, 0x301a5c18, 0x5c584770, - 0x47703010, 0x30165c58, 0x0a884770, 0x0908d104, 0x5c58d104, 0x4770300a, 0x47705c18, 0x30065c18, 0xa3274770, - 0xd00f0401, 0xd0050188, 0xd0070181, 0x31100f09, 0x47705c58, 0x5c580e89, 0x4770300a, 0x5c180e80, 0x47703004, - 0xd0060181, 0xd0080188, 0x30100f00, 0x30105c18, 0x0e804770, 0x301a5c18, 0x0e894770, 0x30145c58, 0x00004770, - 0x49249249, 0xc71c71c7, 0x04004004, 0xcccccccc, 0xf0f0f0f0, 0x04040506, 0x03030303, 0x02020202, 0x02020202, - 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, - 0x00000000, 0x00000000, 0x00000000, 0x00010006, 0x00010002, 0x00010003, 0x00010002, 0x00010004, 0x00010002, - 0x00010003, 0x00010002, 0x00010005, 0x00010002, 0x00010003, 0x00010002, 0x00010004, 0x00010002, 0x00010003, - 0x00010002, 0x20e10031, 0x1c492169, 0x1dcd1ba9, 0xbe0022fd, 0x68934a02, 0x60933b40, 0x46c04770, 0x50100a7c, - 0x7d934a02, 0x4a027513, 0x47706013, 0x501009ec, 0x50110000, 0x60012300, 0x60426103, 0x60c3784b, 0x47706083, - 0x68834904, 0x741a2201, 0x781b681b, 0x43135acb, 0x00004718, 0x0000043c, 0xb5706803, 0xd0021e19, 0x230f7899, - 0x24004019, 0x4a122500, 0x189a00cb, 0x60556014, 0x4e100004, 0x8b323428, 0x78258282, 0x2d002440, 0x1924d000, - 0x83341914, 0xd00e2900, 0x2d002154, 0x3120d000, 0x30290649, 0x78011852, 0x181b4806, 0xd0032900, 0x2200601a, - 0xbd70605a, 0xe7fb6019, 0x50100080, 0x501009ec, 0x50100000, 0x1c45b530, 0x77e9b2c9, 0x702a3528, 0x000382c3, - 0x7824ac03, 0x701c3328, 0x00492301, 0x1889405a, 0x77d91cc3, 0xbe00bd30, 0xb5f00013, 0x000c0006, 0xb0850015, - 0x60013308, 0x78239303, 0x786318e4, 0xd1fa2b05, 0x79217963, 0x430b021b, 0x2101270f, 0x910078a2, 0x00394017, - 0x09d26828, 0xffd0f7ff, 0x4a05cd08, 0x601c00bf, 0x9b0350bb, 0xd1e442ab, 0xb0050030, 0x46c0bdf0, 0x50100a08, - 0x7fdb1c43, 0x78023029, 0x00db4903, 0x2a001858, 0x4a02d001, 0x47701898, 0x50100084, 0x50100080, 0x0006b570, - 0x00047f83, 0x2b003618, 0x8ac3d11f, 0x00037743, 0x78193329, 0x29001d43, 0x1d83d100, 0x8aa37fdd, 0x1e50002a, - 0x480a4182, 0x181b0192, 0x61a3189b, 0x42992300, 0x0020d106, 0xffd0f7ff, 0x1945006d, 0xb2db882b, 0x23017723, - 0x003077a3, 0x46c0bd70, 0x50100000, 0xb5702200, 0x77da1dc3, 0x00043301, 0x42907fd8, 0x2380d12f, 0x431900db, - 0x33290023, 0x2b00781b, 0x2380d002, 0x4319021b, 0x7fd31ca2, 0x001d2601, 0x41851e68, 0x77d34073, 0x036d0020, - 0xf7ff430d, 0x1d63ffa3, 0xb2ad7fda, 0x188040b2, 0x80052200, 0x77a21d21, 0x3a017fca, 0x68a177ca, 0x688a3428, - 0x608a3a01, 0x2a007822, 0x7fdad002, 0x77de4056, 0x20a0bd70, 0x43010140, 0xe7cd77da, 0x0004b570, 0x2b0068a3, - 0x689bd015, 0xd0122b00, 0x7ff51d26, 0xd00e2d00, 0x7fdb1de3, 0xd10a2b00, 0x33290023, 0x2b00781b, 0x0020d006, - 0xfef0f7ff, 0x42ab7ff3, 0xbd70d1e6, 0x8ae10020, 0xffa2f7ff, 0xb570e7e0, 0x0004000d, 0x00280011, 0xf7ff001a, - 0x2300fed7, 0x330160e3, 0x002060a5, 0xf7ff74ab, 0xbd70ffcf, 0x0003b510, 0x49034a02, 0xf7ff4803, 0xbd10ffe8, - 0x00003f59, 0x50100a24, 0x50100dc0, 0x68836841, 0x428bb510, 0x2300d003, 0xf7ff680a, 0xbd10ffd8, 0xb5100003, - 0x7fdb3333, 0x2b00302c, 0xf7ffd101, 0xbd10ffed, 0xb5700003, 0x781b3329, 0x000d0004, 0x2b000016, 0xf7ffd003, - 0x2300ff37, 0x00337103, 0x00200029, 0xf7ff4a01, 0xbd70ffba, 0x00003f59, 0x2200b510, 0x48034902, 0xffe4f7ff, - 0x46c0bd10, 0x501009c4, 0x50100f68, 0x2200b510, 0x48034902, 0xffd8f7ff, 0x46c0bd10, 0x501009d8, 0x50100f94, - 0x4b03b510, 0x49044a03, 0xf7ff4804, 0xbd10ff98, 0x0000075d, 0x00003f59, 0x501009c4, 0x50100f68, 0x1dc3b510, - 0x68817fda, 0xd1052a00, 0x2b0068cb, 0x7c0bd10a, 0xd1072b00, 0x23002401, 0x608374cc, 0xd002429a, 0x608b60cb, - 0x684bbd10, 0xd0012b00, 0xe7f94798, 0x2b0068c3, 0x6083d0f6, 0x749c60c2, 0xff4cf7ff, 0xbe00e7f0, 0x1dc6b570, - 0x00047ff3, 0x2b00000d, 0x1c43d11a, 0x2b007fdb, 0x0003d107, 0x781b3329, 0x4153425a, 0x33014a0a, 0x00206693, - 0xfebaf7ff, 0x68022380, 0x4313011b, 0x77f56003, 0x2b006923, 0x0020d001, 0xbd704798, 0xd2fc428b, 0xe7fa77f1, - 0x50112000, 0x2800b570, 0x4b18d00e, 0x58c40080, 0x7fcb1d21, 0x77cb3301, 0x2d0068a5, 0x2102d109, 0xf7ff0020, - 0xbd70ffc7, 0x29004c11, 0x4c11d1f0, 0x7c2be7ee, 0xd0022b00, 0x746b2301, 0x1da3e7f3, 0x332377da, 0x2b00781b, - 0x68abd002, 0xd0022b00, 0xf7ff0020, 0x7cebfe01, 0xd1e42b00, 0x002068eb, 0x60eb3b01, 0xff82f7ff, 0x46c0e7dd, - 0x50100a08, 0x50100f68, 0x50100f94, 0xb5702300, 0x00046885, 0xf7ff742b, 0x7cebff73, 0xd11b2b00, 0x36290026, - 0x2b007833, 0x0020d003, 0xf7ff7f21, 0x2300fe97, 0x7c6a77a3, 0xd00e429a, 0x1d22746b, 0x3b017fd3, 0x220177d3, - 0x7fdb1da3, 0x78313401, 0x405a7fe0, 0xffa0f7ff, 0x0020bd70, 0xfebef7ff, 0xbe00e7fa, 0x0004b5f8, 0xfe3ef7ff, - 0x68020025, 0x35284b19, 0xd01d421a, 0x2b00782b, 0x1d23d003, 0x2b017fdb, 0x2301d104, 0x7fd11ca2, 0x77d3404b, - 0x7fda1ce3, 0x40932301, 0x4a114910, 0x4e124811, 0x660b6653, 0x423b6e77, 0x3801d102, 0xd1112800, 0x66536613, - 0xf7ff0020, 0x782bfe17, 0x1e592200, 0x1d21418b, 0x60023301, 0x230177cb, 0x340884a2, 0xbdf877e3, 0xe7e5660b, - 0x04000400, 0x50112000, 0x50113000, 0x000186a0, 0x50110000, 0x4d08b570, 0xf7ff0028, 0x4c07ffb7, 0xf7ff0020, - 0x0028ffb3, 0xf7ff2101, 0x2101ff21, 0xf7ff0020, 0xbd70ff1d, 0x50100f68, 0x50100f94, 0x2500b570, 0x000e6085, - 0xf7ff0004, 0x42aeff9f, 0x1ca2d001, 0x77a577d5, 0x7fd11de2, 0xd0052900, 0x692377d5, 0xd0012b00, 0x47980020, - 0x7d5b4b07, 0xd0092b00, 0x29006861, 0x68a3d006, 0xd1032b00, 0x680a0020, 0xfe5ff7ff, 0x46c0bd70, 0x501009ec, - 0x4b1eb510, 0x601c4c1e, 0x4b1e2480, 0x601c05e4, 0x04e424e0, 0x4b1c601c, 0xd02c2800, 0x43202401, 0x61dc4c1a, - 0x426469dc, 0x621c4044, 0x62986259, 0x008921fa, 0x605a434a, 0x68114a15, 0x42112202, 0x4914d10b, 0x68094814, - 0x2103404a, 0x4913400a, 0x2204600a, 0x42116b81, 0x220cd0fc, 0x4b1062da, 0x32ff32f5, 0x2280601a, 0x05d24b0e, - 0x2201601a, 0x701a4b0d, 0x61d8bd10, 0x46c0e7da, 0x40010008, 0x0001fffc, 0x4005b000, 0x40058000, 0xb007c0d3, - 0x4006c000, 0x40008030, 0x40008000, 0x40009030, 0x4005a02c, 0x4005a000, 0x50100eb4, 0x0004b570, 0x20001845, - 0xd20142ac, 0xd0002800, 0x4b04bd70, 0x681b0020, 0x479868db, 0x015b2380, 0xe7f118e4, 0x50100dbc, 0x061222e0, - 0x22841881, 0x02d20003, 0x42912001, 0x22ebd907, 0x189b0612, 0x20002280, 0x429a01d2, 0x47704140, 0x1d85b530, - 0xb2e20f0c, 0x33370013, 0xd8002c09, 0x70033b07, 0x01093001, 0xd1f342a8, 0xbe00bd30, 0xb5102200, 0x48064b05, - 0x7a1b725a, 0xd8002b7f, 0x4a054804, 0xf7ff4905, 0xbd10fdf5, 0x50100ab8, 0x50100e58, 0x50100e24, 0x00000b5d, - 0x501008b0, 0x6a024b04, 0xb5106cdb, 0xd101429a, 0xffe0f7ff, 0x46c0bd10, 0x50100ac8, 0x4804b510, 0x7fdb1dc3, - 0xd1012b00, 0xfdc2f7ff, 0x46c0bd10, 0x50100e58, 0x48040003, 0xd8032b03, 0x3b014a03, 0x5898009b, 0x46c04770, - 0x00003db7, 0x00003ee8, 0x02004b02, 0x681b6018, 0x46c04770, 0x4001800c, 0x0004b570, 0x000d2002, 0xfff2f7ff, - 0x230422c0, 0x43290621, 0x0e080552, 0x66103b01, 0x2b000209, 0xbd70d1f9, 0x4a044b03, 0x00016018, 0x43996893, - 0x4770d1fb, 0x4000f000, 0x4000c000, 0x3029b5f7, 0x4c7a7803, 0xd00a2b00, 0x60632301, 0x8510f3ef, 0x0020b672, - 0x479868a3, 0x8810f385, 0x4b74bdf7, 0x781b2007, 0xd1092b00, 0x7fde1d63, 0xb25b7fdb, 0xda252b00, 0x07eb68e5, - 0x2005d502, 0xe7e56060, 0xf7ff0028, 0x2800ff55, 0x2004d101, 0x23ebe7f6, 0x061b2780, 0x01ff18eb, 0xd9f642bb, - 0x301c0028, 0xff46f7ff, 0xd0f02800, 0x18eb4b62, 0xd9ec42bb, 0x0028221c, 0xf0014960, 0x4b60fcf3, 0x601a68e2, - 0xd50e06f3, 0x7fd91da3, 0x1e53000a, 0x4b5c419a, 0x3329b2d2, 0x2902701a, 0x2201d103, 0x33074b59, 0x07f377da, - 0x0673d418, 0x230ad41d, 0xd00c421e, 0x7fda1de3, 0x33080023, 0x4b537fd9, 0xd0032900, 0x20067819, 0xd1b94291, - 0x07b3701a, 0x230cd410, 0xd125421e, 0xe7b12000, 0x681b4b48, 0x4798689b, 0xd0e02800, 0x2301e7aa, 0x431368e2, - 0xe7dc4798, 0x05036920, 0x6961d1a1, 0xd19e050b, 0x228025f0, 0x1943062d, 0x42930552, 0x1843d89f, 0x4293195b, - 0x4b3ad89b, 0x691b681b, 0x28004798, 0xe78dd0d7, 0x002868e5, 0xfee4f7ff, 0xd0052800, 0x18e869e3, 0xfedef7ff, - 0xd11e1e07, 0xd40a0733, 0x01922280, 0xd2064295, 0x18eb69e3, 0xd3004293, 0x2701e77d, 0x21f0e011, 0x06092280, - 0x0552186b, 0xd9004293, 0x69e3e773, 0x185b18eb, 0xd9004293, 0xb2efe76d, 0xd0002f00, 0x0733e761, 0x4b20d518, - 0x469c69a1, 0x2f00681b, 0x69e2d023, 0x42ab9501, 0x9301d900, 0x18a8331c, 0xd2004283, 0x9b010018, 0xd2024283, - 0x4b154660, 0x00286003, 0xfc5af001, 0xd5070773, 0x69a068e3, 0xd0102f00, 0x69e20019, 0xfc50f001, 0xd58706b3, - 0x681b4b0d, 0x4798685b, 0x0028e736, 0x4798695b, 0xd0e92800, 0x4a08e730, 0x68120001, 0x69920018, 0x28004790, - 0xe727d0ea, 0x50100d8c, 0x50100eb4, 0xeb00001c, 0x00003ecc, 0x50100dbc, 0x50100fc0, 0x50100a7c, 0x50100a1c, - 0x6a46b5f8, 0x00330004, 0x781b3329, 0x2b000030, 0xf7ffd023, 0x6963fbcd, 0x001f69e2, 0x69a00005, 0x37401e51, - 0x40192240, 0xd9004287, 0x712a1ac2, 0x68286a23, 0xf0011859, 0x792bfc0d, 0x00206962, 0x616318d3, 0x681b6aa3, - 0x4b064798, 0x681a0030, 0x601a4b05, 0xfd2ef7ff, 0xf7ffbdf8, 0x0005fba9, 0x46c0e7eb, 0x50100f64, 0xd0000018, - 0x4c0cb570, 0x6ce26a03, 0x42930005, 0x6843d111, 0x60734e09, 0xd0052b00, 0x6a602102, 0xfcb2f7ff, 0x72732300, - 0x6ba269eb, 0x18d30020, 0xf7ff63a3, 0xbd70ffb1, 0x50100ac8, 0x50100ab8, 0x0004b570, 0x4d0a2601, 0x4b0a7568, - 0x58d000b2, 0xd0022800, 0xf7ff2101, 0x3601fd7f, 0xd1f42e05, 0x41841e60, 0x0028686b, 0x4798b2e1, 0x46c0bd70, - 0x501009ec, 0x50100a08, 0x2000b510, 0xffe0f7ff, 0x4a052300, 0x4a057513, 0x22016013, 0x42524b04, 0x651a659a, - 0x46c0bd10, 0x501009ec, 0x50110000, 0x50113000, 0x4b0e2220, 0x601ab510, 0x68184b0d, 0x061b23d0, 0x62586158, - 0xfa27f7ff, 0x4b0a2240, 0x009b18c3, 0x430a6819, 0x601a2180, 0x438a681a, 0x4b06601a, 0x230518c0, 0x604300c0, - 0x46c0bd10, 0x4000f000, 0x50100f64, 0x10007001, 0x08002800, 0x000ab510, 0xf0012100, 0xbd10fbc7, 0xb5704b11, - 0x4c11781a, 0xd1072a00, 0x49114a10, 0x60116322, 0x61224a10, 0x701a2201, 0x21284d0f, 0xf7ff0028, 0x2370ffe7, - 0x3b66736b, 0x2500752b, 0x00294b0b, 0x611d0020, 0xfd10f7ff, 0x00290020, 0xf7ff302c, 0xbd70fd0b, 0x50100e20, - 0x50100dc0, 0x50100a38, 0x00003dda, 0x0000144d, 0x50100a7c, 0x50100d3c, 0x780b2260, 0x401ab510, 0x2a202400, - 0xb25bd114, 0x42a3784a, 0x2afeda12, 0x884bd10e, 0xd10b42a3, 0x42a388cb, 0x4813d008, 0xfadef7ff, 0x701c6803, - 0x71043401, 0xfbc4f7ff, 0xbd100020, 0xd1fb2aff, 0x2b00884b, 0x88cad1f8, 0x2a00001c, 0x4b0ad1f4, 0x7fd11dda, - 0xd1012903, 0x77d13901, 0x781a3352, 0xd1012a03, 0x701a3a01, 0xff98f7ff, 0xf7ff2401, 0xe7e1fb8f, 0x50100f68, - 0x50100dc0, 0x000db5f7, 0x00102180, 0x00140089, 0xff82f7ff, 0xd1202d00, 0x30c30020, 0x494f220b, 0xf00130ff, - 0x23fffaf7, 0x005b2255, 0x4b4c54e2, 0x54e21892, 0x791a4b4b, 0xd1042a00, 0x6a924a4a, 0x2201601a, 0x0020711a, - 0x30b9681b, 0x30ff9301, 0xa9012204, 0xfadef001, 0x2d01e023, 0x4b41d111, 0x2a00791a, 0x4a40d103, 0x711d6a92, - 0x681b601a, 0x00202240, 0x9301493d, 0xfaccf001, 0x30270020, 0x2281e7e6, 0x00521eab, 0xd20c4293, 0xd9002b80, - 0x2b003b81, 0x3b08d105, 0x33078023, 0x80a38063, 0x200080e3, 0x1f6bbdfe, 0x2b1f3bff, 0x2b00d839, 0x492ed1f7, - 0x312b220b, 0xf0010020, 0x2328faab, 0x002372e3, 0x00202264, 0x4d2a4e29, 0x701a332d, 0x3a594929, 0x862585e6, - 0x872586e6, 0xf0013020, 0x0023fa99, 0x332b2121, 0x23027019, 0x33ef8763, 0x002763e3, 0x22640023, 0x3740334e, - 0x491f737a, 0x805d801e, 0x815d811e, 0x00383a59, 0xfa82f001, 0x21210023, 0x335a2203, 0x801a72f9, 0x65e3233e, - 0x3d25e7bf, 0x076b3dff, 0x08edd1bb, 0x0022d110, 0x4813219f, 0xfa4ff001, 0x4d120020, 0x0029220c, 0xf0013062, - 0x0020fa67, 0x0029220c, 0xe78130c2, 0xd1a62d01, 0x0020223e, 0xe77b490b, 0x00003df8, 0x000001ff, 0x50100db4, - 0x40054000, 0x00003e6c, 0xffff8299, 0x00003925, 0x00003dac, 0x00003db8, 0x00003ffa, 0x50100eb5, 0x00003ef4, - 0x4b85b5f0, 0x68120016, 0x9003b085, 0xd002429a, 0xb0052000, 0x4b81bdf0, 0x429a6872, 0x22fed1f8, 0x4b7f0052, - 0x429a58b2, 0x68b3d1f2, 0xd5ef049a, 0x69f14a7c, 0xd1eb4291, 0x001d2201, 0x42134015, 0x2380d1e6, 0x005b6932, - 0xd1e1429a, 0x003868f7, 0xfc62f7ff, 0xd0042800, 0x30ff0038, 0xfc5cf7ff, 0x69b30005, 0x2b004c70, 0x22f0d009, - 0x18ba0612, 0x2d009201, 0x0011d106, 0x42914a6c, 0x2000d907, 0xe7c66120, 0x00119a01, 0x42914a68, 0xb2ffd802, - 0xd1f42f00, 0x32294a66, 0x2a007812, 0x3201d1ef, 0x9202402a, 0x27086922, 0xd0354293, 0x21500020, 0xfe6cf7ff, - 0x9a020023, 0x485e334c, 0x2d00701a, 0x20a8d101, 0x42690540, 0x23a04169, 0x42494d5a, 0x402900db, 0x606118c9, - 0x602008c9, 0xfe56f7ff, 0x001a9b01, 0x429a4b51, 0x4854d806, 0x49554b54, 0x60e360a0, 0xfe4af7ff, 0x686269b3, - 0xd9004293, 0x6123e786, 0x27002301, 0x9a01425b, 0x4b4761e3, 0x429361a7, 0x3708417f, 0x334c0023, 0x9a02781b, - 0xd1ac4293, 0x69b26973, 0xd2a84293, 0x21280020, 0xf7ff3024, 0x0022fe2b, 0x32489b03, 0x62636971, 0x68f36163, - 0x4a407017, 0x46942001, 0x228062e2, 0x64220052, 0x324a0022, 0x221f7010, 0x4090400a, 0x094a6825, 0x62210092, - 0x18aa6323, 0x36206815, 0x900163e6, 0xd0004205, 0x2580e74a, 0x05ad69e6, 0xd20b42b3, 0x41bf42ab, 0x0038427f, - 0x41bf42ae, 0x9702427f, 0x98020007, 0xd0034287, 0xd20242ae, 0xd30042ab, 0x002561e3, 0x782d354c, 0xd11a2d00, - 0x05c9020e, 0x35010ec9, 0x0c71408d, 0x008968a6, 0x680e1871, 0xd10e422e, 0x031b0b1b, 0x23806363, 0x63a3015b, - 0x431d680b, 0x0021600d, 0x31482302, 0x432b780d, 0x69a3700b, 0x33019801, 0x681361a3, 0x43180021, 0x4d0e4663, - 0x35286010, 0x222862e3, 0x480b3124, 0xf001782b, 0x2001f93f, 0xbf407028, 0x7020344b, 0x46c0e6fd, 0x0a324655, - 0x9e5d5157, 0x0ab16f30, 0xe48bff56, 0x50100d3c, 0x0fffff01, 0x50100fc0, 0x50100ec4, 0x0001dce0, 0x15003c3c, - 0x00001e1e, 0x000003c3, 0x00001631, 0x000db570, 0xf8c8f7ff, 0x00290004, 0xf7ff6800, 0x6820fd97, 0xbd707125, - 0xb5102200, 0x210d4c07, 0x48071da3, 0xf7ff77da, 0x220dffeb, 0xf0010021, 0x4804f905, 0xf94cf7ff, 0x46c0bd10, - 0x50100a7c, 0x50100dc0, 0x00000705, 0x1dc3b510, 0x2b007fdb, 0x4b05d109, 0xd1064298, 0x33064b04, 0x2b007fdb, - 0xf7ffd001, 0xbd10ffd9, 0x50100dc0, 0x50100a7c, 0x4a0cb510, 0x7fd91d53, 0xd1072901, 0x005b2380, 0x84934809, - 0xf7ff3101, 0xbd10f9a7, 0xd1052902, 0x48052200, 0x302c77da, 0xf99ef7ff, 0xffbcf7ff, 0x46c0e7f3, 0x50100a7c, - 0x50100dc0, 0xb5702300, 0x1d654c11, 0x622377eb, 0x33017b05, 0xd8002d7f, 0x429a3301, 0x1d62d007, 0x230277d3, - 0xf7ff7323, 0x2000ffcf, 0x6883e00f, 0xd008428b, 0x77c21d60, 0xd201428b, 0x73222202, 0xd900428b, 0x2001000b, - 0x2b006223, 0xbd70d0eb, 0x50100a7c, 0x7c82b5f8, 0x02127c43, 0x7cc3431a, 0x041b000f, 0x7d03431a, 0x061b7dc1, - 0x7d824313, 0x43110209, 0xba494e20, 0xba1b0032, 0x2f01b289, 0x322cd000, 0x02494c1d, 0x003a6262, 0xf7ff62e3, - 0x2800ffb9, 0x4b1ad029, 0x2d3f6a1d, 0x4a19d928, 0x68130020, 0x33014918, 0x233f6013, 0x4b17439d, 0x62a361a5, - 0x4a174b16, 0x23806223, 0x61e3009b, 0x61632300, 0xff7ef7fe, 0x68a3353f, 0x195b09ad, 0x68e360a3, 0x60e5195d, - 0xd1072f01, 0x00302300, 0x60f360b4, 0xf7ff74a7, 0xbdf8f86d, 0xe7fc63b4, 0xff6af7ff, 0x46c0e7f9, 0x50100dc0, - 0x50100a4c, 0x50100a7c, 0x50100a20, 0x00003e17, 0x00003de4, 0x50100b1c, 0x00001475, 0x0005b570, 0xf7fe480c, - 0x68a9ffe7, 0x00047903, 0xd9004299, 0x22010019, 0xf7ff0028, 0x2800ff67, 0x4a06d009, 0x6a134806, 0x68917123, - 0x1acbb2db, 0xf7ff6093, 0xbd70f86d, 0x50100dc0, 0x50100a7c, 0x00001475, 0x4b082100, 0x1d5ab510, 0x688277d1, - 0xd006428a, 0x22017b01, 0xd800297f, 0x33051892, 0xf7ff77da, 0xbd10ff25, 0x50100a7c, 0x68024b1a, 0xb5106959, - 0x428a0004, 0x6840d111, 0xd10e2800, 0x6919699a, 0xd10a428a, 0x324c001a, 0x2a007812, 0x69d8d000, 0x491122fa, - 0xf7ff0092, 0xcc0af9cd, 0x8410f3ef, 0x4a0eb672, 0x42916812, 0x2b00d111, 0x2201d00c, 0x731a4b0b, 0x73da3206, - 0x765a3219, 0x769a3a1e, 0x77da3305, 0xfef2f7ff, 0xf7ff4806, 0xf384fbab, 0xbd108810, 0x50100d3c, 0x20042000, - 0x50100a20, 0x50100a7c, 0x50100a4c, 0xb5102100, 0xf7ff480f, 0x480ff979, 0xf7ff2100, 0x480ef975, 0x2b007a43, - 0x2280d003, 0x02924b0c, 0x2110601a, 0xfc36f7ff, 0x22004b0a, 0x33290019, 0x700a3128, 0x4b08701a, 0x33290019, - 0x700a3128, 0xbd10701a, 0x50100e58, 0x50100e24, 0x50100ab8, 0x4001a01c, 0x50100fc0, 0x50100e84, 0x4b052280, - 0xb5100292, 0x2900601a, 0xf7ffd003, 0xf7fffc19, 0xbd10ffc7, 0x4001a01c, 0x780b2260, 0xb5102000, 0x2a40401a, - 0xb25bd113, 0x4283784a, 0x2a42da10, 0x88ccd10d, 0xd10a2c10, 0x48090021, 0xfe5af7ff, 0x49080022, 0xff74f000, - 0xf80cf7ff, 0xbd102001, 0xd1fc2a41, 0xffa4f7ff, 0xffecf7fe, 0x46c0e7f6, 0x50100f68, 0x50100ab8, 0x4e18b5f8, - 0x001524c0, 0x056446b4, 0x4316001e, 0xd103432e, 0xf7ff2003, 0xbdf8f9ff, 0x6a666a27, 0xd0152a00, 0x2f0d19bf, - 0x1e07d812, 0x7807d001, 0x66273001, 0x2e003a01, 0x6e26d0e8, 0xd0012b00, 0xe7e33b01, 0xd0012900, 0x3101700e, - 0xe7dd3d01, 0xd1f22e00, 0x27c04666, 0x02bf6836, 0xd0d5423e, 0x46c0e7d8, 0x4001801c, 0x000cb510, 0x20030001, - 0xf9daf7ff, 0x23042280, 0x20000021, 0xf7ff0052, 0xbd10ffbf, 0xb51023f0, 0x18c0061b, 0xffecf7ff, 0xbd102000, - 0x2002b510, 0xf9bef7ff, 0x220623c0, 0x661a055b, 0x23012200, 0x00100011, 0xffa8f7ff, 0xbe00bd10, 0x26c0b573, - 0x05762401, 0xf7ff2002, 0x2305f9ab, 0x466b6633, 0x00221ddd, 0x00290023, 0xf7ff2000, 0x782bff95, 0xd0054223, - 0x681a4b03, 0x029b23c0, 0xd0e9421a, 0x46c0bd73, 0x4001801c, 0x0005b570, 0xf7ff000c, 0x0029ffcd, 0xf7ff0020, - 0x2200f993, 0x00112304, 0xf7ff0010, 0xf7ffff79, 0xbd70ffd1, 0xb51023f0, 0x18c0061b, 0xf7ff2120, 0x2000ffe7, - 0xb570bd10, 0x000c0005, 0xffb2f7ff, 0x20020029, 0xf978f7ff, 0x23042280, 0x00202100, 0xf7ff0052, 0xf7ffff5d, - 0xbd70ffb5, 0xb51023f0, 0x18c0061b, 0xffe7f7ff, 0xbd102000, 0x4b822280, 0x0452b5f0, 0x4a81601a, 0x68120006, - 0xb085000d, 0xd4390792, 0x4a7e2003, 0x4c7f497e, 0x4a7f6011, 0x4a7f6010, 0x67a2487f, 0x3aff3aff, 0x6c606002, - 0xd0fc4210, 0x00522280, 0x65a26422, 0x60114a7a, 0x29006851, 0x2080dafc, 0x60180140, 0xf94ef7ff, 0x4b762201, - 0x601a2121, 0x609a3263, 0x02d222aa, 0x4a7360da, 0x68196011, 0xdafc2900, 0x60132308, 0x63e32300, 0x22012382, - 0x6563011b, 0x601a4b6d, 0x6c622302, 0xd0fc421a, 0x04402080, 0xf92ef7ff, 0x4b69220c, 0x62da2180, 0x32f54b68, - 0x601a32ff, 0x4b672201, 0x601a4867, 0xf7ff0149, 0x4b66fadf, 0x43eb601e, 0xd100079b, 0x2e002500, 0xf7ffd001, - 0x4c62faaf, 0x00204b62, 0xf7ff6819, 0x4b61f8a7, 0x68191da0, 0xf8a2f7ff, 0x4c5f2601, 0xd01f2d00, 0x22204f5e, - 0x00380021, 0xfe3ef000, 0x70bb2320, 0x26012300, 0x002b70fb, 0x93034033, 0xd0074235, 0x00380021, 0x31202217, - 0xf0003009, 0x2600fe2d, 0x713b2301, 0x72fb2300, 0x003c9b03, 0xd1082b00, 0x4f4f0021, 0x00384a4f, 0xf7fe3109, - 0x4b4efd91, 0x07ab607b, 0x2117d40f, 0x4e4c4371, 0x4a4c3109, 0x18610030, 0xfd84f7fe, 0x4a4b4b4a, 0x4a4b601a, - 0x4b4b6053, 0x4b4b6073, 0xd1002d01, 0x4d4a3304, 0x60ec4a4a, 0x4f4a2400, 0x4b4a612b, 0x602a0021, 0x220160ab, - 0x94002340, 0xf7fe0038, 0x4e46fd55, 0x00210022, 0x94002340, 0xf7fe0030, 0x2380fd4d, 0x005b0021, 0x832b0038, - 0xfd12f7fe, 0x00212380, 0x0030005b, 0xf7fe832b, 0x23c0fd0b, 0x832b005b, 0x4b3a3401, 0x58d000a2, 0xd0022800, - 0xf7fe2100, 0x3401fcff, 0xd1f42c05, 0x20004b35, 0x4b35606b, 0x001c4a35, 0x33040019, 0x42936008, 0x2309d1fa, - 0x33036763, 0x3b0b67a3, 0xf7ff6423, 0x4b2ffa03, 0x64e34a2f, 0x601a4b2f, 0x4a2f2320, 0x4a2f6013, 0xf7fe6013, - 0x46c0fbe5, 0x4000e000, 0x4006c000, 0x40060000, 0x00fab000, 0x40008000, 0x4000b030, 0x000001ff, 0x4000b03c, - 0x40024000, 0x40028000, 0x4002b004, 0x4000a03c, 0x40058000, 0x4005a02c, 0x14003000, 0x50100000, 0x50100f64, - 0x50100eb5, 0x40000040, 0x00000050, 0x00003e19, 0x50100d1c, 0x50100e18, 0x00003ddc, 0x00000fb5, 0x50100e50, - 0x00003f38, 0x50100aa4, 0x00003dec, 0x50100e58, 0x00001729, 0x00003e64, 0x501009ec, 0x00003e50, 0x50100f68, - 0x00000b75, 0x50100f94, 0x50100a08, 0x0000170d, 0x50110000, 0x50110084, 0x20010000, 0x000113f0, 0x50110090, - 0xe000e280, 0xe000e100, 0xb5104b02, 0x691968d8, 0xfe98f7ff, 0x40058000, 0xb5f74b24, 0x4b24681a, 0x601a6884, - 0x69e50002, 0x69633229, 0x1e6e7812, 0x9201401e, 0xd0152a00, 0xd0032e00, 0xf7ff0020, 0xbdf7f90d, 0x195969a2, - 0xd9004291, 0x2d001ad5, 0x6aa3d0f4, 0x791b4a17, 0x5ad30028, 0x28004798, 0xe7eed0ec, 0xfcd2f7fe, 0x69a26961, - 0x00073140, 0x429169e3, 0x0015d312, 0x400d1e59, 0xd100420a, 0x2e00001d, 0x69e1d103, 0xf7ff6a20, 0x6a23f993, - 0x6839793a, 0xf0001998, 0xe7d8fd0b, 0x0033001d, 0x42ab3340, 0x9d01d2ed, 0x46c0e7eb, 0x50100f64, 0xd0000014, - 0x0000043c, 0x0006b5f8, 0xfca6f7fe, 0x2b1f7903, 0xe0a0d000, 0x4b556805, 0x429a682a, 0xe09ad000, 0x2b007b6b, - 0xe096d000, 0x337f7b29, 0x401a000a, 0xd0004219, 0x7babe08f, 0x2b0f3b01, 0xe08ad900, 0x4b4c4c4b, 0x686b6023, - 0x68ab6063, 0x7beb60a3, 0xd0022b03, 0x766273e2, 0x270076a2, 0x2b237327, 0xd818d047, 0xd03b2b1a, 0x2b03d80d, - 0x2b12d051, 0x42bbd025, 0x2301d05a, 0x33047323, 0x331b73e3, 0x23007663, 0x2b1be05c, 0x2b1ed05c, 0x0028d1f3, - 0xfc98f7ff, 0x2b2ae00a, 0xd80bd029, 0xd0312b25, 0x2b282101, 0x0028d1e7, 0xfc08f7ff, 0xf7fe0030, 0xbdf8fdd5, - 0xd0ea2b2f, 0xd0e82b35, 0x2124e7db, 0xf7ff482d, 0x2119fb7d, 0x482c0004, 0x18400022, 0xfc75f000, 0x70632380, - 0xf7ff0028, 0xe7e5fc51, 0x48252104, 0xfb6cf7ff, 0x60032303, 0x2102e7f4, 0x210ce7d9, 0xf7ff4820, 0x220cfb63, - 0xf0004920, 0xe7e9fc7d, 0x481c2108, 0xfb5af7ff, 0x491d2208, 0x2112e7f5, 0xf7ff4818, 0x0021fb53, 0x310d2212, - 0xfc6cf000, 0x766773e7, 0xe7d576a7, 0x7fd21de2, 0xd0ac2a00, 0x73222201, 0x73e21892, 0x76623238, 0xe7a476a3, - 0x7ceb2203, 0x2b024013, 0x3407d19f, 0x77e33b01, 0x4c08e79b, 0x00202103, 0xfd1cf7fe, 0x21030020, 0xf7fe302c, - 0xe79ffd17, 0x43425355, 0x50100a7c, 0x53425355, 0x50100dc0, 0x00003f40, 0x00003e0b, 0x00003e03, 0x4b09b510, - 0x6a5a4c09, 0x78123229, 0xd1002a00, 0x4a084c07, 0x1c486811, 0x60106ad9, 0x62da1c4a, 0x47a04a05, 0x46c0bd10, - 0x50100a4c, 0x00001031, 0x000011b9, 0x50100a20, 0x50100b1c, 0x4baab5f7, 0x9301681b, 0xd40003db, 0xf3bfe088, - 0x4fa78f5f, 0x00382100, 0xf7fe2401, 0x4ea5fdc3, 0x00302100, 0xfdbef7fe, 0x1cb3211f, 0x4aa277dc, 0x77dc1cbb, - 0x00157813, 0x42a14019, 0xe0a2d100, 0xd1002902, 0x2900e0ca, 0x3160d157, 0xd154420b, 0x2b00b25b, 0x0038da4c, - 0xfb9cf7fe, 0x0007786b, 0x2b066806, 0x2b08d00d, 0x2b00d03e, 0x8033d145, 0x88eb1924, 0xdd0042a3, 0x713b0023, - 0xfc76f7fe, 0x8868e04a, 0x2b020a03, 0x2b03d00d, 0x2b01d016, 0x4b8ad133, 0x68192412, 0xd0ea2900, 0x00300022, - 0xfbcaf000, 0xb2c0e7e5, 0xd1262800, 0x68d94b83, 0x788b78cc, 0x431c0224, 0xe7efd0db, 0x2800b2c0, 0x4b7ed00f, - 0x689b2402, 0x30014798, 0x781b1e43, 0xd1032b00, 0x70343303, 0xe7ca7073, 0x34025333, 0x2404e7f3, 0xe7d94976, - 0x7d5b4b74, 0xe7c07033, 0x2b057853, 0x2b09d004, 0xf7fed038, 0xe00bfd3d, 0x56d32302, 0x2b008851, 0x4b6cddf7, - 0x75990038, 0x496d4a6c, 0xfbfcf7fe, 0x4b6c2280, 0x651a0292, 0x06db9b01, 0x2600d50a, 0x4f692401, 0x2d006dbd, - 0x2e0ad004, 0xe082d000, 0x659d4b64, 0x04db9b01, 0xf7fed505, 0x2280ffbf, 0x03124b60, 0x23f8651a, 0x009b9a01, - 0xd008421a, 0x6d1b4b5d, 0xdb002b00, 0x2280e07e, 0x06124b59, 0xbdf7651a, 0x28007890, 0x4b52d004, 0x795b68db, - 0xd1be4283, 0xff86f7fe, 0xfbdcf7fe, 0x8893e7c8, 0x7d514a4c, 0xd0b42900, 0x420b21fe, 0x6912d1b1, 0x009bb2db, - 0x28005898, 0x6843d0ab, 0xd1142b00, 0x782b2260, 0xd1a44213, 0x0a1288aa, 0xb25bd1a1, 0xda9e2b00, 0x2c00786c, - 0x0038d19b, 0xfae6f7fe, 0x601c6803, 0x71032302, 0x0029e754, 0x28004798, 0xe7e5d19e, 0x2a008892, 0x2a80d041, - 0x4935d060, 0x29007d49, 0xe784d100, 0x00a04938, 0x68065840, 0x42b278b6, 0x3401d034, 0xd1f62c05, 0x7869e779, - 0xd00a2901, 0xd0002903, 0x886be773, 0xd0002b00, 0x2102e76f, 0xfbdaf7fe, 0x886be7ae, 0xd0002b00, 0x1dc2e767, - 0x2a027fd2, 0xf7fed802, 0xe7a3fcb7, 0x77c33002, 0x422ce7a0, 0x2101d00b, 0x4b216dfa, 0x659c4022, 0x1e530870, - 0x43b1419a, 0xfbe6f7fe, 0x006443a5, 0xe7663601, 0xff30f7fe, 0x0030e781, 0x42132260, 0xe744d000, 0x2b00b25b, - 0x786bdac7, 0xd0002b00, 0x886be73d, 0xd0002b00, 0x88ece739, 0xd0002c02, 0x2501e735, 0x7fc33007, 0x429d0038, - 0xf7fe41ad, 0x6803fa7b, 0x601d426d, 0xe6e97104, 0xe7dd0038, 0x50110098, 0x50100f68, 0x50100f94, 0x50100000, - 0x501009ec, 0x00003f32, 0x0000045d, 0x501009c4, 0x50113000, 0x50110000, 0x50100a08, 0xf7feb510, 0xbd10fbdd, - 0x4a1ab5f8, 0x601a4b1a, 0x8510f3ef, 0xf3bfb672, 0x4c188f5f, 0x37280027, 0xb2de783b, 0xd1132b00, 0x8810f385, - 0x8510f3ef, 0xf3bfb672, 0x4c128f5f, 0x37280027, 0x2b00783b, 0x2228d013, 0x480f0021, 0xfa8af000, 0xe006703e, - 0x00212228, 0xf000480b, 0x2300fa83, 0xf385703b, 0x00208810, 0xfd4af7fe, 0xf385e7d2, 0xbf208810, 0x46c0e7ce, - 0x00003ecc, 0x50100dbc, 0x50100fc0, 0x50100e84, 0x50100d8c, 0x9001b5f7, 0xfa16f7fe, 0x2b207903, 0xe09ed000, - 0x4b536806, 0x429a6832, 0xe098d000, 0x21284c51, 0x302c0020, 0xfedaf7fe, 0x68134a4f, 0x60133b01, 0x62e36872, - 0x7a3364e2, 0x4b4c9300, 0x601a9900, 0x725a2200, 0x605a3201, 0x72196932, 0x63a263e2, 0x00224694, 0x32516971, - 0x7c306421, 0x227f7010, 0x40029800, 0x28081e50, 0xe070d900, 0x43422003, 0x7a77483f, 0x42af5c85, 0x2500d161, - 0x605d1880, 0x78407843, 0xb240000d, 0xdb002800, 0x68f3001d, 0xd15642ab, 0x189a4b36, 0x78979b00, 0xd1092b02, - 0x69b24660, 0xfbf6f7fe, 0xfc80f7fe, 0xf7fe9801, 0xbdf7fb43, 0xd0472f00, 0x33500023, 0x0023701f, 0x26012202, - 0x701a3352, 0x725e4b28, 0xd0282d00, 0x4a294b28, 0x62236463, 0x005b2380, 0x230061e3, 0x616362a2, 0x61a54a25, - 0x49250020, 0xf90cf7fe, 0x68a3353f, 0x195b09ad, 0x68e360a3, 0x195d003a, 0x60e52308, 0x421f401a, 0x4b1ed003, - 0x60dc6263, 0x481de7cc, 0x626074a6, 0x60c26084, 0xf9f4f7fe, 0x481ae7c4, 0x00050021, 0x35284b19, 0x22286363, - 0x782b312c, 0xf9d0f000, 0xbf40702e, 0x2202e7b6, 0x9b00e004, 0xd0ab2b02, 0x4b082203, 0x2102605a, 0xf7fe480d, - 0x2102fa8f, 0xf7fe480a, 0xe7a5fa8b, 0x431fd10b, 0x50100ac8, 0x50100eb0, 0x50100ab8, 0x00003eac, 0x501008c4, - 0x00003df0, 0x00000b15, 0x00003e17, 0x50100e58, 0x50100e24, 0x50100e84, 0x00000b45, 0x4c09b570, 0x64a04b09, - 0x00214809, 0x63630005, 0x22283528, 0x782b312c, 0xf994f000, 0x70282001, 0x3453bf40, 0xbd707020, 0x50100ac8, - 0x00000e59, 0x50100e84, 0x220023c0, 0x609a055b, 0x49054a04, 0x001a601a, 0x601132f4, 0x609a2201, 0x46c04770, - 0x001f0300, 0x03000218, 0xf7ffb510, 0x2000ffeb, 0xbe00bd10, 0x230122a0, 0x0552b510, 0x68526053, 0x20004a02, - 0xf7fe6013, 0xbd10fc0d, 0x14002000, 0x0004b5f8, 0x001f0015, 0x42b41846, 0x22c0d205, 0x02924b0c, 0x4213681b, - 0xbdf8d000, 0x421c1e6b, 0x1b33d108, 0xd30542ab, 0x00390020, 0xfa5ef7ff, 0xe7ea1964, 0x21200020, 0xfa58f7ff, - 0x015b2380, 0xe7e218e4, 0x4001801c, 0x0005b5f8, 0x0004000f, 0x1b791886, 0x42b41909, 0x22c0d205, 0x02924b05, - 0x4213681b, 0xbdf8d000, 0x34010020, 0xfa59f7ff, 0xe7ee34ff, 0x4001801c, 0xb5f02301, 0x425bb085, 0x803baf03, - 0x220023c0, 0x609a055b, 0x6c9a6a9a, 0x615a2206, 0x02d222e0, 0x2201601a, 0x611a4c1f, 0x6826609a, 0x0032238c, - 0x439a2584, 0x43152003, 0xfbb0f7fe, 0x93012302, 0x60252380, 0x6065011b, 0x60e560a5, 0xd1fd3b01, 0x22042300, - 0x00180019, 0xf994f7ff, 0x2108230c, 0x2002439d, 0xf7fe430d, 0x9901fb99, 0xd1162901, 0x6026220c, 0x43966066, - 0x26080033, 0x60a6431e, 0x60e62002, 0xfb8af7fe, 0x22022300, 0x00380019, 0xf978f7ff, 0x4b042200, 0xb005601a, - 0x2301bdf0, 0x46c0e7cd, 0x40020008, 0x4001800c, 0x4b072090, 0xb5100080, 0xf7fe6018, 0x2200fb8b, 0x605a4b04, - 0x615a60da, 0x625a61da, 0xbd1062da, 0x4000e000, 0x40018000, 0xf7ffb510, 0xf7ffffe9, 0x4b04ff91, 0x2b00681b, - 0xf7fed001, 0x2000fd0d, 0x46c0bd10, 0x50100f64, 0x4a2bb5f8, 0x60134b2b, 0x60134a2b, 0x4b2b2202, 0x4213681b, - 0x2105d008, 0x60194b29, 0x061b23d0, 0x641a631a, 0x641a2200, 0x005b23c8, 0xd1fd3b01, 0x240920d0, 0x25042200, - 0x06002101, 0x3b01002b, 0x6883d1fd, 0x085b3c01, 0x18d2400b, 0xd1f52c00, 0xd9232a04, 0xf7ff26c0, 0xf7ffffaf, - 0x0576ff57, 0x22c02700, 0x683360b7, 0x43934d16, 0x4313b2e2, 0x23016033, 0x60b30029, 0xf7ff0038, 0x2201f93d, - 0x002821fc, 0xf0004252, 0x4b0ff833, 0x4298681b, 0x2380d008, 0x019b3440, 0xd1e1429c, 0x00082100, 0xf9a4f7ff, - 0xfee8f7ff, 0x46be3501, 0x46c04728, 0x4000e000, 0x00200240, 0x4000f000, 0x4006c000, 0x4001800c, 0x20041f00, - 0x20041ffc, 0xb5104b05, 0x60d8220a, 0x48046119, 0xf7fe4904, 0xbf30fa1b, 0x46c0e7fd, 0x40058000, 0x00001b99, - 0x20042000, 0x1809b530, 0xe00c4d5d, 0xba137804, 0x0624405c, 0x00642308, 0x406cd300, 0xd1fa3b01, 0x40620212, - 0x42883001, 0x1c10dbf0, 0xb530bd30, 0x5c434249, 0x0a1c3201, 0xd10541ad, 0x3101b25d, 0x41aa5c44, 0x5d631b14, - 0x35015553, 0x3101d1fb, 0xbd30d1ef, 0x2a084684, 0xb470d33a, 0x3a01e01c, 0x54835c8b, 0x4660d1fb, 0x46c04770, - 0x2a084684, 0x1a43d32e, 0xd1f2079b, 0x1a09b470, 0x08431c05, 0x5c44d302, 0x30017004, 0xd3020883, 0x80045a44, - 0x18093002, 0x19521a2d, 0xd3033a10, 0xc078c978, 0xd2fb3a10, 0xd3010752, 0xc018c918, 0xd3010052, 0xc008c908, - 0x0052d009, 0x880bd304, 0xd0048003, 0x30023102, 0x780bd001, 0xbc707003, 0x47704660, 0x0092a309, 0x33011a9b, - 0x46c04718, 0x7183798b, 0x7143794b, 0x7103790b, 0x70c378cb, 0x7083788b, 0x7043784b, 0x7003780b, 0x47704660, - 0xb2c94684, 0x4319020b, 0x46c0e011, 0x2a084684, 0x0843d32a, 0x7001d301, 0xb2c93001, 0x4319020b, 0xd3010883, - 0x30028001, 0x1a1b4663, 0xba0b18d2, 0x1c0b4319, 0xd3073a10, 0x1c0cb430, 0x46c01c0d, 0x3a10c03a, 0xbc30d2fc, - 0xd3000752, 0x0052c00a, 0xc002d300, 0x0052d006, 0x8001d302, 0x3002d002, 0x7001d000, 0x47704660, 0x1a9ba305, - 0x33011a9b, 0x71814718, 0x71017141, 0x708170c1, 0x70017041, 0x47704660, 0x04c11db7, 0x4608b505, 0xbd0a461a, - 0x02400dc2, 0x24010a40, 0x432005e4, 0xb2d22aff, 0x4240d900, 0x2afe3a01, 0x3a7ed201, 0x28004770, 0xd5004620, - 0x3a7e4240, 0x32800092, 0x0fc44770, 0xd50407e4, 0xd0002d00, 0x42403001, 0x3a01d403, 0xd0121800, 0x3281d5fb, - 0x3080d101, 0x3080d205, 0x2d00d203, 0x0040d00f, 0x2afe3a01, 0x3201da06, 0x0a40dd07, 0x431005d2, 0x47704320, - 0x05c020ff, 0x2000e7fa, 0x06054770, 0x0a40d1ed, 0xe7eb0280, 0x140cb283, 0x14044363, 0x436cb28d, 0xb284191b, - 0x0425436c, 0x191b0c24, 0x14091400, 0x02c04348, 0x430d06d9, 0x18401159, 0x00424770, 0xd0010e12, 0xd1012aff, - 0x05c00dc0, 0x0e12004a, 0x2affd001, 0x0dc9d101, 0x220105c9, 0xd4094041, 0xd5004041, 0x42884252, 0xdb00dc02, - 0x42522200, 0x47701e10, 0x18494301, 0x2800d0f8, 0xe7f6daf8, 0xb5102100, 0xff86f7ff, 0x33820013, 0x440ad410, - 0xdb073a17, 0xdd192a07, 0x43c917c1, 0x07c02001, 0xbd104048, 0x2a204252, 0x2220db00, 0xbd104110, 0xbd102000, - 0xb5102100, 0xff6cf7ff, 0x0001440a, 0x3a17d4ee, 0x43c1dbee, 0xdce92a08, 0xbd104090, 0xb5302200, 0xd5062900, - 0x430507cd, 0x3a010848, 0x2200e010, 0x0005b530, 0xd015430d, 0x160c17cd, 0xd10542ac, 0x0e4401c9, 0x01c04321, - 0xe7f63207, 0x00080005, 0x323d4252, 0x2100e004, 0x221db530, 0x25001a52, 0xff55f7ff, 0x2100bd30, 0x2800b530, - 0x221edaf5, 0x07c51a52, 0xe7f30840, 0x46a42500, 0x2900e00d, 0xe005dc02, 0xda032a00, 0x427f1b89, 0xe0011912, - 0x1b121989, 0x43674664, 0xcb101bc0, 0xd2000864, 0x46063501, 0x460f412e, 0x0864412f, 0xb5c04770, 0xffe2f7ff, - 0xffe6f7ff, 0x1386d3fc, 0x10d2138f, 0x43574356, 0x43674664, 0x133f1336, 0x19891bc0, 0xb5c0bdc0, 0xffd0f7ff, - 0xffd1f7ff, 0x2900d3fc, 0x1989dc02, 0xe0011b12, 0x19121b89, 0x10641076, 0xe7edd1f5, 0x2118b530, 0xff69f7ff, - 0x09244c5d, 0xdafd1b00, 0xd4fd1900, 0x00650082, 0x21004853, 0xdb0242a2, 0x42401b52, 0x00d2e7fa, 0x2401a355, - 0xffc5f7ff, 0x22003109, 0x25002300, 0xf80cf000, 0xfeedf7ff, 0xfed0f7ff, 0xf806f000, 0xb500e78f, 0xffd8f7ff, - 0xbd004608, 0x07642401, 0xdc0342a0, 0x42a04264, 0x4770dd00, 0x47700020, 0xf7ffb570, 0xe18cffc9, 0x2118b530, - 0xff31f7ff, 0x4a3c1401, 0x14c94351, 0x10493101, 0x0142b402, 0x43414839, 0x48391a52, 0xa3482100, 0xf7ff43cc, - 0x4408ff90, 0xe764bc04, 0xf7ffb530, 0x0001fea3, 0x4933d415, 0xfedef7ff, 0xd3011051, 0x10403101, 0x4601b402, - 0x009b4b2e, 0x1ac918c0, 0xa33b2200, 0xf7ff43d4, 0x4611ff88, 0x0013bc04, 0x22ffe749, 0xb530e7fb, 0xffe0f7ff, - 0xdc0a2b46, 0x2b46425b, 0x4824dc06, 0x31084358, 0x1a081109, 0xe7382205, 0x22ff43c0, 0xb530e735, 0xfe74f7ff, - 0xfe6ef7ff, 0xfe70f7ff, 0x01490140, 0x126418d4, 0xd40a3401, 0xda051ad4, 0x41204264, 0xd30c2c1c, 0xe00a17c0, - 0x2c1c4121, 0x2800d307, 0x4813da03, 0x404817c9, 0x17c8e013, 0x2200e011, 0xda022800, 0x42494240, 0xa30d4a0d, - 0xf7ff2401, 0x4610ff46, 0x18844a0a, 0x1a84d202, 0x1aa0d400, 0x22003801, 0x46c0e701, 0x136e9db4, 0x00001715, - 0x162e42ff, 0x2c9e15ca, 0x0593c2b9, 0x0162e430, 0x6487ed51, 0x3b58ce0c, 0x1f5b75f8, 0x0feadd4c, 0x07fd56ec, - 0x03ffaab8, 0x01fff554, 0x00fffeac, 0x007fffd4, 0x003ffffc, 0x001ffffc, 0x00100000, 0x00080002, 0x464fa9ec, - 0x464fa9ed, 0x20b15df4, 0x1015891c, 0x0802ac44, 0x0802ac45, 0x04005564, 0x02000aac, 0x01000154, 0x0080002c, - 0x00400004, 0x00200004, 0x00100000, 0x00080000, 0x00080003, 0x40514ab9, 0x17c4b570, 0x0e120042, 0x2affd051, - 0x17cdd052, 0x0e1b004b, 0x2bffd051, 0x4eb3d052, 0x40314030, 0x43303601, 0x40604331, 0x1b004069, 0x1a9d1b49, - 0xd40d1ad4, 0xda082c1e, 0x00133520, 0x40aa000a, 0xe00b4121, 0x00082200, 0x0013e00a, 0xe0072200, 0xdaf72d1e, - 0x00023420, 0x412840a2, 0xd0191840, 0xd0030fc1, 0x425243c0, 0x3001d100, 0x42b019b6, 0x1892d204, 0x3b014140, - 0xd3fa42b0, 0xd3020840, 0x2a003001, 0x2bfed009, 0x07c9d20a, 0x05db4408, 0xbd704418, 0xd0fc2a00, 0x0840e7e2, - 0xe7f20040, 0x07c8da01, 0x0208bd70, 0x05c030ff, 0x3a20bd70, 0xe7ac1912, 0x44220212, 0x3b20e7a9, 0xe7ac195b, - 0x442b021b, 0x46c0e7a9, 0x4602b580, 0x0fd2404a, 0x469607d2, 0x00490040, 0xd03d0e02, 0xd03c2aff, 0xd03c0e0b, - 0xd03b2bff, 0x3f8018d7, 0x02090200, 0x0a490a40, 0x46941842, 0x09cb09c2, 0x4348435a, 0xd3020c92, 0xd4002800, - 0x02433201, 0x02520dc0, 0x44601880, 0xd10e0dc1, 0xd22b2ffe, 0xd301005b, 0x3001d005, 0x05ff3701, 0x44704438, - 0x3001bd80, 0x00400840, 0x3701e7f6, 0xd2132ffe, 0xd3020840, 0x2b003001, 0x19ffd005, 0x05bf3701, 0x44704438, - 0x0840bd80, 0xe7f60040, 0x02123a10, 0x3b10e7c0, 0xe7c1021b, 0x3701da12, 0x3002d10e, 0x28030dc0, 0xe005d10a, - 0x3701da0a, 0x3001d106, 0xd0030dc0, 0x05c02001, 0xbd804470, 0xbd804670, 0x05c020ff, 0xbd804470, 0x2401b570, - 0x05e44266, 0x0a52024a, 0x09d34322, 0x062d25d0, 0x666b662e, 0xb2f30dc6, 0x0a400240, 0x0dc94320, 0x0a36404e, - 0x6f2d07f6, 0x2900b2c9, 0x29ffd030, 0x2b00d02c, 0x2bffd039, 0x1a5bd02a, 0x0a01337d, 0x0c094369, 0x001403c0, - 0x1b04434c, 0x436c12a4, 0x03491424, 0x0f0c1909, 0x3105d108, 0xd30f090c, 0x028008c9, 0x1a404351, 0xe008d40a, - 0x31093301, 0xd305094c, 0x02400909, 0x1a404351, 0x3401d400, 0xd2092bfe, 0x186005d9, 0xbd701980, 0xd10c2bff, - 0x05c020ff, 0xbd704330, 0x1c59dafa, 0x0e61d105, 0x2001d303, 0x433005c0, 0x0030bd70, 0x46c0bd70, 0x0041b410, - 0x0209d23a, 0x22010a49, 0x188905d2, 0xd03a0dc2, 0xd0362aff, 0x1052327d, 0x0049d300, 0x0d4ba41a, 0x09c85ce4, - 0x43604360, 0x43601300, 0x02241340, 0x34aa1a24, 0x43400020, 0x0a0b0bc0, 0x13004358, 0x15404360, 0x43631a24, - 0x00180bdb, 0x02494340, 0x11401a08, 0x01db4344, 0x301013e0, 0x44031180, 0x461cd306, 0x43644164, 0x1b090409, - 0x3301d400, 0x18d005d2, 0x4770bc10, 0xd0040e09, 0x05c017c0, 0x0dc0e7f8, 0x0fc0e7fb, 0xe7f307c0, 0xbbc9daf1, - 0x979ea6b0, 0x82868b91, 0x80000000, 0x007fffff, 0x2401b5f0, 0x406307e4, 0x46c0e001, 0x0d0cb5f0, 0x1e660fcf, - 0x1b890536, 0xd3030564, 0x424043c9, 0x3101d300, 0xd0030d64, 0x0af61c66, 0x1be4d007, 0x007f2000, 0x07891c79, - 0x3c801289, 0x0d1d0324, 0x1e6e0fdf, 0x1b9b0536, 0xd303056d, 0x425243db, 0x3301d300, 0xd0030d6d, 0x0af61c6e, - 0x1bedd007, 0x007f2200, 0x079b1c7b, 0x3d80129b, 0x1b2f032d, 0xd4581b66, 0x2e2046a4, 0x3720da46, 0x40bc0014, - 0x40bd001d, 0x413340f2, 0x1880432a, 0x0fcb4159, 0x43c9d005, 0x220043c0, 0x41504264, 0x46624151, 0xd1280d4d, - 0xd1070d0d, 0xd01f2800, 0x41401924, 0x3a014149, 0xd0f90d0d, 0xd3060064, 0xd3003001, 0x2c003101, 0x0840d101, - 0x3a010040, 0x1c94d40b, 0xd1040ae4, 0x44110512, 0x441907db, 0x07d9bdf0, 0x43194b20, 0x07d9e000, 0xbdf02000, - 0xd1dd2900, 0xd1db2c00, 0x3201bdf0, 0x084007c6, 0x432807cd, 0x2e000849, 0xe7d9d0e1, 0xda292e3c, 0x37403e20, - 0x40bc0014, 0x2401d000, 0x431440f2, 0x40bb001a, 0x17d3431c, 0x46ace7ac, 0xda082f20, 0x00043620, 0x000d40b4, - 0x40f840b5, 0x43284139, 0x2f3ce7a5, 0x3f20da0c, 0x00043640, 0xd00040b4, 0x40f82401, 0x00084304, 0x430c40b1, - 0xe7ea17c1, 0x00190010, 0xe7942400, 0x7ff00000, 0x0d0cb5f0, 0x05361e66, 0x0ae61b89, 0x0d640564, 0x1c65d002, - 0xd0040aed, 0x21012000, 0x3c800509, 0x46a40324, 0x1e670d1c, 0x1bdb053f, 0x05640ae7, 0xd0020d64, 0x0aed1c65, - 0x2200d004, 0x051b2301, 0x03243c80, 0x44644077, 0xb284b497, 0x4374b296, 0x437e0c07, 0x436f0c15, 0x4368b280, - 0xd3021836, 0x04002001, 0x0430183f, 0x19000c35, 0x4684417d, 0xb29ab288, 0x0c0c4350, 0x0c1f4362, 0xb28e437c, - 0x1992437e, 0x2601d302, 0x19a40436, 0x0c170416, 0x41671836, 0xb281bc01, 0x4351b29a, 0x43620c04, 0x435c0c1b, - 0x4358b280, 0xd3021812, 0x04002001, 0x04101824, 0x18400c13, 0x182d4163, 0x2000415e, 0xbc064147, 0xb293b288, - 0x0c0c4358, 0x0c124363, 0xb2894354, 0x185b4351, 0x2101d302, 0x18640409, 0x0c1a0419, 0x41621809, 0x4156186d, - 0x41472000, 0x02f9bc18, 0x43110d72, 0x0d6a02f0, 0x02ed4310, 0xd1030d0a, 0x4140196d, 0x3b014149, 0x1b9b4e12, - 0x42b30076, 0x006dd20e, 0x3001d307, 0x41712600, 0x43354666, 0x0840d101, 0x051b0040, 0x07e418c9, 0xbdf04421, - 0x3301da0b, 0x3001d106, 0x3101d104, 0xd0010d4f, 0xe7f20849, 0x200007e1, 0x3601bdf0, 0x20000531, 0x0000e7eb, - 0x000003ff, 0x0d1cb5f0, 0x053f1e67, 0x0ae71bdb, 0x0d640564, 0x1c66d002, 0xd0040af6, 0x23012200, 0x3c80051b, - 0x25d00324, 0x2600062d, 0x662e43f6, 0x666e091e, 0x19f60fce, 0x004946b4, 0xd0020d4f, 0x0af61c7e, 0x2000d003, - 0x3f402100, 0x1b3e033f, 0x44b400b6, 0x057f3f01, 0x08491bc9, 0x36016f2e, 0x029c0876, 0x43250d95, 0x13ed4375, - 0x13ad4375, 0x106d3501, 0x1b7603f6, 0x02ccb40c, 0x432c0d45, 0xb2b3b2a2, 0x0c27435a, 0x0c35437b, 0xb2a4436f, - 0x191b436c, 0x2401d302, 0x193f0424, 0x0c1d041c, 0x417d18a4, 0x416d1924, 0xb2919a00, 0x4361b2ac, 0x437c0c17, - 0x435f0c2b, 0x435ab292, 0xd30218a4, 0x04122201, 0x042218bf, 0x18520c23, 0x9c01417b, 0x191b436c, 0x01d90e52, - 0x0144430a, 0xb2811aa0, 0x4351b2b2, 0x435a1403, 0x43730c36, 0x4377b287, 0x19d217d6, 0x417e2700, 0x199b0436, - 0x0c160417, 0x415e187f, 0x18ed1673, 0x260001f3, 0x41753380, 0xd1060fa9, 0x0a690064, 0x0a5b05e8, 0xd2094318, - 0x2204e02f, 0x33804494, 0x0aa94175, 0x0a9b05a8, 0xd3264318, 0x41494140, 0x9a000424, 0x000d9b01, 0x1b644355, - 0x1ae44343, 0xb286b295, 0x0c174375, 0x0c03437e, 0xb292435f, 0x18b6435a, 0x2201d302, 0x18bf0412, 0x0c330432, - 0x417b1952, 0x419c4252, 0xd4022c00, 0x30012200, 0x08404151, 0x431007ca, 0xb0020849, 0x07d74662, 0x4b591092, - 0x4b5918d2, 0xd203429a, 0x18890512, 0xbdf019c9, 0x2a002000, 0x0039dc01, 0x3301bdf0, 0xe7f50519, 0x2100da07, - 0x0fc9e007, 0x0d5207c9, 0x12c9d003, 0x494ee001, 0x20000509, 0x46c04770, 0xd2f2004a, 0x3a010d52, 0x429a4b48, - 0xb5f0d2ea, 0x1b090514, 0xd3010852, 0x41491800, 0x18d2089b, 0x46940512, 0x0c4aa441, 0x090b5ca2, 0x43534353, - 0x4353131b, 0x0212135b, 0x00131ad2, 0x0b5b435b, 0x4363084c, 0x435313db, 0x330115db, 0x1ad2105b, 0x1ad20c13, - 0x435b0013, 0x0d840289, 0xb28d4321, 0x4375b29e, 0x437e0c0f, 0x435f0c1b, 0x435cb28c, 0xd3021936, 0x04242401, - 0x0434193f, 0x19640c33, 0x019d417b, 0x432c0ea4, 0xb2a53420, 0x14244355, 0x0c2d4354, 0x11a41964, 0x1b1203d2, - 0xb28eb295, 0x0c174375, 0x0c0c437e, 0xb2934367, 0x18f64363, 0x2301d302, 0x18ff041b, 0x0c340433, 0x417c195b, - 0x416418db, 0x230018db, 0xb29e4163, 0xb29d4376, 0x437d0c1f, 0x046c437f, 0x19a40bed, 0x0206417d, 0x1b36088f, - 0x077d41af, 0x416e08f6, 0xb295b2b4, 0x1437436c, 0x0c12437d, 0xb2b64357, 0x17ea4356, 0x260019ad, 0x04124172, - 0x042e18bf, 0x19360c2a, 0x3208417a, 0xd2191152, 0x059c0a9d, 0x191017d1, 0x44614169, 0x0000bdf0, 0x000003fd, - 0x000007fe, 0x000007ff, 0xd6dfebf8, 0xb8bec5cd, 0xa4a8adb2, 0x95999ca0, 0x8a8d8f92, 0x81838588, 0x0a5d4152, - 0x17d105dc, 0x414d1914, 0x4363002b, 0x4376b2a6, 0x0c27b2a2, 0x437f437a, 0x0bd20451, 0x417a1989, 0x18d218d2, - 0x42490580, 0xd4024190, 0x34012300, 0x0860415d, 0x07ed0869, 0x44614328, 0xb5d0bdf0, 0xb5d0e011, 0x004c4fb3, - 0xd0010d64, 0xd10242bc, 0x0d092000, 0x005c0509, 0xd0010d64, 0xd10242bc, 0x0d1b2200, 0x2601051b, 0xd40c404b, - 0xd500404b, 0x42994276, 0x4290d103, 0xd301d803, 0xdc002600, 0x1e304276, 0x430bbdd0, 0x430318db, 0xd0f54313, - 0xdaf62900, 0x4644e7f4, 0x4656464d, 0xb4f0465f, 0xbcf04770, 0x46a946a0, 0x46bb46b2, 0x46624770, 0x4694ca18, - 0x2a00465a, 0xe004db20, 0xca184662, 0x29004694, 0x18c0da1a, 0x465b4161, 0x465c413b, 0x465240b4, 0x432240fa, - 0x464d4644, 0x416b4162, 0x46994690, 0x40b3462b, 0x40fc413d, 0x4652431c, 0x41a2465b, 0x469241ab, 0x4770469b, - 0x41a11ac0, 0x413b464b, 0x40b4464c, 0x40fa4642, 0x46544322, 0x4162465d, 0x4692416b, 0x462b469b, 0x413d40b3, - 0x431c40fc, 0x464b4642, 0x41ab41a2, 0x46994690, 0x20004770, 0x47702100, 0xb5002200, 0xf0003220, 0x0008f82a, - 0x2200bd00, 0x3220b500, 0xf830f000, 0xbd000008, 0xb5002100, 0xf804f000, 0x2100e01e, 0xd4e615c3, 0x468cb510, - 0x004017c3, 0xd00a0e02, 0xd00c2aff, 0x3a7f1e51, 0x1a400609, 0x1ac04058, 0x07001101, 0x2000e01f, 0x00030001, - 0x43d8bd10, 0xbd1043d9, 0xb5002200, 0xf80cf000, 0x429a17ca, 0xbd00d100, 0x210143d8, 0x404107c9, 0x2200bd00, - 0xd4be150b, 0x4694b510, 0xf8b8f000, 0x34011414, 0x2100da00, 0x446217cb, 0xd40c3a34, 0xda072a0c, 0x40910004, - 0x42524090, 0x40d43220, 0xbd104321, 0x43d943d8, 0x3220bd10, 0x460cd407, 0x42524094, 0x41113220, 0x432040d0, - 0x0008bd10, 0x322017c9, 0x4252d403, 0x41103220, 0x0018bd10, 0xbd100019, 0x07db0fc3, 0x0e0a0041, 0x2affd007, - 0x0909d008, 0x18894a3d, 0x07404319, 0x00194770, 0x47702000, 0x18c9493a, 0x004ae7fa, 0x4b390d52, 0xdd131ad2, - 0xda1e2aff, 0x0fcb05d2, 0x431a07db, 0x0f4000c3, 0x0a490309, 0x43104308, 0xd301005b, 0x3001d001, 0x08434770, - 0x4770d2fb, 0x0fc8d002, 0x477007c0, 0x1312030a, 0xd1f83201, 0x2a070f42, 0x2201d1f5, 0x22ffe000, 0x02000fc8, - 0x05c01880, 0x21004770, 0x2100000a, 0x2100e004, 0x17c1000a, 0x2200e003, 0xe0052300, 0x17cb2200, 0x40594058, - 0x41991ac0, 0x4c1cb530, 0x29001aa2, 0x0001d103, 0x2000d010, 0x154c3a20, 0xd204d112, 0x18003a01, 0x0d4c4149, - 0x4c15d3fa, 0xd20442a2, 0x18890512, 0x18c907db, 0x43d2bd30, 0x20000d52, 0xe7f52100, 0x3a01d403, 0x41491800, - 0x320bd5fb, 0x0ac00544, 0x4328054d, 0x00640ac9, 0x2400d003, 0x41614160, 0xd3e0e7e1, 0xe7f80844, 0x000007ff, - 0x38000000, 0x7ff00000, 0x00000380, 0x00000432, 0x000007fe, 0x0fcc0d0a, 0x051b1e53, 0x05521ac9, 0x43c9d303, - 0xd3004240, 0x0d523101, 0x1c53d003, 0xd0070adb, 0x20001b12, 0x1c610064, 0x12890789, 0x03123a80, 0x1ad24b62, - 0x32024770, 0x2a0cd425, 0x2511da1c, 0x000b1aad, 0x3208412b, 0x00063507, 0x409040ee, 0x43314091, 0x4363ccf0, - 0x2300151a, 0x4355415a, 0x43574356, 0x12f402bf, 0x19760576, 0x17ed4167, 0x1b80197f, 0x477041b9, 0x2000220c, - 0x004917c9, 0x05093101, 0x0209e7db, 0x43190e03, 0x42530200, 0xd4083220, 0x4119000c, 0x40d84094, 0x22004320, - 0x41514150, 0x00084770, 0x3b2017c9, 0xd5f13220, 0x21002000, 0x47702200, 0xf7ffb5f0, 0xf000fe56, 0x4684f81d, - 0xf83ef000, 0x4660b403, 0xf858f000, 0xf7ffbc0c, 0xe474fe50, 0xf7ffb5f0, 0xf000fe46, 0xf000f80d, 0xe006f82f, - 0xf7ffb5f0, 0xf000fe3e, 0xf000f805, 0xf7fff845, 0xbdf0fe3e, 0xf7ffb500, 0xa431ff7f, 0xff97f7ff, 0x4d2d2400, - 0x07d24e2d, 0x43f6d302, 0x4166426d, 0xd2040052, 0x46a346a2, 0x46b146a8, 0x46a0e003, 0x46aa46a1, 0xa46f46b3, - 0x270146a4, 0xf7ff261f, 0x3701fe2b, 0x2f213e01, 0xbd00d1f9, 0xb2844659, 0x436cb28d, 0x43751406, 0x435e140b, - 0x435ab282, 0x17ea18ad, 0x43d2d700, 0x18b60412, 0x0c2b042a, 0x41731912, 0x46494640, 0x179b009d, 0x432a0f92, - 0x41994190, 0xe6fc223e, 0xb2844649, 0x436cb28d, 0x43751406, 0x435e140b, 0x435ab282, 0x17ea18ad, 0x43d2d700, - 0x18b60412, 0x0c2b042a, 0x41731912, 0x46594650, 0x179b009d, 0x432a0f92, 0x41594150, 0xe6de223e, 0x000003ff, - 0x9df04dbb, 0x36f656c5, 0x0000517d, 0x0014611a, 0x000a8885, 0x001921fb, 0xf7ffb5f0, 0x4d40fdc6, 0x402c000c, - 0x42acd001, 0x0d09d102, 0x20000509, 0x402c001c, 0x42acd001, 0x0d1bd102, 0x2200051b, 0x02ed2600, 0xd5042b00, - 0x406b2602, 0xd4004069, 0x194f4276, 0x4299d504, 0x3601dd0c, 0xe0034069, 0xda0742bb, 0x406b3e01, 0x00100007, - 0x000f003a, 0x003b0019, 0x2a00b440, 0x2b00d10f, 0x005cd00a, 0x34011564, 0x004cd109, 0x34011564, 0x3901d102, - 0xe0023b01, 0x21002000, 0xf7ffe02e, 0x223efbb1, 0xfe03f7ff, 0x468b4682, 0x21002000, 0x22014680, 0x46910792, - 0x46a4a41d, 0x261f2701, 0xfd81f7ff, 0x3e013701, 0xd1f92f21, 0x4653464a, 0x24013a0c, 0x27000764, 0x001b0852, - 0x4193d405, 0x41791900, 0xd1f70864, 0x4153e004, 0x41b91b00, 0xd1f10864, 0x104907ce, 0x43300840, 0x2e00bc40, - 0x4c09d00a, 0xd5014d09, 0x43ed43e4, 0xd10107f6, 0x41691900, 0x41691900, 0xf7ff223d, 0xf7fffe50, 0xbdf0fd4c, - 0x7ff00000, 0x885a308d, 0x3243f6a8, 0x61bb4f69, 0x1dac6705, 0x96406eb1, 0x0fadbafc, 0xab0bdb72, 0x07f56ea6, - 0xe59fbd39, 0x03feab76, 0xba97624b, 0x01ffd55b, 0xdddb94d6, 0x00fffaaa, 0x56eeea5d, 0x007fff55, 0xaab7776e, - 0x003fffea, 0x5555bbbc, 0x001ffffd, 0xaaaaadde, 0x000fffff, 0xf555556f, 0x0007ffff, 0xfeaaaaab, 0x0003ffff, - 0xffd55555, 0x0001ffff, 0xfffaaaab, 0x0000ffff, 0xffff5555, 0x00007fff, 0xffffeaab, 0x00003fff, 0xfffffd55, - 0x00001fff, 0xffffffab, 0x00000fff, 0xfffffff5, 0x000007ff, 0xffffffff, 0x000003ff, 0x00000000, 0x00000200, - 0x00000000, 0x00000100, 0x00000000, 0x00000080, 0x00000000, 0x00000040, 0x00000000, 0x00000020, 0x00000000, - 0x00000010, 0x00000000, 0x00000008, 0x00000000, 0x00000004, 0x00000000, 0x00000002, 0x00000000, 0x00000001, - 0x80000000, 0x00000000, 0x40000000, 0x00000000, 0xf7ffb5f0, 0xa454fe07, 0xfe1ff7ff, 0xda042900, 0x4d214c20, - 0x41691900, 0xb4043a01, 0xa6522701, 0x23012200, 0xce30079b, 0x1b0046b4, 0xd40b41a9, 0x3620427e, 0x413d001d, - 0x40b4001c, 0x40fe0016, 0x41624334, 0xe001416b, 0x41691900, 0x37014666, 0xd1e82f21, 0xb29eb285, 0x14074375, - 0x0c19437e, 0xb284434f, 0x17f1434c, 0x24001936, 0x04094161, 0x0434187f, 0x19640c31, 0x0fa44179, 0x43200088, - 0x18801789, 0xbc044159, 0x323e4252, 0xfd7ff7ff, 0x0000bdf0, 0xf473de6b, 0x2c5c85fd, 0x004fb5f0, 0x157fd250, - 0x3701d04e, 0xf7ffd04f, 0xb404fdb3, 0x0dc20249, 0x02404311, 0xa62b2701, 0x220046b4, 0x427e2300, 0x000d3620, - 0x000c413d, 0x000640b4, 0x433440fe, 0x414d4144, 0xd1050fae, 0x00290020, 0xce304666, 0x41ab1b12, 0x44a42408, - 0x2f213701, 0x0089d1e7, 0x18121089, 0xbc80414b, 0xcc13a417, 0x43783701, 0x437c4379, 0x12c9054f, 0x19c017cd, - 0x02a74169, 0x17cd15a4, 0x416c19c9, 0x188017dd, 0x416c4159, 0x17cd223e, 0xd00842ac, 0x070e0900, 0x09094330, - 0x43310726, 0x3a041124, 0xf7ffe7f3, 0xbdf0fd26, 0x20004902, 0x4902bdf0, 0xbdf02000, 0xfff00000, 0x7ff00000, - 0x0000b8aa, 0x0013de6b, 0x000fefa3, 0x000b1721, 0xbf984bf3, 0x19f323ec, 0xcd4d10d6, 0x0e47fbe3, 0x8abcb97a, - 0x0789c1db, 0x022c54cc, 0x03e14618, 0xe7833005, 0x01f829b0, 0x87e01f1e, 0x00fe0545, 0xac419e24, 0x007f80a9, - 0x45621781, 0x003fe015, 0xa9ab10e6, 0x001ff802, 0x55455888, 0x000ffe00, 0x0aa9aac4, 0x0007ff80, 0x01554556, - 0x0003ffe0, 0x002aa9ab, 0x0001fff8, 0x00055545, 0x0000fffe, 0x8000aaaa, 0x00007fff, 0xe0001555, 0x00003fff, - 0xf80002ab, 0x00001fff, 0xfe000055, 0x00000fff, 0xff80000b, 0x000007ff, 0xffe00001, 0x000003ff, 0xfff80000, - 0x000001ff, 0xfffe0000, 0x000000ff, 0xffff8000, 0x0000007f, 0xffffe000, 0x0000003f, 0xfffff800, 0x0000001f, - 0xfffffe00, 0x0000000f, 0xffffff80, 0x00000007, 0xffffffe0, 0x00000003, 0xfffffff8, 0x00000001, 0xfffffffe, - 0x00000000, 0x80000000, 0x00000000, 0x40000000, 0x00000000, 0x45444e49, 0x20202058, 0x004d5448, 0x4f464e49, - 0x3246555f, 0x00545854, 0x70736152, 0x72726562, 0x69502079, 0x32505200, 0x6f6f4220, 0x01060074, 0x50100dc0, - 0x50100dec, 0x0000044d, 0x0000000a, 0xbe000104, 0x0000004f, 0x0000000c, 0x0000000e, 0x00000001, 0x0003ffff, - 0x00ffff03, 0x00000200, 0x00000000, 0x02ffff03, 0x08000200, 0x37020900, 0x00010200, 0x0409fa80, 0x08020000, - 0x07005006, 0x40028105, 0x05070000, 0x00400202, 0x01040900, 0x00ff0200, 0x05070000, 0x00400203, 0x84050700, - 0x00004002, 0x01100112, 0x40000000, 0x00032e8a, 0x02010100, 0xbe000103, 0x50100e18, 0x50100e50, 0x4d903ceb, - 0x4e495753, 0x00312e34, 0x00010802, 0x00020002, 0x0081f800, 0x00010001, 0x00000001, 0x0003ffff, 0x00290000, - 0x52000000, 0x522d4950, 0x20203250, 0x41462020, 0x20363154, 0xfeeb2020, 0x01000000, 0x000c1000, 0x02000800, - 0x08048008, 0x00000880, 0x20000001, 0x04400004, 0xbe008000, 0x0000001c, 0x00002355, 0x000024c9, 0x0000188d, - 0x00000aa9, 0x000018c5, 0x000017fd, 0x00003dc4, 0x00003dd1, 0x50100eb5, 0x20324655, 0x746f6f42, 0x64616f6c, - 0x76207265, 0x0a302e32, 0x65646f4d, 0x52203a6c, 0x62707361, 0x79727265, 0x20695020, 0x0a325052, 0x72616f42, - 0x44492d64, 0x5052203a, 0x50522d49, 0x03040a32, 0xbe000409, 0x50100e58, 0x50100e24, 0x02020000, 0x00000020, - 0x20495052, 0x505201fc, 0x2008fb32, 0xfd3206f9, 0x3c010216, 0x6c6d7468, 0x65683c3e, 0x3c3e6461, 0x6174656d, - 0x74746820, 0x71652d70, 0x3d766975, 0x66657222, 0x68736572, 0x6f632022, 0x6e65746e, 0x30223d74, 0x4c52553b, - 0x25fc273d, 0x2f2f3a73, 0x70736172, 0x72726562, 0x2e697079, 0x2f6d6f63, 0x69766564, 0x522f6563, 0x763f3250, - 0x69737265, 0x613d6e6f, 0x63626261, 0x65646463, 0x27666665, 0x3c3e2f22, 0x626dfa2f, 0x3e79646f, 0x69646552, - 0x74636572, 0x20676e69, 0x3c206f74, 0x65727ffd, 0x3e60c666, 0x2f3c91f1, 0x6271fd61, 0x2f3c6bfc, 0xbe00ebfb, - 0xbe00be00, -]) diff --git a/src/main/modules/simulator/simulator-module-rp2040.ts b/src/main/modules/simulator/simulator-module-rp2040.ts deleted file mode 100644 index 07718f417..000000000 --- a/src/main/modules/simulator/simulator-module-rp2040.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { readFile } from 'fs/promises' -import { RP2040 } from 'rp2040js' - -import { bootromB1 } from './bootrom' - -// RP2040 flash starts at 0x10000000 (268435456 decimal) -const FLASH_START_ADDRESS = 0x10000000 - -// UF2 block constants -const UF2_MAGIC_START0 = 0x0a324655 -const UF2_MAGIC_START1 = 0x9e5d5157 -const UF2_MAGIC_END = 0x0ab16f30 -const UF2_BLOCK_SIZE = 512 - -// Nanoseconds per CPU cycle at 125 MHz -const CYCLE_NANOS = 1e9 / 125_000_000 // 8 ns - -// Iterations per execution batch. Each batch yields to the event loop via -// setImmediate so that Modbus UART I/O can be processed between batches. -const ITERATIONS_PER_BATCH = 1_000_000 - -/** - * Minimal simulation clock that satisfies the RP2040 IClock interface and - * exposes `tick()` / `nanosToNextAlarm` for the execution loop. - * Avoids a deep import from rp2040js internals. - */ -class SimClock { - readonly frequency: number - private nanosCounter = 0 - private nextAlarm: SimAlarm | null = null - - constructor(frequency = 125e6) { - this.frequency = frequency - } - - get nanos(): number { - return this.nanosCounter - } - - createAlarm(callback: () => void): SimAlarm { - return new SimAlarm(this, callback) - } - - linkAlarm(nanos: number, alarm: SimAlarm): void { - alarm.nanos = this.nanos + nanos - let item = this.nextAlarm - let last: SimAlarm | null = null - while (item && item.nanos < alarm.nanos) { - last = item - item = item.next - } - if (last) { - last.next = alarm - alarm.next = item - } else { - this.nextAlarm = alarm - alarm.next = item - } - alarm.scheduled = true - } - - unlinkAlarm(alarm: SimAlarm): void { - let item = this.nextAlarm - let last: SimAlarm | null = null - while (item) { - if (item === alarm) { - if (last) { - last.next = item.next - } else { - this.nextAlarm = item.next - } - return - } - last = item - item = item.next - } - } - - tick(deltaNanos: number): void { - const target = this.nanosCounter + deltaNanos - let alarm = this.nextAlarm - while (alarm && alarm.nanos <= target) { - this.nextAlarm = alarm.next - this.nanosCounter = alarm.nanos - alarm.callback() - alarm = this.nextAlarm - } - this.nanosCounter = target - } - - get nanosToNextAlarm(): number { - return this.nextAlarm ? this.nextAlarm.nanos - this.nanos : 0 - } -} - -class SimAlarm { - next: SimAlarm | null = null - nanos = 0 - scheduled = false - constructor( - private readonly clock: SimClock, - readonly callback: () => void, - ) {} - schedule(deltaNanos: number): void { - if (this.scheduled) this.cancel() - this.clock.linkAlarm(deltaNanos, this) - } - cancel(): void { - this.clock.unlinkAlarm(this) - this.scheduled = false - } -} - -/** - * Parses a UF2 firmware binary and writes its payload blocks to the RP2040 flash memory. - * UF2 format: 512-byte blocks with magic numbers, target address, and payload data. - * See https://github.com/microsoft/uf2 for format specification. - */ -function loadUF2(data: Uint8Array, mcu: RP2040): void { - const view = new DataView(data.buffer, data.byteOffset, data.byteLength) - - for (let offset = 0; offset + UF2_BLOCK_SIZE <= data.length; offset += UF2_BLOCK_SIZE) { - const magic0 = view.getUint32(offset + 0, true) - const magic1 = view.getUint32(offset + 4, true) - const magicEnd = view.getUint32(offset + UF2_BLOCK_SIZE - 4, true) - - if (magic0 !== UF2_MAGIC_START0 || magic1 !== UF2_MAGIC_START1 || magicEnd !== UF2_MAGIC_END) { - continue // skip invalid blocks - } - - const targetAddress = view.getUint32(offset + 12, true) - const payloadSize = view.getUint32(offset + 16, true) - - if (payloadSize > 476 || targetAddress < FLASH_START_ADDRESS) { - continue // skip blocks outside flash range - } - - const flashOffset = targetAddress - FLASH_START_ADDRESS - const payload = data.subarray(offset + 32, offset + 32 + payloadSize) - mcu.flash.set(payload, flashOffset) - } -} - -/** - * Manages the rp2040js emulator lifecycle in the main process. - * - * The firmware is compiled with SIMULATOR_MODE which inserts WFI at the end - * of each loop() iteration. When the CPU hits WFI, the execution loop - * fast-forwards the clock to the next alarm (typically SysTick at 1ms), - * avoiding millions of wasted busy-wait instruction cycles and allowing - * the simulation to run at near real-time speed. - */ -export class SimulatorModule { - private mcu: RP2040 | null = null - private clock: SimClock | null = null - private running = false - private timerHandle: ReturnType | null = null - - // Wall-clock pacing: track start times to keep sim time ≈ wall time - private wallStartMs = 0 - private simStartNanos = 0 - - /** Callback fired for each byte transmitted by the emulated UART0 */ - onUartByte: ((byte: number) => void) | null = null - - /** - * Loads a UF2 firmware file and starts the emulated RP2040. - * Stops any currently running emulation first. - */ - async loadAndRun(uf2Path: string): Promise { - this.stop() - - const uf2Data = await readFile(uf2Path) - - this.clock = new SimClock() - this.mcu = new RP2040(this.clock) - this.mcu.loadBootrom(bootromB1) - loadUF2(new Uint8Array(uf2Data), this.mcu) - - // Wire UART0 output to the Modbus RTU bridge callback - this.mcu.uart[0].onByte = (byte: number) => { - this.onUartByte?.(byte) - } - - // Set program counter to flash start and begin execution - this.mcu.core.PC = FLASH_START_ADDRESS - this.running = true - this.wallStartMs = performance.now() - this.simStartNanos = 0 - this.executeBatch() - } - - /** - * Runs a batch of CPU instructions, then reschedules. - * - * When the CPU enters WFI (waiting), the loop fast-forwards the clock - * to the next alarm instead of stepping through idle cycles. - * - * After each batch, compares simulated time against wall time: - * - If sim is ahead: schedules next batch with setTimeout(delay) to - * let wall time catch up, keeping timers accurate. - * - If sim is behind or on time: schedules with setTimeout(0). - */ - private executeBatch = (): void => { - if (!this.running || !this.mcu || !this.clock) return - - this.timerHandle = null - const { mcu, clock } = this - - for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { - if (mcu.core.waiting) { - const { nanosToNextAlarm } = clock - clock.tick(nanosToNextAlarm) - i += nanosToNextAlarm / CYCLE_NANOS - } else { - const cycles = mcu.core.executeInstruction() - clock.tick(cycles * CYCLE_NANOS) - } - } - - if (this.running) { - // Pace simulation to wall time - const simElapsedMs = (clock.nanos - this.simStartNanos) / 1e6 - const wallElapsedMs = performance.now() - this.wallStartMs - const aheadMs = simElapsedMs - wallElapsedMs - this.timerHandle = setTimeout(this.executeBatch, aheadMs > 1 ? Math.floor(aheadMs) : 0) - } - } - - /** Send a byte to the emulated UART0 RX (host → device) */ - feedByte(byte: number): void { - this.mcu?.uart[0].feedByte(byte) - } - - /** Stop the emulator and release resources */ - stop(): void { - this.running = false - if (this.timerHandle !== null) { - clearTimeout(this.timerHandle) - this.timerHandle = null - } - if (this.mcu) { - this.mcu.uart[0].onByte = undefined - this.mcu = null - } - this.clock = null - this.onUartByte = null - } - - /** Check if the emulator is currently running */ - isRunning(): boolean { - return this.running - } -} diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index 57cc5cc57..b7fecf7a8 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -15,7 +15,13 @@ import { readFile } from 'fs/promises' // ATmega2560 specs const CPU_FREQ_HZ = 16_000_000 const FLASH_SIZE_BYTES = 256 * 1024 -const SRAM_BYTES = 8448 // 8192 SRAM + 256 extended I/O (CPU adds 0x100 for low regs/IO) +// Expanded SRAM: fill the entire 16-bit address space (64 KB). +// The CPU constructor adds 0x100 internally for registers + standard I/O (0x00–0xFF). +// We supply 0xFF00 (65280) to cover extended I/O (0x100–0x1FF) plus usable SRAM +// (0x200–0xFFFF = 65024 bytes ≈ 63.5 KB). This is the maximum addressable with +// AVR's 16-bit data pointers. The linker flags in hals.json tell avr-gcc about +// the expanded space so it actually uses it. +const SRAM_BYTES = 0xff00 // SLEEP opcode – the firmware inserts `__asm volatile("sleep")` at the end // of each loop() iteration. We detect it before execution and fast-forward @@ -25,9 +31,14 @@ const SLEEP_OPCODE = 0x9588 // Nanoseconds per CPU cycle at 16 MHz const CYCLE_NS = 1e9 / CPU_FREQ_HZ // 62.5 ns -// Iterations per execution batch. Each batch yields to the event loop via -// setTimeout so that Modbus UART I/O can be processed between batches. -const ITERATIONS_PER_BATCH = 1_000_000 +// Maximum real (non-skipped) instructions per batch. SLEEP fast-forwards +// don't count against this budget, so idle periods are essentially free. +const MAX_REAL_INSTRUCTIONS = 100_000 + +// Maximum simulated time per batch (in CPU cycles). Prevents runaway +// batches when the firmware is mostly idle (SLEEP fast-forwards could +// cover seconds of sim time without hitting the instruction limit). +const MAX_SIM_CYCLES_PER_BATCH = CPU_FREQ_HZ / 10 // 100ms // --------------------------------------------------------------------------- // ATmega2560 peripheral configs – register addresses are identical to the @@ -184,8 +195,9 @@ export class SimulatorModule { // has consumed the current byte before the next one arrives, preventing // rxByte from being silently overwritten when interrupts are disabled // (e.g. while the CPU is inside another ISR like Timer0). - const originalUdrReadHook = this.cpu.readHooks[0xc6] - this.cpu.readHooks[0xc6] = (addr: number) => { + const udrAddr = mega2560Usart0Config.UDR + const originalUdrReadHook = this.cpu.readHooks[udrAddr] + this.cpu.readHooks[udrAddr] = (addr: number) => { const result = originalUdrReadHook?.(addr) this.drainRxQueue() return result @@ -208,6 +220,8 @@ export class SimulatorModule { * * When the CPU hits a SLEEP opcode, the loop fast-forwards the clock to * the next scheduled timer event instead of stepping through idle cycles. + * SLEEP fast-forwards don't count against the instruction budget, so idle + * periods between scan cycles are essentially free. * * After each batch, compares simulated time against wall time: * - If sim is ahead: schedules next batch with setTimeout(delay) to @@ -220,22 +234,37 @@ export class SimulatorModule { this.timerHandle = null const { cpu } = this - for (let i = 0; i < ITERATIONS_PER_BATCH && this.running; i++) { + // Kick-start the RX delivery chain if bytes are queued. + // feedByte() only attempts writeByte when the queue transitions from + // empty to non-empty. If that initial attempt is rejected (rxBusy was + // true because a previous byte's clock event hadn't fired yet), no + // further delivery attempts happen until drainRxQueue() is called from + // the UDR read hook — which itself requires a successful delivery. + // Retrying here at the top of each batch breaks that deadlock. + if (this.rxQueue.length > 0 && this.usart0) { + const byte = this.rxQueue[0] + if (this.usart0.writeByte(byte)) { + this.rxQueue.shift() + } + } + + const simCycleCap = cpu.cycles + MAX_SIM_CYCLES_PER_BATCH + let realCount = 0 + + while (this.running && realCount < MAX_REAL_INSTRUCTIONS && cpu.cycles < simCycleCap) { if (cpu.progMem[cpu.pc] === SLEEP_OPCODE) { // Execute the SLEEP instruction (advances PC, adds 1 cycle) avrInstruction(cpu) // Fast-forward to next scheduled clock event const nextEvent = (cpu as unknown as { nextClockEvent: { cycles: number } | null }).nextClockEvent if (nextEvent && nextEvent.cycles > cpu.cycles) { - const skipped = nextEvent.cycles - cpu.cycles cpu.cycles = nextEvent.cycles - // Account for fast-forwarded cycles in the batch counter - i += skipped } cpu.tick() } else { avrInstruction(cpu) cpu.tick() + realCount++ } } diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index a6b86230a..8e9d9cd09 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -316,7 +316,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa }) } - // Load firmware into simulator when compilation finishes with a UF2 path + // Load firmware into simulator when compilation finishes with a HEX path if (data.simulatorFirmwarePath) { ;(window.bridge.simulatorLoadFirmware as (p: string) => Promise<{ success: boolean; error?: string }>)( data.simulatorFirmwarePath, diff --git a/src/utils/device.ts b/src/utils/device.ts index 519d5e216..be8753aab 100644 --- a/src/utils/device.ts +++ b/src/utils/device.ts @@ -29,7 +29,7 @@ export function isOpenPLCRuntimeTarget(boardInfo: AvailableBoardInfo | undefined /** * Determines if a board is the built-in simulator target. - * The simulator uses an emulated RP2040 and requires no physical hardware. + * The simulator uses an emulated ATmega2560 and requires no physical hardware. * * @param boardInfo - The board information from availableBoards map * @returns true if the board is the simulator target, false otherwise From 699601f31ae61ff38b7e9c4c7e20ee56a12b2c78 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 24 Feb 2026 22:37:08 -0500 Subject: [PATCH 24/25] fix: pin avr8js version, sync simulator state, remove stale plan doc - Pin avr8js to exact 0.20.0 (private nextClockEvent field used for SLEEP fast-forward could break on minor version bumps) - Add simulator:stopped IPC event so renderer UI stays in sync when main process stops the simulator on project open/create - Remove outdated docs/simulator-rp2040-plan.md (superseded by AVR ATmega2560 migration) - Fix ESLint warnings in workspace-screen.tsx by hoisting isSimulatorTarget call into a const Co-Authored-By: Claude Opus 4.6 --- docs/simulator-rp2040-plan.md | 613 ------------------ package-lock.json | 2 +- package.json | 2 +- src/main/modules/ipc/main.ts | 14 +- src/main/modules/ipc/renderer.ts | 5 + .../modules/simulator/simulator-module.ts | 4 +- .../workspace-activity-bar/default.tsx | 11 +- src/renderer/screens/workspace-screen.tsx | 5 +- 8 files changed, 34 insertions(+), 622 deletions(-) delete mode 100644 docs/simulator-rp2040-plan.md diff --git a/docs/simulator-rp2040-plan.md b/docs/simulator-rp2040-plan.md deleted file mode 100644 index 57dece3a4..000000000 --- a/docs/simulator-rp2040-plan.md +++ /dev/null @@ -1,613 +0,0 @@ -# Simulator Mode: rp2040js Integration Plan - -## Overview - -Add a built-in simulator to OpenPLC Editor using [Wokwi's rp2040js](https://github.com/wokwi/rp2040js) (MIT, zero dependencies, ~200KB). The simulator appears as a device in the board list. When selected, "Build" compiles for RP2040, loads the UF2 firmware into the emulated CPU, and starts execution. "Debugger" connects via Modbus RTU over the emulated UART — identical to real hardware. - -The user never sees any emulation details. They press Build, the code compiles and runs. They press Debugger, values appear. - ---- - -## 1. New Device Entry in hals.json - -**File:** `resources/sources/boards/hals.json` - -Add a new entry at the **top** of the JSON (so it appears first in the device dropdown): - -```json -{ - "OpenPLC Simulator": { - "compiler": "simulator", - "core": "rp2040:rp2040", - "board_manager_url": "https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json", - "c_flags": ["-MMD", "-c", "-Wno-incompatible-pointer-types"], - "extra_libraries": [], - "platform": "rp2040:rp2040:rpipico", - "source": "rp2040pico.cpp", - "preview": "simulator.png", - "specs": { - "CPU": "Emulated RP2040 ARM Cortex-M0+", - "RAM": "264 KB", - "Flash": "2 MB", - "Note": "Built-in simulator - no hardware required" - } - }, - ...existing entries... -} -``` - -Key decisions: -- **`"compiler": "simulator"`** — new compiler type, distinct from `"arduino-cli"` and `"openplc-compiler"`. This is the discriminator used throughout the codebase to determine behavior. -- Reuses the real Raspberry Pico `core`, `platform`, and `source` (rp2040pico.cpp) because compilation is identical. -- Pin mapping defaults match the Raspberry Pico HAL (DI: 6-13, DO: 14-21, AI: 26-28, AO: 4-5). - ---- - -## 2. Device Type Utility Functions - -**File:** `src/utils/device.ts` - -Add a new utility function alongside the existing `isArduinoTarget` and `isOpenPLCRuntimeTarget`: - -```typescript -export function isSimulatorTarget(boardInfo: AvailableBoardInfo | undefined): boolean { - if (!boardInfo) return false - return boardInfo.compiler === 'simulator' -} -``` - -This function will be used across the UI to conditionally hide/show configuration fields. - ---- - -## 3. Default Device for New Projects - -**File:** `src/renderer/store/slices/device/data/constants.ts` - -Change the default device board from `'OpenPLC Runtime v3'` to `'OpenPLC Simulator'`: - -```typescript -defaultDeviceConfiguration: DeviceConfiguration = { - deviceBoard: 'OpenPLC Simulator', // was 'OpenPLC Runtime v3' - ...rest unchanged... -} -``` - -This ensures every new project starts with the simulator selected, so users can build and debug immediately. - ---- - -## 4. Device Editor UI — Hide Configuration for Simulator - -**File:** `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` - -The board.tsx component currently branches on `isOpenPLCRuntimeTarget(currentBoardInfo)`: -- If runtime target → shows IP address field, Connect button, scan cycle stats -- If arduino target → shows Communication Port dropdown, board specs, Pin Mapping table - -For the simulator, **none of these should appear**. The board panel should show: -- The Device dropdown (so users can switch away from simulator) -- The board preview image (`simulator.png`) -- A simple message like "Built-in simulator — no configuration required" -- No communication port selector -- No IP address field -- No Connect button -- No pin mapping table (pins are fixed by the HAL) -- No Compile Only checkbox - -Implementation: Add an `isSimulatorTarget(currentBoardInfo)` check at the top of the render logic: - -``` -Line 361: {isOpenPLCRuntimeTarget(currentBoardInfo) ? ( -``` - -Change to a three-way branch: - -``` -{isSimulatorTarget(currentBoardInfo) ? ( - // Simple component showing "Built-in simulator" message -) : isOpenPLCRuntimeTarget(currentBoardInfo) ? ( - ... existing runtime UI ... -) : ( - ... existing arduino UI ... -)} -``` - -Similarly, the section after `
` (line 488) that shows either scan-cycle stats or pin-mapping should show nothing (or a brief description) for the simulator. - -**File:** `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` - -The Communication panel (Modbus RTU/TCP checkboxes and config) should be completely hidden when the simulator is selected. The simulator handles Modbus RTU internally and automatically. - -Implementation: Early return or conditional render: -```tsx -if (isSimulatorTarget(currentBoardInfo)) { - return ( - -

Modbus RTU is automatically configured for the simulator.

-
- ) -} -``` - ---- - -## 5. Compilation Flow — Handle Simulator Target - -### 5a. Compiler Module Changes (Build Button Only) - -**File:** `src/main/modules/compiler/compiler-module.ts` - -The `compileProgram()` method (line 1341) needs a new branch for `compiler === 'simulator'`. The compilation pipeline is: - -1. **Steps 1-10 are identical to Arduino/RP2040** — XML generation, xml2st, iec2c, debug.c, LOCATED_VARIABLES.h, C/C++ blocks. The simulator uses the exact same `rp2040pico.cpp` HAL and `rp2040:rp2040:rpipico` platform. - -2. **Step 11 (Arduino CLI compile)** — also identical. The simulator target still calls `arduino-cli compile` with the RP2040 core to produce a `.uf2` firmware binary. Arduino CLI must be installed. - -3. **Step 12 (Upload) — this is where the simulator diverges.** Instead of calling `arduino-cli upload` to a serial port, the compiled `.uf2` file path is sent back to the renderer via the MessageChannel port so the renderer can load it into rp2040js. - -Implementation in `compileProgram()`: - -```typescript -// After successful Arduino CLI compilation... -if (boardRuntimeType === 'simulator') { - // Find the compiled .uf2 file - const uf2Path = path.join(buildDir, 'firmware.uf2') // or wherever arduino-cli outputs it - mainProcessPort.postMessage({ - logLevel: 'info', - message: 'Compilation successful. Loading firmware into simulator...', - }) - mainProcessPort.postMessage({ - simulatorFirmwarePath: uf2Path, - closePort: true, - }) - return -} -``` - -The `compileForDebugger()` method (line 2102) does **not** need a simulator-specific branch. This function only runs the first-stage compilation (XML → ST → C → debug metadata) and never invokes Arduino CLI or uploads anything. It works as-is for all targets, including the simulator. - -### 5b. IPC Handler Changes - -**File:** `src/main/modules/ipc/main.ts` - -The `handleRunCompileProgram` handler (line 777) passes args to `compilerModule.compileProgram()`. The args array includes `boardTarget` at index 1. The compiler module will read the board info from hals.json and detect `compiler === 'simulator'` to route appropriately. - -No IPC handler changes needed — the existing MessageChannel pattern already supports sending arbitrary data back to the renderer. - -### 5c. Build Button Changes (Renderer) - -**File:** `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` - -The build button handler (around line 267) calls `window.bridge.runCompileProgram(...)`. The callback receives messages from the compiler. - -For the simulator target, the callback will receive a new message type: `{ simulatorFirmwarePath: string, closePort: true }`. - -When this message arrives: -1. Read the UF2 file from the path -2. Load it into the rp2040js emulator instance (see Section 7) -3. Start emulator execution -4. Log "Simulator running" to the console - ---- - -## 6. rp2040js Emulator Module - -### 6a. New Module - -**File:** `src/main/modules/simulator/simulator-module.ts` (NEW) - -This module manages the rp2040js emulator lifecycle in the **main process**. It lives in main because the Modbus RTU client (which the debugger uses) runs in main. - -```typescript -import { RP2040, loadUF2, USBCDC } from 'rp2040js' - -class SimulatorModule { - private mcu: RP2040 | null = null - private running: boolean = false - private executionTimer: NodeJS.Timer | null = null - - // Callbacks for UART bridge - onUartByte: ((byte: number) => void) | null = null - - async loadAndRun(uf2Data: Buffer): Promise { - this.stop() - - this.mcu = new RP2040() - this.mcu.loadBootrom(bootromB1) // bundled bootrom - loadUF2(new Uint8Array(uf2Data), this.mcu) - - // Wire UART0 output to the Modbus RTU bridge - this.mcu.uart[0].onByte = (byte: number) => { - this.onUartByte?.(byte) - } - - // Start execution loop - this.mcu.PC = 0x10000000 - this.running = true - this.executeLoop() - } - - feedByte(byte: number): void { - this.mcu?.uart[0].feedByte(byte) - } - - stop(): void { - this.running = false - if (this.executionTimer) { - clearTimeout(this.executionTimer) - this.executionTimer = null - } - this.mcu = null - } - - isRunning(): boolean { - return this.running - } - - private executeLoop(): void { - if (!this.running || !this.mcu) return - // Execute a batch of cycles, then yield to event loop - this.mcu.execute() // runs until next yield point - this.executionTimer = setTimeout(() => this.executeLoop(), 0) - } -} -``` - -### 6b. Bootrom - -The RP2040 bootrom binary (~16KB) needs to be bundled. Options: -- Include as a `.ts` file with the binary data exported as a Uint8Array (same pattern as rp2040js's `demo/bootrom.ts`) -- Place in `resources/` and load at runtime - -Recommended: Bundle as a TypeScript constant in `src/main/modules/simulator/bootrom.ts` for simplicity and zero filesystem dependency. - -### 6c. npm Dependency - -```bash -npm install rp2040js -``` - -Add to `package.json` dependencies. The package is ~200KB, zero transitive dependencies, supports both ESM and CJS. - ---- - -## 7. Modbus RTU Bridge for Simulator — VirtualSerialPort Approach - -### Design Principle - -Rather than duplicating the Modbus RTU protocol logic in a separate `SimulatorModbusClient` class, we create a `VirtualSerialPort` that mimics the `serialport` npm package's event-based API and adapts rp2040js's UART. The existing `ModbusRtuClient` is then reused unchanged — all CRC calculation, frame assembly, response parsing, retries, and timeout logic stays in one place. - -This avoids code duplication and ensures any future bug fixes to the Modbus RTU protocol automatically apply to both physical hardware and the simulator. - -### 7a. VirtualSerialPort - -**File:** `src/main/modules/simulator/virtual-serial-port.ts` (NEW) - -`ModbusRtuClient.serialPort` is typed as `any` and uses these `serialport` APIs: -- `on('open', cb)` / `on('data', cb)` / `on('error', cb)` / `once('error', cb)` — events -- `write(data, callback)` — send bytes -- `flush(callback)` — flush input buffer -- `close()` — close port -- `isOpen` — boolean state -- `removeListener(event, fn)` — cleanup - -`VirtualSerialPort` extends `EventEmitter` and implements all of these, routing bytes through `SimulatorModule.feedByte()` (TX to device) and `SimulatorModule.onUartByte` (RX from device): - -```typescript -import { EventEmitter } from 'events' -import { SimulatorModule } from './simulator-module' - -export class VirtualSerialPort extends EventEmitter { - public isOpen = false - private simulator: SimulatorModule - - constructor(simulator: SimulatorModule) { - super() - this.simulator = simulator - } - - open(): void { - this.isOpen = true - // Wire UART RX: bytes from emulated device → ModbusRtuClient via 'data' events - this.simulator.onUartByte = (byte: number) => { - this.emit('data', Buffer.from([byte])) - } - // Emit 'open' asynchronously (matches real SerialPort behavior) - process.nextTick(() => this.emit('open')) - } - - write(data: Uint8Array | Buffer, callback?: (err?: Error | null) => void): void { - // Send each byte to the emulated UART TX (host → device) - for (const byte of data) { - this.simulator.feedByte(byte) - } - callback?.(null) - } - - flush(callback?: (err?: Error | null) => void): void { - // No hardware buffer to flush in virtual port - callback?.(null) - } - - close(): void { - this.isOpen = false - this.simulator.onUartByte = null - this.removeAllListeners() - } -} -``` - -**Why byte-by-byte emission works:** `ModbusRtuClient.sendRequest()` already accumulates bytes into `responseBuffer` via `Buffer.concat` and uses a 50ms frame-completion timeout to detect end-of-frame. Each byte resets the timer. Since the emulated CPU processes response bytes in batches (within the same `executeLoop()` tick), they arrive nearly instantly and the 50ms gap correctly signals frame completion. - -**No bootloader delay:** Physical serial ports have a 2.5s bootloader delay after opening. The `VirtualSerialPort` skips this entirely — the emulated UART is ready immediately. - -### 7b. ModbusRtuClient — Accept Injected Serial Port - -**File:** `src/main/modules/modbus/modbus-rtu-client.ts` (MODIFIED — minimal change) - -Add an optional `serialPort` field to the constructor options: - -```typescript -interface ModbusRtuClientOptions { - port: string - baudRate: number - slaveId: number - timeout: number - serialPort?: any // Pre-built serial port (e.g. VirtualSerialPort for simulator) -} -``` - -Store it in the constructor and add an early branch in `connect()`: - -```typescript -private injectedSerialPort: any = null - -constructor(options: ModbusRtuClientOptions) { - this.port = options.port - this.baudRate = options.baudRate - this.slaveId = options.slaveId - this.timeout = options.timeout - this.injectedSerialPort = options.serialPort ?? null -} - -async connect(): Promise { - // If a pre-built serial port was provided (e.g. VirtualSerialPort), use it directly - if (this.injectedSerialPort) { - this.serialPort = this.injectedSerialPort - return new Promise((resolve, reject) => { - this.serialPort.on('open', () => resolve()) - this.serialPort.on('error', (err: Error) => reject(err)) - this.serialPort.open() - }) - } - // ...existing SerialPort creation code (unchanged)... -} -``` - -This is the **only change** to `ModbusRtuClient`. All protocol logic — `assembleRequest()`, `sendRequest()`, `calculateCrc()`, `getMd5Hash()`, `getVariablesList()`, `setVariable()` — remains untouched and is shared between physical hardware and the simulator. - -### 7c. No Separate SimulatorModbusClient or Shared Utils Needed - -This approach eliminates: -- ~~`simulator-modbus-bridge.ts`~~ — not needed, `ModbusRtuClient` is reused directly -- ~~`modbus-rtu-utils.ts`~~ — not needed, no protocol code to extract/share - ---- - -## 8. Debugger Connection for Simulator - -### 8a. IPC Handler — New Connection Type - -**File:** `src/main/modules/ipc/main.ts` - -The `handleDebuggerConnect` method (line 1114) currently supports `connectionType: 'tcp' | 'rtu' | 'websocket'`. Add `'simulator'`: - -```typescript -handleDebuggerConnect = async ( - _event: IpcMainInvokeEvent, - connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', - connectionParams: { ... } -): Promise<{ success: boolean; error?: string }> -``` - -New branch using `ModbusRtuClient` with `VirtualSerialPort` (no separate client class needed): - -```typescript -case 'simulator': - const virtualPort = new VirtualSerialPort(this.simulatorModule) - this.debuggerModbusClient = new ModbusRtuClient({ - port: 'simulator', // label only, not used for real I/O - baudRate: 115200, - slaveId: 1, - timeout: 5000, - serialPort: virtualPort, // injected virtual port - }) - await this.debuggerModbusClient.connect() - break -``` - -Since `ModbusRtuClient` is used directly, all existing debug polling logic (`handleDebuggerGetVariablesList`, `handleDebuggerVerifyMd5`, `handleDebuggerSetVariable`) works unchanged. - -### 8b. Debugger Button — Simulator Flow (Corrected) - -**File:** `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` - -The debugger button does **not** compile the full firmware. It only runs the first-stage compilation (`compileForDebugger`) to generate debug metadata and extract the MD5 hash. This is already the existing behavior for all targets — `compileForDebugger()` never invokes Arduino CLI or uploads anything. - -For the simulator, there is one extra check: whether the simulator has firmware loaded. If the user has never pressed Build, the simulator is "empty" and there's nothing to debug. - -**Complete simulator debugger flow:** - -1. Detect `isSimulatorTarget` → skip all connection parameter checks (no IP, no port, no Modbus config) -2. **Check if simulator is "empty"** via `window.bridge.simulatorIsRunning()` - - If empty (no firmware loaded) → show warning dialog: *"No firmware is running on the simulator. Would you like to build and upload the project first?"* - - If user agrees → trigger full build (`runCompileProgram`), which compiles and auto-loads firmware into emulator. After build completes, restart the debugger flow from step 3. - - If user declines → abort debugger -3. Run `compileForDebugger()` (first-stage only: XML → ST → C → debug files). **No simulator-specific branch needed** — this function works as-is for all targets. -4. Extract local MD5 from generated `program.st` (existing `debuggerReadProgramStMd5`) -5. Connect to simulator and get its MD5 via `debuggerVerifyMd5('simulator', {}, expectedMd5)` — uses Modbus RTU over virtual UART -6. **Compare MD5s** (existing logic): - - If match → proceed to parse debug.c, build variable tree, connect debugger, start polling - - If mismatch → show existing "Program Mismatch" dialog asking user to rebuild/upload. If user agrees, trigger full build and retry MD5 verification. (This is the same dialog that appears for real hardware when the running firmware doesn't match the current project.) - -```typescript -// In handleDebuggerClick(): -if (isSimulatorTarget(currentBoardInfo)) { - // Check if simulator has firmware loaded - const running = await window.bridge.simulatorIsRunning() - if (!running) { - const response = await window.bridge.showMessageDialog({ - type: 'warning', - title: 'Simulator Empty', - message: 'No firmware is running on the simulator. Would you like to build and upload the project first?', - buttons: ['Build & Upload', 'Cancel'], - }) - if (response === 0) { - // Trigger full build (same as build button), then restart debugger flow - // ... - } else { - setIsDebuggerProcessing(false) - return - } - } - connectionType = 'simulator' - // Fall through to normal debug compilation + MD5 verification -} -``` - -### 8c. What Doesn't Change - -- `compileForDebugger()` — works as-is, no simulator branch needed (first-stage only, no hardware) -- `handleMd5Verification()` — works as-is once `'simulator'` connection type is supported -- MD5 mismatch dialog — works as-is (triggers full build + retry) -- Debug file parsing, variable tree building, debug polling — all unchanged - ---- - -## 9. New IPC Endpoints - -**File:** `src/main/modules/ipc/main.ts` — add handlers -**File:** `src/main/modules/ipc/renderer.ts` — add renderer-side wrappers -**File:** `src/main/modules/preload/preload.ts` — expose via bridge - -### New IPC Calls - -| Channel | Direction | Purpose | -|---------|-----------|---------| -| `simulator:load-firmware` | renderer → main | Load UF2 into emulator and start execution | -| `simulator:stop` | renderer → main | Stop the emulator | -| `simulator:is-running` | renderer → main | Check if emulator is currently running | - -These are thin wrappers around `SimulatorModule` methods. - ---- - -## 10. Firmware Modbus RTU Configuration - -The existing RP2040 HAL (`resources/sources/hal/rp2040pico.cpp`) and the OpenPLC Arduino runtime already include Modbus RTU slave support. The compiler generates `defines.h` with Modbus configuration based on the device editor settings. - -For the simulator, Modbus RTU must be **always enabled** with fixed defaults: -- Interface: `Serial` (UART0) -- Baud rate: `115200` -- Slave ID: `1` -- RS485 EN pin: none - -**File:** `src/main/modules/compiler/compiler-module.ts` - -In the code generation step that produces `defines.h`, when the target is `'simulator'`: - -```c -#define MODBUS_ENABLED -#define MODBUS_RTU -#define MODBUS_RTU_INTERFACE Serial -#define MODBUS_RTU_BAUD 115200 -#define MODBUS_RTU_SLAVE_ID 1 -``` - -These defines are hardcoded regardless of what the device editor shows (which for the simulator shows nothing). - ---- - -## 11. Assets - -### Simulator Preview Image - -**File:** `resources/sources/boards/images/simulator.png` (NEW) - -Create a preview image for the simulator device. Suggestion: a stylized chip/CPU icon with a "play" symbol overlay to convey "virtual device." Should match the dimensions and style of existing board preview images. - ---- - -## 12. File Summary - -### New Files - -| File | Purpose | -|------|---------| -| `src/main/modules/simulator/simulator-module.ts` | rp2040js emulator lifecycle management | -| `src/main/modules/simulator/virtual-serial-port.ts` | EventEmitter-based serial port mock that routes bytes through emulated UART | -| `src/main/modules/simulator/bootrom.ts` | Bundled RP2040 bootrom binary | -| `resources/sources/boards/images/simulator.png` | Device preview image | - -### Modified Files - -| File | Changes | -|------|---------| -| `resources/sources/boards/hals.json` | Add "OpenPLC Simulator" entry at top | -| `src/utils/device.ts` | Add `isSimulatorTarget()` function | -| `src/renderer/store/slices/device/data/constants.ts` | Change default device to "OpenPLC Simulator" | -| `src/renderer/components/_features/[workspace]/editor/device/configuration/board.tsx` | Hide comm port, pin mapping, IP address, Connect button for simulator | -| `src/renderer/components/_features/[workspace]/editor/device/configuration/communication.tsx` | Hide Modbus RTU/TCP config for simulator | -| `src/main/modules/compiler/compiler-module.ts` | Handle `compiler === 'simulator'` in compileProgram (skip upload, return UF2 path); force Modbus RTU defines | -| `src/main/modules/ipc/main.ts` | Add simulator IPC handlers; add `'simulator'` connection type to debugger | -| `src/main/modules/ipc/renderer.ts` | Add renderer wrappers for simulator IPC | -| `src/main/modules/preload/preload.ts` | Expose simulator bridge methods | -| `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` | Handle simulator in build callback and debugger click (empty simulator check) | -| `src/main/modules/modbus/modbus-rtu-client.ts` | Accept optional injected serial port in constructor (for VirtualSerialPort) | -| `package.json` | Add `rp2040js` dependency | - ---- - -## 13. Implementation Order - -### Phase 1: Foundation (Week 1) -1. `npm install rp2040js` -2. Add "OpenPLC Simulator" entry to `hals.json` -3. Add `isSimulatorTarget()` to `src/utils/device.ts` -4. Update default device to "OpenPLC Simulator" in constants.ts -5. Create `SimulatorModule` with basic load/run/stop -6. Bundle bootrom binary -7. Add simulator IPC endpoints (load-firmware, stop, is-running) - -### Phase 2: Compilation (Week 2) -8. Handle `compiler === 'simulator'` in `compileProgram()` — reuse Arduino CLI compilation for RP2040, skip upload step, return UF2 path -9. Force Modbus RTU defines in generated `defines.h` for simulator -10. Wire up build button callback to load UF2 into emulator on success - -### Phase 3: Debugger (Week 3) -11. Create `VirtualSerialPort` (EventEmitter mock adapting rp2040js UART to `serialport` API) -12. Modify `ModbusRtuClient` to accept optional injected serial port (single constructor change) -13. Add `'simulator'` connection type to `handleDebuggerConnect` (creates `ModbusRtuClient` + `VirtualSerialPort`) -14. Wire up debugger button flow for simulator: check if simulator is empty → first-stage compilation → MD5 verification → connect - -### Phase 4: UI Polish (Week 4) -15. Update board.tsx to show simulator-specific UI (hide irrelevant fields) -16. Update communication.tsx to show "auto-configured" message -17. Create simulator preview image -18. Testing: full flow (new project → build → debugger → see values) -19. Edge cases: re-compile while running, stop simulator, switch device away from simulator - ---- - -## 14. Open Questions / Decisions - -1. **Execution model**: rp2040js's `mcu.execute()` may block. Need to verify whether it runs synchronously to completion or yields. If it blocks, run in a Worker thread or use `mcu.step()` in a `setImmediate` loop. - -2. **Firmware output location**: Arduino CLI outputs compiled binaries to a temp directory or the sketch build path. Need to verify the exact `.uf2` output path for the RP2040 platform. - -3. **Emulator in main vs renderer**: Plan puts it in main process (where Modbus client runs). Alternative: run in renderer and use IPC for UART bytes. Main process is simpler since the debugger already runs there. - -4. **Bootrom licensing**: The RP2040 bootrom is Raspberry Pi proprietary. The rp2040js demo includes a bundled copy. Need to verify redistribution rights or use the open-source bootrom alternative if available. - -5. **Execution speed**: The emulated UART never blocks (runs faster than real hardware). This means Modbus RTU frame timing may differ. The 50ms frame-completion timeout in the existing RTU client should still work since it's based on silence gaps, not absolute timing. diff --git a/package-lock.json b/package-lock.json index a7471c361..7e3d431cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@tanstack/react-table": "^8.10.7", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", - "avr8js": "^0.20.0", + "avr8js": "0.20.0", "clsx": "^2.0.0", "cva": "npm:class-variance-authority@^0.7.0", "dompurify": "^3.2.4", diff --git a/package.json b/package.json index 4ba3b5cc3..51b1c7adf 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@tanstack/react-table": "^8.10.7", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", - "avr8js": "^0.20.0", + "avr8js": "0.20.0", "clsx": "^2.0.0", "cva": "npm:class-variance-authority@^0.7.0", "dompurify": "^3.2.4", diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index cba680c4b..55154b123 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -614,12 +614,12 @@ class MainProcessBridge implements MainIpcModule { // ===================== HANDLER METHODS ===================== // Project-related handlers handleProjectCreate = async (_event: IpcMainInvokeEvent, data: CreateProjectFileProps) => { - this.simulatorModule.stop() + this.stopSimulatorAndNotify() const response = await this.projectService.createProject(data) return response } handleProjectOpen = async () => { - this.simulatorModule.stop() + this.stopSimulatorAndNotify() const response = await this.projectService.openProject() if (response.success && response.data?.meta.path) { this.currentProjectPath = response.data.meta.path @@ -659,7 +659,7 @@ class MainProcessBridge implements MainIpcModule { handleProjectSave = (_event: IpcMainInvokeEvent, { projectPath, content }: IDataToWrite) => this.projectService.saveProject({ projectPath, content }) handleProjectOpenByPath = async (_event: IpcMainInvokeEvent, projectPath: string) => { - this.simulatorModule.stop() + this.stopSimulatorAndNotify() try { const response = await this.projectService.openProjectByPath(projectPath) if (response.success && response.data?.meta.path) { @@ -1354,6 +1354,14 @@ class MainProcessBridge implements MainIpcModule { // ===================== SIMULATOR HANDLERS ===================== + /** Stops the simulator and notifies the renderer so it can update UI state. */ + private stopSimulatorAndNotify(): void { + if (this.simulatorModule.isRunning()) { + this.simulatorModule.stop() + this.mainWindow?.webContents.send('simulator:stopped') + } + } + handleSimulatorLoadFirmware = async ( _event: IpcMainInvokeEvent, hexPath: string, diff --git a/src/main/modules/ipc/renderer.ts b/src/main/modules/ipc/renderer.ts index 547b77eb5..24d3d5e0b 100644 --- a/src/main/modules/ipc/renderer.ts +++ b/src/main/modules/ipc/renderer.ts @@ -365,6 +365,11 @@ const rendererProcessBridge = { ipcRenderer.invoke('simulator:load-firmware', hexPath), simulatorStop: (): Promise<{ success: boolean }> => ipcRenderer.invoke('simulator:stop'), simulatorIsRunning: (): Promise => ipcRenderer.invoke('simulator:is-running'), + onSimulatorStopped: (callback: () => void) => { + const listener = () => callback() + ipcRenderer.on('simulator:stopped', listener) + return () => ipcRenderer.removeListener('simulator:stopped', listener) + }, // ===================== FILE WATCHER METHODS ===================== fileWatchStart: (filePath: string): Promise<{ success: boolean; error?: string }> => diff --git a/src/main/modules/simulator/simulator-module.ts b/src/main/modules/simulator/simulator-module.ts index b7fecf7a8..cf8f5eb75 100644 --- a/src/main/modules/simulator/simulator-module.ts +++ b/src/main/modules/simulator/simulator-module.ts @@ -255,7 +255,9 @@ export class SimulatorModule { if (cpu.progMem[cpu.pc] === SLEEP_OPCODE) { // Execute the SLEEP instruction (advances PC, adds 1 cycle) avrInstruction(cpu) - // Fast-forward to next scheduled clock event + // Fast-forward to next scheduled clock event. + // NOTE: nextClockEvent is private in avr8js — pinned to 0.20.0 in package.json. + // If upgrading avr8js, verify this field still exists and has a `cycles` property. const nextEvent = (cpu as unknown as { nextClockEvent: { cycles: number } | null }).nextClockEvent if (nextEvent && nextEvent.cycles > cpu.cycles) { cpu.cycles = nextEvent.cycles diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index 8e9d9cd09..ba7c675d1 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -27,7 +27,7 @@ import { parsePlcStatus } from '@root/utils/plc-status' import { addPythonLocalVariables } from '@root/utils/python/addPythonLocalVariables' import { generateSTCode } from '@root/utils/python/generateSTCode' import { injectPythonCode } from '@root/utils/python/injectPythonCode' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { DebuggerButton, @@ -111,6 +111,15 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const currentBoardInfo = availableBoards.get(deviceDefinitions.configuration.deviceBoard) const isCurrentBoardSimulator = isSimulatorTarget(currentBoardInfo) + // Sync simulatorRunning when the main process stops the simulator + // (e.g. on project open/create) so the UI reflects the actual state. + useEffect(() => { + const cleanup = (window.bridge.onSimulatorStopped as (cb: () => void) => () => void)(() => { + setSimulatorRunning(false) + }) + return cleanup + }, []) + const applyEarlyCommentWrapping = (projectData: PLCProjectData): PLCProjectData => { return { ...projectData, diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index 8d60080ed..2ad35c224 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -213,6 +213,7 @@ const WorkspaceScreen = () => { const boardTarget = deviceDefinitions.configuration.deviceBoard const currentBoardInfo = availableBoards.get(boardTarget) const isRuntimeTarget = isOpenPLCRuntimeTarget(currentBoardInfo) + const isSimulator = isSimulatorTarget(currentBoardInfo) const isRTU = deviceDefinitions.configuration.communicationConfiguration.communicationPreferences.enabledRTU const isTCP = deviceDefinitions.configuration.communicationConfiguration.communicationPreferences.enabledTCP @@ -230,7 +231,7 @@ const WorkspaceScreen = () => { console.warn('No runtime IP address configured') return } - } else if (!isSimulatorTarget(currentBoardInfo)) { + } else if (!isSimulator) { if (isTCP && !debuggerTargetIp) { console.warn('No debugger target IP address configured') return @@ -243,7 +244,7 @@ const WorkspaceScreen = () => { } let batchSize = 60 - if ((isRTU && !isTCP) || isSimulatorTarget(currentBoardInfo)) { + if ((isRTU && !isTCP) || isSimulator) { batchSize = 20 } From 9ed3b03cc07c93f16a76d174a2729b0f8e440a10 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 24 Feb 2026 22:48:48 -0500 Subject: [PATCH 25/25] fix: close MessagePort in simulator branch, derive HEX path from FQBN - Add _mainProcessPort.close() after postMessage in the simulator compilation branch, matching all other early-return paths - Derive build sub-directory from platform FQBN instead of hardcoding 'arduino.avr.mega', so it stays in sync with hals.json Co-Authored-By: Claude Opus 4.6 --- src/main/modules/compiler/compiler-module.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/modules/compiler/compiler-module.ts b/src/main/modules/compiler/compiler-module.ts index 20e10d424..e0a0a374d 100644 --- a/src/main/modules/compiler/compiler-module.ts +++ b/src/main/modules/compiler/compiler-module.ts @@ -2087,8 +2087,11 @@ class CompilerModule { // Step 13: Upload program to board or load into simulator if (boardRuntime === 'simulator') { - // For simulator targets, send the HEX firmware path back to the renderer - const hexPath = join(compilationPath, 'examples', 'Baremetal', 'build', 'arduino.avr.mega', 'Baremetal.ino.hex') + // For simulator targets, send the HEX firmware path back to the renderer. + // Derive the build sub-directory from the platform FQBN (e.g. "arduino:avr:mega" → "arduino.avr.mega") + // so it stays in sync with the hals.json entry. + const fqbnSubDir = halsContent[boardTarget]['platform'].replaceAll(':', '.') + const hexPath = join(compilationPath, 'examples', 'Baremetal', 'build', fqbnSubDir, 'Baremetal.ino.hex') _mainProcessPort.postMessage({ logLevel: 'info', message: 'Compilation successful. Loading firmware into simulator...', @@ -2097,6 +2100,7 @@ class CompilerModule { simulatorFirmwarePath: hexPath, closePort: true, }) + _mainProcessPort.close() return }