Files
mht-siemens/ARCHITECTURE_AND_SPEC.md
Samuele Locatelli e8ea999de3 Update architettura
2026-06-22 10:18:53 +02:00

379 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 pushed
- `develop`: up-to-date with origin, clean working tree
## Component Architecture (Mermaid)
```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
```csharp
// 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
```json
{
"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 `DatiPronti` flag.
- **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 `PLCRecord` with unique ID, timestamp, and payload.
## Persistence Pipeline
Strategy pattern per target. Pipeline runs after every PLC read batch or record event.
1. **File System** (always enabled in production migration): Serializes `PLCStateSnapshot` to JSON files with rotation.
2. **Redis** (optional): Stores state as Redis Hash keyed by `plc:{areaId}`.
3. **MariaDB** (optional): Ef Core `Pomelo.EntityFrameworkCore.MySql` provider, table `PLCRecords`.
## 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.Logging` structured 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.