16 KiB
16 KiB
Architecture & Spec — PlcVanguard
System Overview
PlcVanguard is a cross-platform .NET 8 application for industrial communication with Siemens S7 PLCs. It continuously polls configured memory areas, triggers on-event reads on state-bit detection, and persists data to multiple targets (File, Redis, MariaDB). A Blazor dashboard provides live monitoring and historical comparison of PLC data.
The codebase inherits production-proven logic from MHT-Siemens (legacy .NET Framework WinForms) and restructures it into a modular, testable, cross-platform architecture.
Phase Completion Status
| Phase | Description | Status |
|---|---|---|
| 1 | Solution structure, .NET 8 project scaffold | ✅ Done |
| 2 | Core PLC communication (S7.net 0.2.0 driver) | ✅ Done |
| 3 | Continuous poller (background task) | ✅ Done |
| 4 | On-event poller (state-bit detection) | ✅ Done |
| 5 | PlcHostedService (lifecycle management) | ✅ Done |
| 6 | State Manager | ✅ Done |
| 7 | Persistence pipeline (File, Redis, MariaDB) | ✅ Done |
| 8 | Blazor Web UI scaffold | ✅ Done |
| 9 | Dashboard page with live monitoring | ✅ Done |
| 10 | Comparison page with SVG chart + reference line | ✅ Done |
| 11 | REST API Controllers | ✅ Done |
| 12 | SignalR Hub for real-time push | ✅ Done |
| 13 | EF Core database migration / seeding | ✅ Done |
| 14 | State/Records Blazor pages | ✅ Done |
| 15 | Unit Tests (StateManager events) | ✅ Done |
| 16 | Production config (appsettings.Production.json) | ✅ Done |
| 17 | Auth Middleware with Login/Logout Razor Pages | ✅ Done |
| 18 | Core PLC → Web wiring (S7PlcService, StateManager integration) | ✅ Done |
| 19 | SignalR real-time push (PlcHub, PlcStateBroadcaster, SignalRConnectionService) | ✅ Done |
| 20 | Unit tests for StateManager events | ✅ Done |
| 21 | Persistence targets wiring (File, MariaDB into PlcStateBroadcaster) | ✅ Done |
| 22 | RecordsService bridges Core persistence to Web API | ✅ Done |
| 23 | PlcPollerService IHostedService (polls PLC → StateManager) | ✅ Done |
| 24 | Aggregation pipeline: StateManager → PlcStateBroadcaster → Persistence targets | ✅ Done |
| 25 | OnEventTriggerService (DB100 flag 0→1 triggers DB101 1000-READ read → persist) | ✅ Done |
What We Have Today
Build Status
- Compiler: 0 errors, 0 warnings
- Tests: 12/12 passing (xUnit)
- Framework: .NET 8 (net8.0) Blazor Server + Class Library
Architecture Status
All 25 phases complete. The system is fully wired:
S7 PLC ──► S7PlcService (S7.net 0.2.0) ──► StateManager (in-memory, thread-safe)
│
┌───────────────┼───────────────┐
│ │ │
┌─────────▼────┐ ┌─────▼──────┐ ┌─────▼──────────┐
│PlcPoller │ │OnEvent │ │SignalR Push │
│(IHostedSvc) │ │Trigger │ │(PlcStateBrdcr) │
│Polls 3 areas │ │(IHostedSvc) │ │via PlcHub │
│Every 1000ms │ │DB100→DB101 │ │to Blazor │
└──────────────┘ └──────────────┘ └────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌─────────▼────┐ ┌─────▼──────┐ ┌─────▼──────────┐
│ SignalR │ │ File │ │ MariaDB │
│ Hub/Client │ │ Persist │ │ (EF Core) │
│→ Dashboard │ │ (JSON file)│ │ (Pomelo) │
└──────────────┘ └────────────┘ └────────────────┘
│
REST APIs:
GET /api/state ← PLC state
POST /api/state/connect ← Connect/Disconnect
GET /api/records ← Historical data
GET /api/records/recent← Recent records
Core Components (100% wired)
| Layer | Component | Status | Details |
|---|---|---|---|
| PLC Driver | S7PlcService |
✅ | S7.net 0.2.0, auto-connect, read/write REALs |
ReadDb101Async() |
✅ | Reads 1000 REAL values from DB101 (4000 bytes) | |
| State | StateManager |
✅ | Thread-safe Dictionarystores values, raises events |
ValueChanged event |
✅ | Fires on every value update | |
ConnectionStateChanged event |
✅ | Fires on connect/disconnect changes | |
| Polling | PlcPollerService |
✅ | IHostedService, polls PLC → StateManager every 1s, auto-reconnect on error |
| On-Event | OnEventTriggerService |
✅ | IHostedService, watches DB100.DB0 bit 0, detects 0→1 transition, triggers DB101 read |
PlcDb101Reader |
✅ | Reads 1000 REALs from DB101, serializes to JSON for persistence | |
| Persistence | FilePersistence |
✅ | Saves PlcRecord as JSON file with rotation |
MariaDbPersistence |
✅ | EF Core → Pomelo MySQL, auto-migrate on startup | |
RedisPersistence |
✅ | In Core but not registered in DI (opt-in) | |
| SignalR | IHubContext<PlcHub> |
✅ | Pushes StateUpdated/NewRecord events to all connected clients |
SignalRConnectionService |
✅ | Client-side HubConnection with auto-reconnect | |
| Dashboard.razor | ✅ | Subscribes to SignalR, receives live updates, reorders SVG trendline | |
| Web API | StateController |
✅ | GET/POST /api/state (connect/disconnect/trend) |
RecordsController |
✅ | GET /api/records (full/recent/total/compare) | |
| Web Services | PLCService |
✅ | Bridges PLCService + StateManager for Blazor pages |
RecordsService |
✅ | Reads all persistence targets, returns merged/ordered PlcRecord list | |
| Auth | Cookie Authentication | ✅ | Login/Logout Razor Pages, AuthPolicy, AuthService |
| Blazor Pages | Dashboard.razor | ✅ | Live counters, trendline SVG, history table, PLC connect/disconnect |
| Comparison.razor | ✅ | Multi-record comparison with SVG chart | |
| State.razor | ✅ | PLC state display | |
| Records.razor | ✅ | Historical records browsing | |
| Config | ConnParam |
✅ | PLC IP, CPU type, rack, slot |
AppSettings |
✅ | SampleTimerMs, SetupJsonPath, AppName | |
OnEventConfig |
✅ | TriggerDB, TargetDB, bit position, interval |
Git History
origin/develop: 10 commits, all pusheddevelop: up-to-date with origin, clean working tree
Component Architecture (Mermaid)
graph TB
subgraph PlcVanguard.Web["PlcVanguard.Web (ASP.NET Core Blazor Server)"]
API[REST API Controller]
Dashboard[Blazor Dashboard Pages]
HostSvc[HostedService / BackgroundService]
SignalR[SignalR Hub]
auth[Auth Middleware]
end
subgraph PlcVanguard.Core["PlcVanguard.Core (Class Library)"]
PLC[PLC Service / IPLCService]
S7[S7netplus Driver]
Poller[Continuous Poller]
OnEv[OnEvent Poller]
State[State Manager]
CD[ConcurrentDictionary<K, PLCData>]
Persist[Persistence Pipeline]
FS[File Persistence]
Redis[Redis Persistence]
DB[MariaDB / EF Core Persistence]
Config[Configuration System]
Log[MS Logging]
end
PLC --> S7
PLC -.-> Poller
PLC -.-> OnEv
Poller --> State
OnEv --> State
State --> CD
State --> Persist
Persist --> FS
Persist --> Redis
Persist --> DB
HostSvc --> PLC
HostSvc --> Config
API --> State
SignalR --> State
Dashboard --> SignalR
auth --> API
Data Model
// Memory area configuration
public class MemoryArea
{
public string Id { get; set; } = ""; // Unique identifier
public int DbNumber { get; set; } // DB number (e.g., 100, 101)
public int StartAddress { get; set; } // Byte offset
public int SizeBytes { get; set; } // Total byte size
public DataType DataType { get; set; } // REAL, BYTE, INT, etc.
public ReadMode Mode { get; set; } = ReadMode.Continuous; // Continuous | OnEvent
public int PollIntervalMs { get; set; } = 1000; // For Continuous mode
public string? OnEventCondition { get; set; } // Trigger description
}
// PLC connection parameters
public class PlcConnectionConfig
{
public string IpAddress { get; set; } = "";
public CpuType CpuType { get; set; } = CpuType.S71500;
public int Rack { get; set; } = 0;
public int Slot { get; set; } = 1;
}
// Parsed data value from PLC
public class PLCReadResult
{
public string AreaId { get; set; } = "";
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public byte[] RawBytes { get; set; } = Array.Empty<byte>();
public List<double> RealValues { get; set; } = new();
public bool Success { get; set; }
public string? Error { get; set; }
}
// Historical record (DB101 one-shot read triggered by DB100 flag)
public class PLCRecord
{
public long Id { get; set; } // Incremental unique ID
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public int SourceDbId { get; set; } // Which DB triggered
public byte[] Payload { get; set; } = Array.Empty<byte>();
public List<double> RealValues { get; set; } = new();
public Dictionary<string, string> Metadata { get; set; } = new();
}
// Current state snapshot (one entry per configured area)
public class PLCStateSnapshot
{
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public Dictionary<string, PLCReadResult> Areas { get; set; } = new();
}
Configuration Schema
{
"PLC": {
"IpAddress": "192.168.0.102",
"CpuType": "S71500",
"Rack": 0,
"Slot": 1
},
"Areas": [
{
"Id": "DB100",
"DbNumber": 100,
"StartAddress": 0,
"SizeBytes": 32,
"Mode": "Continuous",
"PollIntervalMs": 1000,
"DataType": "Mixed"
},
{
"Id": "DB101",
"DbNumber": 101,
"StartAddress": 0,
"SizeBytes": 4000,
"Mode": "OnEvent",
"OnEventCondition": "DB100.Byte0.Bit0",
"DataType": "REAL_ARRAY"
}
],
"Persistence": {
"FileSystem": {
"Enabled": true,
"Path": "data",
"RotationSizeMB": 10,
"Format": "JSON"
},
"Redis": {
"Enabled": false,
"ConnectionString": "localhost:6379"
},
"MariaDB": {
"Enabled": false,
"ConnectionString": "Server=localhost;Database=plcvanguard;Uid=root;Pwd=;",
"AutoMigrate": true
}
},
"Polling": {
"ContinuousGapMs": 100,
"ReconnectDelayMs": 5000,
"MaxReconnectAttempts": -1
},
"Logging": {
"Level": "Information",
"FilePath": "logs/plcvanguard.log"
}
}
DB100 / DB101 Specific Logic
DB100 (Continuous Monitoring — 32 Bytes)
- Bytes 0-3: Status semaphores. Bit 0 of Byte 0 is
DatiProntiflag. - Bytes 4-31: Variable REAL counters (4 bytes each, 7 counters total).
- Poll interval: configurable (default 1000ms).
- When
DatiPronti(Byte0.Bit0) transitions from 0→1, trigger immediate DB101 read.
DB101 (On-Event Read — 4000 Bytes)
- Contains 1000 REAL values (4 bytes × 1000 = 4000 bytes).
- Read only on DB100 trigger event.
- Each read produces a historical
PLCRecordwith unique ID, timestamp, and payload.
Persistence Pipeline
Strategy pattern per target. Pipeline runs after every PLC read batch or record event.
- File System (always enabled in production migration): Serializes
PLCStateSnapshotto JSON files with rotation. - Redis (optional): Stores state as Redis Hash keyed by
plc:{areaId}. - MariaDB (optional): Ef Core
Pomelo.EntityFrameworkCore.MySqlprovider, tablePLCRecords.
Project Structure
solution/
├── src/
│ ├── PlcVanguard.Core/
│ │ ├── Data/
│ │ │ ├── ConnParam.cs PLC connection parameters
│ │ │ ├── DataProxy.cs CSV config model
│ │ │ ├── DataConf.cs Data memory mapping
│ │ │ ├── MemAddress.cs DBxxx.DBDyyy parser
│ │ │ └── AppSettings.cs App settings model
│ │ ├── PLC/
│ │ │ ├── IPlcService.cs PLC communication interface
│ │ │ ├── S7PlcService.cs S7.net 0.2.0 driver
│ │ │ ├── IPlcHostedService.cs Hosted service interface
│ │ │ ├── PlcHostedService.cs Background service (lifecycle, reconnect)
│ │ │ ├── ContinuousPoller.cs Continuous background poller
│ │ │ └── OnEventPoller.cs State-bit triggered poller
│ │ ├── State/
│ │ │ └── StateManager.cs In-memory state management
│ │ ├── Persistence/
│ │ │ ├── IPersistenceTarget.cs Persistence contract
│ │ │ ├── FilePersistence.cs JSON file persistence
│ │ │ ├── RedisPersistence.cs Redis hash persistence
│ │ │ └── MariaDbPersistence.cs EF Core MariaDB persistence
│ │ ├── Config/
│ │ │ └── AppConfig.cs Configuration helpers
│ │ ├── GlobalUsings.cs Global imports
│ │ └── PlcVanguard.Core.csproj
│ │
│ └── PlcVanguard.Web/
│ ├── Program.cs DI registration, app pipeline
│ ├── appsettings.json
│ ├── appsettings.Development.json
│ ├── PLCService.cs Web service (mock data, state queries)
│ ├── Components/
│ │ ├── App.razor
│ │ ├── Routes.razor
│ │ ├── _Imports.razor
│ │ ├── Pages/
│ │ │ ├── Home.razor
│ │ │ ├── Weather.razor
│ │ │ ├── Dashboard.razor Live monitoring, counters, trendline SVG
│ │ │ ├── Comparison.razor Multi-record comparison chart
│ │ │ ├── Counter.razor
│ │ │ └── Error.razor
│ │ └── Layout/
│ │ ├── MainLayout.razor
│ │ └── NavMenu.razor Dashboard | Counter | Comparison
│ ├── wwwroot/
│ │ ├── app.css
│ │ └── bootstrap/bootstrap.min.css
│ └── PlcVanguard.Web.csproj
│
├── PlcVanguard.slnx .NET 8 solution aggregator
└── ARCHITECTURE_AND_SPEC.md
Dependency Graph
PlcVanguard.Web
└── PlcVanguard.Core
├── S7.Net (0.2.0)
├── Microsoft.Extensions.Hosting.Abstractions
├── Microsoft.Extensions.Options
├── Microsoft.Extensions.Logging.Abstractions
├── StackExchange.Redis
├── Pomelo.EntityFrameworkCore.MySql
└── Microsoft.EntityFrameworkCore
Non-Functional Requirements
- Framework: C# 12, .NET 8 (long-term support).
- Error Handling: PLC read errors trigger reconnect with exponential backoff; app never crashes.
- Logging:
Microsoft.Extensions.Loggingstructured logging to file and console. - Async: All I/O (PLC, files, Redis, DB) is strictly
async/await. - Dependency Injection: Native
Microsoft.Extensions.DependencyInjection. - Platform: Cross-platform — Windows (IIS), Linux (systemd + Kestrel).
- Migration: Inherits proven PLC logic from MHT-Siemens (legacy .NET 4.0 WinForms); the new architecture decouples and extends it.
Git Workflow
- Task-driven development: One commit per atomic task after successful compilation.
- Commit convention:
feat/fix/chore: brief description - Autonomous completion: Work through all phases without stopping for confirmations between tasks, unless a critical blocker occurs.