Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7e0334d
docs: add implementation plan for rp2040js simulator mode
thiagoralves Feb 19, 2026
7b7c4e1
docs: rename device from 'Simulator' to 'OpenPLC Simulator'
thiagoralves Feb 19, 2026
d5a77c8
docs: revise simulator plan - VirtualSerialPort approach and correcte…
thiagoralves Feb 19, 2026
a1e48dc
feat: add simulator foundation - rp2040js, device entry, SimulatorMod…
thiagoralves Feb 19, 2026
885c7ae
feat: handle simulator target in compilation flow (Phase 2)
thiagoralves Feb 19, 2026
fddd7c7
feat: add VirtualSerialPort and debugger flow for simulator (Phase 3)
thiagoralves Feb 19, 2026
d6f4b51
feat: update device editor UI for simulator target (Phase 4)
thiagoralves Feb 19, 2026
e6c650e
fix: force fixed Modbus RTU defines for simulator compilation
thiagoralves Feb 19, 2026
b05cfab
fix: correct UF2 firmware filename to Baremetal.ino.uf2
thiagoralves Feb 19, 2026
de234bc
fix: use rp2040js Simulator class for proper clock and UART support
thiagoralves Feb 19, 2026
071519f
fix: use Serial1 (UART0) instead of Serial (USB CDC) for simulator
thiagoralves Feb 19, 2026
281fd34
fix: add simulator reconnection case to debugger variable polling
thiagoralves Feb 19, 2026
ee9e4e5
fix: skip Modbus TCP/RTU check for simulator in debugger polling
thiagoralves Feb 19, 2026
acdc954
feat: cosmetic improvements for simulator UI
thiagoralves Feb 19, 2026
ece33b3
perf: use setImmediate and larger batch size for simulator speed
thiagoralves Feb 19, 2026
cb81bcb
perf: dynamically calibrate simulator clock to match wall time
thiagoralves Feb 19, 2026
10de6c9
perf: add WFI to firmware loop for simulator real-time execution
thiagoralves Feb 19, 2026
95ada40
perf: pace simulator execution to wall-clock time
thiagoralves Feb 19, 2026
ea0f2d1
fix: stop simulator on project open/create, window reload, and app quit
thiagoralves Feb 19, 2026
eafd669
feat: migrate simulator from RP2040 to AVR ATmega2560
thiagoralves Feb 20, 2026
4dd6da2
perf: decouple SLEEP fast-forward from batch instruction budget
thiagoralves Feb 20, 2026
3ae167b
Revert "perf: decouple SLEEP fast-forward from batch instruction budget"
thiagoralves Feb 24, 2026
8097eee
fix: resolve simulator USART deadlock, expand SRAM to 63KB, remove rp…
thiagoralves Feb 24, 2026
699601f
fix: pin avr8js version, sync simulator state, remove stale plan doc
thiagoralves Feb 25, 2026
9ed3b03
fix: close MessagePort in simulator branch, derive HEX path from FQBN
thiagoralves Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions resources/sources/Baremetal/Baremetal.ino
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,12 @@ void loop()
modbusTask();
}
#endif

#ifdef SIMULATOR_MODE
// 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
}
25 changes: 25 additions & 0 deletions resources/sources/boards/hals.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
{
"OpenPLC Simulator": {
"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",
"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": "arduino:avr:mega",
"source": "mega_due.cpp",
"preview": "simulator.png",
"specs": {
"CPU": "Emulated ATmega2560 at 16MHz",
"RAM": "63.5 KB",
"Flash": "256 KB",
"Digital Pins": "70",
"Analog Pins": "16",
"PWM Pins": "15",
"WiFi": "No",
"Bluetooth": "No",
"Ethernet": "No"
}
},
"Arduino Due (native USB port)": {
"compiler": "arduino-cli",
"core": "arduino:sam",
Expand Down
Binary file added resources/sources/boards/previews/simulator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 44 additions & 6 deletions src/main/modules/compiler/compiler-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down Expand Up @@ -753,10 +755,19 @@ 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 USART0.
// On ATmega2560, Serial = USART0. avr8js bridges usart0.
DEFINES_CONTENT += '#define SIMULATOR_MODE\n'
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
Expand All @@ -769,7 +780,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'
}
Expand Down Expand Up @@ -1004,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',
Expand Down Expand Up @@ -2019,6 +2038,7 @@ class CompilerModule {
projectPath: normalizedProjectPath,
boardTarget,
buildMD5Hash,
boardRuntime,
_handleOutputData: (data, logLevel) => {
_mainProcessPort.postMessage({ logLevel, message: data })
},
Expand Down Expand Up @@ -2065,7 +2085,25 @@ 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 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...',
})
_mainProcessPort.postMessage({
simulatorFirmwarePath: hexPath,
closePort: true,
})
_mainProcessPort.close()
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!compileOnly) {
_mainProcessPort.postMessage({ logLevel: 'info', message: 'Uploading program to board...' })
try {
Expand Down
3 changes: 2 additions & 1 deletion src/main/modules/compiler/compiler-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ArduinoCoreControlSchema = z.array(z.record(z.string(), z.string()))
type ArduinoCoreControl = z.infer<typeof ArduinoCoreControlSchema>

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(),
Expand All @@ -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(),
})

Expand Down
8 changes: 3 additions & 5 deletions src/main/modules/hardware/hardware-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/main/modules/hardware/hardware-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading