# OpenPLC Runtime v4 β€” Architecture Analysis

## 1. High-Level Architecture

**Dual-process model:**
- **Process 1: Python Flask web server** (`webserver/app.py`) β€” HTTPS on port 8443, REST API, WebSocket debug, compilation orchestration, runtime process management.
- **Process 2: C runtime** (`build/plc_main`) β€” Real-time PLC execution thread at SCHED_FIFO priority, handles the scan cycle, I/O, plugin I/O drivers, and debug protocol.

**IPC between processes:**
- **Command socket**: `/run/runtime/plc_runtime.socket` β€” text-line protocol for START/STOP/STATUS/PING/STATS/DEBUG commands.
- **Log socket**: `/run/runtime/log_runtime.socket` β€” real-time log streaming from runtime to webserver.
- Webserver acts as a `RuntimeManager` that spawns, monitors, and communicates with the runtime process.

## 2. Scan Cycle & Timing (`scan_cycle_manager.c`)

**Key pattern: Decoupled timing measurement**
- `scan_cycle_time_start()` / `scan_cycle_time_end()` bracket each PLC cycle.
- Uses `CLOCK_MONOTONIC_RAW` (unaffected by NTP) for microsecond timestamps.
- Tracks: scan time (user program execution), cycle time (wall-clock between cycle starts), cycle latency (vs. expected periodic start).
- **Exponential moving average** for cycle_time_avg and cycle_latency_avg.
- Tracks overruns (when actual cycle start exceeds expected).
- All stats under `pthread_mutex_t` β€” exposed via `format_timing_stats_response()` as JSON.

**Deterministic scheduling in the cycle thread:**
```c
// After each cycle:
timer_start.tv_nsec += *ext_common_ticktime__;
normalize_timespec(&timer_start);
sleep_until(&timer_start);  // clock_nanosleep with TIMER_ABSTIME
```
This is a **periodic timer pattern** β€” each cycle aims to start at NΓ—ticktime, not "sleep ticktime after last cycle end". This minimizes drift.

## 3. PLC State Machine (`plc_state_manager.c`)

**States:** `EMPTY β†’ INIT β†’ RUNNING ↔ STOPPED β†’ ERROR`

**Key data structure:**
```c
static PLCState plc_state = PLC_STATE_STOPPED;
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
```

**State transition API:**
- `plc_set_state(new_state)` β€” mutex-protected transition with side effects:
  - `RUNNING`: finds `libplc_*.so` in build dir, creates `PluginManager`, calls `load_plc_program()`.
  - `STOPPED`: calls `unload_plc_program()` β€” joins PLC thread, stops plugins, cleans up journal/image tables, calls Python cleanup before `dlclose()`.
- `plc_get_state()` β€” mutex-protected read.
- `plc_force_error_state()` β€” emergency transition to ERROR.

**Crash recovery (3 layers):**
1. **Layer 1**: `sigsetjmp`/`siglongjmp` catches SIGFPE/SIGSEGV in the PLC cycle thread. Releases buffer mutex if held, transitions to ERROR state.
2. **Layer 2**: Watchdog monitors `plc_heartbeat` atomic variable β€” if the scan loop stops updating, it's a hang.
3. **Layer 3**: Webserver detects rapid crash pattern β†’ restarts runtime in "safe mode" (no PLC program loaded).

**Main scan loop inside `plc_cycle_thread()`:**
```c
while (plc_state == PLC_STATE_RUNNING) {
    scan_cycle_time_start();
    plugin_mutex_take(&plugin_driver->buffer_mutex);
    journal_apply_and_clear();           // Apply deferred writes from previous cycle
    plugin_driver_cycle_start(driver);    // Pre-cycle hooks
    ext_config_run__(tick__++);          // Execute PLC user program
    ext_updateTime();                    // Update IEC time function blocks
    plugin_driver_cycle_end(driver);      // Post-cycle hooks
    atomic_store(&plc_heartbeat, time(NULL));
    plugin_mutex_give(&plugin_driver->buffer_mutex);
    scan_cycle_time_end();
    sleep_until(&timer_start);           // Wait for next cycle
}
```

## 4. Image Tables / I/O Buffers (`image_tables.c`)

**Pattern: Centralized I/O process image with pointer indirection**

```c
// Global arrays β€” pointers to IEC types, indexed by PLC address number
IEC_BOOL *bool_input[BUFFER_SIZE][8];   // %IX β€” 8 bits per byte
IEC_BOOL *bool_output[BUFFER_SIZE][8];  // %QX
IEC_BYTE *byte_input[BUFFER_SIZE];      // %IB
IEC_BYTE *byte_output[BUFFER_SIZE];     // %QB
IEC_UINT *int_input[BUFFER_SIZE];       // %IW (16-bit)
IEC_UINT *int_output[BUFFER_SIZE];      // %QW
IEC_UDINT *dint_input[BUFFER_SIZE];     // %ID (32-bit)
IEC_UDINT *dint_output[BUFFER_SIZE];    // %QD
IEC_ULINT *lint_input[BUFFER_SIZE];     // %IL (64-bit)
IEC_ULINT *lint_output[BUFFER_SIZE];    // %QL
IEC_UINT *int_memory[BUFFER_SIZE];      // %MW
IEC_UDINT *dint_memory[BUFFER_SIZE];    // %MD
IEC_ULINT *lint_memory[BUFFER_SIZE];    // %ML
IEC_BOOL *bool_memory[BUFFER_SIZE][8];  // %MX
```

**Symbol resolution at load time:**
- `symbols_init(PluginManager *pm)` β€” uses `plugin_manager_get_func()` (typed dlsym) to resolve function pointers from the loaded PLC shared library.
- Resolves: `config_run__`, `config_init__`, `glueVars`, `updateTime`, `setBufferPointers_v4`, `common_ticktime__`, `plc_program_md5`, debug functions (`get_var_count`, `get_var_addr`, `get_var_size`, `set_trace`).
- Passes image table pointers INTO the .so via `setBufferPointers_v4()` β€” the generated code populates these arrays during `ext_glueVars()`.

**Null-pointer safety:**
- `image_tables_fill_null_pointers()` β€” fills any NULL entries with static temporary buffers (zeroed) so plugins never dereference NULL.
- `image_tables_clear_null_pointers()` β€” resets everything on program unload.

## 5. Debug Handler (`debug_handler.c`)

**Binary frame protocol (Modbus-inspired):**
| Function Code | Operation |
|---|---|
| 0x41 DEBUG_INFO | Return variable count |
| 0x42 DEBUG_SET | Set trace/force on a variable |
| 0x43 DEBUG_GET | Get values for range of variables |
| 0x44 DEBUG_GET_LIST | Get values for specific list of variables |
| 0x45 DEBUG_GET_MD5 | Get program MD5 + set endianness |

- Responses use status codes: `0x7E` success, `0x81` out of bounds, `0x82` out of memory.
- MAX_DEBUG_FRAME = 4096 bytes per frame.
- Accesses PLC variables by index using `ext_get_var_count()`, `ext_get_var_addr(idx)`, `ext_get_var_size(idx)`.
- Includes tick counter in responses for timing correlation.
- Endianness negotiation via `0xDEAD`/`0xADDE` marker.

## 6. Shared Library Loading (`plcapp_manager.c`)

**Minimal PluginManager abstraction:**
```c
struct PluginManager {
    char *so_path;
    void *handle;  // dlopen handle
};
```

- `find_libplc_file(build_dir)` β€” scans build dir for `libplc_*.so`.
- `plugin_manager_create(path)` β€” allocates + stores path.
- `plugin_manager_load(pm)` β€” `dlopen(path, RTLD_NOW)`.
- `plugin_manager_get_symbol(pm, name)` β€” `dlsym` with error handling.
- `plugin_manager_get_func(pm, type, name)` β€” typed symbol resolution:
  ```c
  *(void **)(&func_ptr) = plugin_manager_get_symbol(pm, "symbol_name");
  ```
- `plugin_manager_destroy(pm)` β€” `dlclose()` + free.

## 7. UNIX Socket IPC (`unix_socket.c`)

**Simple text-line protocol:**
```
COMMAND β†’ RESPONSE
PING β†’ PING:OK
STATUS β†’ STATUS:RUNNING|STOPPED|INIT|ERROR|EMPTY
START β†’ START:OK|START:ERROR|START:ERROR_ALREADY_RUNNING
STOP β†’ STOP:OK|STOP:ERROR
STATS β†’ STATS:{json}
DEBUG:hexstring β†’ DEBUG:hexresponse
```

- Single-threaded accept loop β€” handles one client at a time (sufficient for admin control).
- `handle_unix_socket_commands()` dispatches by string comparison.
- DEBUG commands use hex-encoded binary frames forwarded to `process_debug_data()`.

## 8. REST API (Webserver)

**Flask + Flask-SocketIO on HTTPS:8443**

**GET endpoints:**
- `/api?get-data=start-plc` β€” starts PLC via UNIX socket
- `/api?get-data=stop-plc` β€” stops PLC
- `/api?get-data=status` β€” returns PLC state + optional timing stats
- `/api?get-data=ping` β€” liveness check
- `/api?get-data=runtime-logs` β€” log streaming with min_id + level filtering
- `/api?get-data=compilation-status` β€” build status + logs
- `/api?get-data=serial-ports` β€” system serial port enumeration

**POST endpoints:**
- `/api?post-data=upload-file` β€” uploads program ZIP β†’ validates β†’ extracts to `core/generated/` β†’ triggers compilation β†’ returns build status

**Build state machine:** `IDLE β†’ UNZIPPING β†’ COMPILING β†’ SUCCESS/FAILED`

**WebSocket debug interface:** Flask-SocketIO bridge between browser debug client and runtime's binary debug protocol over UNIX socket.

## 9. Plugin/Driver Architecture (`core/src/drivers/`)

**Two plugin types:**
- **Type 0: Python** β€” loaded via CPython C API (`PyImport_ImportModule`), runs in separate threads, communicates via `PyGILState_Ensure/Release`.
- **Type 1: Native** β€” loaded via `dlopen`, standard C shared library with init/start/stop/cleanup function pointers.

**Plugin lifecycle:**
1. **`plugin_driver_load_config()`** β€” parses `plugins.conf`, resolves symbols for each plugin.
2. **`plugin_driver_init()`** β€” calls init() for ALL plugins (enabled or not). Contract: set up internal state, parse config, allocate resources. Do NOT start threads/servers.
3. **`plugin_driver_start()`** β€” called from PLC cycle thread AFTER image tables populated. Starts only enabled plugins.
4. **`plugin_driver_cycle_start/end()`** β€” hooks called each cycle around the PLC program execution.
5. **`plugin_driver_stop()`** β€” stops all plugins, joins threads, cleans up.

**Shared runtime args structure passed to plugins:**
```c
struct plugin_runtime_args_t {
    plugin_runtime_args_header_t header;  // type, plugin_index, buffer_count
    // Function pointers for safe I/O access:
    int (*read_bool)(int type, int index, int bit);
    void (*write_bool)(int type, int index, int bit, int value);
    // ... similar for byte, int, dint, lint
    // Direct buffer pointers (read-only, for zero-copy):
    IEC_BOOL **bool_input;
    // ... etc.
};
```

**Journal buffer pattern for race-free writes:**
- Plugins write to a journal buffer instead of directly to image tables.
- Journal entries are applied atomically at the start of each PLC cycle (`journal_apply_and_clear()`).
- Prevents torn reads when PLC cycle thread and plugin threads access shared I/O concurrently.
- Protected by `PTHREAD_PRIO_INHERIT` mutex to prevent priority inversion.

**Mutex with priority inheritance:**
```c
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
```
Prevents priority inversion when low-priority plugin threads hold the buffer mutex.

## 10. Compilation Flow

```
Editor β†’ POST /api/upload-file (ZIP)
  β†’ analyze_zip() (safety validation)
  β†’ safe_extract() to core/generated/
  β†’ update_plugin_configurations()
  β†’ run_compile() in background thread:
    β†’ builds CMake project in core/generated/
    β†’ outputs libplc_<md5>.so to build/
  β†’ On next START, plc_set_state(RUNNING) finds new .so via find_libplc_file()
    β†’ loads via plugin_manager_create/load
    β†’ resolves symbols
    β†’ creates PLC cycle thread
```

## 11. Patterns to Borrow for mechbase-plc

### A. Decoupled Timing Measurement
- Separate `scan_cycle_time_start()`/`scan_cycle_time_end()` as instrumentation points.
- Track min/max/avg for both scan time (user code) and cycle latency (jitter).
- Use `CLOCK_MONOTONIC_RAW` for drift-free timestamps.

### B. Periodic Timer Scheduling (Not Drift-Accumulating)
- Target absolute timestamps: `next_cycle = base_time + N Γ— tick_time`.
- Use `clock_nanosleep(TIMER_ABSTIME)` for precise waking.
- This prevents cycle drift over long runs.

### C. Centralized I/O Image with Pointer Arrays
- Global pointer arrays indexed by PLC address, populated at program load time.
- Null-pointer fill ensures safe access even to unused addresses.
- Versioned `setBufferPointers_v4()` for backward-compatible API evolution.

### D. Crash Recovery with sigsetjmp/siglongjmp
- Catch SIGFPE/SIGSEGV in the PLC execution thread specifically (pthread_self check).
- Jump to known-good state, release held mutexes, transition to ERROR.
- Safe for generated PLC code (no heap allocation, no recursion).

### E. Journal Buffer for Concurrent I/O
- Plugins write to a write-ahead journal instead of shared buffers.
- Apply journal atomically at cycle boundary.
- Prevents torn reads without complex lock-free data structures.

### F. Plugin Lifecycle Contract
- Clear init/start/stop/cleanup phases with explicit contracts.
- Init runs for ALL plugins, start only for enabled ones.
- Structured runtime args with function pointers for safe I/O access.

### G. Priority Inheritance Mutexes
- Use `PTHREAD_PRIO_INHERIT` for any mutex held by real-time threads.
- Prevents priority inversion when background threads hold shared locks.

### H. Binary Debug Protocol
- Simple frame-based protocol with function codes.
- Variable access by index (resolved at load time).
- Includes tick counter for timing correlation.
- Hex-encoded over text transport (UNIX socket / WebSocket).

### I. State Machine with Mutex Protection
- Central `plc_state` with mutex-protected transitions.
- Side effects (load/unload program, create/destroy threads) triggered by state transitions.
- `plc_set_state()` returns bool for success/failure.

### J. Dual-Process Architecture
- Lightweight C runtime for real-time work.
- Full-featured Python web server for management, API, compilation.
- Clean IPC via UNIX sockets with simple text protocol.
- Web server manages runtime process lifecycle (spawn, monitor, restart).

### K. Typed Symbol Resolution Pattern
```c
void (*ext_config_run__)(unsigned long tick);
*(void **)(&ext_config_run__) = dlsym(handle, "config_run__");
```
- Compile-time type checking via function pointer declarations.
- Runtime resolution via dlsym.
- Validate all symbols loaded before proceeding.

### L. Safe Mode Recovery
- `--safe-mode` flag prevents loading PLC program on startup.
- Enables uploading a corrected program after a crash.
- Webserver detects rapid crash pattern and auto-restarts in safe mode.