Skip to content

PiPLC Architecture Overview

This document describes the high-level architecture of PiPLC, a software PLC implementation for Raspberry Pi and other single-board computers.

System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Qt Application                            │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              ApplicationCore (app-level)                  │   │
│  │  ┌──────────────┐  ┌───────────────────┐  ┌───────────┐  │   │
│  │  │EngineManager │  │EmbeddedEngineSvr  │  │RemoteConn │  │   │
│  │  └──────┬───────┘  └───────────────────┘  └───────────┘  │   │
│  │         │  manages up to 8 EngineContexts                 │   │
│  └─────────┼─────────────────────────────────────────────────┘   │
│            │ (shared)                                            │
│       ┌────┴────┐                                                │
│       ▼         ▼                                                │
│  ┌─────────────┐  ┌─────────────┐                               │
│  │ MainWindow  │  │ HmiWindow   │   (independent top-level)     │
│  │ IDE Editor  │  │ HMI Runtime │                               │
│  └──────┬──────┘  └─────────────┘                               │
│         │                                                        │
│  ┌──────┴──────────────────────────────────────────────────┐    │
│  │                    Data Model Layer                       │    │
│  │  ┌─────────┐  ┌──────┐  ┌───────────┐  ┌──────────┐     │    │
│  │  │ Program │──│ Rung │──│ RungElem  │──│ Variable │     │    │
│  │  └─────────┘  └──────┘  └─────┬─────┘  └──────────┘     │    │
│  │                               │                          │    │
│  │            ┌──────────────────┼──────────────────┐      │    │
│  │     ┌──────┴──────┐    ┌──────┴──────┐    ┌─────┴─────┐│    │
│  │     │ Instruction │    │   Branch    │    │  RungPath ││    │
│  │     └──────┬──────┘    └─────────────┘    └───────────┘│    │
│  │            │                                            │    │
│  │  ┌─────────┴─────────────────────────────────────────┐ │    │
│  │  │ ContactInstr │ CoilInstr │ TimerInstr │ MathInstr │ │    │
│  │  │ CounterInstr │ ResetInstr│ CompareInstr│ ...      │ │    │
│  │  └───────────────────────────────────────────────────┘ │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Runtime Layer                          │   │
│  │  ┌──────────────────────────────────────────────────┐     │   │
│  │  │EngineContext                                     │     │   │
│  │  │  ┌────────────────┐  ┌────────────┐  ┌────────┐ │     │   │
│  │  │  │ExecutionEngine │──│LadderExec. │──│MemMgr  │ │     │   │
│  │  │  └───────┬────────┘  └────────────┘  └────────┘ │     │   │
│  │  │          │                                       │     │   │
│  │  │  ┌───────┴────────┐                              │     │   │
│  │  │  │  IIOProvider   │  (VirtualIO or GPIO)         │     │   │
│  │  │  └────────────────┘                              │     │   │
│  │  └──────────────────────────────────────────────────┘     │   │
│  │                                                            │   │
│  │  ┌──────────────────────────────────────────────────┐     │   │
│  │  │SignalRouter (own thread) ── inter-context routing │     │   │
│  │  └──────────────────────────────────────────────────┘     │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                 Persistence Layer                         │   │
│  │  ┌──────────────────┐  ┌─────────────────┐               │   │
│  │  │ProjectSerializer │  │ SymbolTableCSV  │               │   │
│  │  │   (XML v3.1)     │  │   (CSV I/O)     │               │   │
│  │  └──────────────────┘  └─────────────────┘               │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Layer Descriptions

Application Layer (src/app/)

The application layer provides the Qt main window and action management.

Class Responsibility
MainWindow Main application window with menus, toolbars, dock widgets, and status bar
ActionManager Centralized QAction creation and keyboard shortcut management

Editor Layer (src/editor/)

The editor layer implements the visual ladder logic editor using Qt Graphics View.

Class Responsibility
LadderEditorWidget Top-level editor widget coordinating scene, view, and model
LadderScene QGraphicsScene managing rung graphics items
LadderView QGraphicsView with zoom, pan, and selection handling
RungGraphicsItem Visual representation of a ladder rung
InstructionGraphicsItem Base class for instruction graphics (Contact, Coil, Timer, etc.)
BranchGraphicsItem Visual representation of parallel branches

Command Pattern (src/editor/commands/)

All editing operations are implemented as QUndoCommand subclasses for undo/redo support:

  • InsertRungCommand, DeleteRungCommand, MoveRungCommand
  • InsertInstructionCommand, DeleteInstructionCommand, MoveInstructionCommand
  • InsertBranchCommand, AddPathToBranchCommand
  • ModifyInstructionCommand

Widgets Layer (src/widgets/)

Reusable dialogs and panels used by the application.

Class Responsibility
InstructionPalette Docked panel listing available instructions for drag-and-drop
InstructionConfigDialog Modal dialog for editing instruction address, symbol, and presets
SymbolTableDialog Modal dialog for managing the program symbol table (add, edit, delete, CSV import/export)
ProjectSettingsDialog Preferences dialog for editor display, layout, and color settings
DiagnosticsPanel Docked panel showing program validation results
SimulationPanel Docked panel for virtual I/O control during simulation
WatchPanel Docked panel for monitoring variable values at runtime
SignalRouterPanel Docked panel for configuring inter-context signal routes
EngineSelector Toolbar widget for creating, selecting, and removing engine contexts
DecoratorConfigDialog Dialog for configuring contact decorators (timers, counters, one-shots)

Model Layer (src/model/)

The model layer defines the PLC data structures using Qt's parent-child ownership model.

Core Classes

Class Responsibility
Program Top-level container holding rungs and symbol table; tracks modification state
Rung Container for ladder elements; supports comments and metadata
RungElement Abstract base class for rung content (instructions and branches)
Variable Symbol table entry mapping names to addresses with descriptions
Address PLC address parser supporting I:x/y, O:x/y, N:x, T:x, C:x formats

Instruction Hierarchy

RungElement (abstract)
├── Instruction (abstract)
│   ├── ContactInstruction (XIC, XIO) + Decorators
│   ├── CoilInstruction (OTE, OTL, OTU)
│   ├── TimerInstruction (TON, TOF, RTO)
│   ├── CounterInstruction (CTU, CTD)
│   ├── ResetInstruction (RES)
│   ├── MathInstruction (ADD, SUB, MUL, DIV, MOD, NEG, ABS)
│   ├── ScaleInstruction (SCL)
│   ├── CompareInstruction (EQU, NEQ, LES, LEQ, GRT, GEQ)
│   └── BitwiseInstruction (BAND, BOR, BXOR, BNOT)
├── Branch (OR logic container)
└── RungPath (AND logic container within branches)

Decorator Pattern (src/model/Decorators/)

Contact decorators modify input contact behavior without changing ladder structure:

Decorator (abstract)
├── TONDecorator    - Timer On-Delay
├── TOFDecorator    - Timer Off-Delay
├── OSRDecorator    - One-Shot Rising
├── OSFDecorator    - One-Shot Falling
├── DebounceDecorator - Input filter
└── CounterDecorator  - Rising edge counter

Decorators are evaluated in chain order (inside-out) when a contact is evaluated.

Runtime Layer (src/runtime/)

The runtime layer executes ladder logic using a PLC scan cycle model with support for multiple independent engine contexts and inter-context signal routing.

Class Responsibility
IEngineContext Abstract interface for all engine contexts (local and remote)
EngineContext Server-side context: ExecutionEngine + MemoryManager + Program
RemoteEngineContext Editor-side proxy forwarding commands via WebSocket, caching binary memory snapshots
EngineManager Manages RemoteEngineContext instances in the editor with active context tracking
EmbeddedEngineServer In-process EngineServer on a background thread (localhost:0)
ExecutionEngine Scan cycle coordinator with state machine (Stopped/Running/Paused/Stepping/Fault)
LadderExecutor Evaluates ladder logic, executes instructions, handles decorator chains
MemoryManager Word-based (32-bit) PLC memory for all regions (I/O/B/N/T/C)
IIOProvider Abstract interface for I/O operations (virtual or hardware)
VirtualIOProvider In-memory I/O implementation for testing without hardware
IContextResolver Interface for resolving engine contexts by name (used by SignalRouter)
SignalRouter Server-side inter-context memory router on a dedicated thread
SignalRoute Route definition mapping a source address in one context to a destination address in another

Multi-Context Architecture

PiPLC supports up to 8 independent engine contexts per server. All contexts run inside an EngineServer — even "local" ones use an embedded server connected via WebSocket on localhost. The editor only holds RemoteEngineContext proxies.

Editor (EngineManager)                    Server (EngineServer)
├── RemoteEngineContext ──ws://──►  ├── EngineContext "Default"
│   (proxy, cached snapshots)       │   ├── ExecutionEngine (worker thread)
│                                   │   ├── MemoryManager
│                                   │   └── Program (downloaded copy)
├── RemoteEngineContext ──ws://──►  ├── EngineContext "Context 2"
│                                   │   ├── ExecutionEngine (worker thread)
│                                   │   ├── MemoryManager
│                                   │   └── Program (downloaded copy)
└── ... (up to 8 per server)        └── SignalRouter (own thread)

The editor maintains per-context state (editor program, file path, watch list) using a stash/restore pattern. When switching contexts, the current editor state is stashed and the target context's state is restored.

Inter-Context Signal Routing

The SignalRouter runs server-side with direct MemoryManager access, eliminating network round-trips for inter-context communication. Routes are configured from the editor via route.sync protocol messages and broadcast to all connected servers.

EngineContext A                    EngineContext B
+-----------------+               +-----------------+
| MemoryManager A |               | MemoryManager B |
|   B:0/0 = true  |--[route]---->|   B:0/3 = true  |
|   N:10 = 42     |--[route]---->|   N:20 = 42     |
+-----------------+               +-----------------+
       ^ read                          write |
       |                                     |
+------------------------------------------------+
|   SignalRouter (server-side, own QThread)       |
|   QTimer -> executeRoutes() each 10ms tick     |
|   Per-route polling intervals (10-5000ms)      |
|   IContextResolver → resolves by context name  |
+------------------------------------------------+

Each route has its own configurable polling interval. The router's internal timer fires at a 10ms base tick; per-route elapsed time accumulators determine when each route executes. Routes are resolved by context display name via the IContextResolver interface.

Scan Cycle

┌─────────────┐
│  Read Inputs │  ← Copy physical inputs to input image table
└──────┬──────┘
┌──────┴──────┐
│Execute Logic │  ← Evaluate all rungs top-to-bottom
└──────┬──────┘
┌──────┴──────┐
│Write Outputs │  ← Copy output image table to physical outputs
└──────┬──────┘
┌──────┴──────┐
│ Housekeeping │  ← Update timers, statistics, watchdog
└─────────────┘

Memory Organization

See Memory Regions for full address format documentation, data structures, and sizes.

Region Address Format Description
Input I:word/bit Physical inputs (8 words × 32 bits)
Output O:word/bit Physical outputs (8 words × 32 bits)
Internal B:word/bit Internal relays (64 words × 32 bits)
Integer N:word 32-bit integers (256 words)
Timer T:word Timer data (64 timers)
Counter C:word Counter data (64 counters)

Persistence Layer (src/model/)

Class Responsibility
ProjectSerializer XML project file save/load (version 3.1 schema)
MultiProjectSerializer Multi-context workspace save/load (.mplcproj format)
SymbolTableCSV CSV import/export for symbol tables

See File Formats for schema details and examples.

Design Patterns

Qt Ownership Model

All model objects use Qt's parent-child ownership: - Instructions are owned by their containing Rung - Rungs are owned by their Program - Decorators are owned by their ContactInstruction

This ensures proper cleanup when objects are deleted.

Signal/Slot Communication

The model layer emits signals for all state changes: - Program::rungAdded, rungRemoved, modifiedChanged - Program::variableAdded, variableRemoved, changed - Rung::elementAdded, elementRemoved, commentChanged - Instruction::addressChanged, typeChanged - Variable::nameChanged, addressChanged, changed

These signals enable the editor to stay synchronized with model changes. LadderScene connects to both rung and variable signals so that instruction graphics items repaint with updated symbol names when variables are added, removed, or renamed.

Command Pattern for Undo/Redo

All editing operations are encapsulated in QUndoCommand subclasses:

// Example: Insert instruction command
class InsertInstructionCommand : public LadderCommand {
    void redo() override { rung->insertElement(index, instruction); }
    void undo() override { rung->removeElement(index); }
};

Strategy Pattern for I/O

The IIOProvider interface allows swapping between virtual and hardware I/O:

class IIOProvider : public QObject {
    virtual void initialize(int inputWords, int outputWords) = 0;
    virtual void readInputs(quint32* buffer, int count) = 0;
    virtual void writeOutputs(const quint32* buffer, int count) = 0;
};

Factory Pattern for Instructions

InstructionFactory creates instruction instances from type identifiers and serialized data:

Instruction* InstructionFactory::create(InstructionType type, const Address& address);
Instruction* InstructionFactory::createFromXml(QXmlStreamReader& xml);

Threading Model

See Threading Model for the full threading architecture, thread layout diagram, and safety mechanisms.

PiPLC uses a multi-threaded architecture: the Qt GUI runs on the main thread, the embedded WebSocket server runs on a background thread, each engine context runs its scan loop on a dedicated worker thread, and the SignalRouter runs on its own thread. Thread safety is achieved via QReadWriteLock on memory, atomics for state, and Qt's automatic signal/slot queuing across thread boundaries.

Extension Points

Adding New Instructions

  1. Create a new class inheriting from Instruction
  2. Add the instruction type to DataTypes.h InstructionType enum
  3. Update Instruction::typeToString() and stringToType()
  4. Add execution logic to LadderExecutor
  5. Add serialization support to ProjectSerializer
  6. Create a graphics item for the editor
  7. Add to the instruction palette

Adding New Decorators

  1. Create a new class inheriting from Decorator
  2. Add the decorator type to DataTypes.h DecoratorType enum
  3. Update Decorator::typeToString() and stringToType()
  4. Implement evaluate(), reset(), clone(), description()
  5. Add serialization support to ProjectSerializer

Adding GPIO Support

  1. Implement IIOProvider interface with libgpiod
  2. Add GPIO configuration file parser
  3. Create GPIO status panel widget
  4. Integrate with ExecutionEngine

File Formats

See File Formats for complete documentation of all file formats (.plcproj, .mplcproj, .ioconfig, CSV).

Testing Strategy

Unit Tests

Each component has dedicated unit tests: - Model tests verify data structure behavior - Runtime tests verify execution logic - Serialization tests verify round-trip fidelity

Integration Tests

LadderExecutor tests verify complete ladder evaluation including: - Instruction execution - Branch logic (OR) - Decorator chains - Timer and counter behavior

Test Coverage

Target: 80%+ code coverage for core classes. Coverage is measured on CI using gcov/lcov.

GPIO Integration

See GPIO Architecture for the full GPIO integration documentation including the IIOProvider abstraction, GPIOProvider (libgpiod v1/v2), board descriptors, I/O configuration panel, and Docker cross-compilation targets.

Unified Engine Architecture

All engine contexts — both "local" and remote — run inside an EngineServer. Even the editor's local contexts use an embedded server running in-process on a background thread, connected via WebSocket on localhost. This eliminates the local/remote distinction: the editor only works with RemoteEngineContext proxies through the IEngineContext interface.

┌─ Editor Process ────────────────────────────────────────────┐
│                                                              │
│  ┌─ Main Thread (Qt GUI) ─────────────────────────────────┐ │
│  │ MainWindow, LadderEditor, SimulationPanel, WatchPanel  │ │
│  │ EngineManager (holds RemoteEngineContext proxies only)  │ │
│  │ RemoteConnection → localhost:auto (embedded)            │ │
│  │ RemoteConnection → 192.168.x.x:9100 (remote RPi)      │ │
│  │ SignalRouterPanel (config UI, sends routes to servers)  │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌─ Embedded Server Thread ──────────────────────────────┐  │
│  │ EngineServer + QWebSocketServer (localhost:0)          │  │
│  │ EngineContext "Default" (ExecutionEngine + MemoryMgr)  │  │
│  │ SignalRouter (server-side, direct MemoryManager access)│  │
│  │ ClientSession per connected editor client              │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─ Engine Worker Threads (one per context) ──────────────┐ │
│  │ ExecutionEngine scan loop + MemoryManager               │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

┌─ Remote RPi ─────────────────────────────────┐
│  piplc-engine (systemd service, port 9100)   │
│  EngineServer                                 │
│   ├─ EngineContext "PLC1"                     │
│   ├─ EngineContext "PLC2"                     │
│   ├─ SignalRouter (server-side)               │
│   └─ ClientSession[] (editor connections)     │
└───────────────────────────────────────────────┘

Key Abstractions

  • IEngineContext — Abstract interface implemented by EngineContext (server-side) and RemoteEngineContext (editor-side proxy). All editor UI code uses only this interface.
  • EmbeddedEngineServer — Thin wrapper that runs an EngineServer on a background QThread, listening on localhost:0 (OS-assigned port). The editor connects to it just like a remote server.
  • EngineServer — WebSocket server hosting up to 8 EngineContext instances. Owns a SignalRouter and implements IContextResolver. Used both embedded (in the editor) and standalone (as piplc-engine).
  • SignalRouter — Runs server-side on a dedicated thread with direct MemoryManager access. Routes are configured from the editor via route.sync protocol messages.
  • piplc-core — Static library containing model/ + runtime/ (QtCore-only). Shared by the GUI editor and the headless engine server.
  • piplc-protocol — Static library defining the WebSocket protocol: JSON control messages + binary memory snapshots.
  • piplc-engine-server — Static library containing EngineServer + ClientSession. Linked by both the standalone piplc-engine and the editor.

CMake Targets

piplc-core           (STATIC lib)  — model/ + runtime/ — QtCore only
piplc-protocol       (STATIC lib)  — protocol/ — QtCore + QtWebSockets
piplc-engine-server  (STATIC lib)  — EngineServer + ClientSession
piplc-hmi-lib        (STATIC lib)  — HMI elements, canvas, panels, commands — QtWidgets
piplc-remote         (STATIC lib)  — RemoteConnection, RemoteEngineContext, dialog — QtWebSockets
piplc                (executable)  — editor + embedded server — QtWidgets + all libs
piplc-engine         (executable)  — headless server — QtCore + piplc-engine-server
piplc-hmi            (executable)  — standalone HMI — piplc-hmi-lib + piplc-remote

Deployment Modes

Mode Description
Editor only Embedded server runs in-process. No external engine needed.
Editor + Remote Editor connects to one or more remote piplc-engine instances via WebSocket. Embedded server handles local contexts.
Headless only piplc-engine runs standalone on a Raspberry Pi as a systemd service. Editor connects remotely for programming and monitoring.
Standalone HMI piplc-hmi connects to any engine server via WebSocket. Provides HMI editor and runtime without the full IDE.

Future Architecture Considerations

Safety Systems (M5)

  • E-stop monitoring with dedicated GPIO
  • Watchdog timer tied to scan loop
  • Safe-state controller for fault handling