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,MoveRungCommandInsertInstructionCommand,DeleteInstructionCommand,MoveInstructionCommandInsertBranchCommand,AddPathToBranchCommandModifyInstructionCommand
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¶
- Create a new class inheriting from
Instruction - Add the instruction type to
DataTypes.hInstructionTypeenum - Update
Instruction::typeToString()andstringToType() - Add execution logic to
LadderExecutor - Add serialization support to
ProjectSerializer - Create a graphics item for the editor
- Add to the instruction palette
Adding New Decorators¶
- Create a new class inheriting from
Decorator - Add the decorator type to
DataTypes.hDecoratorTypeenum - Update
Decorator::typeToString()andstringToType() - Implement
evaluate(),reset(),clone(),description() - Add serialization support to
ProjectSerializer
Adding GPIO Support¶
- Implement
IIOProviderinterface with libgpiod - Add GPIO configuration file parser
- Create GPIO status panel widget
- 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 byEngineContext(server-side) andRemoteEngineContext(editor-side proxy). All editor UI code uses only this interface.EmbeddedEngineServer— Thin wrapper that runs anEngineServeron a backgroundQThread, listening onlocalhost:0(OS-assigned port). The editor connects to it just like a remote server.EngineServer— WebSocket server hosting up to 8EngineContextinstances. Owns aSignalRouterand implementsIContextResolver. Used both embedded (in the editor) and standalone (aspiplc-engine).SignalRouter— Runs server-side on a dedicated thread with directMemoryManageraccess. Routes are configured from the editor viaroute.syncprotocol 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 containingEngineServer+ClientSession. Linked by both the standalonepiplc-engineand 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