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

16 KiB
Raw Permalink Blame History

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)

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 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.